roll-parser 3.3.0 → 3.3.1
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 +12 -1
- package/README.md +6 -5
- package/dist/evaluator/evaluator.d.ts +15 -3
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +115 -91
- 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/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 +7 -8
- package/src/evaluator/evaluator.ts +155 -131
- package/src/evaluator/modifiers/flags.ts +18 -2
- package/src/render.ts +2 -1
- package/src/rng/seeded.ts +4 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.1] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- performance: the evaluator does less work per node and per die. `EvalContext`'s `expression` and `rendered` fragment arrays are single strings carried on `EvalResult`, so no branch can omit one; `rewriteFlags` is one loop instead of a closure, a filter array and a spread; a modifier's target context no longer renders the pool it discards, with every pool render routed through `renderedPool`; and `renderGroup` looks kept indices up in a `Set` rather than an array. No notation evaluates differently and no public type changed ([#336](https://github.com/edloidas/roll-parser/issues/336))
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- `package.json` metadata is tuned for npm search: the `description` leads with queryable nouns and names the systems, `roller`, `die`, `notation`, `pathfinder` and the space-separated `dice notation` join the keywords, and seven keywords that ranked outside the top 100 for their own chip are gone. `README.md` gains a `min+brotli ≤13.5 kB` badge linking to the size breakdown ([#333](https://github.com/edloidas/roll-parser/issues/333))
|
|
19
|
+
|
|
10
20
|
## [3.3.0] - 2026-08-17
|
|
11
21
|
|
|
12
22
|
Error-message work throughout: nine parse and lex errors now name the rule they
|
|
@@ -295,7 +305,8 @@ Dice mechanics (Stage 2):
|
|
|
295
305
|
- Dice count safety limit via `maxDice` option (default 10,000), enforced across the whole expression to prevent DoS via additive groups like `5000d6+5000d6` ([#19](https://github.com/edloidas/roll-parser/issues/19))
|
|
296
306
|
- Parser and evaluator correctness: duplicate `kept` modifier entries, implicit modifier count defaulting to 1 (`4d6kh` → `4d6kh1`), `critical` flag suppression when `sides === 1`, negative `--seed` CLI values ([#21](https://github.com/edloidas/roll-parser/issues/21))
|
|
297
307
|
|
|
298
|
-
[Unreleased]: https://github.com/edloidas/roll-parser/compare/v3.3.
|
|
308
|
+
[Unreleased]: https://github.com/edloidas/roll-parser/compare/v3.3.1...HEAD
|
|
309
|
+
[3.3.1]: https://github.com/edloidas/roll-parser/releases/tag/v3.3.1
|
|
299
310
|
[3.3.0]: https://github.com/edloidas/roll-parser/releases/tag/v3.3.0
|
|
300
311
|
[3.2.2]: https://github.com/edloidas/roll-parser/releases/tag/v3.2.2
|
|
301
312
|
[3.2.1]: https://github.com/edloidas/roll-parser/releases/tag/v3.2.1
|
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Dice notation for tabletop RPGs, rolled into structured results.
|
|
|
13
13
|
<a href="https://github.com/edloidas/roll-parser/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/edloidas/roll-parser/ci.yml?branch=master&label=CI" alt="CI status"></a>
|
|
14
14
|
<a href="https://github.com/edloidas/roll-parser/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/roll-parser?color=blue" alt="MIT license"></a>
|
|
15
15
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/roll-parser?color=5fa04e" alt="Node.js >= 22.12"></a>
|
|
16
|
+
<a href="https://github.com/edloidas/roll-parser#why-roll-parser"><img src="https://img.shields.io/badge/min%2Bbrotli-%E2%89%A413.5%20kB-8b5cf6" alt="min+brotli ≤13.5 kB"></a>
|
|
16
17
|
</p>
|
|
17
18
|
|
|
18
19
|
<p align="center">
|
|
@@ -51,11 +52,11 @@ roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) }).total; // 14, every run
|
|
|
51
52
|
|
|
52
53
|
## Why roll-parser
|
|
53
54
|
|
|
54
|
-
- **Complete notation.**
|
|
55
|
-
min/max clamps, success pools, crit thresholds,
|
|
56
|
-
degrees of success, math functions, variables,
|
|
57
|
-
Pathfinder, World of Darkness, Shadowrun,
|
|
58
|
-
Cthulhu without escape hatches. Spelled the way
|
|
55
|
+
- **Complete notation.** A dice roller that takes keep/drop, three flavours of
|
|
56
|
+
exploding dice, rerolls, min/max clamps, success pools, crit thresholds,
|
|
57
|
+
sorting, grouped rolls, PF2e degrees of success, math functions, variables,
|
|
58
|
+
computed dice — enough for D&D 5e, Pathfinder, World of Darkness, Shadowrun,
|
|
59
|
+
Fate, Savage Worlds, and Call of Cthulhu without escape hatches. Spelled the way
|
|
59
60
|
[Roll20](https://help.roll20.net/hc/en-us/articles/360037773133-Dice-Reference)
|
|
60
61
|
spells it wherever the two overlap, so the notation your table already uses
|
|
61
62
|
keeps working.
|
|
@@ -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;AAmoDF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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) {
|
|
@@ -760,7 +786,7 @@ function evalSuccessCount(node, rng, ctx, env) {
|
|
|
760
786
|
const failureTallyBefore = env.subtotalFailures;
|
|
761
787
|
const targetCtx = createContext();
|
|
762
788
|
const target = evalNode(node.target, rng, targetCtx, env);
|
|
763
|
-
const targetExpr =
|
|
789
|
+
const targetExpr = target.expression;
|
|
764
790
|
const poolAlreadyCounted = env.hasSuccessCount;
|
|
765
791
|
env.hasSuccessCount = true;
|
|
766
792
|
const thresholdValue = resolveThreshold(node.threshold.value, rng, ctx, env, 'threshold');
|
|
@@ -803,9 +829,12 @@ function evalSuccessCount(node, rng, ctx, env) {
|
|
|
803
829
|
}
|
|
804
830
|
}
|
|
805
831
|
if (pool.length === 0) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
832
|
+
return {
|
|
833
|
+
total: 0,
|
|
834
|
+
part: buildPart(0, 0, 0),
|
|
835
|
+
expression: `${targetExpr}${code}`,
|
|
836
|
+
rendered: `${targetExpr}${code}`,
|
|
837
|
+
};
|
|
809
838
|
}
|
|
810
839
|
const result = countSuccesses(pool, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null
|
|
811
840
|
? { operator: node.failThreshold.operator, value: failValue }
|
|
@@ -815,13 +844,13 @@ function evalSuccessCount(node, rng, ctx, env) {
|
|
|
815
844
|
env.subtotalFailures = failureTallyBefore + result.failures;
|
|
816
845
|
}
|
|
817
846
|
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
847
|
return {
|
|
823
848
|
total: result.total,
|
|
824
849
|
part: buildPart(result.total, result.successes, result.failures),
|
|
850
|
+
expression: `${targetExpr}${code}`,
|
|
851
|
+
rendered: bySubtotal
|
|
852
|
+
? `${stripTallyMarkers(target.rendered)}${code}`
|
|
853
|
+
: renderedPool(ctx, `${targetExpr}${code}`, targetCtx.rolls),
|
|
825
854
|
};
|
|
826
855
|
}
|
|
827
856
|
function extractNatural(rolls, env) {
|
|
@@ -879,12 +908,6 @@ function evalVersus(node, rng, ctx, env) {
|
|
|
879
908
|
if (dcCtx.rolls.length > 0)
|
|
880
909
|
env.hasVersusDc = true;
|
|
881
910
|
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
911
|
ctx.versusMetadata = { degree, natural, dcTotal: dcResult.total };
|
|
889
912
|
return {
|
|
890
913
|
total: rollResult.total,
|
|
@@ -896,6 +919,8 @@ function evalVersus(node, rng, ctx, env) {
|
|
|
896
919
|
total: rollResult.total,
|
|
897
920
|
...partSpan(node),
|
|
898
921
|
},
|
|
922
|
+
expression: `${rollResult.expression} vs ${dcResult.expression}`,
|
|
923
|
+
rendered: `${rollResult.rendered} vs ${dcResult.rendered}`,
|
|
899
924
|
};
|
|
900
925
|
}
|
|
901
926
|
finally {
|
|
@@ -924,13 +949,12 @@ export function evaluate(ast, rng, options = {}) {
|
|
|
924
949
|
onMissingVariable,
|
|
925
950
|
};
|
|
926
951
|
const ctx = createContext();
|
|
927
|
-
const { total, part } = evalNode(ast, rng, ctx, env);
|
|
952
|
+
const { total, part, expression, rendered: renderedBody } = evalNode(ast, rng, ctx, env);
|
|
928
953
|
if (!Number.isFinite(total)) {
|
|
929
954
|
throw new EvaluatorError(`Result is not a finite number: ${total}`, 'NON_FINITE_RESULT', ast.type);
|
|
930
955
|
}
|
|
931
|
-
const expression = ctx.expressionParts.join('');
|
|
932
956
|
const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
|
|
933
|
-
const rendered = `${
|
|
957
|
+
const rendered = `${renderedBody} = ${trailing}`;
|
|
934
958
|
const versus = ctx.versusMetadata;
|
|
935
959
|
return {
|
|
936
960
|
total,
|