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,
|
|||
|
|
};
|
|||
|
|
}
|