The expanded pool. Standard and penetrating explosions append dice
that exist nowhere under target, so this is the only view of the
pool the modifier actually produced. Compound explosions accumulate
in place, making it the same dice as target carries.
Like RollResult.rolls — and unlike the pool under target — this
keeps 'meta' dice. Filter them out before counting or displaying.
Optionalthreshold?: ResolvedComparePointThe post-reroll pool: discarded intermediates ('rerolled' +
'dropped') alongside their replacements. Both are appended rather
than substituted, so neither appears under target.
Keeps 'meta' dice, as RollResult.rolls does.
OptionalfailThreshold?: ResolvedComparePointThe tallied pool, sharing DieResult references with target.
Keeps 'meta' dice, as RollResult.rolls does.
Every die the target produced, in sorted order, sharing DieResult
references with target — so flags set after the sort (4d6s dl1)
show through both. Like RollResult.rolls it keeps 'meta' dice,
which rendered omits from the bracket.
import type { RollPart } from 'roll-parser';
function describe(part: RollPart): string {
switch (part.type) {
case 'literal':
return String(part.value);
case 'variable':
return `@${part.name}`;
case 'dice':
return `${part.count}d${part.sides}[${part.rolls.map((d) => d.result).join(', ')}]`;
case 'fateDice':
return `${part.count}dF`;
case 'grouped':
return `(${describe(part.inner)})`;
case 'binaryOp':
return `${describe(part.left)} ${part.operator} ${describe(part.right)}`;
case 'unaryOp':
return `-${describe(part.operand)}`;
case 'keepDrop':
return `${describe(part.target)} [${part.specs.length} keep/drop]`;
case 'explode':
return `${describe(part.target)} (${part.variant} explode)`;
case 'reroll':
return `${describe(part.target)} (reroll${part.once ? ' once' : ''})`;
case 'dieBound':
return `${describe(part.target)} (${part.bound} ${part.value})`;
case 'successCount':
return `${describe(part.target)} => ${part.successes}-${part.failures}`;
case 'versus':
return `${describe(part.roll)} vs ${describe(part.dc)}`;
case 'functionCall':
return `${part.name}(${part.args.map(describe).join(', ')})`;
case 'group':
return `{${part.parts.map(describe).join(', ')}}`;
case 'sort':
return `${describe(part.target)} (${part.order})`;
case 'critThreshold':
return `${describe(part.target)} (crit override)`;
}
}
import { roll } from 'roll-parser';
import { createMockRng } from 'roll-parser/testing';
const result = roll('4d6kh3 + 2', { rng: createMockRng([3, 6, 2, 5]) });
result.parts.type; // 'binaryOp'
result.parts.total; // 16 — always equal to result.total
describe(result.parts); // '4d6[3, 6, 2, 5] [1 keep/drop] + 2'
Structured breakdown of an evaluated expression, mirroring the AST 1:1 — every ASTNode produces exactly one RollPart. Discriminants are lowercase camelCase to distinguish evaluation-tree types from
ASTNode.type(PascalCase) at a glance.Invariants:
RollResult.parts.total === RollResult.total.successCount.total === successes - failures.literal.total === valueandvariable.total === value.rolls[]sharesDieResultreferences withRollResult.rolls[]; both reflect post-evaluation state (explode accumulation, reroll flags, keep/drop flags, success/failure tags). No deep clone. A part's own numbers record what that part computed, so an innersuccessCountre-scored by an outer one ({4d6>=5}<=2f5) keeps its tally while the dice it shares show the outer count's tags.Meta-expression sub-trees (
4d6kh(1d2),(1+1)d6counts/sides, computed thresholds) are not surfaced as nested parts — their resolved numbers appear in the owning part, and their dice are inspectable inRollResult.rollsvia the'meta'modifier tag.