roll-parser 3.0.0-alpha.0 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +166 -30
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/format.d.ts +1 -1
  5. package/dist/cli/format.d.ts.map +1 -1
  6. package/dist/cli.js +1110 -277
  7. package/dist/cli.js.map +28 -0
  8. package/dist/errors.d.ts +7 -4
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/evaluator/evaluator.d.ts +63 -7
  11. package/dist/evaluator/evaluator.d.ts.map +1 -1
  12. package/dist/evaluator/index.d.ts +2 -2
  13. package/dist/evaluator/index.d.ts.map +1 -1
  14. package/dist/evaluator/modifiers/compare.d.ts +1 -1
  15. package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
  16. package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
  17. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
  18. package/dist/evaluator/modifiers/explode.d.ts +8 -4
  19. package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
  20. package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
  21. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  22. package/dist/evaluator/modifiers/reroll.d.ts +4 -4
  23. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
  24. package/dist/evaluator/modifiers/sort.d.ts +23 -0
  25. package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
  26. package/dist/evaluator/modifiers/success-count.d.ts +1 -1
  27. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
  28. package/dist/index.d.ts +13 -13
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1112 -275
  31. package/dist/index.js.map +26 -0
  32. package/dist/lexer/lexer.d.ts +18 -3
  33. package/dist/lexer/lexer.d.ts.map +1 -1
  34. package/dist/lexer/tokens.d.ts +22 -2
  35. package/dist/lexer/tokens.d.ts.map +1 -1
  36. package/dist/parser/ast.d.ts +209 -24
  37. package/dist/parser/ast.d.ts.map +1 -1
  38. package/dist/parser/parser.d.ts +48 -5
  39. package/dist/parser/parser.d.ts.map +1 -1
  40. package/dist/rng/index.d.ts +2 -2
  41. package/dist/rng/index.d.ts.map +1 -1
  42. package/dist/rng/mock.d.ts +1 -1
  43. package/dist/rng/mock.d.ts.map +1 -1
  44. package/dist/rng/seeded.d.ts +8 -1
  45. package/dist/rng/seeded.d.ts.map +1 -1
  46. package/dist/roll.d.ts +6 -2
  47. package/dist/roll.d.ts.map +1 -1
  48. package/dist/testing.d.ts +1 -1
  49. package/dist/testing.d.ts.map +1 -1
  50. package/dist/testing.js +3 -0
  51. package/dist/testing.js.map +11 -0
  52. package/dist/types.d.ts +143 -2
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +22 -18
  55. package/src/cli/args.ts +2 -1
  56. package/src/cli/format.ts +8 -4
  57. package/src/cli/index.ts +22 -5
  58. package/src/errors.ts +15 -3
  59. package/src/evaluator/evaluator.ts +826 -111
  60. package/src/evaluator/index.ts +2 -2
  61. package/src/evaluator/modifiers/compare.ts +1 -1
  62. package/src/evaluator/modifiers/crit-threshold.ts +59 -0
  63. package/src/evaluator/modifiers/explode.ts +29 -25
  64. package/src/evaluator/modifiers/keep-drop.ts +1 -1
  65. package/src/evaluator/modifiers/reroll.ts +18 -25
  66. package/src/evaluator/modifiers/sort.ts +30 -0
  67. package/src/evaluator/modifiers/success-count.ts +2 -2
  68. package/src/index.ts +33 -15
  69. package/src/lexer/lexer.ts +101 -8
  70. package/src/lexer/tokens.ts +42 -2
  71. package/src/parser/ast.ts +397 -30
  72. package/src/parser/parser.ts +590 -67
  73. package/src/rng/index.ts +2 -2
  74. package/src/rng/mock.ts +1 -1
  75. package/src/rng/seeded.ts +31 -1
  76. package/src/roll.ts +14 -6
  77. package/src/testing.ts +1 -1
  78. package/src/types.ts +127 -2
  79. package/dist/index.mjs +0 -1724
  80. package/dist/testing.mjs +0 -39
package/dist/cli.js CHANGED
@@ -15,14 +15,23 @@ var ROLL_PARSER_ERROR_CODES = [
15
15
  "UNKNOWN_OPERATOR",
16
16
  "UNKNOWN_NODE_TYPE",
17
17
  "INVALID_MODIFIER_COUNT",
18
+ "INVALID_MODIFIER_TARGET",
18
19
  "EXPLODE_LIMIT_EXCEEDED",
19
20
  "INVALID_EXPLODE_TARGET",
20
21
  "REROLL_LIMIT_EXCEEDED",
22
+ "INVALID_REROLL_TARGET",
21
23
  "INVALID_SUCCESS_COUNT_TARGET",
24
+ "INVALID_SORT_TARGET",
25
+ "INVALID_CRIT_THRESHOLD_TARGET",
22
26
  "INVALID_THRESHOLD",
23
27
  "NESTED_VERSUS",
24
28
  "INVALID_FUNCTION_ARITY",
25
- "UNKNOWN_FUNCTION"
29
+ "UNKNOWN_FUNCTION",
30
+ "UNDEFINED_VARIABLE",
31
+ "INVALID_VARIABLE_VALUE",
32
+ "AMBIGUOUS_DICE_CHAIN",
33
+ "MAX_DEPTH_EXCEEDED",
34
+ "NON_FINITE_RESULT"
26
35
  ];
27
36
 
28
37
  class RollParserError extends Error {
@@ -73,7 +82,14 @@ var TokenType;
73
82
  TokenType2[TokenType2["FAIL"] = 27] = "FAIL";
74
83
  TokenType2[TokenType2["FUNCTION"] = 28] = "FUNCTION";
75
84
  TokenType2[TokenType2["VS"] = 29] = "VS";
76
- TokenType2[TokenType2["EOF"] = 30] = "EOF";
85
+ TokenType2[TokenType2["LBRACE"] = 30] = "LBRACE";
86
+ TokenType2[TokenType2["RBRACE"] = 31] = "RBRACE";
87
+ TokenType2[TokenType2["AT"] = 32] = "AT";
88
+ TokenType2[TokenType2["SORT_ASC"] = 33] = "SORT_ASC";
89
+ TokenType2[TokenType2["SORT_DESC"] = 34] = "SORT_DESC";
90
+ TokenType2[TokenType2["CRIT_SUCCESS"] = 35] = "CRIT_SUCCESS";
91
+ TokenType2[TokenType2["CRIT_FAIL"] = 36] = "CRIT_FAIL";
92
+ TokenType2[TokenType2["EOF"] = 37] = "EOF";
77
93
  })(TokenType ||= {});
78
94
 
79
95
  // src/lexer/lexer.ts
@@ -103,8 +119,23 @@ var IDENTIFIER_KEYWORDS = {
103
119
  round: 28 /* FUNCTION */,
104
120
  abs: 28 /* FUNCTION */,
105
121
  max: 28 /* FUNCTION */,
106
- min: 28 /* FUNCTION */
122
+ min: 28 /* FUNCTION */,
123
+ s: 33 /* SORT_ASC */,
124
+ sa: 33 /* SORT_ASC */,
125
+ sd: 34 /* SORT_DESC */,
126
+ cs: 35 /* CRIT_SUCCESS */,
127
+ cf: 36 /* CRIT_FAIL */
107
128
  };
129
+ function buildIdentifierHint(identifier) {
130
+ for (let length = identifier.length - 1;length >= 1; length--) {
131
+ const prefix = identifier.slice(0, length);
132
+ if (IDENTIFIER_KEYWORDS[prefix] == null)
133
+ continue;
134
+ const rest = identifier.slice(length);
135
+ return ` (did you mean '${prefix}' followed by '${rest}'? separate modifiers with a count or space, e.g. '${prefix}1${rest}')`;
136
+ }
137
+ return "";
138
+ }
108
139
 
109
140
  class Lexer {
110
141
  pos = 0;
@@ -117,7 +148,7 @@ class Lexer {
117
148
  while (true) {
118
149
  const token = this.nextToken();
119
150
  tokens.push(token);
120
- if (token.type === 30 /* EOF */)
151
+ if (token.type === 37 /* EOF */)
121
152
  break;
122
153
  }
123
154
  return tokens;
@@ -125,7 +156,7 @@ class Lexer {
125
156
  nextToken() {
126
157
  this.skipWhitespace();
127
158
  if (this.isAtEnd()) {
128
- return this.createToken(30 /* EOF */, "");
159
+ return this.createToken(37 /* EOF */, "");
129
160
  }
130
161
  const startPos = this.pos;
131
162
  const char = this.peek();
@@ -135,6 +166,9 @@ class Lexer {
135
166
  if (this.isAlpha(char)) {
136
167
  return this.scanIdentifier();
137
168
  }
169
+ if (char === "@") {
170
+ return this.scanAt();
171
+ }
138
172
  this.advance();
139
173
  switch (char) {
140
174
  case "+":
@@ -158,6 +192,10 @@ class Lexer {
158
192
  return this.createTokenAt(16 /* RPAREN */, char, startPos);
159
193
  case ",":
160
194
  return this.createTokenAt(17 /* COMMA */, char, startPos);
195
+ case "{":
196
+ return this.createTokenAt(30 /* LBRACE */, char, startPos);
197
+ case "}":
198
+ return this.createTokenAt(31 /* RBRACE */, char, startPos);
161
199
  case ">":
162
200
  if (this.match("=")) {
163
201
  return this.createTokenAt(11 /* GREATER_EQUAL */, ">=", startPos);
@@ -179,8 +217,11 @@ class Lexer {
179
217
  return this.createTokenAt(24 /* EXPLODE_PENETRATING */, "!p", startPos);
180
218
  }
181
219
  return this.createTokenAt(22 /* EXPLODE */, char, startPos);
182
- default:
183
- throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, char);
220
+ default: {
221
+ const codePoint = this.input.codePointAt(startPos);
222
+ const display = codePoint == null ? char : String.fromCodePoint(codePoint);
223
+ throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, display);
224
+ }
184
225
  }
185
226
  }
186
227
  skipWhitespace() {
@@ -224,7 +265,36 @@ class Lexer {
224
265
  if (tokenType != null) {
225
266
  return this.createTokenAt(tokenType, lower, startPos);
226
267
  }
227
- throw new LexerError("Unexpected identifier", "UNEXPECTED_IDENTIFIER", startPos, lower);
268
+ throw new LexerError(`Unexpected identifier${buildIdentifierHint(lower)}`, "UNEXPECTED_IDENTIFIER", startPos, lower);
269
+ }
270
+ scanAt() {
271
+ const startPos = this.pos;
272
+ this.advance();
273
+ let name;
274
+ if (!this.isAtEnd() && this.peek() === "{") {
275
+ this.advance();
276
+ const nameStart = this.pos;
277
+ while (!this.isAtEnd() && this.peek() !== "}" && this.peek() !== `
278
+ `) {
279
+ this.advance();
280
+ }
281
+ if (this.isAtEnd() || this.peek() !== "}") {
282
+ throw new LexerError("Unterminated @{...} variable", "UNEXPECTED_CHARACTER", startPos, "@");
283
+ }
284
+ name = this.input.slice(nameStart, this.pos);
285
+ this.advance();
286
+ } else {
287
+ const nameStart = this.pos;
288
+ if (this.isAtEnd() || !this.isIdentifierStart(this.peek())) {
289
+ throw new LexerError("Empty @ variable name", "UNEXPECTED_CHARACTER", startPos, "@");
290
+ }
291
+ this.advance();
292
+ while (!this.isAtEnd() && this.isIdentifierPart(this.peek())) {
293
+ this.advance();
294
+ }
295
+ name = this.input.slice(nameStart, this.pos);
296
+ }
297
+ return this.createTokenAt(32 /* AT */, name, startPos);
228
298
  }
229
299
  peek() {
230
300
  return this.input[this.pos] ?? "";
@@ -253,15 +323,21 @@ class Lexer {
253
323
  const c = char.toLowerCase();
254
324
  return c >= "a" && c <= "z";
255
325
  }
326
+ isIdentifierStart(char) {
327
+ return this.isAlpha(char) || char === "_";
328
+ }
329
+ isIdentifierPart(char) {
330
+ return this.isAlpha(char) || this.isDigit(char) || char === "_";
331
+ }
256
332
  isWhitespace(char) {
257
333
  return char === " " || char === "\t" || char === `
258
334
  ` || char === "\r";
259
335
  }
260
336
  createToken(type, value) {
261
- return { type, value, position: this.pos };
337
+ return { type, value, position: this.pos, end: this.pos };
262
338
  }
263
339
  createTokenAt(type, value, position) {
264
- return { type, value, position };
340
+ return { type, value, position, end: this.pos };
265
341
  }
266
342
  }
267
343
  function lex(input) {
@@ -275,26 +351,178 @@ function isModifier(node) {
275
351
  function isSuccessCount(node) {
276
352
  return node.type === "SuccessCount";
277
353
  }
278
- function containsDice(node) {
354
+ function isCritThreshold(node) {
355
+ return node.type === "CritThreshold";
356
+ }
357
+ function unwrapTransparent(node, kinds) {
358
+ let current = node;
359
+ while (true) {
360
+ switch (current.type) {
361
+ case "Grouped":
362
+ if (!kinds.includes("Grouped"))
363
+ return current;
364
+ current = current.expression;
365
+ break;
366
+ case "Modifier":
367
+ if (!kinds.includes("Modifier"))
368
+ return current;
369
+ current = current.target;
370
+ break;
371
+ case "Sort":
372
+ if (!kinds.includes("Sort"))
373
+ return current;
374
+ current = current.target;
375
+ break;
376
+ case "CritThreshold":
377
+ if (!kinds.includes("CritThreshold"))
378
+ return current;
379
+ current = current.target;
380
+ break;
381
+ default:
382
+ return current;
383
+ }
384
+ }
385
+ }
386
+ function containsDicePool(node) {
387
+ switch (node.type) {
388
+ case "Dice":
389
+ case "FateDice":
390
+ case "Modifier":
391
+ case "Explode":
392
+ case "Reroll":
393
+ return true;
394
+ case "Sort":
395
+ case "CritThreshold":
396
+ return containsDicePool(node.target);
397
+ case "Grouped":
398
+ return containsDicePool(node.expression);
399
+ case "Group":
400
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
401
+ default:
402
+ return false;
403
+ }
404
+ }
405
+ function deepContainsDicePool(node) {
279
406
  switch (node.type) {
280
407
  case "Dice":
281
408
  case "FateDice":
282
409
  return true;
283
- case "Literal":
410
+ case "BinaryOp":
411
+ return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
412
+ case "UnaryOp":
413
+ return deepContainsDicePool(node.operand);
414
+ case "Modifier":
415
+ case "Explode":
416
+ case "Reroll":
417
+ case "SuccessCount":
418
+ case "Sort":
419
+ case "CritThreshold":
420
+ return deepContainsDicePool(node.target);
421
+ case "Versus":
422
+ return deepContainsDicePool(node.roll) || deepContainsDicePool(node.dc);
423
+ case "FunctionCall":
424
+ return node.args.some(deepContainsDicePool);
425
+ case "Grouped":
426
+ return deepContainsDicePool(node.expression);
427
+ case "Group":
428
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
429
+ default:
284
430
  return false;
431
+ }
432
+ }
433
+ function containsFatePool(node) {
434
+ switch (node.type) {
435
+ case "FateDice":
436
+ return true;
437
+ case "Modifier":
438
+ case "Explode":
439
+ case "Reroll":
440
+ case "Sort":
441
+ case "CritThreshold":
442
+ return containsFatePool(node.target);
443
+ case "Grouped":
444
+ return containsFatePool(node.expression);
445
+ case "Group":
446
+ return node.expressions.some(deepContainsFatePool);
447
+ default:
448
+ return false;
449
+ }
450
+ }
451
+ function deepContainsFatePool(node) {
452
+ switch (node.type) {
453
+ case "FateDice":
454
+ return true;
285
455
  case "BinaryOp":
286
- return containsDice(node.left) || containsDice(node.right);
456
+ return deepContainsFatePool(node.left) || deepContainsFatePool(node.right);
287
457
  case "UnaryOp":
288
- return containsDice(node.operand);
458
+ return deepContainsFatePool(node.operand);
289
459
  case "Modifier":
290
460
  case "Explode":
291
461
  case "Reroll":
292
462
  case "SuccessCount":
293
- return containsDice(node.target);
463
+ case "Sort":
464
+ case "CritThreshold":
465
+ return deepContainsFatePool(node.target);
294
466
  case "Versus":
295
- return containsDice(node.roll) || containsDice(node.dc);
467
+ return deepContainsFatePool(node.roll) || deepContainsFatePool(node.dc);
296
468
  case "FunctionCall":
297
- return node.args.some(containsDice);
469
+ return node.args.some(deepContainsFatePool);
470
+ case "Grouped":
471
+ return deepContainsFatePool(node.expression);
472
+ case "Group":
473
+ return node.expressions.some(deepContainsFatePool);
474
+ default:
475
+ return false;
476
+ }
477
+ }
478
+ function containsMultiSubGroup(node) {
479
+ switch (node.type) {
480
+ case "Group":
481
+ return node.expressions.length >= 2 || node.expressions.some(containsMultiSubGroup);
482
+ case "BinaryOp":
483
+ return containsMultiSubGroup(node.left) || containsMultiSubGroup(node.right);
484
+ case "UnaryOp":
485
+ return containsMultiSubGroup(node.operand);
486
+ case "Modifier":
487
+ case "Explode":
488
+ case "Reroll":
489
+ case "SuccessCount":
490
+ case "Sort":
491
+ case "CritThreshold":
492
+ return containsMultiSubGroup(node.target);
493
+ case "Versus":
494
+ return containsMultiSubGroup(node.roll) || containsMultiSubGroup(node.dc);
495
+ case "FunctionCall":
496
+ return node.args.some(containsMultiSubGroup);
497
+ case "Grouped":
498
+ return containsMultiSubGroup(node.expression);
499
+ default:
500
+ return false;
501
+ }
502
+ }
503
+ function containsVersus(node) {
504
+ switch (node.type) {
505
+ case "Versus":
506
+ return true;
507
+ case "BinaryOp":
508
+ return containsVersus(node.left) || containsVersus(node.right);
509
+ case "UnaryOp":
510
+ return containsVersus(node.operand);
511
+ case "Modifier":
512
+ case "Explode":
513
+ case "Reroll":
514
+ case "SuccessCount":
515
+ case "Sort":
516
+ case "CritThreshold":
517
+ return containsVersus(node.target);
518
+ case "FunctionCall":
519
+ return node.args.some(containsVersus);
520
+ case "Grouped":
521
+ return containsVersus(node.expression);
522
+ case "Group":
523
+ return node.expressions.some(containsVersus);
524
+ default:
525
+ return false;
298
526
  }
299
527
  }
300
528
 
@@ -312,6 +540,7 @@ class ParseError extends RollParserError {
312
540
  var BP = {
313
541
  VS_LEFT: 2,
314
542
  VS_RIGHT: 3,
543
+ COMPARE: 8,
315
544
  ADD_LEFT: 10,
316
545
  ADD_RIGHT: 11,
317
546
  MUL_LEFT: 20,
@@ -323,6 +552,14 @@ var BP = {
323
552
  DICE_LEFT: 40,
324
553
  DICE_RIGHT: 41
325
554
  };
555
+ var MAX_PARSE_DEPTH = 128;
556
+ var TOKEN_DISPLAY = {
557
+ [15 /* LPAREN */]: `'('`,
558
+ [16 /* RPAREN */]: `')'`,
559
+ [30 /* LBRACE */]: `'{'`,
560
+ [31 /* RBRACE */]: `'}'`,
561
+ [17 /* COMMA */]: `','`
562
+ };
326
563
  var FUNCTION_ARITY = {
327
564
  floor: { min: 1, max: 1 },
328
565
  ceil: { min: 1, max: 1 },
@@ -335,31 +572,40 @@ var FUNCTION_ARITY = {
335
572
  class Parser {
336
573
  tokens;
337
574
  pos = 0;
575
+ depth = 0;
338
576
  constructor(tokens) {
339
577
  this.tokens = tokens;
340
578
  }
341
579
  parse() {
342
- if (this.peek().type === 30 /* EOF */) {
580
+ if (this.peek().type === 37 /* EOF */) {
343
581
  throw new ParseError("Unexpected end of input", "UNEXPECTED_END", this.peek().position);
344
582
  }
345
583
  const ast = this.parseExpression(0);
346
- if (this.peek().type !== 30 /* EOF */) {
584
+ if (this.peek().type !== 37 /* EOF */) {
347
585
  const token = this.peek();
348
586
  throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
349
587
  }
350
588
  return ast;
351
589
  }
352
590
  parseExpression(minBp) {
353
- let left = this.parseNud();
354
- while (this.hasTokens()) {
355
- const token = this.peek();
356
- const leftBp = this.getLeftBp(token);
357
- if (leftBp < minBp)
358
- break;
359
- this.advance();
360
- left = this.parseLed(left, token);
591
+ this.depth += 1;
592
+ if (this.depth > MAX_PARSE_DEPTH) {
593
+ throw new ParseError(`Expression nesting exceeds the maximum depth of ${MAX_PARSE_DEPTH}`, "MAX_DEPTH_EXCEEDED", this.peek().position, this.peek());
594
+ }
595
+ try {
596
+ let left = this.parseNud();
597
+ while (this.hasTokens()) {
598
+ const token = this.peek();
599
+ const leftBp = this.getLeftBp(token);
600
+ if (leftBp < minBp)
601
+ break;
602
+ this.advance();
603
+ left = this.parseLed(left, token);
604
+ }
605
+ return left;
606
+ } finally {
607
+ this.depth -= 1;
361
608
  }
362
- return left;
363
609
  }
364
610
  parseNud() {
365
611
  const token = this.advance();
@@ -367,18 +613,22 @@ class Parser {
367
613
  case 0 /* NUMBER */:
368
614
  return this.parseLiteral(token);
369
615
  case 5 /* MINUS */:
370
- return this.parseUnaryMinus();
616
+ return this.parseUnaryMinus(token);
371
617
  case 1 /* DICE */:
372
- return this.parsePrefixDice();
618
+ return this.parsePrefixDice(token);
373
619
  case 2 /* DICE_PERCENT */:
374
- return this.parsePrefixDicePercent();
620
+ return this.parsePrefixDicePercent(token);
375
621
  case 3 /* DICE_FATE */:
376
- return this.parsePrefixFateDice();
622
+ return this.parsePrefixFateDice(token);
377
623
  case 15 /* LPAREN */:
378
- return this.parseGrouped();
624
+ return this.parseGrouped(token);
625
+ case 30 /* LBRACE */:
626
+ return this.parseGroup(token);
379
627
  case 28 /* FUNCTION */:
380
628
  return this.parseFunctionCall(token);
381
- case 30 /* EOF */:
629
+ case 32 /* AT */:
630
+ return this.parseVariable(token);
631
+ case 37 /* EOF */:
382
632
  throw new ParseError("Unexpected end of input", "UNEXPECTED_END", token.position);
383
633
  default:
384
634
  throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
@@ -387,11 +637,11 @@ class Parser {
387
637
  parseLed(left, token) {
388
638
  switch (token.type) {
389
639
  case 1 /* DICE */:
390
- return this.parseInfixDice(left);
640
+ return this.parseInfixDice(left, token);
391
641
  case 2 /* DICE_PERCENT */:
392
- return this.parseInfixDicePercent(left);
642
+ return this.parseInfixDicePercent(left, token);
393
643
  case 3 /* DICE_FATE */:
394
- return this.parseInfixFateDice(left);
644
+ return this.parseInfixFateDice(left, token);
395
645
  case 4 /* PLUS */:
396
646
  case 5 /* MINUS */:
397
647
  case 6 /* MULTIPLY */:
@@ -411,6 +661,12 @@ class Parser {
411
661
  case 25 /* REROLL */:
412
662
  case 26 /* REROLL_ONCE */:
413
663
  return this.parseReroll(left, token);
664
+ case 33 /* SORT_ASC */:
665
+ case 34 /* SORT_DESC */:
666
+ return this.parseSort(left, token);
667
+ case 35 /* CRIT_SUCCESS */:
668
+ case 36 /* CRIT_FAIL */:
669
+ return this.parseCritThreshold(left, token);
414
670
  case 10 /* GREATER */:
415
671
  case 11 /* GREATER_EQUAL */:
416
672
  case 12 /* LESS */:
@@ -423,78 +679,134 @@ class Parser {
423
679
  throw new ParseError(`Unexpected infix token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
424
680
  }
425
681
  }
682
+ static syntheticLiteral(value, token) {
683
+ return { type: "Literal", value, start: token.position, end: token.position };
684
+ }
426
685
  parseLiteral(token) {
427
686
  return {
428
687
  type: "Literal",
429
- value: Number.parseFloat(token.value)
688
+ value: Number.parseFloat(token.value),
689
+ start: token.position,
690
+ end: token.end
430
691
  };
431
692
  }
432
- parseUnaryMinus() {
693
+ parseUnaryMinus(token) {
433
694
  const operand = this.parseExpression(BP.UNARY);
695
+ this.rejectSuccessCountTarget(operand, token);
434
696
  return {
435
697
  type: "UnaryOp",
436
698
  operator: "-",
437
- operand
699
+ operand,
700
+ start: token.position,
701
+ end: operand.end ?? token.end
438
702
  };
439
703
  }
440
- parsePrefixDice() {
704
+ parsePrefixDice(token) {
441
705
  const sides = this.parseExpression(BP.DICE_RIGHT);
706
+ this.rejectSuccessCountTarget(sides, token);
707
+ this.rejectVersusTarget(sides, token);
442
708
  return {
443
709
  type: "Dice",
444
- count: { type: "Literal", value: 1 },
445
- sides
710
+ count: Parser.syntheticLiteral(1, token),
711
+ sides,
712
+ start: token.position,
713
+ end: sides.end ?? token.end
446
714
  };
447
715
  }
448
- parseInfixDice(left) {
716
+ parseInfixDice(left, token) {
717
+ this.rejectSuccessCountTarget(left, token);
718
+ this.rejectVersusTarget(left, token);
719
+ this.rejectBareDiceChain(left, token);
449
720
  const sides = this.parseExpression(BP.DICE_RIGHT);
721
+ this.rejectSuccessCountTarget(sides, token);
722
+ this.rejectVersusTarget(sides, token);
450
723
  return {
451
724
  type: "Dice",
452
725
  count: left,
453
- sides
726
+ sides,
727
+ start: left.start ?? token.position,
728
+ end: sides.end ?? token.end
454
729
  };
455
730
  }
456
- parsePrefixDicePercent() {
731
+ parsePrefixDicePercent(token) {
457
732
  return {
458
733
  type: "Dice",
459
- count: { type: "Literal", value: 1 },
460
- sides: { type: "Literal", value: 100 }
734
+ count: Parser.syntheticLiteral(1, token),
735
+ sides: Parser.syntheticLiteral(100, token),
736
+ start: token.position,
737
+ end: token.end
461
738
  };
462
739
  }
463
- parseInfixDicePercent(left) {
740
+ parseInfixDicePercent(left, token) {
741
+ this.rejectSuccessCountTarget(left, token);
742
+ this.rejectVersusTarget(left, token);
743
+ this.rejectBareDiceChain(left, token);
464
744
  return {
465
745
  type: "Dice",
466
746
  count: left,
467
- sides: { type: "Literal", value: 100 }
747
+ sides: Parser.syntheticLiteral(100, token),
748
+ start: left.start ?? token.position,
749
+ end: token.end
468
750
  };
469
751
  }
470
- parsePrefixFateDice() {
752
+ parsePrefixFateDice(token) {
471
753
  return {
472
754
  type: "FateDice",
473
- count: { type: "Literal", value: 1 }
755
+ count: Parser.syntheticLiteral(1, token),
756
+ start: token.position,
757
+ end: token.end
474
758
  };
475
759
  }
476
- parseInfixFateDice(left) {
760
+ parseInfixFateDice(left, token) {
761
+ this.rejectSuccessCountTarget(left, token);
762
+ this.rejectVersusTarget(left, token);
763
+ this.rejectBareDiceChain(left, token);
477
764
  return {
478
765
  type: "FateDice",
479
- count: left
766
+ count: left,
767
+ start: left.start ?? token.position,
768
+ end: token.end
480
769
  };
481
770
  }
482
- parseGrouped() {
483
- const expr = this.parseExpression(0);
484
- this.expect(16 /* RPAREN */);
485
- return expr;
771
+ parseGrouped(token) {
772
+ const expression = this.parseExpression(0);
773
+ const close = this.expect(16 /* RPAREN */);
774
+ return { type: "Grouped", expression, start: token.position, end: close.end };
775
+ }
776
+ parseGroup(startToken) {
777
+ if (this.peek().type === 31 /* RBRACE */) {
778
+ throw new ParseError("Empty group", "UNEXPECTED_TOKEN", startToken.position, startToken);
779
+ }
780
+ const expressions = [this.parseExpression(0)];
781
+ while (this.peek().type === 17 /* COMMA */) {
782
+ this.advance();
783
+ expressions.push(this.parseExpression(0));
784
+ }
785
+ if (this.peek().type !== 31 /* RBRACE */) {
786
+ const unterminated = this.peek();
787
+ throw new ParseError(`Unterminated group: expected '}' or ','`, "EXPECTED_TOKEN", unterminated.position, unterminated);
788
+ }
789
+ const close = this.advance();
790
+ return { type: "Group", expressions, start: startToken.position, end: close.end };
791
+ }
792
+ parseVariable(token) {
793
+ return { type: "Variable", name: token.value, start: token.position, end: token.end };
486
794
  }
487
795
  parseFunctionCall(token) {
488
796
  this.expect(15 /* LPAREN */);
489
797
  const args = [];
490
798
  if (this.peek().type !== 16 /* RPAREN */) {
491
- args.push(this.parseExpression(0));
799
+ const first = this.parseExpression(0);
800
+ this.rejectSuccessCountTarget(first, token);
801
+ args.push(first);
492
802
  while (this.peek().type === 17 /* COMMA */) {
493
803
  this.advance();
494
- args.push(this.parseExpression(0));
804
+ const next = this.parseExpression(0);
805
+ this.rejectSuccessCountTarget(next, token);
806
+ args.push(next);
495
807
  }
496
808
  }
497
- this.expect(16 /* RPAREN */);
809
+ const close = this.expect(16 /* RPAREN */);
498
810
  const arity = FUNCTION_ARITY[token.value];
499
811
  if (arity === undefined) {
500
812
  throw new ParseError(`Unknown function '${token.value}'`, "UNKNOWN_FUNCTION", token.position, token);
@@ -503,84 +815,244 @@ class Parser {
503
815
  const expected = arity.max === Number.POSITIVE_INFINITY ? `at least ${arity.min}` : arity.min === arity.max ? `${arity.min}` : `${arity.min}–${arity.max}`;
504
816
  throw new ParseError(`Function '${token.value}' expects ${expected} argument${arity.min === 1 && arity.max === 1 ? "" : "s"}, got ${args.length}`, "INVALID_FUNCTION_ARITY", token.position, token);
505
817
  }
506
- return { type: "FunctionCall", name: token.value, args };
818
+ return {
819
+ type: "FunctionCall",
820
+ name: token.value,
821
+ args,
822
+ start: token.position,
823
+ end: close.end
824
+ };
507
825
  }
508
826
  parseBinaryOp(left, token) {
827
+ this.rejectSuccessCountTarget(left, token);
509
828
  const operator = this.getOperatorSymbol(token);
510
829
  const rightBp = this.getRightBp(token);
511
830
  const right = this.parseExpression(rightBp);
831
+ this.rejectSuccessCountTarget(right, token);
512
832
  return {
513
833
  type: "BinaryOp",
514
834
  operator,
515
835
  left,
516
- right
836
+ right,
837
+ start: left.start ?? token.position,
838
+ end: right.end ?? token.end
517
839
  };
518
840
  }
841
+ rejectBareDiceChain(left, token) {
842
+ switch (left.type) {
843
+ case "Dice":
844
+ case "FateDice":
845
+ case "Explode":
846
+ case "Reroll":
847
+ case "Modifier":
848
+ case "Sort":
849
+ case "CritThreshold":
850
+ throw new ParseError(`Ambiguous dice chain: use 'dl'/'dh' to drop dice (4d6dl1) or parentheses for nested dice ((4d6)d1)`, "AMBIGUOUS_DICE_CHAIN", token.position, token);
851
+ default:
852
+ return;
853
+ }
854
+ }
519
855
  rejectSuccessCountTarget(target, token) {
520
- if (isSuccessCount(target)) {
856
+ const node = unwrapTransparent(target, ["Grouped"]);
857
+ if (isSuccessCount(node)) {
521
858
  throw new ParseError(`Cannot apply modifier after success counting`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
522
859
  }
523
860
  }
861
+ rejectGroupTarget(target, token, action, code, singleSubRollPasses = false) {
862
+ const node = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
863
+ if (node.type !== "Group")
864
+ return;
865
+ if (singleSubRollPasses && node.expressions.length === 1) {
866
+ const inner = node.expressions[0];
867
+ if (inner != null && containsMultiSubGroup(inner)) {
868
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
869
+ }
870
+ return;
871
+ }
872
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
873
+ }
874
+ rejectVersusTarget(target, token) {
875
+ const node = unwrapTransparent(target, ["Grouped"]);
876
+ if (node.type === "Versus") {
877
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
878
+ }
879
+ if (node.type === "Group" && node.expressions.length === 1) {
880
+ const inner = node.expressions[0];
881
+ if (inner != null && containsVersus(inner)) {
882
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
883
+ }
884
+ }
885
+ }
524
886
  parseModifier(target, token) {
525
887
  this.rejectSuccessCountTarget(target, token);
888
+ this.rejectVersusTarget(target, token);
889
+ if (!containsDicePool(target)) {
890
+ throw new ParseError(`Keep/drop modifiers require a dice pool target`, "INVALID_MODIFIER_TARGET", token.position, token);
891
+ }
526
892
  const modifier = token.type === 18 /* KEEP_HIGH */ || token.type === 19 /* KEEP_LOW */ ? "keep" : "drop";
527
893
  const selector = token.type === 18 /* KEEP_HIGH */ || token.type === 20 /* DROP_HIGH */ ? "highest" : "lowest";
528
894
  const nextToken = this.peek().type;
529
- const count = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ ? this.parseExpression(BP.DICE_LEFT) : { type: "Literal", value: 1 };
895
+ const hasExplicitCount = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ || nextToken === 32 /* AT */;
896
+ const count = hasExplicitCount ? this.parseExpression(BP.DICE_LEFT) : Parser.syntheticLiteral(1, token);
897
+ this.rejectSuccessCountTarget(count, token);
898
+ this.rejectVersusTarget(count, token);
530
899
  return {
531
900
  type: "Modifier",
532
901
  modifier,
533
902
  selector,
534
903
  count,
535
- target
904
+ target,
905
+ start: target.start ?? token.position,
906
+ end: hasExplicitCount ? count.end ?? token.end : token.end
536
907
  };
537
908
  }
538
909
  parseExplode(target, token) {
539
910
  this.rejectSuccessCountTarget(target, token);
911
+ this.rejectGroupTarget(target, token, "explode", "INVALID_EXPLODE_TARGET");
912
+ if (!containsDicePool(target)) {
913
+ throw new ParseError(`Explode modifier requires a dice pool target`, "INVALID_EXPLODE_TARGET", token.position, token);
914
+ }
915
+ if (containsFatePool(target)) {
916
+ throw new ParseError(`Fate dice cannot explode`, "INVALID_EXPLODE_TARGET", token.position, token);
917
+ }
540
918
  if (target.type === "Explode") {
541
919
  throw new ParseError(`Cannot chain explode modifiers`, "INVALID_EXPLODE_TARGET", token.position, token);
542
920
  }
543
921
  const variant = token.type === 22 /* EXPLODE */ ? "standard" : token.type === 23 /* EXPLODE_COMPOUND */ ? "compound" : "penetrating";
544
- const node = { type: "Explode", variant, target };
922
+ const node = {
923
+ type: "Explode",
924
+ variant,
925
+ target,
926
+ start: target.start ?? token.position,
927
+ end: token.end
928
+ };
545
929
  if (this.isComparePointAhead()) {
546
930
  node.threshold = this.parseComparePoint();
931
+ node.end = node.threshold.value.end ?? token.end;
547
932
  }
548
933
  return node;
549
934
  }
550
935
  parseReroll(target, token) {
551
936
  this.rejectSuccessCountTarget(target, token);
937
+ this.rejectGroupTarget(target, token, "reroll", "INVALID_REROLL_TARGET");
938
+ if (!containsDicePool(target)) {
939
+ throw new ParseError(`Reroll modifier requires a dice pool target`, "INVALID_REROLL_TARGET", token.position, token);
940
+ }
552
941
  if (!this.isComparePointAhead()) {
553
942
  throw new ParseError(`Expected comparison operator after '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
554
943
  }
555
944
  const once = token.type === 26 /* REROLL_ONCE */;
556
945
  const condition = this.parseComparePoint();
557
- return { type: "Reroll", once, condition, target };
946
+ return {
947
+ type: "Reroll",
948
+ once,
949
+ condition,
950
+ target,
951
+ start: target.start ?? token.position,
952
+ end: condition.value.end ?? token.end
953
+ };
954
+ }
955
+ parseSort(target, token) {
956
+ this.rejectSuccessCountTarget(target, token);
957
+ this.rejectVersusTarget(target, token);
958
+ if (!deepContainsDicePool(target)) {
959
+ throw new ParseError(`Sort modifier requires a dice pool target`, "INVALID_SORT_TARGET", token.position, token);
960
+ }
961
+ const base = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
962
+ if (base.type === "Group" && base.expressions.length >= 2) {
963
+ throw new ParseError(`Sort modifier does not yet support multi-sub-roll groups`, "INVALID_SORT_TARGET", token.position, token);
964
+ }
965
+ const order = token.type === 33 /* SORT_ASC */ ? "ascending" : "descending";
966
+ return {
967
+ type: "Sort",
968
+ order,
969
+ target,
970
+ start: target.start ?? token.position,
971
+ end: token.end
972
+ };
973
+ }
974
+ parseCritThreshold(target, token) {
975
+ this.rejectSuccessCountTarget(target, token);
976
+ this.rejectVersusTarget(target, token);
977
+ this.rejectGroupTarget(target, token, "apply crit threshold to", "INVALID_CRIT_THRESHOLD_TARGET", true);
978
+ if (!containsDicePool(target)) {
979
+ throw new ParseError(`Crit threshold modifier requires a dice pool target`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
980
+ }
981
+ if (!this.isComparePointAhead() && containsFatePool(target)) {
982
+ throw new ParseError(`Bare cs/cf cannot apply to Fate dice`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
983
+ }
984
+ const threshold = this.isComparePointAhead() ? this.parseComparePoint() : "default";
985
+ const end = threshold === "default" ? token.end : threshold.value.end ?? token.end;
986
+ let chainTarget = target;
987
+ while (chainTarget.type === "Grouped") {
988
+ chainTarget = chainTarget.expression;
989
+ }
990
+ if (isCritThreshold(chainTarget)) {
991
+ if (token.type === 35 /* CRIT_SUCCESS */) {
992
+ chainTarget.successThresholds.push(threshold);
993
+ } else {
994
+ chainTarget.failThresholds.push(threshold);
995
+ }
996
+ chainTarget.end = end;
997
+ return chainTarget;
998
+ }
999
+ return {
1000
+ type: "CritThreshold",
1001
+ successThresholds: token.type === 35 /* CRIT_SUCCESS */ ? [threshold] : [],
1002
+ failThresholds: token.type === 36 /* CRIT_FAIL */ ? [threshold] : [],
1003
+ target,
1004
+ start: target.start ?? token.position,
1005
+ end
1006
+ };
558
1007
  }
559
1008
  parseSuccessCount(target, token) {
560
1009
  this.rejectSuccessCountTarget(target, token);
561
- if (!containsDice(target)) {
562
- throw new ParseError(`Success counting requires a dice expression`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
1010
+ if (!containsDicePool(target)) {
1011
+ throw new ParseError(`Success counting requires a dice pool target`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
563
1012
  }
564
1013
  const operator = this.getCompareOp(token);
565
1014
  const value = this.parseExpression(BP.DICE_LEFT);
1015
+ this.rejectSuccessCountTarget(value, token);
1016
+ this.rejectVersusTarget(value, token);
566
1017
  const node = {
567
1018
  type: "SuccessCount",
568
1019
  target,
569
- threshold: { operator, value }
1020
+ threshold: { operator, value },
1021
+ start: target.start ?? token.position,
1022
+ end: value.end ?? token.end
570
1023
  };
571
1024
  if (this.peek().type === 27 /* FAIL */) {
572
1025
  this.advance();
573
- const failValue = this.parseExpression(BP.DICE_LEFT);
574
- node.failThreshold = { operator: "=", value: failValue };
1026
+ if (this.isComparePointAhead()) {
1027
+ node.failThreshold = this.parseComparePoint();
1028
+ } else {
1029
+ const failValue = this.parseExpression(BP.DICE_LEFT);
1030
+ this.rejectSuccessCountTarget(failValue, token);
1031
+ this.rejectVersusTarget(failValue, token);
1032
+ node.failThreshold = { operator: "=", value: failValue };
1033
+ }
1034
+ node.end = node.failThreshold.value.end ?? node.end ?? token.end;
575
1035
  }
576
1036
  return node;
577
1037
  }
578
1038
  parseVersus(left, token) {
579
- if (left.type === "Versus") {
1039
+ this.rejectSuccessCountTarget(left, token);
1040
+ let leftChain = left;
1041
+ while (leftChain.type === "Grouped") {
1042
+ leftChain = leftChain.expression;
1043
+ }
1044
+ if (leftChain.type === "Versus") {
580
1045
  throw new ParseError("Cannot chain versus operators", "NESTED_VERSUS", token.position, token);
581
1046
  }
582
1047
  const dc = this.parseExpression(BP.VS_RIGHT);
583
- return { type: "Versus", roll: left, dc };
1048
+ this.rejectSuccessCountTarget(dc, token);
1049
+ return {
1050
+ type: "Versus",
1051
+ roll: left,
1052
+ dc,
1053
+ start: left.start ?? token.position,
1054
+ end: dc.end ?? token.end
1055
+ };
584
1056
  }
585
1057
  isComparePointAhead() {
586
1058
  const type = this.peek().type;
@@ -591,6 +1063,8 @@ class Parser {
591
1063
  const operator = this.getCompareOp(token);
592
1064
  this.advance();
593
1065
  const value = this.parseExpression(BP.DICE_LEFT);
1066
+ this.rejectSuccessCountTarget(value, token);
1067
+ this.rejectVersusTarget(value, token);
594
1068
  return { operator, value };
595
1069
  }
596
1070
  getCompareOp(token) {
@@ -653,16 +1127,23 @@ class Parser {
653
1127
  case 24 /* EXPLODE_PENETRATING */:
654
1128
  case 25 /* REROLL */:
655
1129
  case 26 /* REROLL_ONCE */:
1130
+ case 33 /* SORT_ASC */:
1131
+ case 34 /* SORT_DESC */:
1132
+ case 35 /* CRIT_SUCCESS */:
1133
+ case 36 /* CRIT_FAIL */:
1134
+ return BP.MODIFIER;
656
1135
  case 10 /* GREATER */:
657
1136
  case 11 /* GREATER_EQUAL */:
658
1137
  case 12 /* LESS */:
659
1138
  case 13 /* LESS_EQUAL */:
660
1139
  case 14 /* EQUAL */:
661
- return BP.MODIFIER;
1140
+ return BP.COMPARE;
662
1141
  case 16 /* RPAREN */:
663
- case 30 /* EOF */:
1142
+ case 37 /* EOF */:
664
1143
  case 17 /* COMMA */:
665
1144
  case 28 /* FUNCTION */:
1145
+ case 30 /* LBRACE */:
1146
+ case 31 /* RBRACE */:
666
1147
  return -1;
667
1148
  default:
668
1149
  return 0;
@@ -688,7 +1169,7 @@ class Parser {
688
1169
  }
689
1170
  }
690
1171
  peek() {
691
- return this.tokens[this.pos] ?? { type: 30 /* EOF */, value: "", position: this.pos };
1172
+ return this.tokens[this.pos] ?? { type: 37 /* EOF */, value: "", position: this.pos, end: this.pos };
692
1173
  }
693
1174
  advance() {
694
1175
  const token = this.peek();
@@ -698,13 +1179,14 @@ class Parser {
698
1179
  expect(type) {
699
1180
  const token = this.peek();
700
1181
  if (token.type !== type) {
701
- const expected = TokenType[type];
702
- throw new ParseError(`Expected ${expected} but got '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
1182
+ const expected = TOKEN_DISPLAY[type] ?? TokenType[type];
1183
+ const got = token.type === 37 /* EOF */ ? "end of input" : `'${token.value}'`;
1184
+ throw new ParseError(`Expected ${expected} but got ${got}`, "EXPECTED_TOKEN", token.position, token);
703
1185
  }
704
1186
  return this.advance();
705
1187
  }
706
1188
  hasTokens() {
707
- return this.peek().type !== 30 /* EOF */;
1189
+ return this.peek().type !== 37 /* EOF */;
708
1190
  }
709
1191
  }
710
1192
 
@@ -771,6 +1253,9 @@ class SeededRNG {
771
1253
  if (range <= 1) {
772
1254
  return lo;
773
1255
  }
1256
+ if (range > 4294967296) {
1257
+ return lo + this.nextBoundedWide(range);
1258
+ }
774
1259
  const threshold = (4294967296 - range) % range;
775
1260
  let value;
776
1261
  do {
@@ -778,6 +1263,56 @@ class SeededRNG {
778
1263
  } while (value < threshold);
779
1264
  return lo + value % range;
780
1265
  }
1266
+ nextBoundedWide(range) {
1267
+ const MAX_53 = 2 ** 53;
1268
+ if (range > MAX_53) {
1269
+ throw new RangeError(`nextInt range ${range} exceeds 2^53 and cannot be sampled exactly`);
1270
+ }
1271
+ const limit = Math.floor(MAX_53 / range) * range;
1272
+ let value;
1273
+ do {
1274
+ value = this.nextUint32() * 2097152 + (this.nextUint32() >>> 11);
1275
+ } while (value >= limit);
1276
+ return value % range;
1277
+ }
1278
+ }
1279
+
1280
+ // src/evaluator/modifiers/compare.ts
1281
+ function matchesCondition(result, operator, value) {
1282
+ switch (operator) {
1283
+ case ">":
1284
+ return result > value;
1285
+ case ">=":
1286
+ return result >= value;
1287
+ case "<":
1288
+ return result < value;
1289
+ case "<=":
1290
+ return result <= value;
1291
+ case "=":
1292
+ return result === value;
1293
+ }
1294
+ }
1295
+
1296
+ // src/evaluator/modifiers/crit-threshold.ts
1297
+ function applyCritThresholds(dice, successThresholds, failThresholds) {
1298
+ for (const die of dice) {
1299
+ if (die.modifiers.includes("meta"))
1300
+ continue;
1301
+ die.critical = successThresholds.some((t) => matchesCrit(t, die));
1302
+ die.fumble = failThresholds.some((t) => matchesFumble(t, die));
1303
+ }
1304
+ }
1305
+ function matchesCrit(threshold, die) {
1306
+ if (threshold === "default") {
1307
+ return die.result === die.sides && die.sides > 1;
1308
+ }
1309
+ return matchesCondition(die.result, threshold.operator, threshold.value);
1310
+ }
1311
+ function matchesFumble(threshold, die) {
1312
+ if (threshold === "default") {
1313
+ return die.result === 1 && die.sides > 1;
1314
+ }
1315
+ return matchesCondition(die.result, threshold.operator, threshold.value);
781
1316
  }
782
1317
 
783
1318
  // src/evaluator/modifiers/explode.ts
@@ -835,7 +1370,7 @@ function applyStandardExplode(pool, shouldExplode, rng, env) {
835
1370
  result: next,
836
1371
  modifiers: ["exploded", "kept"],
837
1372
  critical: next === sides && sides > 1,
838
- fumble: next === 1
1373
+ fumble: next === 1 && sides > 1
839
1374
  });
840
1375
  last = next;
841
1376
  iterations += 1;
@@ -844,9 +1379,9 @@ function applyStandardExplode(pool, shouldExplode, rng, env) {
844
1379
  return result;
845
1380
  }
846
1381
  function applyCompoundExplode(pool, shouldExplode, rng, env) {
847
- return pool.map((original) => {
1382
+ for (const original of pool) {
848
1383
  if (!canExplode(original))
849
- return original;
1384
+ continue;
850
1385
  const sides = original.sides;
851
1386
  let accumulated = original.result;
852
1387
  let last = original.result;
@@ -863,15 +1398,14 @@ function applyCompoundExplode(pool, shouldExplode, rng, env) {
863
1398
  iterations += 1;
864
1399
  }
865
1400
  if (!exploded)
866
- return original;
867
- return {
868
- sides,
869
- result: accumulated,
870
- modifiers: original.modifiers.includes("exploded") ? original.modifiers : [...original.modifiers, "exploded"],
871
- critical: original.critical,
872
- fumble: original.fumble
873
- };
874
- });
1401
+ continue;
1402
+ original.initialResult = original.result;
1403
+ original.result = accumulated;
1404
+ if (!original.modifiers.includes("exploded")) {
1405
+ original.modifiers = [...original.modifiers, "exploded"];
1406
+ }
1407
+ }
1408
+ return pool;
875
1409
  }
876
1410
  function applyPenetratingExplode(pool, shouldExplode, rng, env) {
877
1411
  const result = [];
@@ -893,7 +1427,7 @@ function applyPenetratingExplode(pool, shouldExplode, rng, env) {
893
1427
  result: stored,
894
1428
  modifiers: ["exploded", "kept"],
895
1429
  critical: raw === sides && sides > 1,
896
- fumble: raw === 1
1430
+ fumble: raw === 1 && sides > 1
897
1431
  });
898
1432
  lastRaw = raw;
899
1433
  iterations += 1;
@@ -983,22 +1517,6 @@ function sumKeptDice(dice) {
983
1517
  return dice.filter((die) => !die.modifiers.includes("dropped")).reduce((sum, die) => sum + die.result, 0);
984
1518
  }
985
1519
 
986
- // src/evaluator/modifiers/compare.ts
987
- function matchesCondition(result, operator, value) {
988
- switch (operator) {
989
- case ">":
990
- return result > value;
991
- case ">=":
992
- return result >= value;
993
- case "<":
994
- return result < value;
995
- case "<=":
996
- return result <= value;
997
- case "=":
998
- return result === value;
999
- }
1000
- }
1001
-
1002
1520
  // src/evaluator/modifiers/reroll.ts
1003
1521
  var DEFAULT_MAX_REROLL_ITERATIONS = 1000;
1004
1522
  function rollReplacement(sides, rng, env) {
@@ -1022,7 +1540,7 @@ function rollReplacement(sides, rng, env) {
1022
1540
  result,
1023
1541
  modifiers: [],
1024
1542
  critical: result === sides && sides > 1,
1025
- fumble: result === 1
1543
+ fumble: result === 1 && sides > 1
1026
1544
  };
1027
1545
  }
1028
1546
  function rerollLimitError(maxIterations) {
@@ -1047,17 +1565,13 @@ function applyRecursiveReroll(pool, operator, value, rng, env) {
1047
1565
  if (iterations >= env.maxRerollIterations) {
1048
1566
  throw rerollLimitError(env.maxRerollIterations);
1049
1567
  }
1050
- result.push({
1051
- ...current,
1052
- modifiers: [...stripSlotFlags(current.modifiers), "rerolled", "dropped"]
1053
- });
1568
+ current.modifiers = [...stripSlotFlags(current.modifiers), "rerolled", "dropped"];
1569
+ result.push(current);
1054
1570
  current = rollReplacement(current.sides, rng, env);
1055
1571
  iterations += 1;
1056
1572
  }
1057
- result.push({
1058
- ...current,
1059
- modifiers: [...stripSlotFlags(current.modifiers), "kept"]
1060
- });
1573
+ current.modifiers = [...stripSlotFlags(current.modifiers), "kept"];
1574
+ result.push(current);
1061
1575
  }
1062
1576
  return result;
1063
1577
  }
@@ -1069,25 +1583,25 @@ function applyRerollOnce(pool, operator, value, rng, env) {
1069
1583
  continue;
1070
1584
  }
1071
1585
  if (!matchesCondition(original.result, operator, value)) {
1072
- result.push({
1073
- ...original,
1074
- modifiers: [...stripSlotFlags(original.modifiers), "kept"]
1075
- });
1586
+ original.modifiers = [...stripSlotFlags(original.modifiers), "kept"];
1587
+ result.push(original);
1076
1588
  continue;
1077
1589
  }
1078
- result.push({
1079
- ...original,
1080
- modifiers: [...stripSlotFlags(original.modifiers), "rerolled", "dropped"]
1081
- });
1590
+ original.modifiers = [...stripSlotFlags(original.modifiers), "rerolled", "dropped"];
1591
+ result.push(original);
1082
1592
  const replacement = rollReplacement(original.sides, rng, env);
1083
- result.push({
1084
- ...replacement,
1085
- modifiers: [...stripSlotFlags(replacement.modifiers), "kept"]
1086
- });
1593
+ replacement.modifiers = [...stripSlotFlags(replacement.modifiers), "kept"];
1594
+ result.push(replacement);
1087
1595
  }
1088
1596
  return result;
1089
1597
  }
1090
1598
 
1599
+ // src/evaluator/modifiers/sort.ts
1600
+ function sortDice(dice, order) {
1601
+ const cmp = order === "ascending" ? (a, b) => a.result - b.result : (a, b) => b.result - a.result;
1602
+ return [...dice].sort(cmp);
1603
+ }
1604
+
1091
1605
  // src/evaluator/modifiers/success-count.ts
1092
1606
  function countSuccesses(dice, threshold, failThreshold) {
1093
1607
  let successes = 0;
@@ -1111,20 +1625,32 @@ function countSuccesses(dice, threshold, failThreshold) {
1111
1625
  // src/evaluator/evaluator.ts
1112
1626
  class EvaluatorError extends RollParserError {
1113
1627
  nodeType;
1628
+ start;
1629
+ end;
1114
1630
  constructor(message, code, nodeType) {
1115
1631
  super(message, code);
1116
1632
  this.name = "EvaluatorError";
1117
1633
  this.nodeType = nodeType ?? undefined;
1634
+ this.start = undefined;
1635
+ this.end = undefined;
1118
1636
  }
1119
1637
  }
1120
1638
  var DEFAULT_MAX_DICE = 1e4;
1639
+ function partSpan(node) {
1640
+ if (node.start === undefined || node.end === undefined)
1641
+ return {};
1642
+ return { start: node.start, end: node.end };
1643
+ }
1644
+ function toPublicSpecs(specs) {
1645
+ return specs.map((spec) => ({ kind: spec.modifier, selector: spec.selector, count: spec.count }));
1646
+ }
1121
1647
  function createDieResult(sides, result) {
1122
1648
  return {
1123
1649
  sides,
1124
1650
  result,
1125
1651
  modifiers: [],
1126
1652
  critical: result === sides && sides > 1,
1127
- fumble: result === 1
1653
+ fumble: result === 1 && sides > 1
1128
1654
  };
1129
1655
  }
1130
1656
  function createFateDieResult(result) {
@@ -1137,7 +1663,7 @@ function createFateDieResult(result) {
1137
1663
  };
1138
1664
  }
1139
1665
  function renderDice(dice) {
1140
- const parts = dice.map((die) => {
1666
+ const parts = dice.filter((die) => !die.modifiers.includes("meta")).map((die) => {
1141
1667
  if (die.modifiers.includes("dropped")) {
1142
1668
  return `~~${die.result}~~`;
1143
1669
  }
@@ -1151,10 +1677,33 @@ function renderDice(dice) {
1151
1677
  });
1152
1678
  return `[${parts.join(", ")}]`;
1153
1679
  }
1680
+ function mergeMetaRolls(parent, source) {
1681
+ for (const die of source.rolls) {
1682
+ parent.rolls.push({
1683
+ ...die,
1684
+ modifiers: [
1685
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
1686
+ "meta",
1687
+ "dropped"
1688
+ ]
1689
+ });
1690
+ }
1691
+ }
1154
1692
  function evalNode(node, rng, ctx, env) {
1693
+ try {
1694
+ return evalNodeInner(node, rng, ctx, env);
1695
+ } catch (error) {
1696
+ if (error instanceof EvaluatorError && error.start === undefined && node.start !== undefined) {
1697
+ error.start = node.start;
1698
+ error.end = node.end;
1699
+ }
1700
+ throw error;
1701
+ }
1702
+ }
1703
+ function evalNodeInner(node, rng, ctx, env) {
1155
1704
  switch (node.type) {
1156
1705
  case "Literal":
1157
- return evalLiteral(node.value, ctx);
1706
+ return evalLiteral(node, ctx);
1158
1707
  case "Dice":
1159
1708
  return evalDice(node, rng, ctx, env);
1160
1709
  case "FateDice":
@@ -1175,20 +1724,64 @@ function evalNode(node, rng, ctx, env) {
1175
1724
  return evalVersus(node, rng, ctx, env);
1176
1725
  case "FunctionCall":
1177
1726
  return evalFunctionCall(node, rng, ctx, env);
1727
+ case "Grouped":
1728
+ return evalGrouped(node, rng, ctx, env);
1729
+ case "Group":
1730
+ return evalGroup(node, rng, ctx, env);
1731
+ case "Sort":
1732
+ return evalSort(node, rng, ctx, env);
1733
+ case "CritThreshold":
1734
+ return evalCritThreshold(node, rng, ctx, env);
1735
+ case "Variable":
1736
+ return evalVariable(node, ctx, env);
1178
1737
  default: {
1179
1738
  const exhaustive = node;
1180
1739
  throw new EvaluatorError(`Unknown node type: ${exhaustive.type}`, "UNKNOWN_NODE_TYPE", exhaustive.type);
1181
1740
  }
1182
1741
  }
1183
1742
  }
1184
- function evalLiteral(value, ctx) {
1743
+ function evalLiteral(node, ctx) {
1744
+ const { value } = node;
1185
1745
  ctx.expressionParts.push(String(value));
1186
1746
  ctx.renderedParts.push(String(value));
1187
- return value;
1747
+ return { total: value, part: { type: "literal", value, total: value, ...partSpan(node) } };
1748
+ }
1749
+ function variableNeedsBraces(name) {
1750
+ return !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
1751
+ }
1752
+ function evalVariable(node, ctx, env) {
1753
+ const present = Object.hasOwn(env.context, node.name);
1754
+ if (!present) {
1755
+ if (env.onMissingVariable === "throw") {
1756
+ throw new EvaluatorError(`Undefined variable: ${node.name}`, "UNDEFINED_VARIABLE", "Variable");
1757
+ }
1758
+ const display2 = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1759
+ ctx.expressionParts.push("0");
1760
+ ctx.renderedParts.push(`${display2}[0]`);
1761
+ return {
1762
+ total: 0,
1763
+ part: { type: "variable", name: node.name, value: 0, total: 0, ...partSpan(node) }
1764
+ };
1765
+ }
1766
+ const value = env.context[node.name];
1767
+ if (!Number.isFinite(value)) {
1768
+ throw new EvaluatorError(`Invalid variable value: ${node.name} = ${value}`, "INVALID_VARIABLE_VALUE", "Variable");
1769
+ }
1770
+ const display = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1771
+ ctx.expressionParts.push(String(value));
1772
+ ctx.renderedParts.push(`${display}[${value}]`);
1773
+ return {
1774
+ total: value,
1775
+ part: { type: "variable", name: node.name, value, total: value, ...partSpan(node) }
1776
+ };
1188
1777
  }
1189
1778
  function evalDice(node, rng, ctx, env) {
1190
- const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1191
- const sides = evalNode(node.sides, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1779
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1780
+ const count = evalNode(node.count, rng, countCtx, env).total;
1781
+ mergeMetaRolls(ctx, countCtx);
1782
+ const sidesCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1783
+ const sides = evalNode(node.sides, rng, sidesCtx, env).total;
1784
+ mergeMetaRolls(ctx, sidesCtx);
1192
1785
  if (!Number.isInteger(count) || count < 0) {
1193
1786
  throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "Dice");
1194
1787
  }
@@ -1210,10 +1803,15 @@ function evalDice(node, rng, ctx, env) {
1210
1803
  const notation = `${count}d${sides}`;
1211
1804
  ctx.expressionParts.push(notation);
1212
1805
  ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1213
- return total;
1806
+ return {
1807
+ total,
1808
+ part: { type: "dice", count, sides, rolls: markedDice, total, ...partSpan(node) }
1809
+ };
1214
1810
  }
1215
1811
  function evalFateDice(node, rng, ctx, env) {
1216
- const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1812
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1813
+ const count = evalNode(node.count, rng, countCtx, env).total;
1814
+ mergeMetaRolls(ctx, countCtx);
1217
1815
  if (!Number.isInteger(count) || count < 0) {
1218
1816
  throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "FateDice");
1219
1817
  }
@@ -1232,21 +1830,51 @@ function evalFateDice(node, rng, ctx, env) {
1232
1830
  const notation = `${count}dF`;
1233
1831
  ctx.expressionParts.push(notation);
1234
1832
  ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1235
- return total;
1833
+ return {
1834
+ total,
1835
+ part: { type: "fateDice", count, rolls: markedDice, total, ...partSpan(node) }
1836
+ };
1837
+ }
1838
+ function propagateMetadata(parent, metadata) {
1839
+ if (!metadata)
1840
+ return;
1841
+ if (parent.versusMetadata) {
1842
+ throw new EvaluatorError("Multiple versus operators in the same expression", "NESTED_VERSUS", "Versus");
1843
+ }
1844
+ parent.versusMetadata = metadata;
1845
+ }
1846
+ function mergeContext(parent, child) {
1847
+ parent.rolls.push(...child.rolls);
1848
+ propagateMetadata(parent, child.versusMetadata);
1236
1849
  }
1237
1850
  function evalBinaryOp(node, rng, ctx, env) {
1238
1851
  const leftCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1239
1852
  const rightCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1240
1853
  const left = evalNode(node.left, rng, leftCtx, env);
1241
1854
  const right = evalNode(node.right, rng, rightCtx, env);
1242
- ctx.rolls.push(...leftCtx.rolls, ...rightCtx.rolls);
1855
+ mergeContext(ctx, leftCtx);
1856
+ mergeContext(ctx, rightCtx);
1243
1857
  const leftExpr = leftCtx.expressionParts.join("");
1244
1858
  const rightExpr = rightCtx.expressionParts.join("");
1245
1859
  const leftRendered = leftCtx.renderedParts.join("");
1246
1860
  const rightRendered = rightCtx.renderedParts.join("");
1247
1861
  ctx.expressionParts.push(`${leftExpr} ${node.operator} ${rightExpr}`);
1248
1862
  ctx.renderedParts.push(`${leftRendered} ${node.operator} ${rightRendered}`);
1249
- switch (node.operator) {
1863
+ const total = applyBinaryOperator(node.operator, left.total, right.total);
1864
+ return {
1865
+ total,
1866
+ part: {
1867
+ type: "binaryOp",
1868
+ operator: node.operator,
1869
+ left: left.part,
1870
+ right: right.part,
1871
+ total,
1872
+ ...partSpan(node)
1873
+ }
1874
+ };
1875
+ }
1876
+ function applyBinaryOperator(operator, left, right) {
1877
+ switch (operator) {
1250
1878
  case "+":
1251
1879
  return left + right;
1252
1880
  case "-":
@@ -1266,37 +1894,80 @@ function evalBinaryOp(node, rng, ctx, env) {
1266
1894
  case "**":
1267
1895
  return left ** right;
1268
1896
  default: {
1269
- const exhaustive = node.operator;
1897
+ const exhaustive = operator;
1270
1898
  throw new EvaluatorError(`Unknown operator: ${exhaustive}`, "UNKNOWN_OPERATOR", "BinaryOp");
1271
1899
  }
1272
1900
  }
1273
1901
  }
1274
1902
  function evalUnaryOp(node, rng, ctx, env) {
1275
1903
  const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1276
- const value = evalNode(node.operand, rng, innerCtx, env);
1277
- ctx.rolls.push(...innerCtx.rolls);
1904
+ const inner = evalNode(node.operand, rng, innerCtx, env);
1905
+ mergeContext(ctx, innerCtx);
1278
1906
  const innerExpr = innerCtx.expressionParts.join("");
1279
1907
  const innerRendered = innerCtx.renderedParts.join("");
1280
1908
  ctx.expressionParts.push(`-${innerExpr}`);
1281
1909
  ctx.renderedParts.push(`-${innerRendered}`);
1282
- return -value;
1910
+ const total = -inner.total;
1911
+ return {
1912
+ total,
1913
+ part: { type: "unaryOp", operator: "-", operand: inner.part, total, ...partSpan(node) }
1914
+ };
1915
+ }
1916
+ function evalGrouped(node, rng, ctx, env) {
1917
+ const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1918
+ const inner = evalNode(node.expression, rng, innerCtx, env);
1919
+ mergeContext(ctx, innerCtx);
1920
+ ctx.expressionParts.push(`(${innerCtx.expressionParts.join("")})`);
1921
+ ctx.renderedParts.push(`(${innerCtx.renderedParts.join("")})`);
1922
+ return {
1923
+ total: inner.total,
1924
+ part: { type: "grouped", inner: inner.part, total: inner.total, ...partSpan(node) }
1925
+ };
1926
+ }
1927
+ function evalGroup(node, rng, ctx, env) {
1928
+ const subExprs = [];
1929
+ const subRendered = [];
1930
+ const subParts = [];
1931
+ let total = 0;
1932
+ for (const expr of node.expressions) {
1933
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1934
+ const sub = evalNode(expr, rng, subCtx, env);
1935
+ mergeContext(ctx, subCtx);
1936
+ subExprs.push(subCtx.expressionParts.join(""));
1937
+ subRendered.push(subCtx.renderedParts.join(""));
1938
+ subParts.push(sub.part);
1939
+ total += sub.total;
1940
+ }
1941
+ ctx.expressionParts.push(`{${subExprs.join(", ")}}`);
1942
+ ctx.renderedParts.push(`{${subRendered.join(", ")}}`);
1943
+ return { total, part: { type: "group", parts: subParts, total, ...partSpan(node) } };
1283
1944
  }
1284
1945
  function evalFunctionCall(node, rng, ctx, env) {
1285
1946
  const argCtxs = [];
1286
- const values = [];
1947
+ const argResults = [];
1287
1948
  for (const arg of node.args) {
1288
1949
  const argCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1289
- values.push(evalNode(arg, rng, argCtx, env));
1950
+ argResults.push(evalNode(arg, rng, argCtx, env));
1290
1951
  argCtxs.push(argCtx);
1291
1952
  }
1292
1953
  for (const argCtx of argCtxs) {
1293
- ctx.rolls.push(...argCtx.rolls);
1954
+ mergeContext(ctx, argCtx);
1294
1955
  }
1295
1956
  const argExprs = argCtxs.map((c) => c.expressionParts.join(""));
1296
1957
  const argRendereds = argCtxs.map((c) => c.renderedParts.join(""));
1297
1958
  ctx.expressionParts.push(`${node.name}(${argExprs.join(", ")})`);
1298
1959
  ctx.renderedParts.push(`${node.name}(${argRendereds.join(", ")})`);
1299
- return applyFunction(node.name, values);
1960
+ const total = applyFunction(node.name, argResults.map((r) => r.total));
1961
+ return {
1962
+ total,
1963
+ part: {
1964
+ type: "functionCall",
1965
+ name: node.name,
1966
+ args: argResults.map((r) => r.part),
1967
+ total,
1968
+ ...partSpan(node)
1969
+ }
1970
+ };
1300
1971
  }
1301
1972
  function applyFunction(name, values) {
1302
1973
  switch (name) {
@@ -1323,12 +1994,13 @@ function requireUnaryArg(name, values) {
1323
1994
  }
1324
1995
  return x;
1325
1996
  }
1326
- function flattenModifierChain(node, rng, env) {
1997
+ function flattenModifierChain(node, rng, ctx, env) {
1327
1998
  const specs = [];
1328
1999
  let current = node;
1329
2000
  while (isModifier(current)) {
1330
2001
  const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1331
- const modCount = evalNode(current.count, rng, countCtx, env);
2002
+ const modCount = evalNode(current.count, rng, countCtx, env).total;
2003
+ mergeMetaRolls(ctx, countCtx);
1332
2004
  if (!Number.isInteger(modCount) || modCount < 0) {
1333
2005
  throw new EvaluatorError(`Invalid modifier count: ${modCount}`, "INVALID_MODIFIER_COUNT", "Modifier");
1334
2006
  }
@@ -1355,10 +2027,10 @@ function mergeDropSets(baseDice, specs) {
1355
2027
  }
1356
2028
  }
1357
2029
  }
1358
- return baseDice.map((die, index) => ({
1359
- ...die,
1360
- modifiers: droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"]
1361
- }));
2030
+ baseDice.forEach((die, index) => {
2031
+ die.modifiers = droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"];
2032
+ });
2033
+ return baseDice;
1362
2034
  }
1363
2035
  function formatExplodeCode(variant, threshold, thresholdValue) {
1364
2036
  const marker = variant === "standard" ? "!" : variant === "compound" ? "!!" : "!p";
@@ -1368,48 +2040,154 @@ function formatExplodeCode(variant, threshold, thresholdValue) {
1368
2040
  }
1369
2041
  function evalExplode(node, rng, ctx, env) {
1370
2042
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1371
- const targetValue = evalNode(node.target, rng, targetCtx, env);
2043
+ const target = evalNode(node.target, rng, targetCtx, env);
1372
2044
  const targetExpr = targetCtx.expressionParts.join("");
1373
2045
  let thresholdValue;
1374
2046
  if (node.threshold != null) {
1375
2047
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1376
- thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env);
2048
+ thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env).total;
2049
+ mergeMetaRolls(ctx, thresholdCtx);
1377
2050
  }
1378
2051
  const code = formatExplodeCode(node.variant, node.threshold, thresholdValue);
2052
+ const buildPart = (total2) => {
2053
+ const part = {
2054
+ type: "explode",
2055
+ variant: node.variant,
2056
+ target: target.part,
2057
+ total: total2,
2058
+ ...partSpan(node)
2059
+ };
2060
+ if (node.threshold != null && thresholdValue !== undefined) {
2061
+ part.threshold = { operator: node.threshold.operator, value: thresholdValue };
2062
+ }
2063
+ return part;
2064
+ };
1379
2065
  if (targetCtx.rolls.length === 0) {
1380
2066
  ctx.expressionParts.push(`${targetExpr}${code}`);
1381
2067
  ctx.renderedParts.push(`${targetExpr}${code}`);
1382
- return targetValue;
2068
+ return { total: target.total, part: buildPart(target.total) };
1383
2069
  }
1384
2070
  const shouldExplode = buildShouldExplode(node.threshold?.operator, thresholdValue);
1385
2071
  const expanded = node.variant === "standard" ? applyStandardExplode(targetCtx.rolls, shouldExplode, rng, env) : node.variant === "compound" ? applyCompoundExplode(targetCtx.rolls, shouldExplode, rng, env) : applyPenetratingExplode(targetCtx.rolls, shouldExplode, rng, env);
1386
2072
  ctx.rolls.push(...expanded);
1387
2073
  ctx.expressionParts.push(`${targetExpr}${code}`);
1388
2074
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(expanded)}`);
1389
- return sumKeptDice(expanded);
2075
+ const total = sumKeptDice(expanded);
2076
+ return { total, part: buildPart(total) };
1390
2077
  }
1391
2078
  function evalReroll(node, rng, ctx, env) {
1392
2079
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1393
- evalNode(node.target, rng, targetCtx, env);
2080
+ const target = evalNode(node.target, rng, targetCtx, env);
1394
2081
  const targetExpr = targetCtx.expressionParts.join("");
1395
2082
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1396
- const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env);
2083
+ const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env).total;
2084
+ mergeMetaRolls(ctx, thresholdCtx);
1397
2085
  const code = `${node.once ? "ro" : "r"}${node.condition.operator}${thresholdValue}`;
2086
+ const condition = {
2087
+ operator: node.condition.operator,
2088
+ value: thresholdValue
2089
+ };
1398
2090
  if (targetCtx.rolls.length === 0) {
1399
2091
  ctx.expressionParts.push(`${targetExpr}${code}`);
1400
2092
  ctx.renderedParts.push(`${targetExpr}${code}`);
1401
- return sumKeptDice(targetCtx.rolls);
2093
+ const total2 = sumKeptDice(targetCtx.rolls);
2094
+ return {
2095
+ total: total2,
2096
+ part: {
2097
+ type: "reroll",
2098
+ once: node.once,
2099
+ condition,
2100
+ target: target.part,
2101
+ total: total2,
2102
+ ...partSpan(node)
2103
+ }
2104
+ };
1402
2105
  }
1403
2106
  const pool = node.once ? applyRerollOnce(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env) : applyRecursiveReroll(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env);
1404
2107
  ctx.rolls.push(...pool);
1405
2108
  ctx.expressionParts.push(`${targetExpr}${code}`);
1406
2109
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(pool)}`);
1407
- return sumKeptDice(pool);
2110
+ const total = sumKeptDice(pool);
2111
+ return {
2112
+ total,
2113
+ part: {
2114
+ type: "reroll",
2115
+ once: node.once,
2116
+ condition,
2117
+ target: target.part,
2118
+ total,
2119
+ ...partSpan(node)
2120
+ }
2121
+ };
2122
+ }
2123
+ function evalSort(node, rng, ctx, env) {
2124
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2125
+ const target = evalNode(node.target, rng, targetCtx, env);
2126
+ const sortedRolls = sortDice(targetCtx.rolls, node.order);
2127
+ ctx.rolls.push(...sortedRolls);
2128
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2129
+ const code = node.order === "ascending" ? "s" : "sd";
2130
+ const targetExpr = targetCtx.expressionParts.join("");
2131
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2132
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(sortedRolls)}`);
2133
+ return {
2134
+ total: target.total,
2135
+ part: {
2136
+ type: "sort",
2137
+ order: node.order,
2138
+ target: target.part,
2139
+ total: target.total,
2140
+ ...partSpan(node)
2141
+ }
2142
+ };
2143
+ }
2144
+ function evalCritThreshold(node, rng, ctx, env) {
2145
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2146
+ const target = evalNode(node.target, rng, targetCtx, env);
2147
+ const successResolved = node.successThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2148
+ const failResolved = node.failThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2149
+ const successApplied = successResolved.length > 0 ? successResolved : ["default"];
2150
+ const failApplied = failResolved.length > 0 ? failResolved : ["default"];
2151
+ applyCritThresholds(targetCtx.rolls, successApplied, failApplied);
2152
+ ctx.rolls.push(...targetCtx.rolls);
2153
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2154
+ const targetExpr = targetCtx.expressionParts.join("");
2155
+ const codes = [
2156
+ ...successResolved.map((t) => t === "default" ? "cs" : `cs${t.operator}${t.value}`),
2157
+ ...failResolved.map((t) => t === "default" ? "cf" : `cf${t.operator}${t.value}`)
2158
+ ].join("");
2159
+ ctx.expressionParts.push(`${targetExpr}${codes}`);
2160
+ ctx.renderedParts.push(`${targetExpr}${codes}${renderDice(targetCtx.rolls)}`);
2161
+ return {
2162
+ total: target.total,
2163
+ part: {
2164
+ type: "critThreshold",
2165
+ successThresholds: successResolved,
2166
+ failThresholds: failResolved,
2167
+ target: target.part,
2168
+ total: target.total,
2169
+ ...partSpan(node)
2170
+ }
2171
+ };
2172
+ }
2173
+ function resolveCritThreshold(threshold, rng, ctx, env) {
2174
+ if (threshold === "default")
2175
+ return "default";
2176
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2177
+ const resolved = evalNode(threshold.value, rng, thresholdCtx, env).total;
2178
+ mergeMetaRolls(ctx, thresholdCtx);
2179
+ if (!Number.isFinite(resolved)) {
2180
+ throw new EvaluatorError(`Invalid crit threshold: ${resolved}`, "INVALID_THRESHOLD", "CritThreshold");
2181
+ }
2182
+ return { operator: threshold.operator, value: resolved };
1408
2183
  }
1409
2184
  function evalModifier(node, rng, ctx, env) {
1410
- const { specs, baseTarget } = flattenModifierChain(node, rng, env);
2185
+ const { specs, baseTarget } = flattenModifierChain(node, rng, ctx, env);
2186
+ if (baseTarget.type === "Group" && baseTarget.expressions.length >= 2) {
2187
+ return evalGroupModifier(node, baseTarget, specs, rng, ctx, env);
2188
+ }
1411
2189
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1412
- evalNode(baseTarget, rng, targetCtx, env);
2190
+ const target = evalNode(baseTarget, rng, targetCtx, env);
1413
2191
  const mergedDice = mergeDropSets(targetCtx.rolls, specs);
1414
2192
  ctx.rolls.push(...mergedDice);
1415
2193
  const total = sumKeptDice(mergedDice);
@@ -1417,38 +2195,141 @@ function evalModifier(node, rng, ctx, env) {
1417
2195
  const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
1418
2196
  ctx.expressionParts.push(`${targetExpr}${modifierCodes}`);
1419
2197
  ctx.renderedParts.push(`${targetExpr}${renderDice(mergedDice)}`);
1420
- return total;
2198
+ return {
2199
+ total,
2200
+ part: {
2201
+ type: "modifier",
2202
+ specs: toPublicSpecs(specs),
2203
+ target: target.part,
2204
+ total,
2205
+ ...partSpan(node)
2206
+ }
2207
+ };
2208
+ }
2209
+ function stripInnerMarkers(rendered) {
2210
+ return rendered.replace(/\*\*(-?\d+)\*\*/g, "$1").replace(/__(-?\d+)__/g, "$1").replace(/~~(-?\d+)~~/g, "$1");
2211
+ }
2212
+ function evalGroupModifier(node, group, specs, rng, ctx, env) {
2213
+ const subRolls = group.expressions.map((expr) => {
2214
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2215
+ const sub = evalNode(expr, rng, subCtx, env);
2216
+ return {
2217
+ subtotal: sub.total,
2218
+ part: sub.part,
2219
+ rolls: subCtx.rolls,
2220
+ expr: subCtx.expressionParts.join(""),
2221
+ rendered: subCtx.renderedParts.join(""),
2222
+ versusMetadata: subCtx.versusMetadata
2223
+ };
2224
+ });
2225
+ const syntheticDice = subRolls.map((sub) => ({
2226
+ sides: 0,
2227
+ result: sub.subtotal,
2228
+ modifiers: [],
2229
+ critical: false,
2230
+ fumble: false
2231
+ }));
2232
+ const mergedSynthetic = mergeDropSets(syntheticDice, specs);
2233
+ const outerRendered = [];
2234
+ const keptIndices = [];
2235
+ let total = 0;
2236
+ for (let i = 0;i < subRolls.length; i++) {
2237
+ const sub = subRolls[i];
2238
+ const synth = mergedSynthetic[i];
2239
+ const isDropped = synth.modifiers.includes("dropped");
2240
+ if (isDropped) {
2241
+ for (const die of sub.rolls) {
2242
+ die.modifiers = [
2243
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
2244
+ "dropped"
2245
+ ];
2246
+ }
2247
+ ctx.rolls.push(...sub.rolls);
2248
+ outerRendered.push(`~~${stripInnerMarkers(sub.rendered)}~~`);
2249
+ } else {
2250
+ keptIndices.push(i);
2251
+ ctx.rolls.push(...sub.rolls);
2252
+ outerRendered.push(sub.rendered);
2253
+ total += sub.subtotal;
2254
+ }
2255
+ if (!isDropped) {
2256
+ propagateMetadata(ctx, sub.versusMetadata);
2257
+ }
2258
+ }
2259
+ const subExprStrs = subRolls.map((s) => s.expr);
2260
+ const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
2261
+ ctx.expressionParts.push(`{${subExprStrs.join(", ")}}${modifierCodes}`);
2262
+ ctx.renderedParts.push(`{${outerRendered.join(", ")}}`);
2263
+ const groupPart = {
2264
+ type: "group",
2265
+ parts: subRolls.map((s) => s.part),
2266
+ keptIndices,
2267
+ total,
2268
+ ...partSpan(group)
2269
+ };
2270
+ return {
2271
+ total,
2272
+ part: {
2273
+ type: "modifier",
2274
+ specs: toPublicSpecs(specs),
2275
+ target: groupPart,
2276
+ total,
2277
+ ...partSpan(node)
2278
+ }
2279
+ };
1421
2280
  }
1422
- function resolveThreshold(value, rng, env, role) {
2281
+ function resolveThreshold(value, rng, ctx, env, role) {
1423
2282
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1424
- const resolved = evalNode(value, rng, thresholdCtx, env);
2283
+ const resolved = evalNode(value, rng, thresholdCtx, env).total;
2284
+ mergeMetaRolls(ctx, thresholdCtx);
1425
2285
  if (!Number.isFinite(resolved)) {
1426
2286
  throw new EvaluatorError(`Invalid ${role}: ${resolved}`, "INVALID_THRESHOLD", "SuccessCount");
1427
2287
  }
1428
2288
  return resolved;
1429
2289
  }
1430
2290
  function evalSuccessCount(node, rng, ctx, env) {
2291
+ env.hasSuccessCount = true;
1431
2292
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1432
- const targetValue = evalNode(node.target, rng, targetCtx, env);
2293
+ const target = evalNode(node.target, rng, targetCtx, env);
1433
2294
  const targetExpr = targetCtx.expressionParts.join("");
1434
- const thresholdValue = resolveThreshold(node.threshold.value, rng, env, "threshold");
1435
- const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, env, "fail threshold") : undefined;
2295
+ const thresholdValue = resolveThreshold(node.threshold.value, rng, ctx, env, "threshold");
2296
+ const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, ctx, env, "fail threshold") : undefined;
1436
2297
  const code = `${node.threshold.operator}${thresholdValue}${failValue != null ? `f${failValue}` : ""}`;
2298
+ const buildPart = (total, successes, failures) => {
2299
+ const part = {
2300
+ type: "successCount",
2301
+ threshold: { operator: node.threshold.operator, value: thresholdValue },
2302
+ target: target.part,
2303
+ successes,
2304
+ failures,
2305
+ total,
2306
+ ...partSpan(node)
2307
+ };
2308
+ if (failValue != null && node.failThreshold != null) {
2309
+ part.failThreshold = { operator: node.failThreshold.operator, value: failValue };
2310
+ }
2311
+ return part;
2312
+ };
1437
2313
  if (targetCtx.rolls.length === 0) {
1438
2314
  ctx.expressionParts.push(`${targetExpr}${code}`);
1439
2315
  ctx.renderedParts.push(`${targetExpr}${code}`);
1440
- return targetValue;
2316
+ return { total: target.total, part: buildPart(target.total, 0, 0) };
1441
2317
  }
1442
2318
  const result = countSuccesses(targetCtx.rolls, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null ? { operator: node.failThreshold.operator, value: failValue } : undefined);
1443
- env.hasSuccessCount = true;
1444
2319
  ctx.rolls.push(...targetCtx.rolls);
1445
2320
  ctx.expressionParts.push(`${targetExpr}${code}`);
1446
2321
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(targetCtx.rolls)}`);
1447
- return result.total;
2322
+ return {
2323
+ total: result.total,
2324
+ part: buildPart(result.total, result.successes, result.failures)
2325
+ };
1448
2326
  }
1449
2327
  function extractNatural(rolls) {
1450
- const keptD20s = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !d.modifiers.includes("rerolled"));
1451
- return keptD20s.length === 1 ? keptD20s[0]?.result : undefined;
2328
+ const primaries = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !(d.modifiers.includes("exploded") && d.initialResult === undefined));
2329
+ if (primaries.length !== 1)
2330
+ return;
2331
+ const die = primaries[0];
2332
+ return die?.initialResult ?? die?.result;
1452
2333
  }
1453
2334
  function calculateDegree(total, dc, natural) {
1454
2335
  let degree;
@@ -1485,11 +2366,11 @@ function evalVersus(node, rng, ctx, env) {
1485
2366
  env.insideVersus = true;
1486
2367
  try {
1487
2368
  const rollCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1488
- const rollTotal = evalNode(node.roll, rng, rollCtx, env);
2369
+ const rollResult = evalNode(node.roll, rng, rollCtx, env);
1489
2370
  const natural = extractNatural(rollCtx.rolls);
1490
2371
  const dcCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1491
- const dcTotal = evalNode(node.dc, rng, dcCtx, env);
1492
- const degree = calculateDegree(rollTotal, dcTotal, natural);
2372
+ const dcResult = evalNode(node.dc, rng, dcCtx, env);
2373
+ const degree = calculateDegree(rollResult.total, dcResult.total, natural);
1493
2374
  ctx.rolls.push(...rollCtx.rolls, ...dcCtx.rolls);
1494
2375
  const rollExpr = rollCtx.expressionParts.join("");
1495
2376
  const dcExpr = dcCtx.expressionParts.join("");
@@ -1497,8 +2378,18 @@ function evalVersus(node, rng, ctx, env) {
1497
2378
  const dcRendered = dcCtx.renderedParts.join("");
1498
2379
  ctx.expressionParts.push(`${rollExpr} vs ${dcExpr}`);
1499
2380
  ctx.renderedParts.push(`${rollRendered} vs ${dcRendered}`);
1500
- ctx.versusMetadata = { degree, natural, dcTotal };
1501
- return rollTotal;
2381
+ ctx.versusMetadata = { degree, natural, dcTotal: dcResult.total };
2382
+ return {
2383
+ total: rollResult.total,
2384
+ part: {
2385
+ type: "versus",
2386
+ roll: rollResult.part,
2387
+ dc: dcResult.part,
2388
+ degree,
2389
+ total: rollResult.total,
2390
+ ...partSpan(node)
2391
+ }
2392
+ };
1502
2393
  } finally {
1503
2394
  env.insideVersus = false;
1504
2395
  }
@@ -1507,20 +2398,27 @@ function evaluate(ast, rng, options = {}) {
1507
2398
  const maxDice = options.maxDice != null && Number.isFinite(options.maxDice) && options.maxDice > 0 ? Math.floor(options.maxDice) : DEFAULT_MAX_DICE;
1508
2399
  const maxExplodeIterations = options.maxExplodeIterations != null && Number.isFinite(options.maxExplodeIterations) && options.maxExplodeIterations >= 0 ? Math.floor(options.maxExplodeIterations) : DEFAULT_MAX_EXPLODE_ITERATIONS;
1509
2400
  const maxRerollIterations = options.maxRerollIterations != null && Number.isFinite(options.maxRerollIterations) && options.maxRerollIterations >= 0 ? Math.floor(options.maxRerollIterations) : DEFAULT_MAX_REROLL_ITERATIONS;
2401
+ const context = options.context ?? {};
2402
+ const onMissingVariable = options.onMissingVariable ?? "throw";
1510
2403
  const env = {
1511
2404
  maxDice,
1512
2405
  maxExplodeIterations,
1513
2406
  maxRerollIterations,
1514
2407
  totalDiceRolled: 0,
1515
2408
  hasSuccessCount: false,
1516
- insideVersus: false
2409
+ insideVersus: false,
2410
+ context,
2411
+ onMissingVariable
1517
2412
  };
1518
2413
  const ctx = {
1519
2414
  rolls: [],
1520
2415
  expressionParts: [],
1521
2416
  renderedParts: []
1522
2417
  };
1523
- const total = evalNode(ast, rng, ctx, env);
2418
+ const { total, part } = evalNode(ast, rng, ctx, env);
2419
+ if (!Number.isFinite(total)) {
2420
+ throw new EvaluatorError(`Result is not a finite number: ${total}`, "NON_FINITE_RESULT", ast.type);
2421
+ }
1524
2422
  const expression = ctx.expressionParts.join("");
1525
2423
  const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
1526
2424
  const rendered = `${ctx.renderedParts.join("")} = ${trailing}`;
@@ -1529,7 +2427,8 @@ function evaluate(ast, rng, options = {}) {
1529
2427
  notation: options.notation ?? expression,
1530
2428
  expression,
1531
2429
  rendered,
1532
- rolls: ctx.rolls
2430
+ rolls: ctx.rolls,
2431
+ parts: part
1533
2432
  };
1534
2433
  if (env.hasSuccessCount) {
1535
2434
  let successes = 0;
@@ -1566,100 +2465,18 @@ function roll(notation, options = {}) {
1566
2465
  if (options.maxRerollIterations != null) {
1567
2466
  evalOptions.maxRerollIterations = options.maxRerollIterations;
1568
2467
  }
2468
+ if (options.context != null)
2469
+ evalOptions.context = options.context;
2470
+ if (options.onMissingVariable != null) {
2471
+ evalOptions.onMissingVariable = options.onMissingVariable;
2472
+ }
1569
2473
  return evaluate(ast, rng, evalOptions);
1570
2474
  }
1571
2475
  // package.json
1572
- var package_default = {
1573
- name: "roll-parser",
1574
- version: "3.0.0-alpha.0",
1575
- description: "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun-optimized.",
1576
- type: "module",
1577
- main: "./dist/index.js",
1578
- module: "./dist/index.mjs",
1579
- types: "./dist/index.d.ts",
1580
- bin: { "roll-parser": "./dist/cli.js" },
1581
- sideEffects: false,
1582
- exports: {
1583
- ".": {
1584
- types: "./dist/index.d.ts",
1585
- import: "./dist/index.mjs",
1586
- require: "./dist/index.js"
1587
- },
1588
- "./testing": {
1589
- types: "./dist/testing.d.ts",
1590
- import: "./dist/testing.mjs",
1591
- require: "./dist/testing.js"
1592
- }
1593
- },
1594
- files: [
1595
- "dist",
1596
- "src",
1597
- "!src/**/*.test.ts"
1598
- ],
1599
- keywords: [
1600
- "parser",
1601
- "dice",
1602
- "roll",
1603
- "rpg",
1604
- "dnd",
1605
- "d20",
1606
- "pratt-parser",
1607
- "typescript",
1608
- "bun"
1609
- ],
1610
- homepage: "https://github.com/edloidas/roll-parser#readme",
1611
- bugs: {
1612
- url: "https://github.com/edloidas/roll-parser/issues"
1613
- },
1614
- license: "MIT",
1615
- author: "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.com)",
1616
- repository: {
1617
- type: "git",
1618
- url: "git+https://github.com/edloidas/roll-parser.git"
1619
- },
1620
- publishConfig: {
1621
- access: "public"
1622
- },
1623
- scripts: {
1624
- typecheck: "tsc --noEmit",
1625
- lint: "biome lint .",
1626
- "lint:fix": "biome lint --write .",
1627
- format: "biome format --write .",
1628
- "format:check": "biome format .",
1629
- check: "bun typecheck && bun lint && bun format:check",
1630
- "check:fix": "bun typecheck && bun lint:fix && bun format",
1631
- "check:version": "bun scripts/check-version.ts",
1632
- "check:changelog": "bun scripts/check-changelog.ts",
1633
- clean: "rm -rf dist coverage",
1634
- build: "bun run clean && bun run build:esm && bun run build:cjs && bun run build:esm:testing && bun run build:cjs:testing && bun run build:cli && bun run build:types",
1635
- "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target bun",
1636
- "build:cjs": "bun build src/index.ts --outfile dist/index.js --target node",
1637
- "build:esm:testing": "bun build src/testing.ts --outfile dist/testing.mjs --target bun",
1638
- "build:cjs:testing": "bun build src/testing.ts --outfile dist/testing.js --target node",
1639
- "build:cli": "bun build src/cli/index.ts --outfile dist/cli.js --target node",
1640
- "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
1641
- test: "bun test",
1642
- "test:node": `node -e "import('./dist/index.mjs').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })" && node -e "const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('CJS smoke failed: total=' + r.total); console.log('Node.js CJS smoke: ok');"`,
1643
- "test:watch": "bun test --watch",
1644
- "test:ci": "bun test --bail",
1645
- validate: "bun run check && bun run build && bun test:ci",
1646
- coverage: "bun test --coverage",
1647
- "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
1648
- prepublishOnly: "bun run release:dry"
1649
- },
1650
- devDependencies: {
1651
- "@biomejs/biome": "^2.4.9",
1652
- "@types/bun": "^1.3.0",
1653
- "fast-check": "^4.6.0",
1654
- typescript: "^6.0.2"
1655
- },
1656
- engines: {
1657
- node: ">= 22.0.0"
1658
- }
1659
- };
2476
+ var version = "3.0.0-beta.0";
1660
2477
 
1661
2478
  // src/index.ts
1662
- var VERSION = package_default.version;
2479
+ var VERSION = version;
1663
2480
 
1664
2481
  // src/cli/args.ts
1665
2482
  function parseArgs(argv) {
@@ -1691,7 +2508,7 @@ function parseArgs(argv) {
1691
2508
  seed = value;
1692
2509
  } else if (arg.startsWith("--")) {
1693
2510
  return { ok: false, error: `Unknown option: ${arg}` };
1694
- } else if (arg.startsWith("-") && arg.length > 1 && !/^\d/.test(arg.slice(1))) {
2511
+ } else if (arg.startsWith("-") && arg.length > 1 && !/^[\ddD(]/.test(arg.slice(1))) {
1695
2512
  return { ok: false, error: `Unknown option: ${arg}` };
1696
2513
  } else {
1697
2514
  positional.push(arg);
@@ -1709,7 +2526,7 @@ function formatResult(result, verbose) {
1709
2526
  return formatRendered(result.rendered);
1710
2527
  }
1711
2528
  function formatRendered(rendered) {
1712
- return rendered.replace(/~~(-?\d+)~~/g, "($1)").replace(/\*\*(-?\d+)\*\*/g, "[$1]").replace(/__(-?\d+)__/g, "{$1}");
2529
+ return rendered.replace(/\*\*(-?\d+)\*\*/g, "[$1]").replace(/__(-?\d+)__/g, "{$1}").replace(/~~([^~]+)~~/g, "($1)");
1713
2530
  }
1714
2531
 
1715
2532
  // src/cli/index.ts
@@ -1728,6 +2545,18 @@ Examples:
1728
2545
  roll-parser 4d6kh3 --verbose
1729
2546
  roll-parser 4d6dl1 --seed "character-str"
1730
2547
  `;
2548
+ function writeErrorContext(notation, error) {
2549
+ const position = "position" in error ? error.position : ("start" in error) ? error.start : undefined;
2550
+ if (typeof position !== "number" || !Number.isInteger(position))
2551
+ return;
2552
+ if (notation.includes(`
2553
+ `) || position < 0 || position > notation.length)
2554
+ return;
2555
+ process.stderr.write(` ${notation}
2556
+ `);
2557
+ process.stderr.write(` ${" ".repeat(position)}^
2558
+ `);
2559
+ }
1731
2560
  function main() {
1732
2561
  const parsed = parseArgs(process.argv.slice(2));
1733
2562
  if (!parsed.ok) {
@@ -1766,6 +2595,7 @@ function main() {
1766
2595
  if (isRollParserError(error)) {
1767
2596
  process.stderr.write(`Error: ${error.message}
1768
2597
  `);
2598
+ writeErrorContext(args.notation, error);
1769
2599
  process.exitCode = 1;
1770
2600
  return;
1771
2601
  }
@@ -1773,3 +2603,6 @@ function main() {
1773
2603
  }
1774
2604
  }
1775
2605
  main();
2606
+
2607
+ //# debugId=E5D058E5C568B60B64756E2164756E21
2608
+ //# sourceMappingURL=cli.js.map