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.
- package/CHANGELOG.md +91 -0
- package/README.md +166 -30
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/format.d.ts +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli.js +1110 -277
- package/dist/cli.js.map +28 -0
- package/dist/errors.d.ts +7 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/evaluator/evaluator.d.ts +63 -7
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/index.d.ts +2 -2
- package/dist/evaluator/index.d.ts.map +1 -1
- package/dist/evaluator/modifiers/compare.d.ts +1 -1
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +8 -4
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.d.ts +4 -4
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/sort.d.ts +23 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +1 -1
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1112 -275
- package/dist/index.js.map +26 -0
- package/dist/lexer/lexer.d.ts +18 -3
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/tokens.d.ts +22 -2
- package/dist/lexer/tokens.d.ts.map +1 -1
- package/dist/parser/ast.d.ts +209 -24
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/parser.d.ts +48 -5
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/rng/index.d.ts +2 -2
- package/dist/rng/index.d.ts.map +1 -1
- package/dist/rng/mock.d.ts +1 -1
- package/dist/rng/mock.d.ts.map +1 -1
- package/dist/rng/seeded.d.ts +8 -1
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/roll.d.ts +6 -2
- package/dist/roll.d.ts.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +11 -0
- package/dist/types.d.ts +143 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +22 -18
- package/src/cli/args.ts +2 -1
- package/src/cli/format.ts +8 -4
- package/src/cli/index.ts +22 -5
- package/src/errors.ts +15 -3
- package/src/evaluator/evaluator.ts +826 -111
- package/src/evaluator/index.ts +2 -2
- package/src/evaluator/modifiers/compare.ts +1 -1
- package/src/evaluator/modifiers/crit-threshold.ts +59 -0
- package/src/evaluator/modifiers/explode.ts +29 -25
- package/src/evaluator/modifiers/keep-drop.ts +1 -1
- package/src/evaluator/modifiers/reroll.ts +18 -25
- package/src/evaluator/modifiers/sort.ts +30 -0
- package/src/evaluator/modifiers/success-count.ts +2 -2
- package/src/index.ts +33 -15
- package/src/lexer/lexer.ts +101 -8
- package/src/lexer/tokens.ts +42 -2
- package/src/parser/ast.ts +397 -30
- package/src/parser/parser.ts +590 -67
- package/src/rng/index.ts +2 -2
- package/src/rng/mock.ts +1 -1
- package/src/rng/seeded.ts +31 -1
- package/src/roll.ts +14 -6
- package/src/testing.ts +1 -1
- package/src/types.ts +127 -2
- package/dist/index.mjs +0 -1724
- 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["
|
|
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 ===
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 "
|
|
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
|
|
446
|
+
return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
|
|
310
447
|
case "UnaryOp":
|
|
311
|
-
return
|
|
448
|
+
return deepContainsDicePool(node.operand);
|
|
312
449
|
case "Modifier":
|
|
313
450
|
case "Explode":
|
|
314
451
|
case "Reroll":
|
|
315
452
|
case "SuccessCount":
|
|
316
|
-
|
|
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
|
|
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(
|
|
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 ===
|
|
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 !==
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
this.
|
|
383
|
-
|
|
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
|
|
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:
|
|
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:
|
|
483
|
-
sides:
|
|
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:
|
|
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:
|
|
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
|
|
507
|
-
this.expect(16 /* RPAREN */);
|
|
508
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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 {
|
|
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 (!
|
|
585
|
-
throw new ParseError(`Success counting requires a dice
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1175
|
+
return BP.COMPARE;
|
|
685
1176
|
case 16 /* RPAREN */:
|
|
686
|
-
case
|
|
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:
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
1429
|
+
for (const original of pool) {
|
|
883
1430
|
if (!canExplode(original))
|
|
884
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
modifiers: [...stripSlotFlags(original.modifiers), "kept"]
|
|
1110
|
-
});
|
|
1633
|
+
original.modifiers = [...stripSlotFlags(original.modifiers), "kept"];
|
|
1634
|
+
result.push(original);
|
|
1111
1635
|
continue;
|
|
1112
1636
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
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
|
|
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(
|
|
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
|
|
1226
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1312
|
-
ctx
|
|
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
|
-
|
|
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
|
|
1994
|
+
const argResults = [];
|
|
1322
1995
|
for (const arg of node.args) {
|
|
1323
1996
|
const argCtx = { rolls: [], expressionParts: [], renderedParts: [] };
|
|
1324
|
-
|
|
1997
|
+
argResults.push(evalNode(arg, rng, argCtx, env));
|
|
1325
1998
|
argCtxs.push(argCtx);
|
|
1326
1999
|
}
|
|
1327
2000
|
for (const argCtx of argCtxs) {
|
|
1328
|
-
ctx
|
|
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
|
-
|
|
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
|
-
|
|
1394
|
-
...die,
|
|
1395
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1486
|
-
|
|
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
|
|
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
|
|
1527
|
-
const degree = calculateDegree(
|
|
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
|
|
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
|
|
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 =
|
|
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
|