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