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, 0) 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, 0) 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 }