b451f83174
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>
113 lines
4.5 KiB
C#
113 lines
4.5 KiB
C#
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,
|
||
};
|
||
}
|