using Theriapolis.Core.Items; namespace Theriapolis.Core.Rules.Stats; /// /// Pure computed values derived from a 's ability /// scores, equipped items, conditions, and encumbrance state. Recomputed on /// demand — nothing here mutates the character. UI panels and the combat /// resolver call these helpers to surface the current AC, speed, etc. /// /// Phase 5 M3 ships the AC, Speed, and CarryCap formulas plus the /// encumbrance band. Class/feature-driven AC bonuses (Bovid Herd Wall +1 /// adjacent ally, Feral Unarmored Defense, etc.) are layered on at combat /// resolution time, not here — those need positional context. /// public static class DerivedStats { public enum EncumbranceBand : byte { Light = 0, // ≤ soft threshold — no penalty Heavy = 1, // > soft threshold — speed -10 ft. Over = 2, // > hard threshold — speed halved + disadvantage on STR/DEX/CON } /// /// Armor Class from base 10 (or unarmored-defense pseudo-armor) plus DEX /// (capped by armor type) plus shield. Out-of-combat baseline; does not /// include feature/positional bonuses. /// public static int ArmorClass(Theriapolis.Core.Rules.Character.Character c) { int dexMod = c.Abilities.ModFor(AbilityId.DEX); int ac; var body = c.Inventory.GetEquipped(EquipSlot.Body); if (body is null) { // Unarmored: 10 + DEX. Feral's "Unarmored Defense" (10 + DEX + CON) // ships at M6 with the rest of class-feature combat effects. ac = 10 + dexMod; } else { int dexAllowed = body.Def.AcMaxDex < 0 ? dexMod : Math.Min(dexMod, body.Def.AcMaxDex); ac = body.Def.AcBase + dexAllowed; } var off = c.Inventory.GetEquipped(EquipSlot.OffHand); if (off is not null && string.Equals(off.Def.Kind, "shield", StringComparison.OrdinalIgnoreCase)) ac += off.Def.AcBase; // Phase 5 M6: class features may replace the unarmored baseline // (Feral Unarmored Defense). Per-encounter combat-time bonuses // (Sentinel Stance) are added at attack-resolution time, not here. ac = Theriapolis.Core.Rules.Combat.FeatureProcessor.ApplyAcFeatures(c, ac); return Math.Clamp(ac, C.AC_FLOOR, C.AC_CEILING); } /// Initiative = DEX modifier. Class features that add to it (Feral L7) layered later. public static int Initiative(Theriapolis.Core.Rules.Character.Character c) => c.Abilities.ModFor(AbilityId.DEX); /// /// Movement speed in feet per turn. Base from species, modified by /// encumbrance band and (later) by conditions and class features. /// public static int SpeedFt(Theriapolis.Core.Rules.Character.Character c) { int speed = c.Species.BaseSpeedFt; switch (Encumbrance(c)) { case EncumbranceBand.Heavy: speed -= 10; break; case EncumbranceBand.Over: speed /= 2; break; } return Math.Max(0, speed); } /// /// Carrying capacity in pounds. Base = STR × 15, scaled by size category /// (Small ½×, Large 2×, etc. per equipment.md). /// public static float CarryCapacityLb(Theriapolis.Core.Rules.Character.Character c) { return c.Abilities.STR * 15f * c.Size.CarryCapacityMult(); } /// Current encumbrance band given inventory weight vs. carry capacity. public static EncumbranceBand Encumbrance(Theriapolis.Core.Rules.Character.Character c) { float cap = CarryCapacityLb(c); if (cap <= 0f) return EncumbranceBand.Over; float w = c.Inventory.TotalWeightLb; float ratio = w / cap; if (ratio > C.ENCUMBRANCE_HARD_MULT) return EncumbranceBand.Over; if (ratio > C.ENCUMBRANCE_SOFT_MULT) return EncumbranceBand.Heavy; return EncumbranceBand.Light; } /// /// Speed multiplier applied to . /// 1.0 = normal walking pace; smaller = encumbered drag. Light = 1.0, /// Heavy ≈ 0.66, Over = 0.5. /// public static float TacticalSpeedMult(Theriapolis.Core.Rules.Character.Character c) => Encumbrance(c) switch { EncumbranceBand.Light => 1.0f, EncumbranceBand.Heavy => 0.66f, EncumbranceBand.Over => 0.50f, _ => 1.0f, }; }