Stats: stat sheets, formulas, archetypes, traits, stat pools, actions, conditions and sheet panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
c6e53dedd3
66 files changed
+3976
No files matched your search
@@ -0,0 +1,215 @@
|
||||
namespace Cthangover.Mods.Stats.Formulas
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
/// <summary>
|
||||
/// Recursive-descent parser of the formula language. Precedence from loose to tight:
|
||||
/// <c>?:</c>, <c>||</c>, <c>&&</c>, comparisons, <c>+ -</c>, <c>* / %</c>,
|
||||
/// prefix <c>- + !</c>, <c>^</c> (right-associative), then numbers, names, calls and
|
||||
/// parentheses. A name may contain dots (<c>a.atk</c>, <c>hero.str</c>): the dot is
|
||||
/// part of the name, the scope decides what it means. Names and functions are case
|
||||
/// insensitive and stored lowercase, like stat ids.
|
||||
/// </summary>
|
||||
public class FormulaParser
|
||||
{
|
||||
private readonly string text;
|
||||
private int pos;
|
||||
|
||||
private FormulaParser(string text)
|
||||
{
|
||||
this.text = text ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Parses <paramref name="formula"/>; throws <see cref="FormulaException"/> on a syntax error.</summary>
|
||||
public static FormulaNode Parse(string formula)
|
||||
{
|
||||
var parser = new FormulaParser(formula);
|
||||
parser.SkipSpace();
|
||||
if (parser.AtEnd)
|
||||
throw new FormulaException(parser.text, 0, "empty formula");
|
||||
var node = parser.ParseConditional();
|
||||
parser.SkipSpace();
|
||||
if (!parser.AtEnd)
|
||||
throw parser.Error($"unexpected '{parser.text[parser.pos]}'");
|
||||
return node;
|
||||
}
|
||||
|
||||
private bool AtEnd => pos >= text.Length;
|
||||
|
||||
private FormulaNode ParseConditional()
|
||||
{
|
||||
var condition = ParseOr();
|
||||
if (!Match("?"))
|
||||
return condition;
|
||||
var then = ParseConditional();
|
||||
Expect(":");
|
||||
var otherwise = ParseConditional();
|
||||
return new FormulaNode(FormulaNodeKind.Conditional, children: new[] { condition, then, otherwise });
|
||||
}
|
||||
|
||||
private FormulaNode ParseOr()
|
||||
{
|
||||
var left = ParseAnd();
|
||||
while (Match("||"))
|
||||
left = Binary("||", left, ParseAnd());
|
||||
return left;
|
||||
}
|
||||
|
||||
private FormulaNode ParseAnd()
|
||||
{
|
||||
var left = ParseComparison();
|
||||
while (Match("&&"))
|
||||
left = Binary("&&", left, ParseComparison());
|
||||
return left;
|
||||
}
|
||||
|
||||
private FormulaNode ParseComparison()
|
||||
{
|
||||
var left = ParseAdditive();
|
||||
foreach (var op in new[] { "<=", ">=", "==", "!=", "<", ">" })
|
||||
{
|
||||
if (Match(op))
|
||||
return Binary(op, left, ParseAdditive());
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private FormulaNode ParseAdditive()
|
||||
{
|
||||
var left = ParseMultiplicative();
|
||||
while (true)
|
||||
{
|
||||
if (Match("+"))
|
||||
left = Binary("+", left, ParseMultiplicative());
|
||||
else if (Match("-"))
|
||||
left = Binary("-", left, ParseMultiplicative());
|
||||
else
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
||||
private FormulaNode ParseMultiplicative()
|
||||
{
|
||||
var left = ParseUnary();
|
||||
while (true)
|
||||
{
|
||||
if (Match("*"))
|
||||
left = Binary("*", left, ParseUnary());
|
||||
else if (Match("/"))
|
||||
left = Binary("/", left, ParseUnary());
|
||||
else if (Match("%"))
|
||||
left = Binary("%", left, ParseUnary());
|
||||
else
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
||||
private FormulaNode ParseUnary()
|
||||
{
|
||||
// "!=" is a comparison, never a negation followed by "=".
|
||||
if (Peek("!") && !Peek("!="))
|
||||
{
|
||||
Match("!");
|
||||
return new FormulaNode(FormulaNodeKind.Unary, op: "!", children: new[] { ParseUnary() });
|
||||
}
|
||||
if (Match("-"))
|
||||
return new FormulaNode(FormulaNodeKind.Unary, op: "-", children: new[] { ParseUnary() });
|
||||
if (Match("+"))
|
||||
return ParseUnary();
|
||||
return ParsePower();
|
||||
}
|
||||
|
||||
private FormulaNode ParsePower()
|
||||
{
|
||||
var left = ParsePrimary();
|
||||
return Match("^") ? Binary("^", left, ParseUnary()) : left;
|
||||
}
|
||||
|
||||
private FormulaNode ParsePrimary()
|
||||
{
|
||||
SkipSpace();
|
||||
if (AtEnd)
|
||||
throw Error("expected a number, a name or '('");
|
||||
|
||||
var c = text[pos];
|
||||
if (Match("("))
|
||||
{
|
||||
var inner = ParseConditional();
|
||||
Expect(")");
|
||||
return inner;
|
||||
}
|
||||
if (char.IsDigit(c) || c == '.')
|
||||
return ParseNumber();
|
||||
if (char.IsLetter(c) || c == '_')
|
||||
return ParseName();
|
||||
throw Error($"unexpected '{c}'");
|
||||
}
|
||||
|
||||
private FormulaNode ParseNumber()
|
||||
{
|
||||
var start = pos;
|
||||
while (!AtEnd && (char.IsDigit(text[pos]) || text[pos] == '.'))
|
||||
pos++;
|
||||
var literal = text.Substring(start, pos - start);
|
||||
if (!double.TryParse(literal, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
|
||||
throw new FormulaException(text, start, $"bad number '{literal}'");
|
||||
return new FormulaNode(FormulaNodeKind.Number, value);
|
||||
}
|
||||
|
||||
private FormulaNode ParseName()
|
||||
{
|
||||
var start = pos;
|
||||
while (!AtEnd && (char.IsLetterOrDigit(text[pos]) || text[pos] == '_' || text[pos] == '.'))
|
||||
pos++;
|
||||
var name = text.Substring(start, pos - start).TrimEnd('.').ToLowerInvariant();
|
||||
if (!Match("("))
|
||||
return new FormulaNode(FormulaNodeKind.Variable, name: name);
|
||||
|
||||
var args = new List<FormulaNode>();
|
||||
if (!Match(")"))
|
||||
{
|
||||
do
|
||||
args.Add(ParseConditional());
|
||||
while (Match(","));
|
||||
Expect(")");
|
||||
}
|
||||
if (!FormulaFunctions.Arity.ContainsKey(name))
|
||||
throw new FormulaException(text, start, $"unknown function '{name}'");
|
||||
if (!FormulaFunctions.Accepts(name, args.Count))
|
||||
throw new FormulaException(text, start, $"'{name}' does not take {args.Count} argument(s)");
|
||||
return new FormulaNode(FormulaNodeKind.Call, name: name, children: args.ToArray());
|
||||
}
|
||||
|
||||
private static FormulaNode Binary(string op, FormulaNode left, FormulaNode right)
|
||||
=> new(FormulaNodeKind.Binary, op: op, children: new[] { left, right });
|
||||
|
||||
private void SkipSpace()
|
||||
{
|
||||
while (!AtEnd && char.IsWhiteSpace(text[pos]))
|
||||
pos++;
|
||||
}
|
||||
|
||||
private bool Peek(string token)
|
||||
{
|
||||
SkipSpace();
|
||||
return string.CompareOrdinal(text, pos, token, 0, token.Length) == 0;
|
||||
}
|
||||
|
||||
private bool Match(string token)
|
||||
{
|
||||
if (!Peek(token))
|
||||
return false;
|
||||
pos += token.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Expect(string token)
|
||||
{
|
||||
if (!Match(token))
|
||||
throw Error($"expected '{token}'");
|
||||
}
|
||||
|
||||
private FormulaException Error(string message) => new(text, pos, message);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user