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