using Theriapolis.Core.Rules.Stats; namespace Theriapolis.Core.Rules.Combat; /// /// Parsed damage expression: NdM+B where N = dice count, M = die /// sides, B = flat modifier (can be negative). Examples: "1d6", "2d8+2", /// "1d4-1". takes a function that returns 1..M for each /// dice and aggregates with the flat modifier. /// public sealed record DamageRoll(int DiceCount, int DiceSides, int FlatMod, DamageType DamageType) { /// /// Roll the damage dice. takes the die size /// (e.g. 6) and returns 1..size. On crit, dice double per d20 rules /// (the flat modifier does NOT double). /// public int Roll(System.Func rollDie, bool isCrit = false) { int diceToRoll = isCrit ? DiceCount * 2 : DiceCount; int total = FlatMod; for (int i = 0; i < diceToRoll; i++) total += rollDie(DiceSides); return System.Math.Max(0, total); } /// Theoretical maximum (every die rolls its top face) + flat mod. public int Max(bool isCrit = false) { int dice = isCrit ? DiceCount * 2 : DiceCount; return dice * DiceSides + FlatMod; } /// Theoretical minimum (every die rolls 1) + flat mod, clamped to 0. public int Min(bool isCrit = false) { int dice = isCrit ? DiceCount * 2 : DiceCount; return System.Math.Max(0, dice * 1 + FlatMod); } public override string ToString() { string mod = FlatMod == 0 ? "" : (FlatMod > 0 ? $"+{FlatMod}" : $"{FlatMod}"); return $"{DiceCount}d{DiceSides}{mod} {DamageType.ToString().ToLowerInvariant()}"; } /// /// Parses an expression like "1d6", "2d8+2", "1d4-1", "5" (flat 5), /// or "0" (no damage). Whitespace is allowed. Throws on malformed input. /// public static DamageRoll Parse(string expr, DamageType damageType) { if (string.IsNullOrWhiteSpace(expr)) throw new System.ArgumentException("Damage expression is empty", nameof(expr)); string s = expr.Replace(" ", "").ToLowerInvariant(); int dIdx = s.IndexOf('d'); if (dIdx < 0) { // No dice — pure flat (e.g. "5" or "-1"). if (!int.TryParse(s, out int flat)) throw new System.FormatException($"Cannot parse damage '{expr}' as flat int."); return new DamageRoll(0, 0, flat, damageType); } // Split into "" "d" "[modifier]" string countStr = s.Substring(0, dIdx); if (countStr.Length == 0) countStr = "1"; // "d6" → 1d6 if (!int.TryParse(countStr, out int diceCount)) throw new System.FormatException($"Bad dice count in '{expr}'"); string rest = s.Substring(dIdx + 1); int signIdx = -1; for (int i = 0; i < rest.Length; i++) { if (rest[i] == '+' || rest[i] == '-') { signIdx = i; break; } } int sides; int flatMod; if (signIdx < 0) { if (!int.TryParse(rest, out sides)) throw new System.FormatException($"Bad dice sides in '{expr}'"); flatMod = 0; } else { if (!int.TryParse(rest.Substring(0, signIdx), out sides)) throw new System.FormatException($"Bad dice sides in '{expr}'"); if (!int.TryParse(rest.Substring(signIdx), out flatMod)) throw new System.FormatException($"Bad flat mod in '{expr}'"); } if (diceCount < 0 || sides < 0) throw new System.FormatException($"Negative dice count or sides in '{expr}'"); return new DamageRoll(diceCount, sides, flatMod, damageType); } }