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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
namespace Theriapolis.Core.Rules.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// The six d20-adjacent ability scores. Score range is 1..30; level-1
|
||||
/// characters typically end up in 8..18 after clade/species mods.
|
||||
/// </summary>
|
||||
public readonly struct AbilityScores : IEquatable<AbilityScores>
|
||||
{
|
||||
public readonly byte STR;
|
||||
public readonly byte DEX;
|
||||
public readonly byte CON;
|
||||
public readonly byte INT;
|
||||
public readonly byte WIS;
|
||||
public readonly byte CHA;
|
||||
|
||||
public AbilityScores(int str, int dex, int con, int @int, int wis, int cha)
|
||||
{
|
||||
STR = ClampScore(str);
|
||||
DEX = ClampScore(dex);
|
||||
CON = ClampScore(con);
|
||||
INT = ClampScore(@int);
|
||||
WIS = ClampScore(wis);
|
||||
CHA = ClampScore(cha);
|
||||
}
|
||||
|
||||
/// <summary>Standard d20 ability modifier: floor((score - 10) / 2).</summary>
|
||||
public static int Mod(int score)
|
||||
{
|
||||
// C# integer division truncates toward zero; for negatives we need
|
||||
// floor toward -infinity to match d20 behaviour (score 9 → -1 not 0).
|
||||
int diff = score - 10;
|
||||
return diff >= 0 ? diff / 2 : (diff - 1) / 2;
|
||||
}
|
||||
|
||||
public int ModFor(AbilityId id) => Mod(Get(id));
|
||||
|
||||
public byte Get(AbilityId id) => id switch
|
||||
{
|
||||
AbilityId.STR => STR,
|
||||
AbilityId.DEX => DEX,
|
||||
AbilityId.CON => CON,
|
||||
AbilityId.INT => INT,
|
||||
AbilityId.WIS => WIS,
|
||||
AbilityId.CHA => CHA,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(id)),
|
||||
};
|
||||
|
||||
/// <summary>Returns a new score block with <paramref name="id"/> replaced.</summary>
|
||||
public AbilityScores With(AbilityId id, int newScore) => id switch
|
||||
{
|
||||
AbilityId.STR => new AbilityScores(newScore, DEX, CON, INT, WIS, CHA),
|
||||
AbilityId.DEX => new AbilityScores(STR, newScore, CON, INT, WIS, CHA),
|
||||
AbilityId.CON => new AbilityScores(STR, DEX, newScore, INT, WIS, CHA),
|
||||
AbilityId.INT => new AbilityScores(STR, DEX, CON, newScore, WIS, CHA),
|
||||
AbilityId.WIS => new AbilityScores(STR, DEX, CON, INT, newScore, CHA),
|
||||
AbilityId.CHA => new AbilityScores(STR, DEX, CON, INT, WIS, newScore),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(id)),
|
||||
};
|
||||
|
||||
/// <summary>Returns a new block with each ability incremented by the supplied dictionary.</summary>
|
||||
public AbilityScores Plus(IReadOnlyDictionary<AbilityId, int> mods)
|
||||
{
|
||||
var s = this;
|
||||
foreach (var kv in mods) s = s.With(kv.Key, s.Get(kv.Key) + kv.Value);
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>The standard array, in descending order: 15, 14, 13, 12, 10, 8.</summary>
|
||||
public static int[] StandardArray => new[] { 15, 14, 13, 12, 10, 8 };
|
||||
|
||||
private static byte ClampScore(int v) => (byte)Math.Clamp(v, 1, 30);
|
||||
|
||||
public bool Equals(AbilityScores o) =>
|
||||
STR == o.STR && DEX == o.DEX && CON == o.CON && INT == o.INT && WIS == o.WIS && CHA == o.CHA;
|
||||
public override bool Equals(object? o) => o is AbilityScores a && Equals(a);
|
||||
public override int GetHashCode() => HashCode.Combine(STR, DEX, CON, INT, WIS, CHA);
|
||||
public override string ToString() => $"STR {STR} DEX {DEX} CON {CON} INT {INT} WIS {WIS} CHA {CHA}";
|
||||
}
|
||||
|
||||
public enum AbilityId : byte
|
||||
{
|
||||
STR = 0,
|
||||
DEX = 1,
|
||||
CON = 2,
|
||||
INT = 3,
|
||||
WIS = 4,
|
||||
CHA = 5,
|
||||
}
|
||||
Reference in New Issue
Block a user