roll-parser 3.3.0 → 3.4.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 +42 -1
- package/README.md +60 -32
- package/dist/cli/args.d.ts +11 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +67 -42
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/format.d.ts +8 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +4 -3
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/main.d.ts +3 -3
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +50 -19
- package/dist/cli/main.js.map +1 -1
- package/dist/evaluator/evaluator.d.ts +15 -3
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +134 -105
- package/dist/evaluator/evaluator.js.map +1 -1
- package/dist/evaluator/modifiers/flags.d.ts +7 -1
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -1
- package/dist/evaluator/modifiers/flags.js +9 -1
- package/dist/evaluator/modifiers/flags.js.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.js +19 -16
- package/dist/evaluator/modifiers/keep-drop.js.map +1 -1
- package/dist/lexer/lexer.d.ts +5 -0
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +3 -0
- package/dist/lexer/lexer.js.map +1 -1
- package/dist/parser/parser.d.ts +1 -0
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +23 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +2 -1
- package/dist/render.js.map +1 -1
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/rng/seeded.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -14
- package/src/cli/args.ts +112 -40
- package/src/cli/format.ts +11 -3
- package/src/cli/main.ts +97 -22
- package/src/evaluator/evaluator.ts +189 -150
- package/src/evaluator/modifiers/flags.ts +18 -2
- package/src/evaluator/modifiers/keep-drop.ts +42 -16
- package/src/lexer/lexer.ts +8 -0
- package/src/parser/parser.ts +36 -32
- package/src/render.ts +2 -1
- package/src/rng/seeded.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -24,7 +24,8 @@ export { EvaluatorError };
|
|
|
24
24
|
export declare const DEFAULT_MAX_DICE = 10000;
|
|
25
25
|
export { DEFAULT_MAX_EXPLODE_ITERATIONS, DEFAULT_MAX_REROLL_ITERATIONS };
|
|
26
26
|
/**
|
|
27
|
-
* Per-branch
|
|
27
|
+
* Per-branch accumulator for what flows up a recursion unchanged or merged. A
|
|
28
|
+
* field a parent reformats instead belongs on {@link EvalResult}.
|
|
28
29
|
*
|
|
29
30
|
* Module-level export, deliberately absent from `src/index.ts` — the package
|
|
30
31
|
* surface never mentions it. See {@link mergeMetaRolls} for why the export
|
|
@@ -32,8 +33,19 @@ export { DEFAULT_MAX_EXPLODE_ITERATIONS, DEFAULT_MAX_REROLL_ITERATIONS };
|
|
|
32
33
|
*/
|
|
33
34
|
export type EvalContext = {
|
|
34
35
|
rolls: DieResult[];
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Set by a parent that discards its child's `rendered`. Every pool modifier
|
|
38
|
+
* does: it rebuilds the breakdown from the target's `expression` plus a
|
|
39
|
+
* `renderDice` pass of its own.
|
|
40
|
+
*
|
|
41
|
+
* ! In a chain each modifier's own context is the next one out's discarded
|
|
42
|
+
* ! target, so a render site that skips the check leaves every level above the
|
|
43
|
+
* ! innermost still rendering into the void.
|
|
44
|
+
*
|
|
45
|
+
* ! Does not propagate into nested contexts — `(4d6)kh1` evaluates into a
|
|
46
|
+
* ! fresh one, so only a modifier's direct dice-pool target is spared.
|
|
47
|
+
*/
|
|
48
|
+
suppressRender?: boolean;
|
|
37
49
|
/**
|
|
38
50
|
* Populated by `evalVersus` with the resolved degree and natural value.
|
|
39
51
|
* `evaluate()` reads this from the top-level ctx to surface `degree` and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluator/evaluator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,cAAc,EAAuC,MAAM,cAAc,CAAC;AAElG,OAAO,KAAK,EACV,OAAO,EAmBR,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,KAAK,EAIV,SAAS,EACT,eAAe,EAKf,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,OAAO,EAKL,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AAWhC,OAAO,EAGL,6BAA6B,EAC9B,MAAM,uBAAuB,CAAC;AAM/B,OAAO,EAAE,cAAc,EAAE,CAAC;AAM1B;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAmCvC,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,CAAC;AAMzE
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluator/evaluator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,cAAc,EAAuC,MAAM,cAAc,CAAC;AAElG,OAAO,KAAK,EACV,OAAO,EAmBR,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,KAAK,EAIV,SAAS,EACT,eAAe,EAKf,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,OAAO,EAKL,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AAWhC,OAAO,EAGL,6BAA6B,EAC9B,MAAM,uBAAuB,CAAC;AAM/B,OAAO,EAAE,cAAc,EAAE,CAAC;AAM1B;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAmCvC,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,CAAC;AAMzE;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,cAAc,CAAC,EAAE;QACf,MAAM,EAAE,eAAe,CAAC;QACxB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAkpDF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,GAAE,eAAoB,GAAG,UAAU,CAgE1F"}
|
|
@@ -26,7 +26,15 @@ function resolveLimit(value, option, fallback, min) {
|
|
|
26
26
|
}
|
|
27
27
|
export { DEFAULT_MAX_EXPLODE_ITERATIONS, DEFAULT_MAX_REROLL_ITERATIONS };
|
|
28
28
|
function createContext() {
|
|
29
|
-
return { rolls: []
|
|
29
|
+
return { rolls: [] };
|
|
30
|
+
}
|
|
31
|
+
function createDiscardedRenderContext() {
|
|
32
|
+
const ctx = createContext();
|
|
33
|
+
ctx.suppressRender = true;
|
|
34
|
+
return ctx;
|
|
35
|
+
}
|
|
36
|
+
function renderedPool(ctx, prefix, dice) {
|
|
37
|
+
return ctx.suppressRender ? '' : `${prefix}${renderDice(dice)}`;
|
|
30
38
|
}
|
|
31
39
|
function partSpan(node) {
|
|
32
40
|
if (node.start == null || node.end == null)
|
|
@@ -97,7 +105,7 @@ function evalNode(node, rng, ctx, env) {
|
|
|
97
105
|
function evalNodeInner(node, rng, ctx, env) {
|
|
98
106
|
switch (node.type) {
|
|
99
107
|
case 'Literal':
|
|
100
|
-
return evalLiteral(node
|
|
108
|
+
return evalLiteral(node);
|
|
101
109
|
case 'Dice':
|
|
102
110
|
return evalDice(node, rng, ctx, env);
|
|
103
111
|
case 'FateDice':
|
|
@@ -129,34 +137,38 @@ function evalNodeInner(node, rng, ctx, env) {
|
|
|
129
137
|
case 'CritThreshold':
|
|
130
138
|
return evalCritThreshold(node, rng, ctx, env);
|
|
131
139
|
case 'Variable':
|
|
132
|
-
return evalVariable(node,
|
|
140
|
+
return evalVariable(node, env);
|
|
133
141
|
default: {
|
|
134
142
|
const exhaustive = node;
|
|
135
143
|
throw new EvaluatorError(`Unknown node type: ${exhaustive.type}`, 'UNKNOWN_NODE_TYPE', exhaustive.type);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
|
-
function evalLiteral(node
|
|
147
|
+
function evalLiteral(node) {
|
|
140
148
|
const { value } = node;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const text = String(value);
|
|
150
|
+
return {
|
|
151
|
+
total: value,
|
|
152
|
+
part: { type: 'literal', value, total: value, ...partSpan(node) },
|
|
153
|
+
expression: text,
|
|
154
|
+
rendered: text,
|
|
155
|
+
};
|
|
144
156
|
}
|
|
145
157
|
function variableNeedsBraces(name) {
|
|
146
158
|
return !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
147
159
|
}
|
|
148
|
-
function evalVariable(node,
|
|
160
|
+
function evalVariable(node, env) {
|
|
149
161
|
const present = Object.hasOwn(env.context, node.name);
|
|
150
162
|
if (!present) {
|
|
151
163
|
if (env.onMissingVariable === 'throw') {
|
|
152
164
|
throw new EvaluatorError(`Undefined variable: ${node.name}`, 'UNDEFINED_VARIABLE', 'Variable');
|
|
153
165
|
}
|
|
154
166
|
const display = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
|
|
155
|
-
ctx.expressionParts.push('0');
|
|
156
|
-
ctx.renderedParts.push(`${display}[0]`);
|
|
157
167
|
return {
|
|
158
168
|
total: 0,
|
|
159
169
|
part: { type: 'variable', name: node.name, value: 0, total: 0, ...partSpan(node) },
|
|
170
|
+
expression: '0',
|
|
171
|
+
rendered: `${display}[0]`,
|
|
160
172
|
};
|
|
161
173
|
}
|
|
162
174
|
const value = env.context[node.name];
|
|
@@ -164,11 +176,11 @@ function evalVariable(node, ctx, env) {
|
|
|
164
176
|
throw new EvaluatorError(`Invalid variable value: ${node.name} = ${value}`, 'INVALID_VARIABLE_VALUE', 'Variable');
|
|
165
177
|
}
|
|
166
178
|
const display = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
|
|
167
|
-
ctx.expressionParts.push(String(value));
|
|
168
|
-
ctx.renderedParts.push(`${display}[${value}]`);
|
|
169
179
|
return {
|
|
170
180
|
total: value,
|
|
171
181
|
part: { type: 'variable', name: node.name, value, total: value, ...partSpan(node) },
|
|
182
|
+
expression: String(value),
|
|
183
|
+
rendered: `${display}[${value}]`,
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
function evalMetaOperand(node, rng, ctx, env) {
|
|
@@ -207,9 +219,12 @@ function rollPool(count, notation, rollDie, ctx) {
|
|
|
207
219
|
total += die.result;
|
|
208
220
|
}
|
|
209
221
|
appendAll(ctx.rolls, dice);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
return {
|
|
223
|
+
total,
|
|
224
|
+
rolls: dice,
|
|
225
|
+
expression: notation,
|
|
226
|
+
rendered: ctx.suppressRender ? '' : `${notation}${renderDice(dice)}`,
|
|
227
|
+
};
|
|
213
228
|
}
|
|
214
229
|
function evalDice(node, rng, ctx, env) {
|
|
215
230
|
const count = evalMetaOperand(node.count, rng, ctx, env);
|
|
@@ -222,15 +237,25 @@ function evalDice(node, rng, ctx, env) {
|
|
|
222
237
|
throw new EvaluatorError(`Dice sides ${sides} exceeds maximum of ${MAX_DICE_SIDES}`, 'INVALID_DICE_SIDES', 'Dice');
|
|
223
238
|
}
|
|
224
239
|
chargeDice(env, count, 'Dice');
|
|
225
|
-
const { total, rolls } = rollPool(count, `${count}d${sides}`, () => createDieResult(sides, rng.nextInt(1, sides), ['kept']), ctx);
|
|
226
|
-
return {
|
|
240
|
+
const { total, rolls, expression, rendered } = rollPool(count, `${count}d${sides}`, () => createDieResult(sides, rng.nextInt(1, sides), ['kept']), ctx);
|
|
241
|
+
return {
|
|
242
|
+
total,
|
|
243
|
+
part: { type: 'dice', count, sides, rolls, total, ...partSpan(node) },
|
|
244
|
+
expression,
|
|
245
|
+
rendered,
|
|
246
|
+
};
|
|
227
247
|
}
|
|
228
248
|
function evalFateDice(node, rng, ctx, env) {
|
|
229
249
|
const count = evalMetaOperand(node.count, rng, ctx, env);
|
|
230
250
|
requireDiceCount(count, 'FateDice');
|
|
231
251
|
chargeDice(env, count, 'FateDice');
|
|
232
|
-
const { total, rolls } = rollPool(count, `${count}dF`, () => createFateDieResult(rng.nextInt(-1, 1), ['kept']), ctx);
|
|
233
|
-
return {
|
|
252
|
+
const { total, rolls, expression, rendered } = rollPool(count, `${count}dF`, () => createFateDieResult(rng.nextInt(-1, 1), ['kept']), ctx);
|
|
253
|
+
return {
|
|
254
|
+
total,
|
|
255
|
+
part: { type: 'fateDice', count, rolls, total, ...partSpan(node) },
|
|
256
|
+
expression,
|
|
257
|
+
rendered,
|
|
258
|
+
};
|
|
234
259
|
}
|
|
235
260
|
function propagateMetadata(parent, metadata) {
|
|
236
261
|
if (!metadata)
|
|
@@ -251,12 +276,6 @@ function evalBinaryOp(node, rng, ctx, env) {
|
|
|
251
276
|
const right = evalNode(node.right, rng, rightCtx, env);
|
|
252
277
|
mergeContext(ctx, leftCtx);
|
|
253
278
|
mergeContext(ctx, rightCtx);
|
|
254
|
-
const leftExpr = leftCtx.expressionParts.join('');
|
|
255
|
-
const rightExpr = rightCtx.expressionParts.join('');
|
|
256
|
-
const leftRendered = leftCtx.renderedParts.join('');
|
|
257
|
-
const rightRendered = rightCtx.renderedParts.join('');
|
|
258
|
-
ctx.expressionParts.push(`${leftExpr} ${node.operator} ${rightExpr}`);
|
|
259
|
-
ctx.renderedParts.push(`${leftRendered} ${node.operator} ${rightRendered}`);
|
|
260
279
|
const total = applyBinaryOperator(node.operator, left.total, right.total);
|
|
261
280
|
return {
|
|
262
281
|
total,
|
|
@@ -268,6 +287,8 @@ function evalBinaryOp(node, rng, ctx, env) {
|
|
|
268
287
|
total,
|
|
269
288
|
...partSpan(node),
|
|
270
289
|
},
|
|
290
|
+
expression: `${left.expression} ${node.operator} ${right.expression}`,
|
|
291
|
+
rendered: `${left.rendered} ${node.operator} ${right.rendered}`,
|
|
271
292
|
};
|
|
272
293
|
}
|
|
273
294
|
function applyBinaryOperator(operator, left, right) {
|
|
@@ -300,25 +321,23 @@ function evalUnaryOp(node, rng, ctx, env) {
|
|
|
300
321
|
const innerCtx = createContext();
|
|
301
322
|
const inner = evalNode(node.operand, rng, innerCtx, env);
|
|
302
323
|
mergeContext(ctx, innerCtx);
|
|
303
|
-
const innerExpr = innerCtx.expressionParts.join('');
|
|
304
|
-
const innerRendered = innerCtx.renderedParts.join('');
|
|
305
|
-
ctx.expressionParts.push(`-${innerExpr}`);
|
|
306
|
-
ctx.renderedParts.push(`-${innerRendered}`);
|
|
307
324
|
const total = -inner.total;
|
|
308
325
|
return {
|
|
309
326
|
total,
|
|
310
327
|
part: { type: 'unaryOp', operator: '-', operand: inner.part, total, ...partSpan(node) },
|
|
328
|
+
expression: `-${inner.expression}`,
|
|
329
|
+
rendered: `-${inner.rendered}`,
|
|
311
330
|
};
|
|
312
331
|
}
|
|
313
332
|
function evalGrouped(node, rng, ctx, env) {
|
|
314
333
|
const innerCtx = createContext();
|
|
315
334
|
const inner = evalNode(node.expression, rng, innerCtx, env);
|
|
316
335
|
mergeContext(ctx, innerCtx);
|
|
317
|
-
ctx.expressionParts.push(`(${innerCtx.expressionParts.join('')})`);
|
|
318
|
-
ctx.renderedParts.push(`(${innerCtx.renderedParts.join('')})`);
|
|
319
336
|
return {
|
|
320
337
|
total: inner.total,
|
|
321
338
|
part: { type: 'grouped', inner: inner.part, total: inner.total, ...partSpan(node) },
|
|
339
|
+
expression: `(${inner.expression})`,
|
|
340
|
+
rendered: `(${inner.rendered})`,
|
|
322
341
|
};
|
|
323
342
|
}
|
|
324
343
|
function evalGroup(node, rng, ctx, env) {
|
|
@@ -330,14 +349,17 @@ function evalGroup(node, rng, ctx, env) {
|
|
|
330
349
|
const subCtx = createContext();
|
|
331
350
|
const sub = evalNode(expr, rng, subCtx, env);
|
|
332
351
|
mergeContext(ctx, subCtx);
|
|
333
|
-
subExprs.push(
|
|
334
|
-
subRendered.push(
|
|
352
|
+
subExprs.push(sub.expression);
|
|
353
|
+
subRendered.push(sub.rendered);
|
|
335
354
|
subParts.push(sub.part);
|
|
336
355
|
total += sub.total;
|
|
337
356
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
return {
|
|
358
|
+
total,
|
|
359
|
+
part: { type: 'group', parts: subParts, total, ...partSpan(node) },
|
|
360
|
+
expression: `{${subExprs.join(', ')}}`,
|
|
361
|
+
rendered: `{${subRendered.join(', ')}}`,
|
|
362
|
+
};
|
|
341
363
|
}
|
|
342
364
|
function evalFunctionCall(node, rng, ctx, env) {
|
|
343
365
|
const argCtxs = [];
|
|
@@ -350,10 +372,6 @@ function evalFunctionCall(node, rng, ctx, env) {
|
|
|
350
372
|
for (const argCtx of argCtxs) {
|
|
351
373
|
mergeContext(ctx, argCtx);
|
|
352
374
|
}
|
|
353
|
-
const argExprs = argCtxs.map((c) => c.expressionParts.join(''));
|
|
354
|
-
const argRendereds = argCtxs.map((c) => c.renderedParts.join(''));
|
|
355
|
-
ctx.expressionParts.push(`${node.name}(${argExprs.join(', ')})`);
|
|
356
|
-
ctx.renderedParts.push(`${node.name}(${argRendereds.join(', ')})`);
|
|
357
375
|
const total = applyFunction(node.name, argResults.map((r) => r.total));
|
|
358
376
|
return {
|
|
359
377
|
total,
|
|
@@ -364,6 +382,8 @@ function evalFunctionCall(node, rng, ctx, env) {
|
|
|
364
382
|
total,
|
|
365
383
|
...partSpan(node),
|
|
366
384
|
},
|
|
385
|
+
expression: `${node.name}(${argResults.map((r) => r.expression).join(', ')})`,
|
|
386
|
+
rendered: `${node.name}(${argResults.map((r) => r.rendered).join(', ')})`,
|
|
367
387
|
};
|
|
368
388
|
}
|
|
369
389
|
function applyFunction(name, values) {
|
|
@@ -474,9 +494,9 @@ function formatExplodeCode(variant, threshold, thresholdValue) {
|
|
|
474
494
|
return `${marker}${threshold.operator}${thresholdValue}`;
|
|
475
495
|
}
|
|
476
496
|
function evalExplode(node, rng, ctx, env) {
|
|
477
|
-
const targetCtx =
|
|
497
|
+
const targetCtx = createDiscardedRenderContext();
|
|
478
498
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
479
|
-
const targetExpr =
|
|
499
|
+
const targetExpr = target.expression;
|
|
480
500
|
let thresholdValue;
|
|
481
501
|
if (node.threshold != null) {
|
|
482
502
|
thresholdValue = evalMetaOperand(node.threshold.value, rng, ctx, env);
|
|
@@ -497,31 +517,35 @@ function evalExplode(node, rng, ctx, env) {
|
|
|
497
517
|
return part;
|
|
498
518
|
};
|
|
499
519
|
if (targetCtx.rolls.length === 0) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
520
|
+
return {
|
|
521
|
+
total: target.total,
|
|
522
|
+
part: buildPart(target.total, targetCtx.rolls),
|
|
523
|
+
expression: `${targetExpr}${code}`,
|
|
524
|
+
rendered: ctx.suppressRender ? '' : `${targetExpr}${code}`,
|
|
525
|
+
};
|
|
503
526
|
}
|
|
504
527
|
const shouldExplode = buildShouldExplode(node.threshold?.operator, thresholdValue);
|
|
505
528
|
const expanded = EXPLODE_APPLIERS[node.variant](targetCtx.rolls, shouldExplode, rng, env);
|
|
506
529
|
appendAll(ctx.rolls, expanded);
|
|
507
|
-
ctx.expressionParts.push(`${targetExpr}${code}`);
|
|
508
|
-
ctx.renderedParts.push(`${targetExpr}${code}${renderDice(expanded)}`);
|
|
509
530
|
const total = sumKeptDice(expanded, env.hasVersusDc);
|
|
510
|
-
return {
|
|
531
|
+
return {
|
|
532
|
+
total,
|
|
533
|
+
part: buildPart(total, expanded),
|
|
534
|
+
expression: `${targetExpr}${code}`,
|
|
535
|
+
rendered: renderedPool(ctx, `${targetExpr}${code}`, expanded),
|
|
536
|
+
};
|
|
511
537
|
}
|
|
512
538
|
function evalReroll(node, rng, ctx, env) {
|
|
513
|
-
const targetCtx =
|
|
539
|
+
const targetCtx = createDiscardedRenderContext();
|
|
514
540
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
515
541
|
const thresholdValue = evalMetaOperand(node.condition.value, rng, ctx, env);
|
|
516
542
|
const code = `${node.once ? 'ro' : 'r'}${node.condition.operator}${thresholdValue}`;
|
|
517
|
-
const modifierExpr = joinModifierCode(
|
|
543
|
+
const modifierExpr = joinModifierCode(target.expression, code);
|
|
518
544
|
const condition = {
|
|
519
545
|
operator: node.condition.operator,
|
|
520
546
|
value: thresholdValue,
|
|
521
547
|
};
|
|
522
548
|
if (targetCtx.rolls.length === 0) {
|
|
523
|
-
ctx.expressionParts.push(modifierExpr);
|
|
524
|
-
ctx.renderedParts.push(modifierExpr);
|
|
525
549
|
const total = sumKeptDice(targetCtx.rolls, env.hasVersusDc);
|
|
526
550
|
return {
|
|
527
551
|
total,
|
|
@@ -534,14 +558,14 @@ function evalReroll(node, rng, ctx, env) {
|
|
|
534
558
|
total,
|
|
535
559
|
...partSpan(node),
|
|
536
560
|
},
|
|
561
|
+
expression: modifierExpr,
|
|
562
|
+
rendered: ctx.suppressRender ? '' : modifierExpr,
|
|
537
563
|
};
|
|
538
564
|
}
|
|
539
565
|
const pool = node.once
|
|
540
566
|
? applyRerollOnce(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env)
|
|
541
567
|
: applyRecursiveReroll(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env);
|
|
542
568
|
appendAll(ctx.rolls, pool);
|
|
543
|
-
ctx.expressionParts.push(modifierExpr);
|
|
544
|
-
ctx.renderedParts.push(`${modifierExpr}${renderDice(pool)}`);
|
|
545
569
|
const total = sumKeptDice(pool, env.hasVersusDc);
|
|
546
570
|
return {
|
|
547
571
|
total,
|
|
@@ -554,10 +578,12 @@ function evalReroll(node, rng, ctx, env) {
|
|
|
554
578
|
total,
|
|
555
579
|
...partSpan(node),
|
|
556
580
|
},
|
|
581
|
+
expression: modifierExpr,
|
|
582
|
+
rendered: renderedPool(ctx, modifierExpr, pool),
|
|
557
583
|
};
|
|
558
584
|
}
|
|
559
585
|
function evalDieBound(node, rng, ctx, env) {
|
|
560
|
-
const targetCtx =
|
|
586
|
+
const targetCtx = createDiscardedRenderContext();
|
|
561
587
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
562
588
|
const boundValue = evalMetaOperand(node.value, rng, ctx, env);
|
|
563
589
|
if (!Number.isFinite(boundValue)) {
|
|
@@ -566,11 +592,11 @@ function evalDieBound(node, rng, ctx, env) {
|
|
|
566
592
|
applyDieBound(targetCtx.rolls, node.bound, boundValue, env.hasVersusDc);
|
|
567
593
|
appendAll(ctx.rolls, targetCtx.rolls);
|
|
568
594
|
const code = boundValue < 0 ? `${node.bound}(${boundValue})` : `${node.bound}${boundValue}`;
|
|
569
|
-
const modifierExpr = joinModifierCode(
|
|
570
|
-
ctx.expressionParts.push(modifierExpr);
|
|
571
|
-
ctx.renderedParts.push(`${modifierExpr}${renderDice(targetCtx.rolls)}`);
|
|
595
|
+
const modifierExpr = joinModifierCode(target.expression, code);
|
|
572
596
|
const total = sumKeptDice(targetCtx.rolls, env.hasVersusDc);
|
|
573
597
|
return {
|
|
598
|
+
expression: modifierExpr,
|
|
599
|
+
rendered: renderedPool(ctx, modifierExpr, targetCtx.rolls),
|
|
574
600
|
total,
|
|
575
601
|
part: {
|
|
576
602
|
type: 'dieBound',
|
|
@@ -583,16 +609,16 @@ function evalDieBound(node, rng, ctx, env) {
|
|
|
583
609
|
};
|
|
584
610
|
}
|
|
585
611
|
function evalSort(node, rng, ctx, env) {
|
|
586
|
-
const targetCtx =
|
|
612
|
+
const targetCtx = createDiscardedRenderContext();
|
|
587
613
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
588
614
|
const sortedRolls = sortDice(targetCtx.rolls, node.order, env.hasVersusDc);
|
|
589
615
|
appendAll(ctx.rolls, sortedRolls);
|
|
590
616
|
propagateMetadata(ctx, targetCtx.versusMetadata);
|
|
591
617
|
const code = node.order === 'ascending' ? 's' : 'sd';
|
|
592
|
-
const modifierExpr = joinModifierCode(
|
|
593
|
-
ctx.expressionParts.push(modifierExpr);
|
|
594
|
-
ctx.renderedParts.push(`${modifierExpr}${renderDice(sortedRolls)}`);
|
|
618
|
+
const modifierExpr = joinModifierCode(target.expression, code);
|
|
595
619
|
return {
|
|
620
|
+
expression: modifierExpr,
|
|
621
|
+
rendered: renderedPool(ctx, modifierExpr, sortedRolls),
|
|
596
622
|
total: target.total,
|
|
597
623
|
part: {
|
|
598
624
|
type: 'sort',
|
|
@@ -605,7 +631,7 @@ function evalSort(node, rng, ctx, env) {
|
|
|
605
631
|
};
|
|
606
632
|
}
|
|
607
633
|
function evalCritThreshold(node, rng, ctx, env) {
|
|
608
|
-
const targetCtx =
|
|
634
|
+
const targetCtx = createDiscardedRenderContext();
|
|
609
635
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
610
636
|
const successResolved = node.successThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
|
|
611
637
|
const failResolved = node.failThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
|
|
@@ -617,10 +643,10 @@ function evalCritThreshold(node, rng, ctx, env) {
|
|
|
617
643
|
const modifierExpr = [
|
|
618
644
|
...successResolved.map((t) => (t === 'default' ? 'cs' : `cs${t.operator}${t.value}`)),
|
|
619
645
|
...failResolved.map((t) => (t === 'default' ? 'cf' : `cf${t.operator}${t.value}`)),
|
|
620
|
-
].reduce(joinModifierCode,
|
|
621
|
-
ctx.expressionParts.push(modifierExpr);
|
|
622
|
-
ctx.renderedParts.push(`${modifierExpr}${renderDice(targetCtx.rolls)}`);
|
|
646
|
+
].reduce(joinModifierCode, target.expression);
|
|
623
647
|
return {
|
|
648
|
+
expression: modifierExpr,
|
|
649
|
+
rendered: renderedPool(ctx, modifierExpr, targetCtx.rolls),
|
|
624
650
|
total: target.total,
|
|
625
651
|
part: {
|
|
626
652
|
type: 'critThreshold',
|
|
@@ -646,15 +672,13 @@ function evalKeepDrop(node, rng, ctx, env) {
|
|
|
646
672
|
if (baseTarget.type === 'Group' && baseTarget.expressions.length >= 2) {
|
|
647
673
|
return evalGroupKeepDrop(node, baseTarget, specs, rng, ctx, env);
|
|
648
674
|
}
|
|
649
|
-
const targetCtx =
|
|
675
|
+
const targetCtx = createDiscardedRenderContext();
|
|
650
676
|
const target = evalNode(baseTarget, rng, targetCtx, env);
|
|
651
677
|
const mergedDice = mergeDropSets(targetCtx.rolls, specs, env.hasVersusDc);
|
|
652
678
|
appendAll(ctx.rolls, mergedDice);
|
|
653
679
|
const total = sumKeptDice(mergedDice, env.hasVersusDc);
|
|
654
|
-
const targetExpr =
|
|
680
|
+
const targetExpr = target.expression;
|
|
655
681
|
const keepDropCodes = specs.map((s) => `${s.code}${s.count}`).join('');
|
|
656
|
-
ctx.expressionParts.push(joinModifierCode(targetExpr, keepDropCodes));
|
|
657
|
-
ctx.renderedParts.push(`${targetExpr}${renderDice(mergedDice)}`);
|
|
658
682
|
return {
|
|
659
683
|
total,
|
|
660
684
|
part: {
|
|
@@ -664,6 +688,8 @@ function evalKeepDrop(node, rng, ctx, env) {
|
|
|
664
688
|
total,
|
|
665
689
|
...partSpan(node),
|
|
666
690
|
},
|
|
691
|
+
expression: joinModifierCode(targetExpr, keepDropCodes),
|
|
692
|
+
rendered: renderedPool(ctx, targetExpr, mergedDice),
|
|
667
693
|
};
|
|
668
694
|
}
|
|
669
695
|
function stripInnerMarkers(rendered) {
|
|
@@ -682,8 +708,8 @@ function evalGroupKeepDrop(node, group, specs, rng, ctx, env) {
|
|
|
682
708
|
subtotal: sub.total,
|
|
683
709
|
part: sub.part,
|
|
684
710
|
rolls: subCtx.rolls,
|
|
685
|
-
expr:
|
|
686
|
-
rendered:
|
|
711
|
+
expr: sub.expression,
|
|
712
|
+
rendered: sub.rendered,
|
|
687
713
|
versusMetadata: subCtx.versusMetadata,
|
|
688
714
|
scoredSuccesses: env.subtotalSuccesses - successTally,
|
|
689
715
|
scoredFailures: env.subtotalFailures - failureTally,
|
|
@@ -725,8 +751,6 @@ function evalGroupKeepDrop(node, group, specs, rng, ctx, env) {
|
|
|
725
751
|
}
|
|
726
752
|
const subExprStrs = subRolls.map((s) => s.expr);
|
|
727
753
|
const keepDropCodes = specs.map((s) => `${s.code}${s.count}`).join('');
|
|
728
|
-
ctx.expressionParts.push(`{${subExprStrs.join(', ')}}${keepDropCodes}`);
|
|
729
|
-
ctx.renderedParts.push(`{${outerRendered.join(', ')}}`);
|
|
730
754
|
const groupPart = {
|
|
731
755
|
type: 'group',
|
|
732
756
|
parts: subRolls.map((s) => s.part),
|
|
@@ -743,6 +767,8 @@ function evalGroupKeepDrop(node, group, specs, rng, ctx, env) {
|
|
|
743
767
|
total,
|
|
744
768
|
...partSpan(node),
|
|
745
769
|
},
|
|
770
|
+
expression: `{${subExprStrs.join(', ')}}${keepDropCodes}`,
|
|
771
|
+
rendered: `{${outerRendered.join(', ')}}`,
|
|
746
772
|
};
|
|
747
773
|
}
|
|
748
774
|
function resolveThreshold(value, rng, ctx, env, role) {
|
|
@@ -755,12 +781,28 @@ function resolveThreshold(value, rng, ctx, env, role) {
|
|
|
755
781
|
function formatFailCode(operator, value) {
|
|
756
782
|
return operator === '=' ? `f${value}` : `f${operator}${value}`;
|
|
757
783
|
}
|
|
784
|
+
function clearTallyFlags(rolls) {
|
|
785
|
+
for (const die of rolls) {
|
|
786
|
+
die.modifiers = stripFlags(die.modifiers, TALLY_FLAGS);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function countablePool(target, targetCtx, bySubtotal) {
|
|
790
|
+
if (!bySubtotal)
|
|
791
|
+
return targetCtx.rolls;
|
|
792
|
+
return target.part.parts.map((sub) => ({
|
|
793
|
+
sides: 0,
|
|
794
|
+
result: sub.total,
|
|
795
|
+
modifiers: [],
|
|
796
|
+
critical: false,
|
|
797
|
+
fumble: false,
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
758
800
|
function evalSuccessCount(node, rng, ctx, env) {
|
|
759
801
|
const successTallyBefore = env.subtotalSuccesses;
|
|
760
802
|
const failureTallyBefore = env.subtotalFailures;
|
|
761
803
|
const targetCtx = createContext();
|
|
762
804
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
763
|
-
const targetExpr =
|
|
805
|
+
const targetExpr = target.expression;
|
|
764
806
|
const poolAlreadyCounted = env.hasSuccessCount;
|
|
765
807
|
env.hasSuccessCount = true;
|
|
766
808
|
const thresholdValue = resolveThreshold(node.threshold.value, rng, ctx, env, 'threshold');
|
|
@@ -788,24 +830,16 @@ function evalSuccessCount(node, rng, ctx, env) {
|
|
|
788
830
|
return part;
|
|
789
831
|
};
|
|
790
832
|
const bySubtotal = node.target.type === 'Group' && node.target.expressions.length >= 2;
|
|
791
|
-
const pool = bySubtotal
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
result: sub.total,
|
|
795
|
-
modifiers: [],
|
|
796
|
-
critical: false,
|
|
797
|
-
fumble: false,
|
|
798
|
-
}))
|
|
799
|
-
: targetCtx.rolls;
|
|
800
|
-
if (bySubtotal && poolAlreadyCounted) {
|
|
801
|
-
for (const die of targetCtx.rolls) {
|
|
802
|
-
die.modifiers = stripFlags(die.modifiers, TALLY_FLAGS);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
833
|
+
const pool = countablePool(target, targetCtx, bySubtotal);
|
|
834
|
+
if (bySubtotal && poolAlreadyCounted)
|
|
835
|
+
clearTallyFlags(targetCtx.rolls);
|
|
805
836
|
if (pool.length === 0) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
837
|
+
return {
|
|
838
|
+
total: 0,
|
|
839
|
+
part: buildPart(0, 0, 0),
|
|
840
|
+
expression: `${targetExpr}${code}`,
|
|
841
|
+
rendered: `${targetExpr}${code}`,
|
|
842
|
+
};
|
|
809
843
|
}
|
|
810
844
|
const result = countSuccesses(pool, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null
|
|
811
845
|
? { operator: node.failThreshold.operator, value: failValue }
|
|
@@ -815,13 +849,13 @@ function evalSuccessCount(node, rng, ctx, env) {
|
|
|
815
849
|
env.subtotalFailures = failureTallyBefore + result.failures;
|
|
816
850
|
}
|
|
817
851
|
appendAll(ctx.rolls, targetCtx.rolls);
|
|
818
|
-
ctx.expressionParts.push(`${targetExpr}${code}`);
|
|
819
|
-
ctx.renderedParts.push(bySubtotal
|
|
820
|
-
? `${stripTallyMarkers(targetCtx.renderedParts.join(''))}${code}`
|
|
821
|
-
: `${targetExpr}${code}${renderDice(targetCtx.rolls)}`);
|
|
822
852
|
return {
|
|
823
853
|
total: result.total,
|
|
824
854
|
part: buildPart(result.total, result.successes, result.failures),
|
|
855
|
+
expression: `${targetExpr}${code}`,
|
|
856
|
+
rendered: bySubtotal
|
|
857
|
+
? `${stripTallyMarkers(target.rendered)}${code}`
|
|
858
|
+
: renderedPool(ctx, `${targetExpr}${code}`, targetCtx.rolls),
|
|
825
859
|
};
|
|
826
860
|
}
|
|
827
861
|
function extractNatural(rolls, env) {
|
|
@@ -879,12 +913,6 @@ function evalVersus(node, rng, ctx, env) {
|
|
|
879
913
|
if (dcCtx.rolls.length > 0)
|
|
880
914
|
env.hasVersusDc = true;
|
|
881
915
|
appendAll(ctx.rolls, dcCtx.rolls);
|
|
882
|
-
const rollExpr = rollCtx.expressionParts.join('');
|
|
883
|
-
const dcExpr = dcCtx.expressionParts.join('');
|
|
884
|
-
const rollRendered = rollCtx.renderedParts.join('');
|
|
885
|
-
const dcRendered = dcCtx.renderedParts.join('');
|
|
886
|
-
ctx.expressionParts.push(`${rollExpr} vs ${dcExpr}`);
|
|
887
|
-
ctx.renderedParts.push(`${rollRendered} vs ${dcRendered}`);
|
|
888
916
|
ctx.versusMetadata = { degree, natural, dcTotal: dcResult.total };
|
|
889
917
|
return {
|
|
890
918
|
total: rollResult.total,
|
|
@@ -896,6 +924,8 @@ function evalVersus(node, rng, ctx, env) {
|
|
|
896
924
|
total: rollResult.total,
|
|
897
925
|
...partSpan(node),
|
|
898
926
|
},
|
|
927
|
+
expression: `${rollResult.expression} vs ${dcResult.expression}`,
|
|
928
|
+
rendered: `${rollResult.rendered} vs ${dcResult.rendered}`,
|
|
899
929
|
};
|
|
900
930
|
}
|
|
901
931
|
finally {
|
|
@@ -924,13 +954,12 @@ export function evaluate(ast, rng, options = {}) {
|
|
|
924
954
|
onMissingVariable,
|
|
925
955
|
};
|
|
926
956
|
const ctx = createContext();
|
|
927
|
-
const { total, part } = evalNode(ast, rng, ctx, env);
|
|
957
|
+
const { total, part, expression, rendered: renderedBody } = evalNode(ast, rng, ctx, env);
|
|
928
958
|
if (!Number.isFinite(total)) {
|
|
929
959
|
throw new EvaluatorError(`Result is not a finite number: ${total}`, 'NON_FINITE_RESULT', ast.type);
|
|
930
960
|
}
|
|
931
|
-
const expression = ctx.expressionParts.join('');
|
|
932
961
|
const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
|
|
933
|
-
const rendered = `${
|
|
962
|
+
const rendered = `${renderedBody} = ${trailing}`;
|
|
934
963
|
const versus = ctx.versusMetadata;
|
|
935
964
|
return {
|
|
936
965
|
total,
|