aboutsummaryrefslogtreecommitdiff
path: root/internal/game/correct_quality_merge_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/correct_quality_merge_test.go')
-rw-r--r--internal/game/correct_quality_merge_test.go201
1 files changed, 201 insertions, 0 deletions
diff --git a/internal/game/correct_quality_merge_test.go b/internal/game/correct_quality_merge_test.go
new file mode 100644
index 0000000..d54c8c9
--- /dev/null
+++ b/internal/game/correct_quality_merge_test.go
@@ -0,0 +1,201 @@
+package game
+
+// Regression: every stack-creation and stack-merge site must agree on the
+// Quality/MaxQuality a fresh stack gets (newInventorySlot / stackProps).
+// Pickups merge only into a stack with those exact properties, so a stack
+// produced by crafting, harvesting, or ammo removal with Quality 0 could no
+// longer be merged into - a full inventory then refused a pickup of an item
+// the player already carried.
+
+import (
+ "path/filepath"
+ "testing"
+
+ "thehouseoficarus/internal/item"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+const correctQualityItemYAML = `id: correct_quality_gem
+name: correct quality gem
+stackable: true
+max_quality: 5
+craft:
+ - type: crafting
+ ingredients:
+ - items:
+ - correct_quality_shard
+ quantity: 1
+ xp: 1
+ ticks_per_cycle: 1
+`
+
+func seedCorrectQualityItems(t *testing.T, h *harness) {
+ t.Helper()
+ writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_gem.yaml"), []byte(correctQualityItemYAML))
+ writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_shard.yaml"),
+ []byte("id: correct_quality_shard\nname: correct quality shard\nstackable: true\nmax_quality: 5\nvalue: 1\n"))
+}
+
+// fillInventoryExcept occupies every slot but slot 0 with filler.
+func fillInventoryExcept(p *player.Player) {
+ for i := 0; i < 28; i++ {
+ if i != 0 {
+ p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1})
+ }
+ }
+}
+
+// TestStackPropsMatchNewInventorySlot guards the one rule that stack creation
+// and stack merging both depend on: the helper the craft/harvest/ammo sites
+// use must stamp what newInventorySlot stamps.
+func TestStackPropsMatchNewInventorySlot(t *testing.T) {
+ h := newHarness(t)
+ seedCorrectQualityItems(t, h)
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ gem := h.g.newInventorySlot("correct_quality_gem", 1)
+ if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_gem")); q != gem.Quality || mq != gem.MaxQuality {
+ t.Errorf("stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, gem.Quality, gem.MaxQuality)
+ }
+ shard := h.g.newInventorySlot("correct_quality_shard", 1)
+ if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_shard")); q != shard.Quality || mq != shard.MaxQuality {
+ t.Errorf("quality-free stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, shard.Quality, shard.MaxQuality)
+ }
+ })
+}
+
+func TestCraftOutputMergesIntoQualityStack(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ p := h.seedCharacter("alice", "alice")
+ seedCorrectQualityItems(t, h)
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ def := mustLoadItem(t, h.g, "correct_quality_gem")
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 5, MaxQuality: 5})
+ fillInventoryExcept(p)
+
+ if !craftOutputFits(p, def.ID, def) {
+ t.Fatal("a full inventory must accept a craft output that merges into an identical stack")
+ }
+ if !placeCraftOutput(p, def.ID, def, 2) {
+ t.Fatal("craft output was not placed")
+ }
+ if slot := p.InvSlot(0); slot.Quantity != 12 || slot.Quality != 5 || slot.MaxQuality != 5 {
+ t.Errorf("merged stack = %+v, want quantity 12 at quality 5", slot)
+ }
+
+ // A differently qualified stack is not a merge target: the same
+ // full inventory must report that nothing fits.
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 1, MaxQuality: 5})
+ if craftOutputFits(p, def.ID, def) {
+ t.Error("craft output would merge into a stack with a different quality")
+ }
+ if placeCraftOutput(p, def.ID, def, 2) {
+ t.Error("craft output was placed with no free slot and no matching stack")
+ }
+ })
+}
+
+func TestCraftOutputCreatesStackWithFreshProps(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ p := h.seedCharacter("alice", "alice")
+ seedCorrectQualityItems(t, h)
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ def := mustLoadItem(t, h.g, "correct_quality_gem")
+ if !placeCraftOutput(p, def.ID, def, 3) {
+ t.Fatal("craft output was not placed in an empty inventory")
+ }
+ slot := p.InvSlot(0)
+ if slot == nil || slot.ItemID != def.ID {
+ t.Fatalf("slot 0 = %+v", slot)
+ }
+ if q, mq := h.g.pickedStackProps(def.ID); slot.Quality != q || slot.MaxQuality != mq {
+ t.Fatalf("crafted stack %+v does not match what a pickup receives (%d,%d)", slot, q, mq)
+ }
+ })
+}
+
+func TestPickupMergesIntoHarvestedStackWhenFull(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ p := h.seedCharacter("alice", "alice")
+ writeSeedFile(t, filepath.Join(h.dir, "items", hardeningCropID+".yaml"),
+ []byte("id: test_potato\nname: test potato\nstackable: true\nmax_quality: 5\n"))
+
+ conn := newChanConn()
+ sess := &net.Session{Conn: conn, Player: p}
+ sess.SetState(net.StateGame)
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ readyHarvest(p, 3, 3)
+ h.g.advanceHarvest(sess, p)
+
+ stack := p.InvSlot(0)
+ if stack == nil || stack.ItemID != hardeningCropID {
+ t.Fatalf("harvested stack = %+v", stack)
+ }
+ quality, maxQuality := h.g.pickedStackProps(hardeningCropID)
+ if stack.Quality != quality || stack.MaxQuality != maxQuality {
+ t.Fatalf("harvested stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality)
+ }
+
+ // A full inventory must still merge a ground pickup into the stack
+ // the harvest just created.
+ fillInventoryExcept(p)
+ h.g.World.AddGroundItem(p.RoomID, hardeningCropID, 2)
+ h.g.doGet(sess, "test potato")
+
+ if got := p.CountItem(hardeningCropID); got != 5 {
+ t.Errorf("potatoes carried = %d, want 5", got)
+ }
+ if got := h.g.World.GroundItems(p.RoomID)[hardeningCropID]; got != 0 {
+ t.Errorf("ground potatoes = %d, want 0 (they fit the existing stack)", got)
+ }
+ })
+}
+
+func TestPickupMergesIntoAmmoReturnedStack(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ p := h.seedCharacter("alice", "alice")
+ seedCorrectQualityItems(t, h)
+
+ conn := newChanConn()
+ sess := &net.Session{Conn: conn, Player: p}
+ sess.SetState(net.StateGame)
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ p.Equipment[item.SlotAmmo] = "correct_quality_shard"
+ p.AmmoQty = 4
+ if !h.g.unequipAmmo(p) {
+ t.Fatal("unequipAmmo refused an empty inventory")
+ }
+ stack := p.InvSlot(0)
+ if stack == nil || stack.ItemID != "correct_quality_shard" {
+ t.Fatalf("returned ammo stack = %+v", stack)
+ }
+ quality, maxQuality := h.g.pickedStackProps("correct_quality_shard")
+ if stack.Quality != quality || stack.MaxQuality != maxQuality {
+ t.Fatalf("returned ammo stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality)
+ }
+
+ fillInventoryExcept(p)
+ h.g.World.AddGroundItem(p.RoomID, "correct_quality_shard", 3)
+ h.g.doGet(sess, "correct quality shard")
+
+ if got := p.CountItem("correct_quality_shard"); got != 7 {
+ t.Errorf("shards carried = %d, want 7", got)
+ }
+ if got := h.g.World.GroundItems(p.RoomID)["correct_quality_shard"]; got != 0 {
+ t.Errorf("ground shards = %d, want 0 (they fit the returned stack)", got)
+ }
+ })
+}