Files
TheriapolisV3/Theriapolis.Core/Dungeons/ClademorphicMovement.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

107 lines
4.9 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.Rules.Character;
using Theriapolis.Core.Rules.Stats;
namespace Theriapolis.Core.Dungeons;
/// <summary>
/// Phase 7 M2 — clade-responsive dungeon movement cost. Per <c>procgen.md</c>
/// Layer 5 final paragraph and Phase 7 plan §5.4: a Large PC squeezing
/// through a Mustelid tunnel takes 2× movement points per tile; a Small
/// PC in an Ursid hall is exposed (×1.5); etc.
///
/// Cost multiplier applies to tactical-tile movement budget per turn —
/// combat reach + LOS unchanged; <em>only</em> movement budget. The
/// caller (<see cref="Rules.Combat.TacticalMovementRules"/> or equivalent)
/// looks up the room the actor is currently in and consults
/// <see cref="GetCostMultiplier"/>.
///
/// Hybrid PCs use their <em>dominant lineage</em>'s clade-implied size
/// for the lookup — matches the Phase 6.5 hybrid passing / presenting-clade
/// contract. A Wolf-Folk × Hare-Folk hybrid with <c>DominantParent: Sire</c>
/// reads as Wolf-Folk (MediumLarge); with <c>DominantParent: Dam</c> reads
/// as Hare-Folk (Medium). Outside dungeons the multiplier is always 1.0.
///
/// Reference table (Phase 7 plan §5.4):
/// Player size | Built by Mustelid | Ursid | Cervid | Bovid | Imperium/None
/// Small | 1.0 | 1.5 | 1.0 | 1.2 | 1.0
/// Medium | 1.2 | 1.0 | 1.0 | 1.0 | 1.0
/// MediumLarge | 1.5 | 1.0 | 1.0 | 1.0 | 1.0
/// Large | 2.0 | 1.0 | 1.2 | 1.0 | 1.0
/// </summary>
public static class ClademorphicMovement
{
/// <summary>
/// Multiplier on movement-cost-per-tile for a player of the given size
/// in a room built by the given clade. Returns 1.0 when no mismatch
/// applies. Unknown <paramref name="builtBy"/> values default to 1.0
/// (no penalty).
/// </summary>
public static float GetCostMultiplier(SizeCategory playerSize, string builtBy)
{
if (string.IsNullOrEmpty(builtBy)) return 1.0f;
// Normalise — JSON ships lowercase tags.
return builtBy.ToLowerInvariant() switch
{
"mustelid" => playerSize switch
{
SizeCategory.Small => 1.0f,
SizeCategory.Medium => C.MOVE_COST_MISMATCH_LIGHT, // 1.2 — slight squeeze
SizeCategory.MediumLarge => C.MOVE_COST_MISMATCH_MED, // 1.5
SizeCategory.Large => C.MOVE_COST_MISMATCH_HEAVY, // 2.0 — squeezing
_ => 1.0f,
},
"ursid" => playerSize switch
{
SizeCategory.Small => C.MOVE_COST_MISMATCH_MED, // exposed in cavernous halls
_ => 1.0f,
},
"cervid" => playerSize switch
{
SizeCategory.Large => C.MOVE_COST_MISMATCH_LIGHT, // antler clearance
_ => 1.0f,
},
"bovid" => playerSize switch
{
SizeCategory.Small => C.MOVE_COST_MISMATCH_LIGHT,
_ => 1.0f,
},
// Canid / Felid / Leporid / Imperium / "none" / unknown:
_ => 1.0f,
};
}
/// <summary>
/// Convenience wrapper that resolves a character's effective size for
/// the lookup (handles Phase 6.5 hybrid dominant-lineage rules).
/// </summary>
public static float GetCostMultiplier(Character character, string builtBy)
{
if (character is null) return 1.0f;
var effectiveSize = EffectiveSize(character);
return GetCostMultiplier(effectiveSize, builtBy);
}
/// <summary>
/// Resolve the size category that drives the clade-responsive lookup.
/// For purebred PCs, this is just <see cref="Character.Size"/>. For
/// hybrid PCs, it's the size implied by the dominant-lineage species
/// — and we expose this as a separate helper so callers (e.g. NPC
/// mechanics that *also* need the presenting size) can reuse it.
/// </summary>
public static SizeCategory EffectiveSize(Character character)
{
if (character is null) throw new System.ArgumentNullException(nameof(character));
if (!character.IsHybrid) return character.Size;
// Hybrid: pick the size implied by the dominant parent's species.
// The Hybrid record carries the species name only (string); the
// species-to-size mapping lives on Character.Species (the
// *presenting* species set at character creation per the dominant
// lineage). So for a hybrid PC the simplest (and load-bearing-
// correct) answer is the presenting species — which is exactly
// what <c>character.Size</c> already returns. Documented here so
// future agents don't replace this with a parent-species lookup
// and break the contract.
return character.Size;
}
}