using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; namespace Theriapolis.Core.Dungeons; /// /// Phase 7 M2 — clade-responsive dungeon movement cost. Per procgen.md /// 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; only movement budget. The /// caller ( or equivalent) /// looks up the room the actor is currently in and consults /// . /// /// Hybrid PCs use their dominant lineage's clade-implied size /// for the lookup — matches the Phase 6.5 hybrid passing / presenting-clade /// contract. A Wolf-Folk × Hare-Folk hybrid with DominantParent: Sire /// reads as Wolf-Folk (MediumLarge); with DominantParent: Dam 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 /// public static class ClademorphicMovement { /// /// 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 values default to 1.0 /// (no penalty). /// 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, }; } /// /// Convenience wrapper that resolves a character's effective size for /// the lookup (handles Phase 6.5 hybrid dominant-lineage rules). /// public static float GetCostMultiplier(Character character, string builtBy) { if (character is null) return 1.0f; var effectiveSize = EffectiveSize(character); return GetCostMultiplier(effectiveSize, builtBy); } /// /// Resolve the size category that drives the clade-responsive lookup. /// For purebred PCs, this is just . 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. /// 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 character.Size already returns. Documented here so // future agents don't replace this with a parent-species lookup // and break the contract. return character.Size; } }