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/index.js CHANGED
@@ -13,14 +13,23 @@ var ROLL_PARSER_ERROR_CODES = [
13
13
  "UNKNOWN_OPERATOR",
14
14
  "UNKNOWN_NODE_TYPE",
15
15
  "INVALID_MODIFIER_COUNT",
16
+ "INVALID_MODIFIER_TARGET",
16
17
  "EXPLODE_LIMIT_EXCEEDED",
17
18
  "INVALID_EXPLODE_TARGET",
18
19
  "REROLL_LIMIT_EXCEEDED",
20
+ "INVALID_REROLL_TARGET",
19
21
  "INVALID_SUCCESS_COUNT_TARGET",
22
+ "INVALID_SORT_TARGET",
23
+ "INVALID_CRIT_THRESHOLD_TARGET",
20
24
  "INVALID_THRESHOLD",
21
25
  "NESTED_VERSUS",
22
26
  "INVALID_FUNCTION_ARITY",
23
- "UNKNOWN_FUNCTION"
27
+ "UNKNOWN_FUNCTION",
28
+ "UNDEFINED_VARIABLE",
29
+ "INVALID_VARIABLE_VALUE",
30
+ "AMBIGUOUS_DICE_CHAIN",
31
+ "MAX_DEPTH_EXCEEDED",
32
+ "NON_FINITE_RESULT"
24
33
  ];
25
34
 
26
35
  class RollParserError extends Error {
@@ -70,7 +79,14 @@ var TokenType;
70
79
  TokenType2[TokenType2["FAIL"] = 27] = "FAIL";
71
80
  TokenType2[TokenType2["FUNCTION"] = 28] = "FUNCTION";
72
81
  TokenType2[TokenType2["VS"] = 29] = "VS";
73
- TokenType2[TokenType2["EOF"] = 30] = "EOF";
82
+ TokenType2[TokenType2["LBRACE"] = 30] = "LBRACE";
83
+ TokenType2[TokenType2["RBRACE"] = 31] = "RBRACE";
84
+ TokenType2[TokenType2["AT"] = 32] = "AT";
85
+ TokenType2[TokenType2["SORT_ASC"] = 33] = "SORT_ASC";
86
+ TokenType2[TokenType2["SORT_DESC"] = 34] = "SORT_DESC";
87
+ TokenType2[TokenType2["CRIT_SUCCESS"] = 35] = "CRIT_SUCCESS";
88
+ TokenType2[TokenType2["CRIT_FAIL"] = 36] = "CRIT_FAIL";
89
+ TokenType2[TokenType2["EOF"] = 37] = "EOF";
74
90
  })(TokenType ||= {});
75
91
 
76
92
  // src/lexer/lexer.ts
@@ -100,8 +116,23 @@ var IDENTIFIER_KEYWORDS = {
100
116
  round: 28 /* FUNCTION */,
101
117
  abs: 28 /* FUNCTION */,
102
118
  max: 28 /* FUNCTION */,
103
- min: 28 /* FUNCTION */
119
+ min: 28 /* FUNCTION */,
120
+ s: 33 /* SORT_ASC */,
121
+ sa: 33 /* SORT_ASC */,
122
+ sd: 34 /* SORT_DESC */,
123
+ cs: 35 /* CRIT_SUCCESS */,
124
+ cf: 36 /* CRIT_FAIL */
104
125
  };
126
+ function buildIdentifierHint(identifier) {
127
+ for (let length = identifier.length - 1;length >= 1; length--) {
128
+ const prefix = identifier.slice(0, length);
129
+ if (IDENTIFIER_KEYWORDS[prefix] == null)
130
+ continue;
131
+ const rest = identifier.slice(length);
132
+ return ` (did you mean '${prefix}' followed by '${rest}'? separate modifiers with a count or space, e.g. '${prefix}1${rest}')`;
133
+ }
134
+ return "";
135
+ }
105
136
 
106
137
  class Lexer {
107
138
  pos = 0;
@@ -114,7 +145,7 @@ class Lexer {
114
145
  while (true) {
115
146
  const token = this.nextToken();
116
147
  tokens.push(token);
117
- if (token.type === 30 /* EOF */)
148
+ if (token.type === 37 /* EOF */)
118
149
  break;
119
150
  }
120
151
  return tokens;
@@ -122,7 +153,7 @@ class Lexer {
122
153
  nextToken() {
123
154
  this.skipWhitespace();
124
155
  if (this.isAtEnd()) {
125
- return this.createToken(30 /* EOF */, "");
156
+ return this.createToken(37 /* EOF */, "");
126
157
  }
127
158
  const startPos = this.pos;
128
159
  const char = this.peek();
@@ -132,6 +163,9 @@ class Lexer {
132
163
  if (this.isAlpha(char)) {
133
164
  return this.scanIdentifier();
134
165
  }
166
+ if (char === "@") {
167
+ return this.scanAt();
168
+ }
135
169
  this.advance();
136
170
  switch (char) {
137
171
  case "+":
@@ -155,6 +189,10 @@ class Lexer {
155
189
  return this.createTokenAt(16 /* RPAREN */, char, startPos);
156
190
  case ",":
157
191
  return this.createTokenAt(17 /* COMMA */, char, startPos);
192
+ case "{":
193
+ return this.createTokenAt(30 /* LBRACE */, char, startPos);
194
+ case "}":
195
+ return this.createTokenAt(31 /* RBRACE */, char, startPos);
158
196
  case ">":
159
197
  if (this.match("=")) {
160
198
  return this.createTokenAt(11 /* GREATER_EQUAL */, ">=", startPos);
@@ -176,8 +214,11 @@ class Lexer {
176
214
  return this.createTokenAt(24 /* EXPLODE_PENETRATING */, "!p", startPos);
177
215
  }
178
216
  return this.createTokenAt(22 /* EXPLODE */, char, startPos);
179
- default:
180
- throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, char);
217
+ default: {
218
+ const codePoint = this.input.codePointAt(startPos);
219
+ const display = codePoint == null ? char : String.fromCodePoint(codePoint);
220
+ throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, display);
221
+ }
181
222
  }
182
223
  }
183
224
  skipWhitespace() {
@@ -221,7 +262,36 @@ class Lexer {
221
262
  if (tokenType != null) {
222
263
  return this.createTokenAt(tokenType, lower, startPos);
223
264
  }
224
- throw new LexerError("Unexpected identifier", "UNEXPECTED_IDENTIFIER", startPos, lower);
265
+ throw new LexerError(`Unexpected identifier${buildIdentifierHint(lower)}`, "UNEXPECTED_IDENTIFIER", startPos, lower);
266
+ }
267
+ scanAt() {
268
+ const startPos = this.pos;
269
+ this.advance();
270
+ let name;
271
+ if (!this.isAtEnd() && this.peek() === "{") {
272
+ this.advance();
273
+ const nameStart = this.pos;
274
+ while (!this.isAtEnd() && this.peek() !== "}" && this.peek() !== `
275
+ `) {
276
+ this.advance();
277
+ }
278
+ if (this.isAtEnd() || this.peek() !== "}") {
279
+ throw new LexerError("Unterminated @{...} variable", "UNEXPECTED_CHARACTER", startPos, "@");
280
+ }
281
+ name = this.input.slice(nameStart, this.pos);
282
+ this.advance();
283
+ } else {
284
+ const nameStart = this.pos;
285
+ if (this.isAtEnd() || !this.isIdentifierStart(this.peek())) {
286
+ throw new LexerError("Empty @ variable name", "UNEXPECTED_CHARACTER", startPos, "@");
287
+ }
288
+ this.advance();
289
+ while (!this.isAtEnd() && this.isIdentifierPart(this.peek())) {
290
+ this.advance();
291
+ }
292
+ name = this.input.slice(nameStart, this.pos);
293
+ }
294
+ return this.createTokenAt(32 /* AT */, name, startPos);
225
295
  }
226
296
  peek() {
227
297
  return this.input[this.pos] ?? "";
@@ -250,15 +320,21 @@ class Lexer {
250
320
  const c = char.toLowerCase();
251
321
  return c >= "a" && c <= "z";
252
322
  }
323
+ isIdentifierStart(char) {
324
+ return this.isAlpha(char) || char === "_";
325
+ }
326
+ isIdentifierPart(char) {
327
+ return this.isAlpha(char) || this.isDigit(char) || char === "_";
328
+ }
253
329
  isWhitespace(char) {
254
330
  return char === " " || char === "\t" || char === `
255
331
  ` || char === "\r";
256
332
  }
257
333
  createToken(type, value) {
258
- return { type, value, position: this.pos };
334
+ return { type, value, position: this.pos, end: this.pos };
259
335
  }
260
336
  createTokenAt(type, value, position) {
261
- return { type, value, position };
337
+ return { type, value, position, end: this.pos };
262
338
  }
263
339
  }
264
340
  function lex(input) {
@@ -298,26 +374,190 @@ function isVersus(node) {
298
374
  function isFunctionCall(node) {
299
375
  return node.type === "FunctionCall";
300
376
  }
301
- function containsDice(node) {
377
+ function isGrouped(node) {
378
+ return node.type === "Grouped";
379
+ }
380
+ function isVariable(node) {
381
+ return node.type === "Variable";
382
+ }
383
+ function isGroup(node) {
384
+ return node.type === "Group";
385
+ }
386
+ function isSort(node) {
387
+ return node.type === "Sort";
388
+ }
389
+ function isCritThreshold(node) {
390
+ return node.type === "CritThreshold";
391
+ }
392
+ function unwrapTransparent(node, kinds) {
393
+ let current = node;
394
+ while (true) {
395
+ switch (current.type) {
396
+ case "Grouped":
397
+ if (!kinds.includes("Grouped"))
398
+ return current;
399
+ current = current.expression;
400
+ break;
401
+ case "Modifier":
402
+ if (!kinds.includes("Modifier"))
403
+ return current;
404
+ current = current.target;
405
+ break;
406
+ case "Sort":
407
+ if (!kinds.includes("Sort"))
408
+ return current;
409
+ current = current.target;
410
+ break;
411
+ case "CritThreshold":
412
+ if (!kinds.includes("CritThreshold"))
413
+ return current;
414
+ current = current.target;
415
+ break;
416
+ default:
417
+ return current;
418
+ }
419
+ }
420
+ }
421
+ function containsDicePool(node) {
302
422
  switch (node.type) {
303
423
  case "Dice":
304
424
  case "FateDice":
425
+ case "Modifier":
426
+ case "Explode":
427
+ case "Reroll":
305
428
  return true;
306
- case "Literal":
429
+ case "Sort":
430
+ case "CritThreshold":
431
+ return containsDicePool(node.target);
432
+ case "Grouped":
433
+ return containsDicePool(node.expression);
434
+ case "Group":
435
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
436
+ default:
307
437
  return false;
438
+ }
439
+ }
440
+ function deepContainsDicePool(node) {
441
+ switch (node.type) {
442
+ case "Dice":
443
+ case "FateDice":
444
+ return true;
308
445
  case "BinaryOp":
309
- return containsDice(node.left) || containsDice(node.right);
446
+ return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
310
447
  case "UnaryOp":
311
- return containsDice(node.operand);
448
+ return deepContainsDicePool(node.operand);
312
449
  case "Modifier":
313
450
  case "Explode":
314
451
  case "Reroll":
315
452
  case "SuccessCount":
316
- return containsDice(node.target);
453
+ case "Sort":
454
+ case "CritThreshold":
455
+ return deepContainsDicePool(node.target);
456
+ case "Versus":
457
+ return deepContainsDicePool(node.roll) || deepContainsDicePool(node.dc);
458
+ case "FunctionCall":
459
+ return node.args.some(deepContainsDicePool);
460
+ case "Grouped":
461
+ return deepContainsDicePool(node.expression);
462
+ case "Group":
463
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
464
+ default:
465
+ return false;
466
+ }
467
+ }
468
+ function containsFatePool(node) {
469
+ switch (node.type) {
470
+ case "FateDice":
471
+ return true;
472
+ case "Modifier":
473
+ case "Explode":
474
+ case "Reroll":
475
+ case "Sort":
476
+ case "CritThreshold":
477
+ return containsFatePool(node.target);
478
+ case "Grouped":
479
+ return containsFatePool(node.expression);
480
+ case "Group":
481
+ return node.expressions.some(deepContainsFatePool);
482
+ default:
483
+ return false;
484
+ }
485
+ }
486
+ function deepContainsFatePool(node) {
487
+ switch (node.type) {
488
+ case "FateDice":
489
+ return true;
490
+ case "BinaryOp":
491
+ return deepContainsFatePool(node.left) || deepContainsFatePool(node.right);
492
+ case "UnaryOp":
493
+ return deepContainsFatePool(node.operand);
494
+ case "Modifier":
495
+ case "Explode":
496
+ case "Reroll":
497
+ case "SuccessCount":
498
+ case "Sort":
499
+ case "CritThreshold":
500
+ return deepContainsFatePool(node.target);
501
+ case "Versus":
502
+ return deepContainsFatePool(node.roll) || deepContainsFatePool(node.dc);
503
+ case "FunctionCall":
504
+ return node.args.some(deepContainsFatePool);
505
+ case "Grouped":
506
+ return deepContainsFatePool(node.expression);
507
+ case "Group":
508
+ return node.expressions.some(deepContainsFatePool);
509
+ default:
510
+ return false;
511
+ }
512
+ }
513
+ function containsMultiSubGroup(node) {
514
+ switch (node.type) {
515
+ case "Group":
516
+ return node.expressions.length >= 2 || node.expressions.some(containsMultiSubGroup);
517
+ case "BinaryOp":
518
+ return containsMultiSubGroup(node.left) || containsMultiSubGroup(node.right);
519
+ case "UnaryOp":
520
+ return containsMultiSubGroup(node.operand);
521
+ case "Modifier":
522
+ case "Explode":
523
+ case "Reroll":
524
+ case "SuccessCount":
525
+ case "Sort":
526
+ case "CritThreshold":
527
+ return containsMultiSubGroup(node.target);
528
+ case "Versus":
529
+ return containsMultiSubGroup(node.roll) || containsMultiSubGroup(node.dc);
530
+ case "FunctionCall":
531
+ return node.args.some(containsMultiSubGroup);
532
+ case "Grouped":
533
+ return containsMultiSubGroup(node.expression);
534
+ default:
535
+ return false;
536
+ }
537
+ }
538
+ function containsVersus(node) {
539
+ switch (node.type) {
317
540
  case "Versus":
318
- return containsDice(node.roll) || containsDice(node.dc);
541
+ return true;
542
+ case "BinaryOp":
543
+ return containsVersus(node.left) || containsVersus(node.right);
544
+ case "UnaryOp":
545
+ return containsVersus(node.operand);
546
+ case "Modifier":
547
+ case "Explode":
548
+ case "Reroll":
549
+ case "SuccessCount":
550
+ case "Sort":
551
+ case "CritThreshold":
552
+ return containsVersus(node.target);
319
553
  case "FunctionCall":
320
- return node.args.some(containsDice);
554
+ return node.args.some(containsVersus);
555
+ case "Grouped":
556
+ return containsVersus(node.expression);
557
+ case "Group":
558
+ return node.expressions.some(containsVersus);
559
+ default:
560
+ return false;
321
561
  }
322
562
  }
323
563
 
@@ -335,6 +575,7 @@ class ParseError extends RollParserError {
335
575
  var BP = {
336
576
  VS_LEFT: 2,
337
577
  VS_RIGHT: 3,
578
+ COMPARE: 8,
338
579
  ADD_LEFT: 10,
339
580
  ADD_RIGHT: 11,
340
581
  MUL_LEFT: 20,
@@ -346,6 +587,14 @@ var BP = {
346
587
  DICE_LEFT: 40,
347
588
  DICE_RIGHT: 41
348
589
  };
590
+ var MAX_PARSE_DEPTH = 128;
591
+ var TOKEN_DISPLAY = {
592
+ [15 /* LPAREN */]: `'('`,
593
+ [16 /* RPAREN */]: `')'`,
594
+ [30 /* LBRACE */]: `'{'`,
595
+ [31 /* RBRACE */]: `'}'`,
596
+ [17 /* COMMA */]: `','`
597
+ };
349
598
  var FUNCTION_ARITY = {
350
599
  floor: { min: 1, max: 1 },
351
600
  ceil: { min: 1, max: 1 },
@@ -358,31 +607,40 @@ var FUNCTION_ARITY = {
358
607
  class Parser {
359
608
  tokens;
360
609
  pos = 0;
610
+ depth = 0;
361
611
  constructor(tokens) {
362
612
  this.tokens = tokens;
363
613
  }
364
614
  parse() {
365
- if (this.peek().type === 30 /* EOF */) {
615
+ if (this.peek().type === 37 /* EOF */) {
366
616
  throw new ParseError("Unexpected end of input", "UNEXPECTED_END", this.peek().position);
367
617
  }
368
618
  const ast = this.parseExpression(0);
369
- if (this.peek().type !== 30 /* EOF */) {
619
+ if (this.peek().type !== 37 /* EOF */) {
370
620
  const token = this.peek();
371
621
  throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
372
622
  }
373
623
  return ast;
374
624
  }
375
625
  parseExpression(minBp) {
376
- let left = this.parseNud();
377
- while (this.hasTokens()) {
378
- const token = this.peek();
379
- const leftBp = this.getLeftBp(token);
380
- if (leftBp < minBp)
381
- break;
382
- this.advance();
383
- left = this.parseLed(left, token);
626
+ this.depth += 1;
627
+ if (this.depth > MAX_PARSE_DEPTH) {
628
+ throw new ParseError(`Expression nesting exceeds the maximum depth of ${MAX_PARSE_DEPTH}`, "MAX_DEPTH_EXCEEDED", this.peek().position, this.peek());
629
+ }
630
+ try {
631
+ let left = this.parseNud();
632
+ while (this.hasTokens()) {
633
+ const token = this.peek();
634
+ const leftBp = this.getLeftBp(token);
635
+ if (leftBp < minBp)
636
+ break;
637
+ this.advance();
638
+ left = this.parseLed(left, token);
639
+ }
640
+ return left;
641
+ } finally {
642
+ this.depth -= 1;
384
643
  }
385
- return left;
386
644
  }
387
645
  parseNud() {
388
646
  const token = this.advance();
@@ -390,18 +648,22 @@ class Parser {
390
648
  case 0 /* NUMBER */:
391
649
  return this.parseLiteral(token);
392
650
  case 5 /* MINUS */:
393
- return this.parseUnaryMinus();
651
+ return this.parseUnaryMinus(token);
394
652
  case 1 /* DICE */:
395
- return this.parsePrefixDice();
653
+ return this.parsePrefixDice(token);
396
654
  case 2 /* DICE_PERCENT */:
397
- return this.parsePrefixDicePercent();
655
+ return this.parsePrefixDicePercent(token);
398
656
  case 3 /* DICE_FATE */:
399
- return this.parsePrefixFateDice();
657
+ return this.parsePrefixFateDice(token);
400
658
  case 15 /* LPAREN */:
401
- return this.parseGrouped();
659
+ return this.parseGrouped(token);
660
+ case 30 /* LBRACE */:
661
+ return this.parseGroup(token);
402
662
  case 28 /* FUNCTION */:
403
663
  return this.parseFunctionCall(token);
404
- case 30 /* EOF */:
664
+ case 32 /* AT */:
665
+ return this.parseVariable(token);
666
+ case 37 /* EOF */:
405
667
  throw new ParseError("Unexpected end of input", "UNEXPECTED_END", token.position);
406
668
  default:
407
669
  throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
@@ -410,11 +672,11 @@ class Parser {
410
672
  parseLed(left, token) {
411
673
  switch (token.type) {
412
674
  case 1 /* DICE */:
413
- return this.parseInfixDice(left);
675
+ return this.parseInfixDice(left, token);
414
676
  case 2 /* DICE_PERCENT */:
415
- return this.parseInfixDicePercent(left);
677
+ return this.parseInfixDicePercent(left, token);
416
678
  case 3 /* DICE_FATE */:
417
- return this.parseInfixFateDice(left);
679
+ return this.parseInfixFateDice(left, token);
418
680
  case 4 /* PLUS */:
419
681
  case 5 /* MINUS */:
420
682
  case 6 /* MULTIPLY */:
@@ -434,6 +696,12 @@ class Parser {
434
696
  case 25 /* REROLL */:
435
697
  case 26 /* REROLL_ONCE */:
436
698
  return this.parseReroll(left, token);
699
+ case 33 /* SORT_ASC */:
700
+ case 34 /* SORT_DESC */:
701
+ return this.parseSort(left, token);
702
+ case 35 /* CRIT_SUCCESS */:
703
+ case 36 /* CRIT_FAIL */:
704
+ return this.parseCritThreshold(left, token);
437
705
  case 10 /* GREATER */:
438
706
  case 11 /* GREATER_EQUAL */:
439
707
  case 12 /* LESS */:
@@ -446,78 +714,134 @@ class Parser {
446
714
  throw new ParseError(`Unexpected infix token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
447
715
  }
448
716
  }
717
+ static syntheticLiteral(value, token) {
718
+ return { type: "Literal", value, start: token.position, end: token.position };
719
+ }
449
720
  parseLiteral(token) {
450
721
  return {
451
722
  type: "Literal",
452
- value: Number.parseFloat(token.value)
723
+ value: Number.parseFloat(token.value),
724
+ start: token.position,
725
+ end: token.end
453
726
  };
454
727
  }
455
- parseUnaryMinus() {
728
+ parseUnaryMinus(token) {
456
729
  const operand = this.parseExpression(BP.UNARY);
730
+ this.rejectSuccessCountTarget(operand, token);
457
731
  return {
458
732
  type: "UnaryOp",
459
733
  operator: "-",
460
- operand
734
+ operand,
735
+ start: token.position,
736
+ end: operand.end ?? token.end
461
737
  };
462
738
  }
463
- parsePrefixDice() {
739
+ parsePrefixDice(token) {
464
740
  const sides = this.parseExpression(BP.DICE_RIGHT);
741
+ this.rejectSuccessCountTarget(sides, token);
742
+ this.rejectVersusTarget(sides, token);
465
743
  return {
466
744
  type: "Dice",
467
- count: { type: "Literal", value: 1 },
468
- sides
745
+ count: Parser.syntheticLiteral(1, token),
746
+ sides,
747
+ start: token.position,
748
+ end: sides.end ?? token.end
469
749
  };
470
750
  }
471
- parseInfixDice(left) {
751
+ parseInfixDice(left, token) {
752
+ this.rejectSuccessCountTarget(left, token);
753
+ this.rejectVersusTarget(left, token);
754
+ this.rejectBareDiceChain(left, token);
472
755
  const sides = this.parseExpression(BP.DICE_RIGHT);
756
+ this.rejectSuccessCountTarget(sides, token);
757
+ this.rejectVersusTarget(sides, token);
473
758
  return {
474
759
  type: "Dice",
475
760
  count: left,
476
- sides
761
+ sides,
762
+ start: left.start ?? token.position,
763
+ end: sides.end ?? token.end
477
764
  };
478
765
  }
479
- parsePrefixDicePercent() {
766
+ parsePrefixDicePercent(token) {
480
767
  return {
481
768
  type: "Dice",
482
- count: { type: "Literal", value: 1 },
483
- sides: { type: "Literal", value: 100 }
769
+ count: Parser.syntheticLiteral(1, token),
770
+ sides: Parser.syntheticLiteral(100, token),
771
+ start: token.position,
772
+ end: token.end
484
773
  };
485
774
  }
486
- parseInfixDicePercent(left) {
775
+ parseInfixDicePercent(left, token) {
776
+ this.rejectSuccessCountTarget(left, token);
777
+ this.rejectVersusTarget(left, token);
778
+ this.rejectBareDiceChain(left, token);
487
779
  return {
488
780
  type: "Dice",
489
781
  count: left,
490
- sides: { type: "Literal", value: 100 }
782
+ sides: Parser.syntheticLiteral(100, token),
783
+ start: left.start ?? token.position,
784
+ end: token.end
491
785
  };
492
786
  }
493
- parsePrefixFateDice() {
787
+ parsePrefixFateDice(token) {
494
788
  return {
495
789
  type: "FateDice",
496
- count: { type: "Literal", value: 1 }
790
+ count: Parser.syntheticLiteral(1, token),
791
+ start: token.position,
792
+ end: token.end
497
793
  };
498
794
  }
499
- parseInfixFateDice(left) {
795
+ parseInfixFateDice(left, token) {
796
+ this.rejectSuccessCountTarget(left, token);
797
+ this.rejectVersusTarget(left, token);
798
+ this.rejectBareDiceChain(left, token);
500
799
  return {
501
800
  type: "FateDice",
502
- count: left
801
+ count: left,
802
+ start: left.start ?? token.position,
803
+ end: token.end
503
804
  };
504
805
  }
505
- parseGrouped() {
506
- const expr = this.parseExpression(0);
507
- this.expect(16 /* RPAREN */);
508
- return expr;
806
+ parseGrouped(token) {
807
+ const expression = this.parseExpression(0);
808
+ const close = this.expect(16 /* RPAREN */);
809
+ return { type: "Grouped", expression, start: token.position, end: close.end };
810
+ }
811
+ parseGroup(startToken) {
812
+ if (this.peek().type === 31 /* RBRACE */) {
813
+ throw new ParseError("Empty group", "UNEXPECTED_TOKEN", startToken.position, startToken);
814
+ }
815
+ const expressions = [this.parseExpression(0)];
816
+ while (this.peek().type === 17 /* COMMA */) {
817
+ this.advance();
818
+ expressions.push(this.parseExpression(0));
819
+ }
820
+ if (this.peek().type !== 31 /* RBRACE */) {
821
+ const unterminated = this.peek();
822
+ throw new ParseError(`Unterminated group: expected '}' or ','`, "EXPECTED_TOKEN", unterminated.position, unterminated);
823
+ }
824
+ const close = this.advance();
825
+ return { type: "Group", expressions, start: startToken.position, end: close.end };
826
+ }
827
+ parseVariable(token) {
828
+ return { type: "Variable", name: token.value, start: token.position, end: token.end };
509
829
  }
510
830
  parseFunctionCall(token) {
511
831
  this.expect(15 /* LPAREN */);
512
832
  const args = [];
513
833
  if (this.peek().type !== 16 /* RPAREN */) {
514
- args.push(this.parseExpression(0));
834
+ const first = this.parseExpression(0);
835
+ this.rejectSuccessCountTarget(first, token);
836
+ args.push(first);
515
837
  while (this.peek().type === 17 /* COMMA */) {
516
838
  this.advance();
517
- args.push(this.parseExpression(0));
839
+ const next = this.parseExpression(0);
840
+ this.rejectSuccessCountTarget(next, token);
841
+ args.push(next);
518
842
  }
519
843
  }
520
- this.expect(16 /* RPAREN */);
844
+ const close = this.expect(16 /* RPAREN */);
521
845
  const arity = FUNCTION_ARITY[token.value];
522
846
  if (arity === undefined) {
523
847
  throw new ParseError(`Unknown function '${token.value}'`, "UNKNOWN_FUNCTION", token.position, token);
@@ -526,84 +850,244 @@ class Parser {
526
850
  const expected = arity.max === Number.POSITIVE_INFINITY ? `at least ${arity.min}` : arity.min === arity.max ? `${arity.min}` : `${arity.min}–${arity.max}`;
527
851
  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);
528
852
  }
529
- return { type: "FunctionCall", name: token.value, args };
853
+ return {
854
+ type: "FunctionCall",
855
+ name: token.value,
856
+ args,
857
+ start: token.position,
858
+ end: close.end
859
+ };
530
860
  }
531
861
  parseBinaryOp(left, token) {
862
+ this.rejectSuccessCountTarget(left, token);
532
863
  const operator = this.getOperatorSymbol(token);
533
864
  const rightBp = this.getRightBp(token);
534
865
  const right = this.parseExpression(rightBp);
866
+ this.rejectSuccessCountTarget(right, token);
535
867
  return {
536
868
  type: "BinaryOp",
537
869
  operator,
538
870
  left,
539
- right
871
+ right,
872
+ start: left.start ?? token.position,
873
+ end: right.end ?? token.end
540
874
  };
541
875
  }
876
+ rejectBareDiceChain(left, token) {
877
+ switch (left.type) {
878
+ case "Dice":
879
+ case "FateDice":
880
+ case "Explode":
881
+ case "Reroll":
882
+ case "Modifier":
883
+ case "Sort":
884
+ case "CritThreshold":
885
+ 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);
886
+ default:
887
+ return;
888
+ }
889
+ }
542
890
  rejectSuccessCountTarget(target, token) {
543
- if (isSuccessCount(target)) {
891
+ const node = unwrapTransparent(target, ["Grouped"]);
892
+ if (isSuccessCount(node)) {
544
893
  throw new ParseError(`Cannot apply modifier after success counting`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
545
894
  }
546
895
  }
896
+ rejectGroupTarget(target, token, action, code, singleSubRollPasses = false) {
897
+ const node = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
898
+ if (node.type !== "Group")
899
+ return;
900
+ if (singleSubRollPasses && node.expressions.length === 1) {
901
+ const inner = node.expressions[0];
902
+ if (inner != null && containsMultiSubGroup(inner)) {
903
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
904
+ }
905
+ return;
906
+ }
907
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
908
+ }
909
+ rejectVersusTarget(target, token) {
910
+ const node = unwrapTransparent(target, ["Grouped"]);
911
+ if (node.type === "Versus") {
912
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
913
+ }
914
+ if (node.type === "Group" && node.expressions.length === 1) {
915
+ const inner = node.expressions[0];
916
+ if (inner != null && containsVersus(inner)) {
917
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
918
+ }
919
+ }
920
+ }
547
921
  parseModifier(target, token) {
548
922
  this.rejectSuccessCountTarget(target, token);
923
+ this.rejectVersusTarget(target, token);
924
+ if (!containsDicePool(target)) {
925
+ throw new ParseError(`Keep/drop modifiers require a dice pool target`, "INVALID_MODIFIER_TARGET", token.position, token);
926
+ }
549
927
  const modifier = token.type === 18 /* KEEP_HIGH */ || token.type === 19 /* KEEP_LOW */ ? "keep" : "drop";
550
928
  const selector = token.type === 18 /* KEEP_HIGH */ || token.type === 20 /* DROP_HIGH */ ? "highest" : "lowest";
551
929
  const nextToken = this.peek().type;
552
- const count = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ ? this.parseExpression(BP.DICE_LEFT) : { type: "Literal", value: 1 };
930
+ const hasExplicitCount = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ || nextToken === 32 /* AT */;
931
+ const count = hasExplicitCount ? this.parseExpression(BP.DICE_LEFT) : Parser.syntheticLiteral(1, token);
932
+ this.rejectSuccessCountTarget(count, token);
933
+ this.rejectVersusTarget(count, token);
553
934
  return {
554
935
  type: "Modifier",
555
936
  modifier,
556
937
  selector,
557
938
  count,
558
- target
939
+ target,
940
+ start: target.start ?? token.position,
941
+ end: hasExplicitCount ? count.end ?? token.end : token.end
559
942
  };
560
943
  }
561
944
  parseExplode(target, token) {
562
945
  this.rejectSuccessCountTarget(target, token);
946
+ this.rejectGroupTarget(target, token, "explode", "INVALID_EXPLODE_TARGET");
947
+ if (!containsDicePool(target)) {
948
+ throw new ParseError(`Explode modifier requires a dice pool target`, "INVALID_EXPLODE_TARGET", token.position, token);
949
+ }
950
+ if (containsFatePool(target)) {
951
+ throw new ParseError(`Fate dice cannot explode`, "INVALID_EXPLODE_TARGET", token.position, token);
952
+ }
563
953
  if (target.type === "Explode") {
564
954
  throw new ParseError(`Cannot chain explode modifiers`, "INVALID_EXPLODE_TARGET", token.position, token);
565
955
  }
566
956
  const variant = token.type === 22 /* EXPLODE */ ? "standard" : token.type === 23 /* EXPLODE_COMPOUND */ ? "compound" : "penetrating";
567
- const node = { type: "Explode", variant, target };
957
+ const node = {
958
+ type: "Explode",
959
+ variant,
960
+ target,
961
+ start: target.start ?? token.position,
962
+ end: token.end
963
+ };
568
964
  if (this.isComparePointAhead()) {
569
965
  node.threshold = this.parseComparePoint();
966
+ node.end = node.threshold.value.end ?? token.end;
570
967
  }
571
968
  return node;
572
969
  }
573
970
  parseReroll(target, token) {
574
971
  this.rejectSuccessCountTarget(target, token);
972
+ this.rejectGroupTarget(target, token, "reroll", "INVALID_REROLL_TARGET");
973
+ if (!containsDicePool(target)) {
974
+ throw new ParseError(`Reroll modifier requires a dice pool target`, "INVALID_REROLL_TARGET", token.position, token);
975
+ }
575
976
  if (!this.isComparePointAhead()) {
576
977
  throw new ParseError(`Expected comparison operator after '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
577
978
  }
578
979
  const once = token.type === 26 /* REROLL_ONCE */;
579
980
  const condition = this.parseComparePoint();
580
- return { type: "Reroll", once, condition, target };
981
+ return {
982
+ type: "Reroll",
983
+ once,
984
+ condition,
985
+ target,
986
+ start: target.start ?? token.position,
987
+ end: condition.value.end ?? token.end
988
+ };
989
+ }
990
+ parseSort(target, token) {
991
+ this.rejectSuccessCountTarget(target, token);
992
+ this.rejectVersusTarget(target, token);
993
+ if (!deepContainsDicePool(target)) {
994
+ throw new ParseError(`Sort modifier requires a dice pool target`, "INVALID_SORT_TARGET", token.position, token);
995
+ }
996
+ const base = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
997
+ if (base.type === "Group" && base.expressions.length >= 2) {
998
+ throw new ParseError(`Sort modifier does not yet support multi-sub-roll groups`, "INVALID_SORT_TARGET", token.position, token);
999
+ }
1000
+ const order = token.type === 33 /* SORT_ASC */ ? "ascending" : "descending";
1001
+ return {
1002
+ type: "Sort",
1003
+ order,
1004
+ target,
1005
+ start: target.start ?? token.position,
1006
+ end: token.end
1007
+ };
1008
+ }
1009
+ parseCritThreshold(target, token) {
1010
+ this.rejectSuccessCountTarget(target, token);
1011
+ this.rejectVersusTarget(target, token);
1012
+ this.rejectGroupTarget(target, token, "apply crit threshold to", "INVALID_CRIT_THRESHOLD_TARGET", true);
1013
+ if (!containsDicePool(target)) {
1014
+ throw new ParseError(`Crit threshold modifier requires a dice pool target`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
1015
+ }
1016
+ if (!this.isComparePointAhead() && containsFatePool(target)) {
1017
+ throw new ParseError(`Bare cs/cf cannot apply to Fate dice`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
1018
+ }
1019
+ const threshold = this.isComparePointAhead() ? this.parseComparePoint() : "default";
1020
+ const end = threshold === "default" ? token.end : threshold.value.end ?? token.end;
1021
+ let chainTarget = target;
1022
+ while (chainTarget.type === "Grouped") {
1023
+ chainTarget = chainTarget.expression;
1024
+ }
1025
+ if (isCritThreshold(chainTarget)) {
1026
+ if (token.type === 35 /* CRIT_SUCCESS */) {
1027
+ chainTarget.successThresholds.push(threshold);
1028
+ } else {
1029
+ chainTarget.failThresholds.push(threshold);
1030
+ }
1031
+ chainTarget.end = end;
1032
+ return chainTarget;
1033
+ }
1034
+ return {
1035
+ type: "CritThreshold",
1036
+ successThresholds: token.type === 35 /* CRIT_SUCCESS */ ? [threshold] : [],
1037
+ failThresholds: token.type === 36 /* CRIT_FAIL */ ? [threshold] : [],
1038
+ target,
1039
+ start: target.start ?? token.position,
1040
+ end
1041
+ };
581
1042
  }
582
1043
  parseSuccessCount(target, token) {
583
1044
  this.rejectSuccessCountTarget(target, token);
584
- if (!containsDice(target)) {
585
- throw new ParseError(`Success counting requires a dice expression`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
1045
+ if (!containsDicePool(target)) {
1046
+ throw new ParseError(`Success counting requires a dice pool target`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
586
1047
  }
587
1048
  const operator = this.getCompareOp(token);
588
1049
  const value = this.parseExpression(BP.DICE_LEFT);
1050
+ this.rejectSuccessCountTarget(value, token);
1051
+ this.rejectVersusTarget(value, token);
589
1052
  const node = {
590
1053
  type: "SuccessCount",
591
1054
  target,
592
- threshold: { operator, value }
1055
+ threshold: { operator, value },
1056
+ start: target.start ?? token.position,
1057
+ end: value.end ?? token.end
593
1058
  };
594
1059
  if (this.peek().type === 27 /* FAIL */) {
595
1060
  this.advance();
596
- const failValue = this.parseExpression(BP.DICE_LEFT);
597
- node.failThreshold = { operator: "=", value: failValue };
1061
+ if (this.isComparePointAhead()) {
1062
+ node.failThreshold = this.parseComparePoint();
1063
+ } else {
1064
+ const failValue = this.parseExpression(BP.DICE_LEFT);
1065
+ this.rejectSuccessCountTarget(failValue, token);
1066
+ this.rejectVersusTarget(failValue, token);
1067
+ node.failThreshold = { operator: "=", value: failValue };
1068
+ }
1069
+ node.end = node.failThreshold.value.end ?? node.end ?? token.end;
598
1070
  }
599
1071
  return node;
600
1072
  }
601
1073
  parseVersus(left, token) {
602
- if (left.type === "Versus") {
1074
+ this.rejectSuccessCountTarget(left, token);
1075
+ let leftChain = left;
1076
+ while (leftChain.type === "Grouped") {
1077
+ leftChain = leftChain.expression;
1078
+ }
1079
+ if (leftChain.type === "Versus") {
603
1080
  throw new ParseError("Cannot chain versus operators", "NESTED_VERSUS", token.position, token);
604
1081
  }
605
1082
  const dc = this.parseExpression(BP.VS_RIGHT);
606
- return { type: "Versus", roll: left, dc };
1083
+ this.rejectSuccessCountTarget(dc, token);
1084
+ return {
1085
+ type: "Versus",
1086
+ roll: left,
1087
+ dc,
1088
+ start: left.start ?? token.position,
1089
+ end: dc.end ?? token.end
1090
+ };
607
1091
  }
608
1092
  isComparePointAhead() {
609
1093
  const type = this.peek().type;
@@ -614,6 +1098,8 @@ class Parser {
614
1098
  const operator = this.getCompareOp(token);
615
1099
  this.advance();
616
1100
  const value = this.parseExpression(BP.DICE_LEFT);
1101
+ this.rejectSuccessCountTarget(value, token);
1102
+ this.rejectVersusTarget(value, token);
617
1103
  return { operator, value };
618
1104
  }
619
1105
  getCompareOp(token) {
@@ -676,16 +1162,23 @@ class Parser {
676
1162
  case 24 /* EXPLODE_PENETRATING */:
677
1163
  case 25 /* REROLL */:
678
1164
  case 26 /* REROLL_ONCE */:
1165
+ case 33 /* SORT_ASC */:
1166
+ case 34 /* SORT_DESC */:
1167
+ case 35 /* CRIT_SUCCESS */:
1168
+ case 36 /* CRIT_FAIL */:
1169
+ return BP.MODIFIER;
679
1170
  case 10 /* GREATER */:
680
1171
  case 11 /* GREATER_EQUAL */:
681
1172
  case 12 /* LESS */:
682
1173
  case 13 /* LESS_EQUAL */:
683
1174
  case 14 /* EQUAL */:
684
- return BP.MODIFIER;
1175
+ return BP.COMPARE;
685
1176
  case 16 /* RPAREN */:
686
- case 30 /* EOF */:
1177
+ case 37 /* EOF */:
687
1178
  case 17 /* COMMA */:
688
1179
  case 28 /* FUNCTION */:
1180
+ case 30 /* LBRACE */:
1181
+ case 31 /* RBRACE */:
689
1182
  return -1;
690
1183
  default:
691
1184
  return 0;
@@ -711,7 +1204,7 @@ class Parser {
711
1204
  }
712
1205
  }
713
1206
  peek() {
714
- return this.tokens[this.pos] ?? { type: 30 /* EOF */, value: "", position: this.pos };
1207
+ return this.tokens[this.pos] ?? { type: 37 /* EOF */, value: "", position: this.pos, end: this.pos };
715
1208
  }
716
1209
  advance() {
717
1210
  const token = this.peek();
@@ -721,13 +1214,14 @@ class Parser {
721
1214
  expect(type) {
722
1215
  const token = this.peek();
723
1216
  if (token.type !== type) {
724
- const expected = TokenType[type];
725
- throw new ParseError(`Expected ${expected} but got '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
1217
+ const expected = TOKEN_DISPLAY[type] ?? TokenType[type];
1218
+ const got = token.type === 37 /* EOF */ ? "end of input" : `'${token.value}'`;
1219
+ throw new ParseError(`Expected ${expected} but got ${got}`, "EXPECTED_TOKEN", token.position, token);
726
1220
  }
727
1221
  return this.advance();
728
1222
  }
729
1223
  hasTokens() {
730
- return this.peek().type !== 30 /* EOF */;
1224
+ return this.peek().type !== 37 /* EOF */;
731
1225
  }
732
1226
  }
733
1227
  function parse(notation) {
@@ -798,6 +1292,9 @@ class SeededRNG {
798
1292
  if (range <= 1) {
799
1293
  return lo;
800
1294
  }
1295
+ if (range > 4294967296) {
1296
+ return lo + this.nextBoundedWide(range);
1297
+ }
801
1298
  const threshold = (4294967296 - range) % range;
802
1299
  let value;
803
1300
  do {
@@ -805,6 +1302,18 @@ class SeededRNG {
805
1302
  } while (value < threshold);
806
1303
  return lo + value % range;
807
1304
  }
1305
+ nextBoundedWide(range) {
1306
+ const MAX_53 = 2 ** 53;
1307
+ if (range > MAX_53) {
1308
+ throw new RangeError(`nextInt range ${range} exceeds 2^53 and cannot be sampled exactly`);
1309
+ }
1310
+ const limit = Math.floor(MAX_53 / range) * range;
1311
+ let value;
1312
+ do {
1313
+ value = this.nextUint32() * 2097152 + (this.nextUint32() >>> 11);
1314
+ } while (value >= limit);
1315
+ return value % range;
1316
+ }
808
1317
  }
809
1318
  // src/types.ts
810
1319
  var DegreeOfSuccess;
@@ -815,6 +1324,44 @@ var DegreeOfSuccess;
815
1324
  DegreeOfSuccess2[DegreeOfSuccess2["CriticalSuccess"] = 3] = "CriticalSuccess";
816
1325
  })(DegreeOfSuccess ||= {});
817
1326
 
1327
+ // src/evaluator/modifiers/compare.ts
1328
+ function matchesCondition(result, operator, value) {
1329
+ switch (operator) {
1330
+ case ">":
1331
+ return result > value;
1332
+ case ">=":
1333
+ return result >= value;
1334
+ case "<":
1335
+ return result < value;
1336
+ case "<=":
1337
+ return result <= value;
1338
+ case "=":
1339
+ return result === value;
1340
+ }
1341
+ }
1342
+
1343
+ // src/evaluator/modifiers/crit-threshold.ts
1344
+ function applyCritThresholds(dice, successThresholds, failThresholds) {
1345
+ for (const die of dice) {
1346
+ if (die.modifiers.includes("meta"))
1347
+ continue;
1348
+ die.critical = successThresholds.some((t) => matchesCrit(t, die));
1349
+ die.fumble = failThresholds.some((t) => matchesFumble(t, die));
1350
+ }
1351
+ }
1352
+ function matchesCrit(threshold, die) {
1353
+ if (threshold === "default") {
1354
+ return die.result === die.sides && die.sides > 1;
1355
+ }
1356
+ return matchesCondition(die.result, threshold.operator, threshold.value);
1357
+ }
1358
+ function matchesFumble(threshold, die) {
1359
+ if (threshold === "default") {
1360
+ return die.result === 1 && die.sides > 1;
1361
+ }
1362
+ return matchesCondition(die.result, threshold.operator, threshold.value);
1363
+ }
1364
+
818
1365
  // src/evaluator/modifiers/explode.ts
819
1366
  var DEFAULT_MAX_EXPLODE_ITERATIONS = 1000;
820
1367
  function buildShouldExplode(operator, thresholdValue) {
@@ -870,7 +1417,7 @@ function applyStandardExplode(pool, shouldExplode, rng, env) {
870
1417
  result: next,
871
1418
  modifiers: ["exploded", "kept"],
872
1419
  critical: next === sides && sides > 1,
873
- fumble: next === 1
1420
+ fumble: next === 1 && sides > 1
874
1421
  });
875
1422
  last = next;
876
1423
  iterations += 1;
@@ -879,9 +1426,9 @@ function applyStandardExplode(pool, shouldExplode, rng, env) {
879
1426
  return result;
880
1427
  }
881
1428
  function applyCompoundExplode(pool, shouldExplode, rng, env) {
882
- return pool.map((original) => {
1429
+ for (const original of pool) {
883
1430
  if (!canExplode(original))
884
- return original;
1431
+ continue;
885
1432
  const sides = original.sides;
886
1433
  let accumulated = original.result;
887
1434
  let last = original.result;
@@ -898,15 +1445,14 @@ function applyCompoundExplode(pool, shouldExplode, rng, env) {
898
1445
  iterations += 1;
899
1446
  }
900
1447
  if (!exploded)
901
- return original;
902
- return {
903
- sides,
904
- result: accumulated,
905
- modifiers: original.modifiers.includes("exploded") ? original.modifiers : [...original.modifiers, "exploded"],
906
- critical: original.critical,
907
- fumble: original.fumble
908
- };
909
- });
1448
+ continue;
1449
+ original.initialResult = original.result;
1450
+ original.result = accumulated;
1451
+ if (!original.modifiers.includes("exploded")) {
1452
+ original.modifiers = [...original.modifiers, "exploded"];
1453
+ }
1454
+ }
1455
+ return pool;
910
1456
  }
911
1457
  function applyPenetratingExplode(pool, shouldExplode, rng, env) {
912
1458
  const result = [];
@@ -928,7 +1474,7 @@ function applyPenetratingExplode(pool, shouldExplode, rng, env) {
928
1474
  result: stored,
929
1475
  modifiers: ["exploded", "kept"],
930
1476
  critical: raw === sides && sides > 1,
931
- fumble: raw === 1
1477
+ fumble: raw === 1 && sides > 1
932
1478
  });
933
1479
  lastRaw = raw;
934
1480
  iterations += 1;
@@ -1018,22 +1564,6 @@ function sumKeptDice(dice) {
1018
1564
  return dice.filter((die) => !die.modifiers.includes("dropped")).reduce((sum, die) => sum + die.result, 0);
1019
1565
  }
1020
1566
 
1021
- // src/evaluator/modifiers/compare.ts
1022
- function matchesCondition(result, operator, value) {
1023
- switch (operator) {
1024
- case ">":
1025
- return result > value;
1026
- case ">=":
1027
- return result >= value;
1028
- case "<":
1029
- return result < value;
1030
- case "<=":
1031
- return result <= value;
1032
- case "=":
1033
- return result === value;
1034
- }
1035
- }
1036
-
1037
1567
  // src/evaluator/modifiers/reroll.ts
1038
1568
  var DEFAULT_MAX_REROLL_ITERATIONS = 1000;
1039
1569
  function rollReplacement(sides, rng, env) {
@@ -1057,7 +1587,7 @@ function rollReplacement(sides, rng, env) {
1057
1587
  result,
1058
1588
  modifiers: [],
1059
1589
  critical: result === sides && sides > 1,
1060
- fumble: result === 1
1590
+ fumble: result === 1 && sides > 1
1061
1591
  };
1062
1592
  }
1063
1593
  function rerollLimitError(maxIterations) {
@@ -1082,17 +1612,13 @@ function applyRecursiveReroll(pool, operator, value, rng, env) {
1082
1612
  if (iterations >= env.maxRerollIterations) {
1083
1613
  throw rerollLimitError(env.maxRerollIterations);
1084
1614
  }
1085
- result.push({
1086
- ...current,
1087
- modifiers: [...stripSlotFlags(current.modifiers), "rerolled", "dropped"]
1088
- });
1615
+ current.modifiers = [...stripSlotFlags(current.modifiers), "rerolled", "dropped"];
1616
+ result.push(current);
1089
1617
  current = rollReplacement(current.sides, rng, env);
1090
1618
  iterations += 1;
1091
1619
  }
1092
- result.push({
1093
- ...current,
1094
- modifiers: [...stripSlotFlags(current.modifiers), "kept"]
1095
- });
1620
+ current.modifiers = [...stripSlotFlags(current.modifiers), "kept"];
1621
+ result.push(current);
1096
1622
  }
1097
1623
  return result;
1098
1624
  }
@@ -1104,25 +1630,25 @@ function applyRerollOnce(pool, operator, value, rng, env) {
1104
1630
  continue;
1105
1631
  }
1106
1632
  if (!matchesCondition(original.result, operator, value)) {
1107
- result.push({
1108
- ...original,
1109
- modifiers: [...stripSlotFlags(original.modifiers), "kept"]
1110
- });
1633
+ original.modifiers = [...stripSlotFlags(original.modifiers), "kept"];
1634
+ result.push(original);
1111
1635
  continue;
1112
1636
  }
1113
- result.push({
1114
- ...original,
1115
- modifiers: [...stripSlotFlags(original.modifiers), "rerolled", "dropped"]
1116
- });
1637
+ original.modifiers = [...stripSlotFlags(original.modifiers), "rerolled", "dropped"];
1638
+ result.push(original);
1117
1639
  const replacement = rollReplacement(original.sides, rng, env);
1118
- result.push({
1119
- ...replacement,
1120
- modifiers: [...stripSlotFlags(replacement.modifiers), "kept"]
1121
- });
1640
+ replacement.modifiers = [...stripSlotFlags(replacement.modifiers), "kept"];
1641
+ result.push(replacement);
1122
1642
  }
1123
1643
  return result;
1124
1644
  }
1125
1645
 
1646
+ // src/evaluator/modifiers/sort.ts
1647
+ function sortDice(dice, order) {
1648
+ const cmp = order === "ascending" ? (a, b) => a.result - b.result : (a, b) => b.result - a.result;
1649
+ return [...dice].sort(cmp);
1650
+ }
1651
+
1126
1652
  // src/evaluator/modifiers/success-count.ts
1127
1653
  function countSuccesses(dice, threshold, failThreshold) {
1128
1654
  let successes = 0;
@@ -1146,20 +1672,32 @@ function countSuccesses(dice, threshold, failThreshold) {
1146
1672
  // src/evaluator/evaluator.ts
1147
1673
  class EvaluatorError extends RollParserError {
1148
1674
  nodeType;
1675
+ start;
1676
+ end;
1149
1677
  constructor(message, code, nodeType) {
1150
1678
  super(message, code);
1151
1679
  this.name = "EvaluatorError";
1152
1680
  this.nodeType = nodeType ?? undefined;
1681
+ this.start = undefined;
1682
+ this.end = undefined;
1153
1683
  }
1154
1684
  }
1155
1685
  var DEFAULT_MAX_DICE = 1e4;
1686
+ function partSpan(node) {
1687
+ if (node.start === undefined || node.end === undefined)
1688
+ return {};
1689
+ return { start: node.start, end: node.end };
1690
+ }
1691
+ function toPublicSpecs(specs) {
1692
+ return specs.map((spec) => ({ kind: spec.modifier, selector: spec.selector, count: spec.count }));
1693
+ }
1156
1694
  function createDieResult(sides, result) {
1157
1695
  return {
1158
1696
  sides,
1159
1697
  result,
1160
1698
  modifiers: [],
1161
1699
  critical: result === sides && sides > 1,
1162
- fumble: result === 1
1700
+ fumble: result === 1 && sides > 1
1163
1701
  };
1164
1702
  }
1165
1703
  function createFateDieResult(result) {
@@ -1172,7 +1710,7 @@ function createFateDieResult(result) {
1172
1710
  };
1173
1711
  }
1174
1712
  function renderDice(dice) {
1175
- const parts = dice.map((die) => {
1713
+ const parts = dice.filter((die) => !die.modifiers.includes("meta")).map((die) => {
1176
1714
  if (die.modifiers.includes("dropped")) {
1177
1715
  return `~~${die.result}~~`;
1178
1716
  }
@@ -1186,10 +1724,33 @@ function renderDice(dice) {
1186
1724
  });
1187
1725
  return `[${parts.join(", ")}]`;
1188
1726
  }
1727
+ function mergeMetaRolls(parent, source) {
1728
+ for (const die of source.rolls) {
1729
+ parent.rolls.push({
1730
+ ...die,
1731
+ modifiers: [
1732
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
1733
+ "meta",
1734
+ "dropped"
1735
+ ]
1736
+ });
1737
+ }
1738
+ }
1189
1739
  function evalNode(node, rng, ctx, env) {
1740
+ try {
1741
+ return evalNodeInner(node, rng, ctx, env);
1742
+ } catch (error) {
1743
+ if (error instanceof EvaluatorError && error.start === undefined && node.start !== undefined) {
1744
+ error.start = node.start;
1745
+ error.end = node.end;
1746
+ }
1747
+ throw error;
1748
+ }
1749
+ }
1750
+ function evalNodeInner(node, rng, ctx, env) {
1190
1751
  switch (node.type) {
1191
1752
  case "Literal":
1192
- return evalLiteral(node.value, ctx);
1753
+ return evalLiteral(node, ctx);
1193
1754
  case "Dice":
1194
1755
  return evalDice(node, rng, ctx, env);
1195
1756
  case "FateDice":
@@ -1210,20 +1771,64 @@ function evalNode(node, rng, ctx, env) {
1210
1771
  return evalVersus(node, rng, ctx, env);
1211
1772
  case "FunctionCall":
1212
1773
  return evalFunctionCall(node, rng, ctx, env);
1774
+ case "Grouped":
1775
+ return evalGrouped(node, rng, ctx, env);
1776
+ case "Group":
1777
+ return evalGroup(node, rng, ctx, env);
1778
+ case "Sort":
1779
+ return evalSort(node, rng, ctx, env);
1780
+ case "CritThreshold":
1781
+ return evalCritThreshold(node, rng, ctx, env);
1782
+ case "Variable":
1783
+ return evalVariable(node, ctx, env);
1213
1784
  default: {
1214
1785
  const exhaustive = node;
1215
1786
  throw new EvaluatorError(`Unknown node type: ${exhaustive.type}`, "UNKNOWN_NODE_TYPE", exhaustive.type);
1216
1787
  }
1217
1788
  }
1218
1789
  }
1219
- function evalLiteral(value, ctx) {
1790
+ function evalLiteral(node, ctx) {
1791
+ const { value } = node;
1220
1792
  ctx.expressionParts.push(String(value));
1221
1793
  ctx.renderedParts.push(String(value));
1222
- return value;
1794
+ return { total: value, part: { type: "literal", value, total: value, ...partSpan(node) } };
1795
+ }
1796
+ function variableNeedsBraces(name) {
1797
+ return !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
1798
+ }
1799
+ function evalVariable(node, ctx, env) {
1800
+ const present = Object.hasOwn(env.context, node.name);
1801
+ if (!present) {
1802
+ if (env.onMissingVariable === "throw") {
1803
+ throw new EvaluatorError(`Undefined variable: ${node.name}`, "UNDEFINED_VARIABLE", "Variable");
1804
+ }
1805
+ const display2 = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1806
+ ctx.expressionParts.push("0");
1807
+ ctx.renderedParts.push(`${display2}[0]`);
1808
+ return {
1809
+ total: 0,
1810
+ part: { type: "variable", name: node.name, value: 0, total: 0, ...partSpan(node) }
1811
+ };
1812
+ }
1813
+ const value = env.context[node.name];
1814
+ if (!Number.isFinite(value)) {
1815
+ throw new EvaluatorError(`Invalid variable value: ${node.name} = ${value}`, "INVALID_VARIABLE_VALUE", "Variable");
1816
+ }
1817
+ const display = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1818
+ ctx.expressionParts.push(String(value));
1819
+ ctx.renderedParts.push(`${display}[${value}]`);
1820
+ return {
1821
+ total: value,
1822
+ part: { type: "variable", name: node.name, value, total: value, ...partSpan(node) }
1823
+ };
1223
1824
  }
1224
1825
  function evalDice(node, rng, ctx, env) {
1225
- const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1226
- const sides = evalNode(node.sides, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1826
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1827
+ const count = evalNode(node.count, rng, countCtx, env).total;
1828
+ mergeMetaRolls(ctx, countCtx);
1829
+ const sidesCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1830
+ const sides = evalNode(node.sides, rng, sidesCtx, env).total;
1831
+ mergeMetaRolls(ctx, sidesCtx);
1227
1832
  if (!Number.isInteger(count) || count < 0) {
1228
1833
  throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "Dice");
1229
1834
  }
@@ -1245,10 +1850,15 @@ function evalDice(node, rng, ctx, env) {
1245
1850
  const notation = `${count}d${sides}`;
1246
1851
  ctx.expressionParts.push(notation);
1247
1852
  ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1248
- return total;
1853
+ return {
1854
+ total,
1855
+ part: { type: "dice", count, sides, rolls: markedDice, total, ...partSpan(node) }
1856
+ };
1249
1857
  }
1250
1858
  function evalFateDice(node, rng, ctx, env) {
1251
- const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1859
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1860
+ const count = evalNode(node.count, rng, countCtx, env).total;
1861
+ mergeMetaRolls(ctx, countCtx);
1252
1862
  if (!Number.isInteger(count) || count < 0) {
1253
1863
  throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "FateDice");
1254
1864
  }
@@ -1267,21 +1877,51 @@ function evalFateDice(node, rng, ctx, env) {
1267
1877
  const notation = `${count}dF`;
1268
1878
  ctx.expressionParts.push(notation);
1269
1879
  ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1270
- return total;
1880
+ return {
1881
+ total,
1882
+ part: { type: "fateDice", count, rolls: markedDice, total, ...partSpan(node) }
1883
+ };
1884
+ }
1885
+ function propagateMetadata(parent, metadata) {
1886
+ if (!metadata)
1887
+ return;
1888
+ if (parent.versusMetadata) {
1889
+ throw new EvaluatorError("Multiple versus operators in the same expression", "NESTED_VERSUS", "Versus");
1890
+ }
1891
+ parent.versusMetadata = metadata;
1892
+ }
1893
+ function mergeContext(parent, child) {
1894
+ parent.rolls.push(...child.rolls);
1895
+ propagateMetadata(parent, child.versusMetadata);
1271
1896
  }
1272
1897
  function evalBinaryOp(node, rng, ctx, env) {
1273
1898
  const leftCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1274
1899
  const rightCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1275
1900
  const left = evalNode(node.left, rng, leftCtx, env);
1276
1901
  const right = evalNode(node.right, rng, rightCtx, env);
1277
- ctx.rolls.push(...leftCtx.rolls, ...rightCtx.rolls);
1902
+ mergeContext(ctx, leftCtx);
1903
+ mergeContext(ctx, rightCtx);
1278
1904
  const leftExpr = leftCtx.expressionParts.join("");
1279
1905
  const rightExpr = rightCtx.expressionParts.join("");
1280
1906
  const leftRendered = leftCtx.renderedParts.join("");
1281
1907
  const rightRendered = rightCtx.renderedParts.join("");
1282
1908
  ctx.expressionParts.push(`${leftExpr} ${node.operator} ${rightExpr}`);
1283
1909
  ctx.renderedParts.push(`${leftRendered} ${node.operator} ${rightRendered}`);
1284
- switch (node.operator) {
1910
+ const total = applyBinaryOperator(node.operator, left.total, right.total);
1911
+ return {
1912
+ total,
1913
+ part: {
1914
+ type: "binaryOp",
1915
+ operator: node.operator,
1916
+ left: left.part,
1917
+ right: right.part,
1918
+ total,
1919
+ ...partSpan(node)
1920
+ }
1921
+ };
1922
+ }
1923
+ function applyBinaryOperator(operator, left, right) {
1924
+ switch (operator) {
1285
1925
  case "+":
1286
1926
  return left + right;
1287
1927
  case "-":
@@ -1301,37 +1941,80 @@ function evalBinaryOp(node, rng, ctx, env) {
1301
1941
  case "**":
1302
1942
  return left ** right;
1303
1943
  default: {
1304
- const exhaustive = node.operator;
1944
+ const exhaustive = operator;
1305
1945
  throw new EvaluatorError(`Unknown operator: ${exhaustive}`, "UNKNOWN_OPERATOR", "BinaryOp");
1306
1946
  }
1307
1947
  }
1308
1948
  }
1309
1949
  function evalUnaryOp(node, rng, ctx, env) {
1310
1950
  const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1311
- const value = evalNode(node.operand, rng, innerCtx, env);
1312
- ctx.rolls.push(...innerCtx.rolls);
1951
+ const inner = evalNode(node.operand, rng, innerCtx, env);
1952
+ mergeContext(ctx, innerCtx);
1313
1953
  const innerExpr = innerCtx.expressionParts.join("");
1314
1954
  const innerRendered = innerCtx.renderedParts.join("");
1315
1955
  ctx.expressionParts.push(`-${innerExpr}`);
1316
1956
  ctx.renderedParts.push(`-${innerRendered}`);
1317
- return -value;
1957
+ const total = -inner.total;
1958
+ return {
1959
+ total,
1960
+ part: { type: "unaryOp", operator: "-", operand: inner.part, total, ...partSpan(node) }
1961
+ };
1962
+ }
1963
+ function evalGrouped(node, rng, ctx, env) {
1964
+ const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1965
+ const inner = evalNode(node.expression, rng, innerCtx, env);
1966
+ mergeContext(ctx, innerCtx);
1967
+ ctx.expressionParts.push(`(${innerCtx.expressionParts.join("")})`);
1968
+ ctx.renderedParts.push(`(${innerCtx.renderedParts.join("")})`);
1969
+ return {
1970
+ total: inner.total,
1971
+ part: { type: "grouped", inner: inner.part, total: inner.total, ...partSpan(node) }
1972
+ };
1973
+ }
1974
+ function evalGroup(node, rng, ctx, env) {
1975
+ const subExprs = [];
1976
+ const subRendered = [];
1977
+ const subParts = [];
1978
+ let total = 0;
1979
+ for (const expr of node.expressions) {
1980
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1981
+ const sub = evalNode(expr, rng, subCtx, env);
1982
+ mergeContext(ctx, subCtx);
1983
+ subExprs.push(subCtx.expressionParts.join(""));
1984
+ subRendered.push(subCtx.renderedParts.join(""));
1985
+ subParts.push(sub.part);
1986
+ total += sub.total;
1987
+ }
1988
+ ctx.expressionParts.push(`{${subExprs.join(", ")}}`);
1989
+ ctx.renderedParts.push(`{${subRendered.join(", ")}}`);
1990
+ return { total, part: { type: "group", parts: subParts, total, ...partSpan(node) } };
1318
1991
  }
1319
1992
  function evalFunctionCall(node, rng, ctx, env) {
1320
1993
  const argCtxs = [];
1321
- const values = [];
1994
+ const argResults = [];
1322
1995
  for (const arg of node.args) {
1323
1996
  const argCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1324
- values.push(evalNode(arg, rng, argCtx, env));
1997
+ argResults.push(evalNode(arg, rng, argCtx, env));
1325
1998
  argCtxs.push(argCtx);
1326
1999
  }
1327
2000
  for (const argCtx of argCtxs) {
1328
- ctx.rolls.push(...argCtx.rolls);
2001
+ mergeContext(ctx, argCtx);
1329
2002
  }
1330
2003
  const argExprs = argCtxs.map((c) => c.expressionParts.join(""));
1331
2004
  const argRendereds = argCtxs.map((c) => c.renderedParts.join(""));
1332
2005
  ctx.expressionParts.push(`${node.name}(${argExprs.join(", ")})`);
1333
2006
  ctx.renderedParts.push(`${node.name}(${argRendereds.join(", ")})`);
1334
- return applyFunction(node.name, values);
2007
+ const total = applyFunction(node.name, argResults.map((r) => r.total));
2008
+ return {
2009
+ total,
2010
+ part: {
2011
+ type: "functionCall",
2012
+ name: node.name,
2013
+ args: argResults.map((r) => r.part),
2014
+ total,
2015
+ ...partSpan(node)
2016
+ }
2017
+ };
1335
2018
  }
1336
2019
  function applyFunction(name, values) {
1337
2020
  switch (name) {
@@ -1358,12 +2041,13 @@ function requireUnaryArg(name, values) {
1358
2041
  }
1359
2042
  return x;
1360
2043
  }
1361
- function flattenModifierChain(node, rng, env) {
2044
+ function flattenModifierChain(node, rng, ctx, env) {
1362
2045
  const specs = [];
1363
2046
  let current = node;
1364
2047
  while (isModifier(current)) {
1365
2048
  const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1366
- const modCount = evalNode(current.count, rng, countCtx, env);
2049
+ const modCount = evalNode(current.count, rng, countCtx, env).total;
2050
+ mergeMetaRolls(ctx, countCtx);
1367
2051
  if (!Number.isInteger(modCount) || modCount < 0) {
1368
2052
  throw new EvaluatorError(`Invalid modifier count: ${modCount}`, "INVALID_MODIFIER_COUNT", "Modifier");
1369
2053
  }
@@ -1390,10 +2074,10 @@ function mergeDropSets(baseDice, specs) {
1390
2074
  }
1391
2075
  }
1392
2076
  }
1393
- return baseDice.map((die, index) => ({
1394
- ...die,
1395
- modifiers: droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"]
1396
- }));
2077
+ baseDice.forEach((die, index) => {
2078
+ die.modifiers = droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"];
2079
+ });
2080
+ return baseDice;
1397
2081
  }
1398
2082
  function formatExplodeCode(variant, threshold, thresholdValue) {
1399
2083
  const marker = variant === "standard" ? "!" : variant === "compound" ? "!!" : "!p";
@@ -1403,48 +2087,154 @@ function formatExplodeCode(variant, threshold, thresholdValue) {
1403
2087
  }
1404
2088
  function evalExplode(node, rng, ctx, env) {
1405
2089
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1406
- const targetValue = evalNode(node.target, rng, targetCtx, env);
2090
+ const target = evalNode(node.target, rng, targetCtx, env);
1407
2091
  const targetExpr = targetCtx.expressionParts.join("");
1408
2092
  let thresholdValue;
1409
2093
  if (node.threshold != null) {
1410
2094
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1411
- thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env);
2095
+ thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env).total;
2096
+ mergeMetaRolls(ctx, thresholdCtx);
1412
2097
  }
1413
2098
  const code = formatExplodeCode(node.variant, node.threshold, thresholdValue);
2099
+ const buildPart = (total2) => {
2100
+ const part = {
2101
+ type: "explode",
2102
+ variant: node.variant,
2103
+ target: target.part,
2104
+ total: total2,
2105
+ ...partSpan(node)
2106
+ };
2107
+ if (node.threshold != null && thresholdValue !== undefined) {
2108
+ part.threshold = { operator: node.threshold.operator, value: thresholdValue };
2109
+ }
2110
+ return part;
2111
+ };
1414
2112
  if (targetCtx.rolls.length === 0) {
1415
2113
  ctx.expressionParts.push(`${targetExpr}${code}`);
1416
2114
  ctx.renderedParts.push(`${targetExpr}${code}`);
1417
- return targetValue;
2115
+ return { total: target.total, part: buildPart(target.total) };
1418
2116
  }
1419
2117
  const shouldExplode = buildShouldExplode(node.threshold?.operator, thresholdValue);
1420
2118
  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);
1421
2119
  ctx.rolls.push(...expanded);
1422
2120
  ctx.expressionParts.push(`${targetExpr}${code}`);
1423
2121
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(expanded)}`);
1424
- return sumKeptDice(expanded);
2122
+ const total = sumKeptDice(expanded);
2123
+ return { total, part: buildPart(total) };
1425
2124
  }
1426
2125
  function evalReroll(node, rng, ctx, env) {
1427
2126
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1428
- evalNode(node.target, rng, targetCtx, env);
2127
+ const target = evalNode(node.target, rng, targetCtx, env);
1429
2128
  const targetExpr = targetCtx.expressionParts.join("");
1430
2129
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1431
- const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env);
2130
+ const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env).total;
2131
+ mergeMetaRolls(ctx, thresholdCtx);
1432
2132
  const code = `${node.once ? "ro" : "r"}${node.condition.operator}${thresholdValue}`;
2133
+ const condition = {
2134
+ operator: node.condition.operator,
2135
+ value: thresholdValue
2136
+ };
1433
2137
  if (targetCtx.rolls.length === 0) {
1434
2138
  ctx.expressionParts.push(`${targetExpr}${code}`);
1435
2139
  ctx.renderedParts.push(`${targetExpr}${code}`);
1436
- return sumKeptDice(targetCtx.rolls);
2140
+ const total2 = sumKeptDice(targetCtx.rolls);
2141
+ return {
2142
+ total: total2,
2143
+ part: {
2144
+ type: "reroll",
2145
+ once: node.once,
2146
+ condition,
2147
+ target: target.part,
2148
+ total: total2,
2149
+ ...partSpan(node)
2150
+ }
2151
+ };
1437
2152
  }
1438
2153
  const pool = node.once ? applyRerollOnce(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env) : applyRecursiveReroll(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env);
1439
2154
  ctx.rolls.push(...pool);
1440
2155
  ctx.expressionParts.push(`${targetExpr}${code}`);
1441
2156
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(pool)}`);
1442
- return sumKeptDice(pool);
2157
+ const total = sumKeptDice(pool);
2158
+ return {
2159
+ total,
2160
+ part: {
2161
+ type: "reroll",
2162
+ once: node.once,
2163
+ condition,
2164
+ target: target.part,
2165
+ total,
2166
+ ...partSpan(node)
2167
+ }
2168
+ };
2169
+ }
2170
+ function evalSort(node, rng, ctx, env) {
2171
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2172
+ const target = evalNode(node.target, rng, targetCtx, env);
2173
+ const sortedRolls = sortDice(targetCtx.rolls, node.order);
2174
+ ctx.rolls.push(...sortedRolls);
2175
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2176
+ const code = node.order === "ascending" ? "s" : "sd";
2177
+ const targetExpr = targetCtx.expressionParts.join("");
2178
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2179
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(sortedRolls)}`);
2180
+ return {
2181
+ total: target.total,
2182
+ part: {
2183
+ type: "sort",
2184
+ order: node.order,
2185
+ target: target.part,
2186
+ total: target.total,
2187
+ ...partSpan(node)
2188
+ }
2189
+ };
2190
+ }
2191
+ function evalCritThreshold(node, rng, ctx, env) {
2192
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2193
+ const target = evalNode(node.target, rng, targetCtx, env);
2194
+ const successResolved = node.successThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2195
+ const failResolved = node.failThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2196
+ const successApplied = successResolved.length > 0 ? successResolved : ["default"];
2197
+ const failApplied = failResolved.length > 0 ? failResolved : ["default"];
2198
+ applyCritThresholds(targetCtx.rolls, successApplied, failApplied);
2199
+ ctx.rolls.push(...targetCtx.rolls);
2200
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2201
+ const targetExpr = targetCtx.expressionParts.join("");
2202
+ const codes = [
2203
+ ...successResolved.map((t) => t === "default" ? "cs" : `cs${t.operator}${t.value}`),
2204
+ ...failResolved.map((t) => t === "default" ? "cf" : `cf${t.operator}${t.value}`)
2205
+ ].join("");
2206
+ ctx.expressionParts.push(`${targetExpr}${codes}`);
2207
+ ctx.renderedParts.push(`${targetExpr}${codes}${renderDice(targetCtx.rolls)}`);
2208
+ return {
2209
+ total: target.total,
2210
+ part: {
2211
+ type: "critThreshold",
2212
+ successThresholds: successResolved,
2213
+ failThresholds: failResolved,
2214
+ target: target.part,
2215
+ total: target.total,
2216
+ ...partSpan(node)
2217
+ }
2218
+ };
2219
+ }
2220
+ function resolveCritThreshold(threshold, rng, ctx, env) {
2221
+ if (threshold === "default")
2222
+ return "default";
2223
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2224
+ const resolved = evalNode(threshold.value, rng, thresholdCtx, env).total;
2225
+ mergeMetaRolls(ctx, thresholdCtx);
2226
+ if (!Number.isFinite(resolved)) {
2227
+ throw new EvaluatorError(`Invalid crit threshold: ${resolved}`, "INVALID_THRESHOLD", "CritThreshold");
2228
+ }
2229
+ return { operator: threshold.operator, value: resolved };
1443
2230
  }
1444
2231
  function evalModifier(node, rng, ctx, env) {
1445
- const { specs, baseTarget } = flattenModifierChain(node, rng, env);
2232
+ const { specs, baseTarget } = flattenModifierChain(node, rng, ctx, env);
2233
+ if (baseTarget.type === "Group" && baseTarget.expressions.length >= 2) {
2234
+ return evalGroupModifier(node, baseTarget, specs, rng, ctx, env);
2235
+ }
1446
2236
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1447
- evalNode(baseTarget, rng, targetCtx, env);
2237
+ const target = evalNode(baseTarget, rng, targetCtx, env);
1448
2238
  const mergedDice = mergeDropSets(targetCtx.rolls, specs);
1449
2239
  ctx.rolls.push(...mergedDice);
1450
2240
  const total = sumKeptDice(mergedDice);
@@ -1452,38 +2242,141 @@ function evalModifier(node, rng, ctx, env) {
1452
2242
  const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
1453
2243
  ctx.expressionParts.push(`${targetExpr}${modifierCodes}`);
1454
2244
  ctx.renderedParts.push(`${targetExpr}${renderDice(mergedDice)}`);
1455
- return total;
2245
+ return {
2246
+ total,
2247
+ part: {
2248
+ type: "modifier",
2249
+ specs: toPublicSpecs(specs),
2250
+ target: target.part,
2251
+ total,
2252
+ ...partSpan(node)
2253
+ }
2254
+ };
2255
+ }
2256
+ function stripInnerMarkers(rendered) {
2257
+ return rendered.replace(/\*\*(-?\d+)\*\*/g, "$1").replace(/__(-?\d+)__/g, "$1").replace(/~~(-?\d+)~~/g, "$1");
1456
2258
  }
1457
- function resolveThreshold(value, rng, env, role) {
2259
+ function evalGroupModifier(node, group, specs, rng, ctx, env) {
2260
+ const subRolls = group.expressions.map((expr) => {
2261
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2262
+ const sub = evalNode(expr, rng, subCtx, env);
2263
+ return {
2264
+ subtotal: sub.total,
2265
+ part: sub.part,
2266
+ rolls: subCtx.rolls,
2267
+ expr: subCtx.expressionParts.join(""),
2268
+ rendered: subCtx.renderedParts.join(""),
2269
+ versusMetadata: subCtx.versusMetadata
2270
+ };
2271
+ });
2272
+ const syntheticDice = subRolls.map((sub) => ({
2273
+ sides: 0,
2274
+ result: sub.subtotal,
2275
+ modifiers: [],
2276
+ critical: false,
2277
+ fumble: false
2278
+ }));
2279
+ const mergedSynthetic = mergeDropSets(syntheticDice, specs);
2280
+ const outerRendered = [];
2281
+ const keptIndices = [];
2282
+ let total = 0;
2283
+ for (let i = 0;i < subRolls.length; i++) {
2284
+ const sub = subRolls[i];
2285
+ const synth = mergedSynthetic[i];
2286
+ const isDropped = synth.modifiers.includes("dropped");
2287
+ if (isDropped) {
2288
+ for (const die of sub.rolls) {
2289
+ die.modifiers = [
2290
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
2291
+ "dropped"
2292
+ ];
2293
+ }
2294
+ ctx.rolls.push(...sub.rolls);
2295
+ outerRendered.push(`~~${stripInnerMarkers(sub.rendered)}~~`);
2296
+ } else {
2297
+ keptIndices.push(i);
2298
+ ctx.rolls.push(...sub.rolls);
2299
+ outerRendered.push(sub.rendered);
2300
+ total += sub.subtotal;
2301
+ }
2302
+ if (!isDropped) {
2303
+ propagateMetadata(ctx, sub.versusMetadata);
2304
+ }
2305
+ }
2306
+ const subExprStrs = subRolls.map((s) => s.expr);
2307
+ const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
2308
+ ctx.expressionParts.push(`{${subExprStrs.join(", ")}}${modifierCodes}`);
2309
+ ctx.renderedParts.push(`{${outerRendered.join(", ")}}`);
2310
+ const groupPart = {
2311
+ type: "group",
2312
+ parts: subRolls.map((s) => s.part),
2313
+ keptIndices,
2314
+ total,
2315
+ ...partSpan(group)
2316
+ };
2317
+ return {
2318
+ total,
2319
+ part: {
2320
+ type: "modifier",
2321
+ specs: toPublicSpecs(specs),
2322
+ target: groupPart,
2323
+ total,
2324
+ ...partSpan(node)
2325
+ }
2326
+ };
2327
+ }
2328
+ function resolveThreshold(value, rng, ctx, env, role) {
1458
2329
  const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1459
- const resolved = evalNode(value, rng, thresholdCtx, env);
2330
+ const resolved = evalNode(value, rng, thresholdCtx, env).total;
2331
+ mergeMetaRolls(ctx, thresholdCtx);
1460
2332
  if (!Number.isFinite(resolved)) {
1461
2333
  throw new EvaluatorError(`Invalid ${role}: ${resolved}`, "INVALID_THRESHOLD", "SuccessCount");
1462
2334
  }
1463
2335
  return resolved;
1464
2336
  }
1465
2337
  function evalSuccessCount(node, rng, ctx, env) {
2338
+ env.hasSuccessCount = true;
1466
2339
  const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1467
- const targetValue = evalNode(node.target, rng, targetCtx, env);
2340
+ const target = evalNode(node.target, rng, targetCtx, env);
1468
2341
  const targetExpr = targetCtx.expressionParts.join("");
1469
- const thresholdValue = resolveThreshold(node.threshold.value, rng, env, "threshold");
1470
- const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, env, "fail threshold") : undefined;
2342
+ const thresholdValue = resolveThreshold(node.threshold.value, rng, ctx, env, "threshold");
2343
+ const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, ctx, env, "fail threshold") : undefined;
1471
2344
  const code = `${node.threshold.operator}${thresholdValue}${failValue != null ? `f${failValue}` : ""}`;
2345
+ const buildPart = (total, successes, failures) => {
2346
+ const part = {
2347
+ type: "successCount",
2348
+ threshold: { operator: node.threshold.operator, value: thresholdValue },
2349
+ target: target.part,
2350
+ successes,
2351
+ failures,
2352
+ total,
2353
+ ...partSpan(node)
2354
+ };
2355
+ if (failValue != null && node.failThreshold != null) {
2356
+ part.failThreshold = { operator: node.failThreshold.operator, value: failValue };
2357
+ }
2358
+ return part;
2359
+ };
1472
2360
  if (targetCtx.rolls.length === 0) {
1473
2361
  ctx.expressionParts.push(`${targetExpr}${code}`);
1474
2362
  ctx.renderedParts.push(`${targetExpr}${code}`);
1475
- return targetValue;
2363
+ return { total: target.total, part: buildPart(target.total, 0, 0) };
1476
2364
  }
1477
2365
  const result = countSuccesses(targetCtx.rolls, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null ? { operator: node.failThreshold.operator, value: failValue } : undefined);
1478
- env.hasSuccessCount = true;
1479
2366
  ctx.rolls.push(...targetCtx.rolls);
1480
2367
  ctx.expressionParts.push(`${targetExpr}${code}`);
1481
2368
  ctx.renderedParts.push(`${targetExpr}${code}${renderDice(targetCtx.rolls)}`);
1482
- return result.total;
2369
+ return {
2370
+ total: result.total,
2371
+ part: buildPart(result.total, result.successes, result.failures)
2372
+ };
1483
2373
  }
1484
2374
  function extractNatural(rolls) {
1485
- const keptD20s = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !d.modifiers.includes("rerolled"));
1486
- return keptD20s.length === 1 ? keptD20s[0]?.result : undefined;
2375
+ const primaries = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !(d.modifiers.includes("exploded") && d.initialResult === undefined));
2376
+ if (primaries.length !== 1)
2377
+ return;
2378
+ const die = primaries[0];
2379
+ return die?.initialResult ?? die?.result;
1487
2380
  }
1488
2381
  function calculateDegree(total, dc, natural) {
1489
2382
  let degree;
@@ -1520,11 +2413,11 @@ function evalVersus(node, rng, ctx, env) {
1520
2413
  env.insideVersus = true;
1521
2414
  try {
1522
2415
  const rollCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1523
- const rollTotal = evalNode(node.roll, rng, rollCtx, env);
2416
+ const rollResult = evalNode(node.roll, rng, rollCtx, env);
1524
2417
  const natural = extractNatural(rollCtx.rolls);
1525
2418
  const dcCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1526
- const dcTotal = evalNode(node.dc, rng, dcCtx, env);
1527
- const degree = calculateDegree(rollTotal, dcTotal, natural);
2419
+ const dcResult = evalNode(node.dc, rng, dcCtx, env);
2420
+ const degree = calculateDegree(rollResult.total, dcResult.total, natural);
1528
2421
  ctx.rolls.push(...rollCtx.rolls, ...dcCtx.rolls);
1529
2422
  const rollExpr = rollCtx.expressionParts.join("");
1530
2423
  const dcExpr = dcCtx.expressionParts.join("");
@@ -1532,8 +2425,18 @@ function evalVersus(node, rng, ctx, env) {
1532
2425
  const dcRendered = dcCtx.renderedParts.join("");
1533
2426
  ctx.expressionParts.push(`${rollExpr} vs ${dcExpr}`);
1534
2427
  ctx.renderedParts.push(`${rollRendered} vs ${dcRendered}`);
1535
- ctx.versusMetadata = { degree, natural, dcTotal };
1536
- return rollTotal;
2428
+ ctx.versusMetadata = { degree, natural, dcTotal: dcResult.total };
2429
+ return {
2430
+ total: rollResult.total,
2431
+ part: {
2432
+ type: "versus",
2433
+ roll: rollResult.part,
2434
+ dc: dcResult.part,
2435
+ degree,
2436
+ total: rollResult.total,
2437
+ ...partSpan(node)
2438
+ }
2439
+ };
1537
2440
  } finally {
1538
2441
  env.insideVersus = false;
1539
2442
  }
@@ -1542,20 +2445,27 @@ function evaluate(ast, rng, options = {}) {
1542
2445
  const maxDice = options.maxDice != null && Number.isFinite(options.maxDice) && options.maxDice > 0 ? Math.floor(options.maxDice) : DEFAULT_MAX_DICE;
1543
2446
  const maxExplodeIterations = options.maxExplodeIterations != null && Number.isFinite(options.maxExplodeIterations) && options.maxExplodeIterations >= 0 ? Math.floor(options.maxExplodeIterations) : DEFAULT_MAX_EXPLODE_ITERATIONS;
1544
2447
  const maxRerollIterations = options.maxRerollIterations != null && Number.isFinite(options.maxRerollIterations) && options.maxRerollIterations >= 0 ? Math.floor(options.maxRerollIterations) : DEFAULT_MAX_REROLL_ITERATIONS;
2448
+ const context = options.context ?? {};
2449
+ const onMissingVariable = options.onMissingVariable ?? "throw";
1545
2450
  const env = {
1546
2451
  maxDice,
1547
2452
  maxExplodeIterations,
1548
2453
  maxRerollIterations,
1549
2454
  totalDiceRolled: 0,
1550
2455
  hasSuccessCount: false,
1551
- insideVersus: false
2456
+ insideVersus: false,
2457
+ context,
2458
+ onMissingVariable
1552
2459
  };
1553
2460
  const ctx = {
1554
2461
  rolls: [],
1555
2462
  expressionParts: [],
1556
2463
  renderedParts: []
1557
2464
  };
1558
- const total = evalNode(ast, rng, ctx, env);
2465
+ const { total, part } = evalNode(ast, rng, ctx, env);
2466
+ if (!Number.isFinite(total)) {
2467
+ throw new EvaluatorError(`Result is not a finite number: ${total}`, "NON_FINITE_RESULT", ast.type);
2468
+ }
1559
2469
  const expression = ctx.expressionParts.join("");
1560
2470
  const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
1561
2471
  const rendered = `${ctx.renderedParts.join("")} = ${trailing}`;
@@ -1564,7 +2474,8 @@ function evaluate(ast, rng, options = {}) {
1564
2474
  notation: options.notation ?? expression,
1565
2475
  expression,
1566
2476
  rendered,
1567
- rolls: ctx.rolls
2477
+ rolls: ctx.rolls,
2478
+ parts: part
1568
2479
  };
1569
2480
  if (env.hasSuccessCount) {
1570
2481
  let successes = 0;
@@ -1600,114 +2511,37 @@ function roll(notation, options = {}) {
1600
2511
  if (options.maxRerollIterations != null) {
1601
2512
  evalOptions.maxRerollIterations = options.maxRerollIterations;
1602
2513
  }
2514
+ if (options.context != null)
2515
+ evalOptions.context = options.context;
2516
+ if (options.onMissingVariable != null) {
2517
+ evalOptions.onMissingVariable = options.onMissingVariable;
2518
+ }
1603
2519
  return evaluate(ast, rng, evalOptions);
1604
2520
  }
1605
2521
  // package.json
1606
- var package_default = {
1607
- name: "roll-parser",
1608
- version: "3.0.0-alpha.0",
1609
- description: "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun-optimized.",
1610
- type: "module",
1611
- main: "./dist/index.js",
1612
- module: "./dist/index.mjs",
1613
- types: "./dist/index.d.ts",
1614
- bin: { "roll-parser": "./dist/cli.js" },
1615
- sideEffects: false,
1616
- exports: {
1617
- ".": {
1618
- types: "./dist/index.d.ts",
1619
- import: "./dist/index.mjs",
1620
- require: "./dist/index.js"
1621
- },
1622
- "./testing": {
1623
- types: "./dist/testing.d.ts",
1624
- import: "./dist/testing.mjs",
1625
- require: "./dist/testing.js"
1626
- }
1627
- },
1628
- files: [
1629
- "dist",
1630
- "src",
1631
- "!src/**/*.test.ts"
1632
- ],
1633
- keywords: [
1634
- "parser",
1635
- "dice",
1636
- "roll",
1637
- "rpg",
1638
- "dnd",
1639
- "d20",
1640
- "pratt-parser",
1641
- "typescript",
1642
- "bun"
1643
- ],
1644
- homepage: "https://github.com/edloidas/roll-parser#readme",
1645
- bugs: {
1646
- url: "https://github.com/edloidas/roll-parser/issues"
1647
- },
1648
- license: "MIT",
1649
- author: "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.com)",
1650
- repository: {
1651
- type: "git",
1652
- url: "git+https://github.com/edloidas/roll-parser.git"
1653
- },
1654
- publishConfig: {
1655
- access: "public"
1656
- },
1657
- scripts: {
1658
- typecheck: "tsc --noEmit",
1659
- lint: "biome lint .",
1660
- "lint:fix": "biome lint --write .",
1661
- format: "biome format --write .",
1662
- "format:check": "biome format .",
1663
- check: "bun typecheck && bun lint && bun format:check",
1664
- "check:fix": "bun typecheck && bun lint:fix && bun format",
1665
- "check:version": "bun scripts/check-version.ts",
1666
- "check:changelog": "bun scripts/check-changelog.ts",
1667
- clean: "rm -rf dist coverage",
1668
- 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",
1669
- "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target bun",
1670
- "build:cjs": "bun build src/index.ts --outfile dist/index.js --target node",
1671
- "build:esm:testing": "bun build src/testing.ts --outfile dist/testing.mjs --target bun",
1672
- "build:cjs:testing": "bun build src/testing.ts --outfile dist/testing.js --target node",
1673
- "build:cli": "bun build src/cli/index.ts --outfile dist/cli.js --target node",
1674
- "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
1675
- test: "bun test",
1676
- "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');"`,
1677
- "test:watch": "bun test --watch",
1678
- "test:ci": "bun test --bail",
1679
- validate: "bun run check && bun run build && bun test:ci",
1680
- coverage: "bun test --coverage",
1681
- "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
1682
- prepublishOnly: "bun run release:dry"
1683
- },
1684
- devDependencies: {
1685
- "@biomejs/biome": "^2.4.9",
1686
- "@types/bun": "^1.3.0",
1687
- "fast-check": "^4.6.0",
1688
- typescript: "^6.0.2"
1689
- },
1690
- engines: {
1691
- node: ">= 22.0.0"
1692
- }
1693
- };
2522
+ var version = "3.0.0-beta.0";
1694
2523
 
1695
2524
  // src/index.ts
1696
- var VERSION = package_default.version;
2525
+ var VERSION = version;
1697
2526
  export {
1698
2527
  roll,
1699
2528
  parse,
1700
2529
  isVersus,
2530
+ isVariable,
1701
2531
  isUnaryOp,
1702
2532
  isSuccessCount,
2533
+ isSort,
1703
2534
  isRollParserError,
1704
2535
  isReroll,
1705
2536
  isModifier,
1706
2537
  isLiteral,
2538
+ isGrouped,
2539
+ isGroup,
1707
2540
  isFunctionCall,
1708
2541
  isFateDice,
1709
2542
  isExplode,
1710
2543
  isDice,
2544
+ isCritThreshold,
1711
2545
  isBinaryOp,
1712
2546
  evaluate,
1713
2547
  VERSION,
@@ -1721,3 +2555,6 @@ export {
1721
2555
  DEFAULT_MAX_EXPLODE_ITERATIONS,
1722
2556
  DEFAULT_MAX_DICE
1723
2557
  };
2558
+
2559
+ //# debugId=46432B1558207A2664756E2164756E21
2560
+ //# sourceMappingURL=index.js.map