Files
TheriapolisV3/Theriapolis.Tests/Dungeons/ClademorphicMovementTests.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

161 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 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.
/// </summary>
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!;
}
}