namespace Theriapolis.Core.Rules.Stats; /// /// The six d20-adjacent ability scores. Score range is 1..30; level-1 /// characters typically end up in 8..18 after clade/species mods. /// public readonly struct AbilityScores : IEquatable { 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); } /// Standard d20 ability modifier: floor((score - 10) / 2). 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)), }; /// Returns a new score block with replaced. 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)), }; /// Returns a new block with each ability incremented by the supplied dictionary. public AbilityScores Plus(IReadOnlyDictionary mods) { var s = this; foreach (var kv in mods) s = s.With(kv.Key, s.Get(kv.Key) + kv.Value); return s; } /// The standard array, in descending order: 15, 14, 13, 12, 10, 8. 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, }