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