feat(builder): composable scale node for per-type damage transforms#4
Merged
Conversation
Add a general `scale` AST node plus `RollBuilder.scaleResult(numerator, denominator, rounding)` and a `sumRolls(parts)` composite factory. Unlike the fixed `half()` node, a scaled sub-roll can be combined with plain rolls via `sumRolls` without the flat-config `.plus()` merge silently dropping it, so the scaling survives into both the PMF and the rendered expression. Renders as `N * (expr)` (e.g. vulnerability, `2 * (1d6)`), `(expr) // D` (e.g. resistance, `(1d6) // 2`), or `(expr) * N // D`. Enables per-damage-type resistance / immunity / vulnerability in downstream consumers: build one sub-roll per damage type, scale (or omit) each, and `sumRolls` them into a single hit/crit/save payload that convolves per-type PMFs (so `floor` stays per-type) while attribution and outcome mixing flow through the existing path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnisdByShvDQBbAgudZggX
`dist/` is normally gitignored and produced by `prepare` on npm install. Committing the built output on this branch lets a consumer install it as a git dependency (before 0.4 is published to npm) without running the build. Revert once @yipe/dice@0.4 is published and consumers pin `^0.4.0`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnisdByShvDQBbAgudZggX
The dist/ was committed only as a bridge so a consumer could install this branch as a git dependency before 0.4.0 was published. It's build output (regenerated by prepublishOnly/prepare), gitignored, and bloated the diff by ~21.5k lines. Removing it from tracking leaves the branch as the ~258-line source change. Publish 0.4.0 to npm; consumers then depend on ^0.4.0 rather than a git ref. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnisdByShvDQBbAgudZggX
Bump version to 0.5.0 and roll the unreleased 0.4.0 changelog into it, documenting the composable scaleResult / sumRolls API alongside the existing PMF provenance work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnisdByShvDQBbAgudZggX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Adds a general, composable scaling primitive to the builder so a sub-roll can be halved / doubled / dropped and still combine with plain rolls into one payload — without the flat-config
.plus()merge silently dropping it (the limitationHalfRollBuilderhits today).This is the building block for per-damage-type enemy resistance / immunity / vulnerability in the DPR calculator (yipe/dpr): build one sub-roll per damage type, scale (or omit) each, and
sumRollsthem into a single hit/crit/save payload. Because scaling stays per-type,floor(½)is applied per type (never to the merged total), and it flows into both the PMF and the rendered expression.API
ScaleNodeAST node:{ type: 'scale', numerator, denominator, rounding, child }, wired intoresolve()(PMF),findDie(),getASTSignature(), and the expression renderer.RollBuilder.scaleResult(numerator, denominator = 1, rounding = 'floor')→ScaleRollBuilder. RendersN * (expr),(expr) // D, or(expr) * N // D.sumRolls(parts)— additive composite whose PMF convolves the parts and whose expression joins them with+, preserving parts that carry hidden state (scaleResult/half).Examples:
d6.scaleResult(1, 2)→(1d6) // 2, floored PMF {0,1,1,2,2,3}d6.scaleResult(2)→2 * (1d6), mean 7sumRolls([base, fire.scaleResult(1, 2)])→1d8 + 3 + (1d6) // 2Testing
New
scale.test.tscovers floor/round/ceil scaling, the///*rendering, the mixed-type per-type floor, and a full attack carrying resistance through both hit and crit payloads with attribution intact. Full suite green (yarn build && yarn typecheck && yarn test, 1230 tests).Notes
.plus()hot path and thehalfnode are untouched.dist/on this branch so consumers can install it as a git dependency before 0.4 is published to npm; revert once@yipe/dice@0.4is published.