aboutsummaryrefslogtreecommitdiff
path: root/internal/game/polish_craft_fail_delivery_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/polish_craft_fail_delivery_test.go')
-rw-r--r--internal/game/polish_craft_fail_delivery_test.go111
1 files changed, 111 insertions, 0 deletions
diff --git a/internal/game/polish_craft_fail_delivery_test.go b/internal/game/polish_craft_fail_delivery_test.go
new file mode 100644
index 0000000..f16e2a9
--- /dev/null
+++ b/internal/game/polish_craft_fail_delivery_test.go
@@ -0,0 +1,111 @@
+package game
+
+// Regression: failed-craft delivery used FirstFreeSlot only, while the
+// pre-roll fit gate (craftFailFits -> craftOutputFits) counts a matching
+// stack. A stackable fail replacement could therefore be destroyed on a full
+// inventory even though the gate promised it would fit. Delivery must merge
+// like craft/harvest/pickup does.
+
+import (
+ "path/filepath"
+ "testing"
+
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/item"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+const polishFailRecipeYAML = `id: polish_out
+name: polish out
+stackable: true
+craft:
+ - type: smithing
+ subtype: smelting
+ fail: polish_ash
+ ingredients:
+ - items:
+ - polish_ore
+ quantity: 1
+ success:
+ base: 0
+ cap: 0
+ per_level: 0
+ output_qty: 1
+ ticks_per_cycle: 1
+`
+
+func seedPolishFailItems(t *testing.T, dir string) {
+ t.Helper()
+ writeSeedFile(t, filepath.Join(dir, "items", "polish_out.yaml"), []byte(polishFailRecipeYAML))
+ writeSeedFile(t, filepath.Join(dir, "items", "polish_ash.yaml"),
+ []byte("id: polish_ash\nname: polish ash\nstackable: true\nvalue: 1\n"))
+ writeSeedFile(t, filepath.Join(dir, "items", "polish_ore.yaml"),
+ []byte("id: polish_ore\nname: polish ore\nstackable: true\nvalue: 1\n"))
+}
+
+func TestPlaceCraftFailMergesIntoMatchingStack(t *testing.T) {
+ h := newHarness(t)
+ seedPolishFailItems(t, h.dir)
+ p := player.New("alice")
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "polish_ash", Quantity: 3})
+ fillInventoryExcept(p)
+
+ h.g.placeCraftFail(p, &item.CraftDef{Fail: "polish_ash"})
+ if slot := p.InvSlot(0); slot == nil || slot.Quantity != 4 {
+ t.Fatalf("fail replacement was not merged into the matching stack: %+v", p.InvSlot(0))
+ }
+ })
+}
+
+func TestCraftFailureMergesFailStackWhenFull(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ p := h.seedCharacter("alice", "alice")
+ seedPolishFailItems(t, h.dir)
+
+ sess := &net.Session{Conn: newChanConn(), Player: p}
+ sess.SetState(net.StateGame)
+
+ h.g.RunOnOwner(func() {
+ h.g.ItemStore.Reload(h.dir)
+ // Full inventory: a matching output stack, a matching fail stack and
+ // the ingredient. Consuming the ingredient frees its slot, so the old
+ // FirstFreeSlot delivery landed the fail item there as a second stack.
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "polish_ash", Quantity: 1})
+ p.SetInvSlot(1, &player.InventorySlot{ItemID: "polish_ore", Quantity: 1})
+ p.SetInvSlot(2, &player.InventorySlot{ItemID: "polish_out", Quantity: 1})
+ for i := 3; i < 28; i++ {
+ p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1})
+ }
+
+ p.Action = &behavior.Action{
+ Type: behavior.TypeSmelt,
+ TargetID: "polish_out",
+ Data: &behavior.ProductionData{ItemID: "polish_out", Phase: 1, Remaining: 1},
+ }
+ h.g.advanceProduction(sess, p)
+
+ if got := p.CountItem("polish_ore"); got != 0 {
+ t.Errorf("ingredient = %d, want 0 (consumed)", got)
+ }
+ if got := p.CountItem("polish_ash"); got != 2 {
+ t.Errorf("fail product = %d, want 2", got)
+ }
+ stacks := 0
+ for i := 0; i < 28; i++ {
+ if s := p.InvSlot(i); s != nil && s.ItemID == "polish_ash" {
+ stacks++
+ }
+ }
+ if stacks != 1 {
+ t.Errorf("fail product was delivered as %d stacks, want 1 merged stack", stacks)
+ }
+ if slot := p.InvSlot(0); slot == nil || slot.Quantity != 2 {
+ t.Errorf("matching fail stack = %+v, want quantity 2", p.InvSlot(0))
+ }
+ })
+}