package game // Regression: combine recipes declare type "combine", which is an action and // not a skill. Resolving the recipe type straight to a player.SkillName made // p.Level read a map miss (level 1) for the success roll and made awardSkillXP // persist a phantom "combine" skill entry. import ( "path/filepath" "testing" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) const correctCombineYAML = `id: correct_combine_out name: correct combine out stackable: false value: 5 craft: - type: combine ingredients: - items: - correct_combine_part quantity: 1 xp: 7 output_qty: 1 ticks_per_cycle: 1 ` func TestCombineSkillResolvesARealSkill(t *testing.T) { for _, tc := range []struct { name string typ string want player.SkillName }{ {"combine is an action, not a skill", "combine", player.Crafting}, {"empty type defaults to combine", "", player.Crafting}, {"a declared real skill is honored", "smithing", player.Smithing}, } { t.Run(tc.name, func(t *testing.T) { got := combineSkill(&item.CraftDef{Type: tc.typ}) if got != tc.want { t.Fatalf("combineSkill(type %q) = %q, want %q", tc.typ, got, tc.want) } if !player.ValidSkill(got) { t.Fatalf("combineSkill(type %q) resolved to unknown skill %q", tc.typ, got) } }) } } func TestCombineXPGoesToCraftingNotAPhantomSkill(t *testing.T) { h := newHarness(t) h.seedAccount("alice") p := h.seedCharacter("alice", "alice") writeSeedFile(t, filepath.Join(h.dir, "items", "correct_combine_out.yaml"), []byte(correctCombineYAML)) writeSeedFile(t, filepath.Join(h.dir, "items", "correct_combine_part.yaml"), []byte("id: correct_combine_part\nname: correct combine part\nstackable: false\nvalue: 1\n")) conn := newChanConn() sess := &net.Session{Conn: conn, Player: p} sess.SetState(net.StateGame) startCrafting := p.Skills[player.Crafting] h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) p.SetInvSlot(0, &player.InventorySlot{ItemID: "correct_combine_part", Quantity: 1}) def := mustLoadItem(t, h.g, "correct_combine_out") h.g.startCombine(sess, p, def, def.FirstCraft(), 1) if p.Action == nil { t.Fatal("combine did not start") } h.g.advanceCombine(sess, p) // phase 0 -> 1 h.g.advanceCombine(sess, p) // phase 1 -> 2 h.g.advanceCombine(sess, p) // phase 2: consume + output if got := p.CountItem("correct_combine_out"); got != 1 { t.Fatalf("combine output = %d, want 1", got) } if got := p.Skills[player.Crafting]; got != startCrafting+7 { t.Errorf("crafting xp = %d, want %d", got, startCrafting+7) } if got, ok := p.Skills[player.SkillName("combine")]; ok { t.Errorf("combine recipes persist a phantom skill: combine = %d", got) } }) saved, err := h.g.AccountStore.LoadCharacter("alice") if err != nil { t.Fatalf("load character: %v", err) } if got, ok := saved.Skills[player.SkillName("combine")]; ok { t.Errorf("phantom combine skill was persisted: combine = %d", got) } if got := saved.Skills[player.Crafting]; got != startCrafting+7 { t.Errorf("persisted crafting xp = %d, want %d", got, startCrafting+7) } }