using Theriapolis.Core.Data; using Theriapolis.Core.Items; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Rules; public sealed class DerivedStatsTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); [Fact] public void ArmorClass_Unarmored_Is10PlusDexMod() { var c = MakeCharacter(dex: 14); // DEX 15 after wolf+canid mods (DEX +0 species, +0 clade) → final DEX 14, +2 mod // Wolf-Folk: STR+1, no DEX mod from species; canid: CON+1, WIS+1. // Base DEX 14 → final 14 → mod +2. // Unarmored: 10 + 2 = 12. Assert.Equal(12, DerivedStats.ArmorClass(c)); } [Fact] public void ArmorClass_LightArmor_AddsBaseAndDex() { var c = MakeCharacter(dex: 14); var hide = c.Inventory.Add(_content.Items["hide_vest"]); c.Inventory.TryEquip(hide, EquipSlot.Body, out _); // Hide vest base 11, max DEX -1 (unlimited), DEX mod +2 → AC 13. Assert.Equal(13, DerivedStats.ArmorClass(c)); } [Fact] public void ArmorClass_MediumArmor_CapsDex() { var c = MakeCharacter(dex: 18); // base 18 → final 18 → mod +4 var chain = c.Inventory.Add(_content.Items["chain_shirt"]); c.Inventory.TryEquip(chain, EquipSlot.Body, out _); // Chain shirt base 13, max DEX 2 → effective DEX bonus 2 → AC 15. Assert.Equal(15, DerivedStats.ArmorClass(c)); } [Fact] public void ArmorClass_HeavyArmor_IgnoresDex() { var c = MakeCharacter(dex: 18); var mail = c.Inventory.Add(_content.Items["chain_mail"]); c.Inventory.TryEquip(mail, EquipSlot.Body, out _); // Chain mail base 16, max DEX 0 → AC 16. Assert.Equal(16, DerivedStats.ArmorClass(c)); } [Fact] public void ArmorClass_ShieldAdds() { var c = MakeCharacter(dex: 14); var chain = c.Inventory.Add(_content.Items["chain_shirt"]); c.Inventory.TryEquip(chain, EquipSlot.Body, out _); var shield = c.Inventory.Add(_content.Items["standard_shield"]); c.Inventory.TryEquip(shield, EquipSlot.OffHand, out _); // 13 + min(2, 2) + 2 (shield) = 17. Assert.Equal(17, DerivedStats.ArmorClass(c)); } [Fact] public void Speed_BaseMatchesSpecies() { var c = MakeCharacter(); Assert.Equal(c.Species.BaseSpeedFt, DerivedStats.SpeedFt(c)); } [Fact] public void CarryCapacity_StrTimes15TimesSizeMult() { var c = MakeCharacter(str: 14); // STR 15 after wolf+1 // Wolf-Folk is medium_large → mult = 1.0 Assert.Equal(15f * 15f * 1.0f, DerivedStats.CarryCapacityLb(c)); } [Fact] public void Encumbrance_LightWhenWellUnderCap() { var c = MakeCharacter(str: 14); c.Inventory.Add(_content.Items["fang_knife"]); // 0.5 lb Assert.Equal(DerivedStats.EncumbranceBand.Light, DerivedStats.Encumbrance(c)); Assert.Equal(1.0f, DerivedStats.TacticalSpeedMult(c)); } [Fact] public void Encumbrance_HeavyWhenOverSoftThreshold() { var c = MakeCharacter(str: 8); // STR 9 after wolf+1, cap = 9 * 15 = 135 lb for (int i = 0; i < 60; i++) c.Inventory.Add(_content.Items["chain_mail"]); // 60 * 40 lb = 2400 lb var enc = DerivedStats.Encumbrance(c); Assert.Equal(DerivedStats.EncumbranceBand.Over, enc); Assert.Equal(0.5f, DerivedStats.TacticalSpeedMult(c)); } [Fact] public void Speed_DropsWhenEncumbered() { var c = MakeCharacter(str: 8); int baseSpeed = DerivedStats.SpeedFt(c); // Pile on chain mail to push past the hard threshold (1.5x cap). for (int i = 0; i < 60; i++) c.Inventory.Add(_content.Items["chain_mail"]); int encSpeed = DerivedStats.SpeedFt(c); Assert.True(encSpeed < baseSpeed, $"Encumbered speed ({encSpeed}) should be less than base ({baseSpeed})"); } [Fact] public void Initiative_EqualsDexMod() { var c = MakeCharacter(dex: 18); // DEX 18 → mod +4 (no DEX mod from wolf-folk or canid clade) Assert.Equal(4, DerivedStats.Initiative(c)); } private Character MakeCharacter(int str = 10, int dex = 10, int con = 10, int @int = 10, int wis = 10, int cha = 10) { var b = new CharacterBuilder { Clade = _content.Clades["canidae"], Species = _content.Species["wolf"], ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(str, dex, con, @int, wis, cha), Name = "Test", }; b.ChosenClassSkills.Add(SkillId.Athletics); b.ChosenClassSkills.Add(SkillId.Intimidation); return b.Build(); // no starting kit — tests build inventory explicitly } }