package game // Regression: swapping worn equipment used to return the displaced item as a // raw Quality 0/MaxQuality 0 slot and leave its stale EquipQuality entry // behind. Reachable with the shipped lighter (the only max_quality > 0 item): // wielding a weapon after the lighter returned it zeroed, and act_burn charged // the stale equipment record instead of the carried stack. The displaced // item's equipped quality must move onto the returned slot and out of the map. import ( "path/filepath" "testing" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) const polishLighterYAML = `id: polish_lighter name: polish lighter stackable: false max_quality: 100 equipment: slot: main_hand type: tool tool: type: fire ` const polishBladeYAML = `id: polish_blade name: polish blade stackable: false equipment: slot: main_hand type: melee_weapon ` func seedPolishWearItems(t *testing.T, dir string) { t.Helper() writeSeedFile(t, filepath.Join(dir, "items", "polish_lighter.yaml"), []byte(polishLighterYAML)) writeSeedFile(t, filepath.Join(dir, "items", "polish_blade.yaml"), []byte(polishBladeYAML)) } // carriedInventSlot returns the first inventory slot holding itemID. func carriedInventSlot(p *player.Player, itemID string) *player.InventorySlot { for i := 0; i < 28; i++ { if s := p.InvSlot(i); s != nil && s.ItemID == itemID { return s } } return nil } func TestWearSwapReturnsEquippedQualityToInventory(t *testing.T) { h := newHarness(t) h.seedAccount("alice") p := h.seedCharacter("alice", "alice") seedPolishWearItems(t, h.dir) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) p.SetInvSlot(0, h.g.newInventorySlot("polish_lighter", 1)) h.g.doWear(sess, "lighter") if got := p.Equipment[item.SlotMainHand]; got != "polish_lighter" { t.Fatalf("lighter not equipped: %q", got) } // Burn two charges while the lighter is equipped. for i := 0; i < 2; i++ { if !h.g.consumeFireTool(sess, p, "polish_lighter") { t.Fatal("consumeFireTool refused a fresh lighter") } } if got := p.EquipQuality["polish_lighter"]; got != 98 { t.Fatalf("equipped lighter charge = %d, want 98", got) } // Wielding a weapon displaces the lighter back into the inventory. p.SetInvSlot(1, h.g.newInventorySlot("polish_blade", 1)) h.g.doWear(sess, "blade") if got := p.Equipment[item.SlotMainHand]; got != "polish_blade" { t.Fatalf("blade not equipped: %q", got) } lighter := carriedInventSlot(p, "polish_lighter") if lighter == nil { t.Fatal("the displaced lighter was not returned to the inventory") } if lighter.Quality != 98 || lighter.MaxQuality != 100 { t.Errorf("returned lighter = %+v, want quality 98/100", lighter) } if _, ok := p.EquipQuality["polish_lighter"]; ok { t.Error("stale EquipQuality entry left behind after the swap") } // act_burn must charge the carried stack, not a stale equipment record. if !h.g.consumeFireTool(sess, p, "polish_lighter") { t.Fatal("consumeFireTool refused the carried lighter stack") } if lighter.Quality != 97 { t.Errorf("carried lighter charge = %d, want 97", lighter.Quality) } }) } func TestWearAllSwapReturnsEquippedQualityToInventory(t *testing.T) { h := newHarness(t) h.seedAccount("alice") p := h.seedCharacter("alice", "alice") seedPolishWearItems(t, h.dir) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) p.SetInvSlot(0, h.g.newInventorySlot("polish_lighter", 1)) h.g.doWear(sess, "lighter") if got := p.EquipQuality["polish_lighter"]; got != 100 { t.Fatalf("equipped lighter charge = %d, want 100", got) } p.SetInvSlot(1, h.g.newInventorySlot("polish_blade", 1)) h.g.doWearAll(sess) lighter := carriedInventSlot(p, "polish_lighter") if lighter == nil { t.Fatal("wear all did not return the displaced lighter") } if lighter.Quality != 100 || lighter.MaxQuality != 100 { t.Errorf("returned lighter = %+v, want quality 100/100", lighter) } if _, ok := p.EquipQuality["polish_lighter"]; ok { t.Error("stale EquipQuality entry left behind by wear all") } }) }