Files
TheriapolisV3/Theriapolis.Core/Rules/Stats/DerivedStats.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

113 lines
4.5 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.Items;
namespace Theriapolis.Core.Rules.Stats;
/// <summary>
/// Pure computed values derived from a <see cref="Character"/>'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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>Initiative = DEX modifier. Class features that add to it (Feral L7) layered later.</summary>
public static int Initiative(Theriapolis.Core.Rules.Character.Character c) => c.Abilities.ModFor(AbilityId.DEX);
/// <summary>
/// Movement speed in feet per turn. Base from species, modified by
/// encumbrance band and (later) by conditions and class features.
/// </summary>
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);
}
/// <summary>
/// Carrying capacity in pounds. Base = STR × 15, scaled by size category
/// (Small ½×, Large 2×, etc. per equipment.md).
/// </summary>
public static float CarryCapacityLb(Theriapolis.Core.Rules.Character.Character c)
{
return c.Abilities.STR * 15f * c.Size.CarryCapacityMult();
}
/// <summary>Current encumbrance band given inventory weight vs. carry capacity.</summary>
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;
}
/// <summary>
/// Speed multiplier applied to <see cref="C.TACTICAL_PLAYER_PX_PER_SEC"/>.
/// 1.0 = normal walking pace; smaller = encumbered drag. Light = 1.0,
/// Heavy ≈ 0.66, Over = 0.5.
/// </summary>
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,
};
}