aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_craft_combine_fletch_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/hardening_craft_combine_fletch_test.go')
-rw-r--r--internal/game/hardening_craft_combine_fletch_test.go260
1 files changed, 260 insertions, 0 deletions
diff --git a/internal/game/hardening_craft_combine_fletch_test.go b/internal/game/hardening_craft_combine_fletch_test.go
new file mode 100644
index 0000000..f3f6832
--- /dev/null
+++ b/internal/game/hardening_craft_combine_fletch_test.go
@@ -0,0 +1,260 @@
+package game
+
+// Regression: combine and fletch must honor the craft fields production
+// already honors — roll craft.Success when defined, create craft.Fail on
+// failure, award craft.XP, and place ingredient byproducts. Shipped combine
+// and fletch recipes set xp:0 and define no fail/success, so these paths
+// must be no-ops for them (asserted indirectly by the existing production
+// regressions).
+
+import (
+ "testing"
+
+ "thehouseoficarus/internal/item"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+const combineSuccessYAML = `id: test_combine_out
+name: test combine out
+stackable: false
+value: 5
+craft:
+ - type: combine
+ ingredients:
+ - items:
+ - combine_part
+ quantity: 1
+ byproducts:
+ - combine_scrap
+ success:
+ base: 1.0
+ per_level: 0
+ cap: 1.0
+ xp: 25
+ output_qty: 1
+ ticks_per_cycle: 1
+`
+
+const combineFailYAML = `id: test_combine_fail_out
+name: test combine fail out
+stackable: false
+value: 5
+craft:
+ - type: combine
+ ingredients:
+ - items:
+ - combine_fail_part
+ quantity: 1
+ success:
+ base: 0
+ per_level: 0
+ cap: 0
+ fail: combine_ash
+ output_qty: 1
+ ticks_per_cycle: 1
+`
+
+const fletchSuccessYAML = `id: test_fletch_out
+name: test fletch out
+stackable: false
+value: 5
+craft:
+ - type: fletching
+ ingredients:
+ - items:
+ - fletch_part
+ quantity: 1
+ byproducts:
+ - fletch_scrap
+ success:
+ base: 1.0
+ per_level: 0
+ cap: 1.0
+ xp: 30
+ output_qty: 1
+ ticks_per_cycle: 1
+`
+
+const fletchFailYAML = `id: test_fletch_fail_out
+name: test fletch fail out
+stackable: false
+value: 5
+craft:
+ - type: fletching
+ ingredients:
+ - items:
+ - fletch_fail_part
+ quantity: 1
+ success:
+ base: 0
+ per_level: 0
+ cap: 0
+ fail: fletch_ash
+ output_qty: 1
+ ticks_per_cycle: 1
+`
+
+func seedCraftFieldItems(t *testing.T, dir string) {
+ t.Helper()
+ for _, id := range []string{
+ "combine_part", "combine_scrap", "combine_fail_part", "combine_ash",
+ "fletch_part", "fletch_scrap", "fletch_fail_part", "fletch_ash",
+ } {
+ writeDataFile(t, dir, "items", id+".yaml", "id: "+id+"\nname: "+id+"\nstackable: false\nvalue: 1\n")
+ }
+ writeDataFile(t, dir, "items", "test_combine_out.yaml", combineSuccessYAML)
+ writeDataFile(t, dir, "items", "test_combine_fail_out.yaml", combineFailYAML)
+ writeDataFile(t, dir, "items", "test_fletch_out.yaml", fletchSuccessYAML)
+ writeDataFile(t, dir, "items", "test_fletch_fail_out.yaml", fletchFailYAML)
+}
+
+func TestCombineHonorsSuccessXPAndByproducts(t *testing.T) {
+ dir := t.TempDir()
+ seedWorld(t, dir)
+ seedCraftFieldItems(t, dir)
+ g := newHardeningGame(t, dir)
+
+ g.RunOnOwner(func() {
+ p := player.New("alice")
+ p.RoomID = 1
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "combine_part", Quantity: 1})
+
+ sess := &net.Session{Conn: newChanConn(), Player: p}
+ sess.SetState(net.StateGame)
+
+ def := mustLoadItem(t, g, "test_combine_out")
+ g.startCombine(sess, p, def, def.FirstCraft(), 1)
+ if p.Action == nil {
+ t.Fatal("combine did not start")
+ }
+ g.advanceCombine(sess, p) // phase 0 -> 1
+ g.advanceCombine(sess, p) // phase 1 -> 2
+ g.advanceCombine(sess, p) // phase 2: consume + output
+
+ if got := p.CountItem("test_combine_out"); got != 1 {
+ t.Fatalf("combine output = %d, want 1", got)
+ }
+ if got := p.CountItem("combine_scrap"); got != 1 {
+ t.Errorf("combine byproduct = %d, want 1", got)
+ }
+ if got := p.CountItem("combine_part"); got != 0 {
+ t.Errorf("combine ingredient = %d, want 0 (consumed)", got)
+ }
+ if got := p.Skills[player.Crafting]; got != 25 {
+ t.Errorf("combine xp = %d, want 25 on crafting", got)
+ }
+ if _, ok := p.Skills[player.SkillName("combine")]; ok {
+ t.Error("combine recipes must not persist a phantom 'combine' skill")
+ }
+ })
+}
+
+func TestCombineCreatesFailOnFailure(t *testing.T) {
+ dir := t.TempDir()
+ seedWorld(t, dir)
+ seedCraftFieldItems(t, dir)
+ g := newHardeningGame(t, dir)
+
+ g.RunOnOwner(func() {
+ p := player.New("alice")
+ p.RoomID = 1
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "combine_fail_part", Quantity: 1})
+
+ sess := &net.Session{Conn: newChanConn(), Player: p}
+ sess.SetState(net.StateGame)
+
+ def := mustLoadItem(t, g, "test_combine_fail_out")
+ g.startCombine(sess, p, def, def.FirstCraft(), 1)
+ g.advanceCombine(sess, p)
+ g.advanceCombine(sess, p)
+ g.advanceCombine(sess, p)
+
+ if got := p.CountItem("test_combine_fail_out"); got != 0 {
+ t.Errorf("combine output = %d, want 0 on a guaranteed failure", got)
+ }
+ if got := p.CountItem("combine_ash"); got != 1 {
+ t.Errorf("combine fail product = %d, want 1", got)
+ }
+ if got := p.CountItem("combine_fail_part"); got != 0 {
+ t.Errorf("combine ingredient = %d, want 0 (consumed)", got)
+ }
+ })
+}
+
+func TestFletchHonorsSuccessXPAndByproducts(t *testing.T) {
+ dir := t.TempDir()
+ seedWorld(t, dir)
+ seedCraftFieldItems(t, dir)
+ g := newHardeningGame(t, dir)
+
+ g.RunOnOwner(func() {
+ p := player.New("alice")
+ p.RoomID = 1
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "fletch_part", Quantity: 1})
+
+ sess := &net.Session{Conn: newChanConn(), Player: p}
+ sess.SetState(net.StateGame)
+
+ def := mustLoadItem(t, g, "test_fletch_out")
+ g.startFletchAction(sess, p, def, 1)
+ if p.BackgroundAction == nil {
+ t.Fatal("fletch did not start")
+ }
+ g.advanceFletch(sess, p) // phase 0 -> 1
+ g.advanceFletch(sess, p) // phase 1: consume + output
+
+ if got := p.CountItem("test_fletch_out"); got != 1 {
+ t.Fatalf("fletch output = %d, want 1", got)
+ }
+ if got := p.CountItem("fletch_scrap"); got != 1 {
+ t.Errorf("fletch byproduct = %d, want 1", got)
+ }
+ if got := p.CountItem("fletch_part"); got != 0 {
+ t.Errorf("fletch ingredient = %d, want 0 (consumed)", got)
+ }
+ if got := p.Skills[player.Fletching]; got != 30 {
+ t.Errorf("fletch xp = %d, want 30", got)
+ }
+ })
+}
+
+func TestFletchCreatesFailOnFailure(t *testing.T) {
+ dir := t.TempDir()
+ seedWorld(t, dir)
+ seedCraftFieldItems(t, dir)
+ g := newHardeningGame(t, dir)
+
+ g.RunOnOwner(func() {
+ p := player.New("alice")
+ p.RoomID = 1
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "fletch_fail_part", Quantity: 1})
+
+ sess := &net.Session{Conn: newChanConn(), Player: p}
+ sess.SetState(net.StateGame)
+
+ def := mustLoadItem(t, g, "test_fletch_fail_out")
+ g.startFletchAction(sess, p, def, 1)
+ g.advanceFletch(sess, p)
+ g.advanceFletch(sess, p)
+
+ if got := p.CountItem("test_fletch_fail_out"); got != 0 {
+ t.Errorf("fletch output = %d, want 0 on a guaranteed failure", got)
+ }
+ if got := p.CountItem("fletch_ash"); got != 1 {
+ t.Errorf("fletch fail product = %d, want 1", got)
+ }
+ if got := p.CountItem("fletch_fail_part"); got != 0 {
+ t.Errorf("fletch ingredient = %d, want 0 (consumed)", got)
+ }
+ })
+}
+
+func mustLoadItem(t *testing.T, g *Game, id string) *item.ItemDef {
+ t.Helper()
+ def, err := g.ItemStore.Load(id)
+ if err != nil {
+ t.Fatalf("load item %s: %v", id, err)
+ }
+ return def
+}