diff options
Diffstat (limited to 'internal/game/polish_craft_fail_delivery_test.go')
| -rw-r--r-- | internal/game/polish_craft_fail_delivery_test.go | 111 |
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)) + } + }) +} |
