using Theriapolis.Core; using Theriapolis.Core.Data; using Theriapolis.Core.Dungeons; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Dungeons; /// /// Phase 7 M2 — clade-responsive movement multiplier tests. Verifies the /// table from Phase 7 plan §5.4 and that hybrid PCs use the dominant- /// lineage's presenting size for the lookup. /// public sealed class ClademorphicMovementTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); // ── Plan §5.4 table values ──────────────────────────────────────────── [Fact] public void LargePc_InMustelidTunnel_PaysHeavyMultiplier() { Assert.Equal(C.MOVE_COST_MISMATCH_HEAVY, ClademorphicMovement.GetCostMultiplier(SizeCategory.Large, "mustelid")); } [Fact] public void MediumLargePc_InMustelidTunnel_PaysMediumMultiplier() { Assert.Equal(C.MOVE_COST_MISMATCH_MED, ClademorphicMovement.GetCostMultiplier(SizeCategory.MediumLarge, "mustelid")); } [Fact] public void MediumPc_InMustelidTunnel_PaysLightMultiplier() { Assert.Equal(C.MOVE_COST_MISMATCH_LIGHT, ClademorphicMovement.GetCostMultiplier(SizeCategory.Medium, "mustelid")); } [Fact] public void SmallPc_InMustelidTunnel_NoPenalty() { Assert.Equal(1.0f, ClademorphicMovement.GetCostMultiplier(SizeCategory.Small, "mustelid")); } [Fact] public void SmallPc_InUrsidHall_PaysExposedMultiplier() { Assert.Equal(C.MOVE_COST_MISMATCH_MED, ClademorphicMovement.GetCostMultiplier(SizeCategory.Small, "ursid")); } [Fact] public void LargePc_InCervidHall_PaysAntlerClearancePenalty() { Assert.Equal(C.MOVE_COST_MISMATCH_LIGHT, ClademorphicMovement.GetCostMultiplier(SizeCategory.Large, "cervid")); } [Fact] public void AnyPc_InImperiumOrNoneRoom_NoPenalty() { foreach (var size in new[] { SizeCategory.Small, SizeCategory.Medium, SizeCategory.MediumLarge, SizeCategory.Large }) { Assert.Equal(1.0f, ClademorphicMovement.GetCostMultiplier(size, "imperium")); Assert.Equal(1.0f, ClademorphicMovement.GetCostMultiplier(size, "none")); Assert.Equal(1.0f, ClademorphicMovement.GetCostMultiplier(size, "")); } } [Fact] public void UnknownBuiltBy_NoPenalty() { Assert.Equal(1.0f, ClademorphicMovement.GetCostMultiplier(SizeCategory.Large, "garbage")); } // ── Hybrid PC presenting-size lookup ───────────────────────────────── [Fact] public void HybridPc_UsesPresentingClade_ForSizeLookup() { // Build a Wolf-Folk × Hare-Folk hybrid presenting as Hare-Folk // (Dam dominant). EffectiveSize should be the presenting species' size. var pc = BuildHybrid( sireClade: "canidae", sireSpecies: "wolf", damClade: "leporidae", damSpecies: "hare", dominant: ParentLineage.Dam); // The hybrid build path picked Hare-Folk as the presenting species, // so EffectiveSize should match Character.Size (which the builder // already set to Hare-Folk's size category). Assert.Equal(pc.Size, ClademorphicMovement.EffectiveSize(pc)); // Whichever species the builder chose, the multiplier should match // its size's lookup in a Mustelid tunnel. var expected = ClademorphicMovement.GetCostMultiplier(pc.Size, "mustelid"); Assert.Equal(expected, ClademorphicMovement.GetCostMultiplier(pc, "mustelid")); } [Fact] public void NonHybridPc_UsesOwnSize_ForLookup() { var pc = BuildPurebred("canidae", "wolf"); // wolf = MediumLarge Assert.Equal(SizeCategory.MediumLarge, pc.Size); Assert.Equal(C.MOVE_COST_MISMATCH_MED, ClademorphicMovement.GetCostMultiplier(pc, "mustelid")); } // ── Helpers ────────────────────────────────────────────────────────── private Character BuildPurebred(string cladeId, string speciesId) { var b = new CharacterBuilder { Clade = _content.Clades[cladeId], Species = _content.Species[speciesId], ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 10, 12, 8), }; int n = b.ClassDef.SkillsChoose; foreach (var raw in b.ClassDef.SkillOptions) { if (b.ChosenClassSkills.Count >= n) break; try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { } } return b.Build(_content.Items); } private Character BuildHybrid( string sireClade, string sireSpecies, string damClade, string damSpecies, ParentLineage dominant) { var b = new CharacterBuilder { ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 10, 12, 8), IsHybridOrigin = true, HybridSireClade = _content.Clades[sireClade], HybridSireSpecies = _content.Species[sireSpecies], HybridDamClade = _content.Clades[damClade], HybridDamSpecies = _content.Species[damSpecies], HybridDominantParent = dominant, }; // Chosen skills don't matter for size-of-character — pick first N. int n = b.ClassDef.SkillsChoose; foreach (var raw in b.ClassDef.SkillOptions) { if (b.ChosenClassSkills.Count >= n) break; try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { } } Assert.True(b.TryBuildHybrid(_content.Items, out var character, out _)); return character!; } }