roll-parser 3.2.2 → 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 +47 -1
- package/README.md +7 -6
- 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/lexer/lexer.d.ts +3 -0
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +30 -8
- package/dist/lexer/lexer.js.map +1 -1
- package/dist/parser/parser.d.ts +11 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +72 -31
- 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 +10 -11
- package/src/evaluator/evaluator.ts +155 -131
- package/src/evaluator/modifiers/flags.ts +18 -2
- package/src/lexer/lexer.ts +73 -13
- package/src/parser/parser.ts +135 -33
- 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,50 @@ 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
|
+
|
|
20
|
+
## [3.3.0] - 2026-08-17
|
|
21
|
+
|
|
22
|
+
Error-message work throughout: nine parse and lex errors now name the rule they
|
|
23
|
+
enforce, or the token actually at fault, instead of reporting parser jargon.
|
|
24
|
+
Every error code is unchanged and no notation evaluates differently — nothing
|
|
25
|
+
that parsed before now throws, and nothing that threw before now parses. Code
|
|
26
|
+
that matches on `code` is unaffected; code that matches on `message` is not.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- An expression that ends where an operand was required now names what it still owed, instead of reporting a bare unexpected end. `1d` reports `Expected a side count after 'd'`, `1d20 vs` reports `Expected a difficulty class after 'vs'`, `2d6r<` reports `Expected a value after '<'`, and a truncated `2d6>=4f` names the `f` rather than the comparison behind it. The code stays `UNEXPECTED_END`, the position still points past the last token, and empty input still reports `Empty notation` at position 0 ([#327](https://github.com/edloidas/roll-parser/issues/327))
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Demo site: the five failure tints derive from `--failure` through `color-mix` instead of hard-coded `rgba()` literals, so they follow the token in both themes rather than painting the pre-`#322` dark red under the light palette ([#324](https://github.com/edloidas/roll-parser/issues/324))
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- The hint for an identifier that merged two modifiers no longer suggests notation that does not parse. It is gated on both halves being separable modifiers — `flor` and `kx` merely start with one, so every split of them still failed — and the separator is picked by whether the first half takes a count, so the count-less `s`, `sa`, `sd`, `cs` and `cf` split on a space instead of `1`. `4d6khs` now suggests `kh1s`; a round-trip test asserts every hint the lexer emits parses ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
39
|
+
- `2d6constructor` reports `Unexpected identifier: 'constructor'` instead of lexing into a token typed with a function. The keyword and separator tables were plain objects, so `constructor` resolved to `Object`'s own; both now have a null prototype ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
40
|
+
- `Cannot apply modifier after success counting` is replaced by `Success counting is terminal: it cannot be part of a larger expression`, which holds for all three shapes that reach the rejection — a trailing modifier, arithmetic (`5d6>=5+3`), and meta-operand reuse (`4d(1d6>=3)`). The wording matches the **Pools and checks** callout in `README.md`, which carries the worked rewrites ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
41
|
+
- `UNEXPECTED_CHARACTER` appends the code point when the rejected character is not printable ASCII, so a pasted no-break space, zero-width space or BOM reads as `Unexpected character (U+00A0): ' '` rather than an empty pair of quotes. Printable ASCII stays unadorned ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
42
|
+
- `Empty @ variable name` is replaced by `@ variable name must start with a letter or '_'`, which was the rule all along — one check rejects a missing name and a wrongly-started one alike, so `@1` and `@力` were being called empty ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
43
|
+
- `()` reports `Empty parentheses` at the opener instead of `Unexpected token ')'` at the closer, the way `{}` already did — `parseGrouped` was missing the check `parseGroup` had ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
44
|
+
- A count after a modifier that takes none — `2d6s1`, `2d6cs1`, `2d6cf1`, `2d6!0` — reports `This modifier takes no count` instead of `Unexpected infix token '1'`. The digit is not the surprise, the modifier it follows is, and "infix token" is parser jargon. The check resolves down the right spine of binary, unary and versus nodes, so `1+2d6s1` is caught too, and it stops at any closer, so `2d(2d6s)1` keeps the generic wording — the stray token there follows the group, not the modifier inside it. Two juxtaposed values (`2d6 3`) and a modifier that already took a comparison (`2d6cs>5 3`) also keep the generic wording, since those are different mistakes ([#326](https://github.com/edloidas/roll-parser/issues/326), [#327](https://github.com/edloidas/roll-parser/issues/327))
|
|
45
|
+
- `vs` with nothing on its left reports `Versus needs a roll on its left` instead of `Unexpected token 'vs'` ([#326](https://github.com/edloidas/roll-parser/issues/326))
|
|
46
|
+
- Demo site: the palette meets WCAG AA. `--text-dim` and `--failure` were retuned in the dark palette, and `--text-dim`, `--accent`, `--accent-soft`, `--success` and `--failure` in both light blocks, each held on its own hue and measured against `--surface-strong`; the light `--gold-gradient` stops shifted by the same lightness delta as `--accent`, lifting the hero total's leading stop from 2.81 to 3.47 ([#322](https://github.com/edloidas/roll-parser/issues/322))
|
|
47
|
+
- Demo site: `site/404.html` no longer carries a stale `--accent`. Its inline light palette kept the pre-`#322` `#a06b1f` while `style.css` moved on, leaving the `code` span and the link hover/focus colour at 3.88 against the page gradient; `#855512` lifts them to 5.42 ([#322](https://github.com/edloidas/roll-parser/issues/322))
|
|
48
|
+
- Demo site: `--border` meets SC 1.4.11's 3:1 non-text bar on every fill it lands on — 3.37 dark and 3.30 light at the tightest adjacency, up from 1.47 and 1.33 against `--bg`. `.widget:hover` moved from `--accent-soft` to `--accent`, since the soft tint now composites fainter than the resting border and hover read as a de-emphasis ([#323](https://github.com/edloidas/roll-parser/issues/323))
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
|
|
52
|
+
- The README bundle-size figures match the current emit: `≈13.2 kB` for the full library and `≈6.1 kB` for `{ parse }` ([#326](https://github.com/edloidas/roll-parser/issues/326), [#327](https://github.com/edloidas/roll-parser/issues/327))
|
|
53
|
+
|
|
10
54
|
## [3.2.2] - 2026-08-14
|
|
11
55
|
|
|
12
56
|
### Changed
|
|
@@ -261,7 +305,9 @@ Dice mechanics (Stage 2):
|
|
|
261
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))
|
|
262
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))
|
|
263
307
|
|
|
264
|
-
[Unreleased]: https://github.com/edloidas/roll-parser/compare/v3.
|
|
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
|
|
310
|
+
[3.3.0]: https://github.com/edloidas/roll-parser/releases/tag/v3.3.0
|
|
265
311
|
[3.2.2]: https://github.com/edloidas/roll-parser/releases/tag/v3.2.2
|
|
266
312
|
[3.2.1]: https://github.com/edloidas/roll-parser/releases/tag/v3.2.1
|
|
267
313
|
[3.2.0]: https://github.com/edloidas/roll-parser/releases/tag/v3.2.0
|
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.
|
|
@@ -68,7 +69,7 @@ roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) }).total; // 14, every run
|
|
|
68
69
|
- **Safe on untrusted input.** Dice count, explosion depth, reroll depth, and
|
|
69
70
|
parse depth are all bounded, and every failure is a typed error with a stable
|
|
70
71
|
code and a source span.
|
|
71
|
-
- **Small and fast.** ≈
|
|
72
|
+
- **Small and fast.** ≈13.2 kB brotli for the whole library, ≈6.1 kB for just
|
|
72
73
|
`parse`, ≈1.4 kB for `roll-parser/render`, ≈213 B for the testing entry
|
|
73
74
|
point. Zero runtime dependencies, zero
|
|
74
75
|
`node:` imports. A `1d20` round trip takes about 0.5 µs.
|
|
@@ -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,
|