Files

134 lines
4.9 KiB
C#
Raw Permalink Normal View History

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
}
}