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,100 @@
|
||||
using Theriapolis.Core.Rules.Stats;
|
||||
|
||||
namespace Theriapolis.Core.Rules.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed damage expression: <c>NdM+B</c> where N = dice count, M = die
|
||||
/// sides, B = flat modifier (can be negative). Examples: "1d6", "2d8+2",
|
||||
/// "1d4-1". <see cref="Roll"/> takes a function that returns 1..M for each
|
||||
/// dice and aggregates with the flat modifier.
|
||||
/// </summary>
|
||||
public sealed record DamageRoll(int DiceCount, int DiceSides, int FlatMod, DamageType DamageType)
|
||||
{
|
||||
/// <summary>
|
||||
/// Roll the damage dice. <paramref name="rollDie"/> takes the die size
|
||||
/// (e.g. 6) and returns 1..size. On crit, dice double per d20 rules
|
||||
/// (the flat modifier does NOT double).
|
||||
/// </summary>
|
||||
public int Roll(System.Func<int, int> 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);
|
||||
}
|
||||
|
||||
/// <summary>Theoretical maximum (every die rolls its top face) + flat mod.</summary>
|
||||
public int Max(bool isCrit = false)
|
||||
{
|
||||
int dice = isCrit ? DiceCount * 2 : DiceCount;
|
||||
return dice * DiceSides + FlatMod;
|
||||
}
|
||||
|
||||
/// <summary>Theoretical minimum (every die rolls 1) + flat mod, clamped to 0.</summary>
|
||||
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()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an expression like "1d6", "2d8+2", "1d4-1", "5" (flat 5),
|
||||
/// or "0" (no damage). Whitespace is allowed. Throws on malformed input.
|
||||
/// </summary>
|
||||
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 "<count>" "d" "<sides>[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user