b451f83174
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>
161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
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!;
|
||
}
|
||
}
|