using Theriapolis.Core.Data; using Theriapolis.Core.Items; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Rules; /// /// Verifies that every class's starting_kit in classes.json /// references real items, that auto-equipped items land in their declared /// slot, and that fresh characters arrive armed and armoured. /// public sealed class StartingKitTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); [Fact] public void EveryClass_HasNonEmptyStartingKit() { foreach (var c in _content.Classes.Values) Assert.NotEmpty(c.StartingKit); } [Fact] public void EveryStartingKitItem_ReferencesARealItem() { foreach (var c in _content.Classes.Values) foreach (var entry in c.StartingKit) Assert.True( _content.Items.ContainsKey(entry.ItemId), $"Class '{c.Id}' starting_kit references unknown item '{entry.ItemId}'"); } [Fact] public void EveryAutoEquipEntry_HasValidEquipSlot() { foreach (var c in _content.Classes.Values) foreach (var entry in c.StartingKit) { if (!entry.AutoEquip) continue; Assert.False( string.IsNullOrEmpty(entry.EquipSlot), $"Class '{c.Id}' auto-equip entry '{entry.ItemId}' has empty equip_slot"); Assert.NotNull(EquipSlotExtensions.FromJson(entry.EquipSlot)); } } [Theory] [InlineData("fangsworn", "rend_sword", "chain_shirt", "buckler")] [InlineData("bulwark", "hoof_club", "chain_mail", "standard_shield")] [InlineData("covenant_keeper", "rend_sword", "chain_shirt", "standard_shield")] [InlineData("claw_wright", "hoof_club", "studded_leather", "buckler")] public void StartingKit_AppliedAndEquipped_FullKit( string classId, string mainHand, string body, string offHand) { var c = BuildWithKit(classId); AssertEquipped(c, EquipSlot.MainHand, mainHand); AssertEquipped(c, EquipSlot.Body, body); AssertEquipped(c, EquipSlot.OffHand, offHand); } [Theory] [InlineData("feral", "paw_axe", "hide_vest")] [InlineData("shadow_pelt", "thorn_blade", "studded_leather")] [InlineData("scent_broker", "fang_knife", "leather_harness")] [InlineData("muzzle_speaker", "fang_knife", "studded_leather")] public void StartingKit_AppliedAndEquipped_NoShield(string classId, string mainHand, string body) { var c = BuildWithKit(classId); AssertEquipped(c, EquipSlot.MainHand, mainHand); AssertEquipped(c, EquipSlot.Body, body); Assert.Null(c.Inventory.GetEquipped(EquipSlot.OffHand)); } [Fact] public void StartingKit_Skipped_WhenItemsTableNotPassed() { var c = MakeBuilder("fangsworn").Build(); // no items dict → no kit applied Assert.Empty(c.Inventory.Items); } [Fact] public void StartingKit_ProducesPositiveAcOverUnarmoredBaseline() { var c = BuildWithKit("fangsworn"); int armored = DerivedStats.ArmorClass(c); Assert.True(armored >= 14, $"Fangsworn starting kit should produce AC ≥ 14 (chain shirt + buckler), got {armored}"); } private Character BuildWithKit(string classId) => MakeBuilder(classId).Build(_content.Items); private CharacterBuilder MakeBuilder(string classId) { var classDef = _content.Classes[classId]; var b = new CharacterBuilder { Clade = _content.Clades["canidae"], Species = _content.Species["wolf"], ClassDef = classDef, Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8), Name = "KitTest", }; // Pick the right number of skills for this class. int n = classDef.SkillsChoose; foreach (var raw in classDef.SkillOptions) { if (b.ChosenClassSkills.Count >= n) break; try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { } } return b; } private static void AssertEquipped(Character c, EquipSlot slot, string expectedItemId) { var inst = c.Inventory.GetEquipped(slot); Assert.NotNull(inst); Assert.Equal(expectedItemId, inst!.Def.Id); } }