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>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user