@yipe/dice 0.3.0 → 0.5.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/.claude/worktrees/amazing-matsumoto-27220c/LICENSE +21 -0
- package/.claude/worktrees/amazing-matsumoto-27220c/README.md +518 -0
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/LICENSE +21 -0
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/README.md +518 -0
- package/.claude/worktrees/wizardly-mclean-e375de/LICENSE +21 -0
- package/.claude/worktrees/wizardly-mclean-e375de/README.md +518 -0
- package/CHANGELOG.md +239 -0
- package/dist/builder/index.cjs +270 -0
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +53 -4
- package/dist/builder/index.d.ts +53 -4
- package/dist/builder/index.js +269 -1
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +263 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -3
- package/dist/index.d.ts +35 -3
- package/dist/index.js +258 -1
- package/dist/index.js.map +1 -1
- package/dist/{pmf-DqUCnYN9.d.cts → pmf-D5VRghZI.d.cts} +116 -1
- package/dist/{pmf-DqUCnYN9.d.ts → pmf-D5VRghZI.d.ts} +116 -1
- package/package.json +2 -2
package/dist/builder/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { P as PMF,
|
|
1
|
+
import { b as RollType, P as PMF, l as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-D5VRghZI.cjs';
|
|
2
2
|
|
|
3
3
|
type RollFactory = {
|
|
4
4
|
(count: number, sides?: number, modifier?: number): RollBuilder;
|
|
@@ -14,7 +14,6 @@ type RollFactory = {
|
|
|
14
14
|
d100(): RollBuilder;
|
|
15
15
|
flat(n: number): RollBuilder;
|
|
16
16
|
};
|
|
17
|
-
type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
|
|
18
17
|
type KeepMode = "highest" | "lowest";
|
|
19
18
|
type RollConfig = {
|
|
20
19
|
count: number;
|
|
@@ -99,7 +98,7 @@ declare class DCBuilder extends RollBuilder {
|
|
|
99
98
|
toPMF(eps?: number): PMF;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode;
|
|
101
|
+
type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode | ScaleNode;
|
|
103
102
|
type DieNode = {
|
|
104
103
|
type: "die";
|
|
105
104
|
sides: number;
|
|
@@ -143,6 +142,22 @@ type MaxOfNode = {
|
|
|
143
142
|
count: number;
|
|
144
143
|
child: ExpressionNode;
|
|
145
144
|
};
|
|
145
|
+
/**
|
|
146
|
+
* Scale the child's result by `numerator / denominator`, then round.
|
|
147
|
+
*
|
|
148
|
+
* Unlike {@link HalfNode} (a fixed `// 2` with floor), this is a general, composable
|
|
149
|
+
* multiplier/divider — the building block for damage-type resistance (`1/2`, floor),
|
|
150
|
+
* vulnerability (`2/1`), and similar per-source transforms. It renders as
|
|
151
|
+
* `N * (child)` when the denominator is 1, `(child) // D` when the numerator is 1,
|
|
152
|
+
* and `(child) * N // D` otherwise.
|
|
153
|
+
*/
|
|
154
|
+
type ScaleNode = {
|
|
155
|
+
type: "scale";
|
|
156
|
+
numerator: number;
|
|
157
|
+
denominator: number;
|
|
158
|
+
rounding: "floor" | "round" | "ceil";
|
|
159
|
+
child: ExpressionNode;
|
|
160
|
+
};
|
|
146
161
|
|
|
147
162
|
declare const defaultConfig: RollConfig;
|
|
148
163
|
declare class RollBuilder {
|
|
@@ -203,6 +218,13 @@ declare class RollBuilder {
|
|
|
203
218
|
get rollType(): RollType;
|
|
204
219
|
get baseReroll(): number;
|
|
205
220
|
half(): HalfRollBuilder;
|
|
221
|
+
/**
|
|
222
|
+
* Scale this roll's result by `numerator / denominator`, rounding each outcome.
|
|
223
|
+
* A general, composable form of {@link half} — used to model damage-type resistance
|
|
224
|
+
* (`scaleResult(1, 2)` → `(expr) // 2`) and vulnerability (`scaleResult(2)` → `2 * (expr)`).
|
|
225
|
+
* Compose several of these (and plain rolls) into one payload with {@link sumRolls}.
|
|
226
|
+
*/
|
|
227
|
+
scaleResult(numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil"): ScaleRollBuilder;
|
|
206
228
|
maxOf(count: number): MaxOfRollBuilder;
|
|
207
229
|
ac(_targetAC: number): ACBuilder;
|
|
208
230
|
dc(_saveDC: number): DCBuilder;
|
|
@@ -218,6 +240,25 @@ declare class HalfRollBuilder extends RollBuilder {
|
|
|
218
240
|
toPMF(eps?: number): PMF;
|
|
219
241
|
copy(): HalfRollBuilder;
|
|
220
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* A roll whose result is scaled by `numerator / denominator` and rounded — the composable
|
|
245
|
+
* generalization of {@link HalfRollBuilder}. Renders as `N * (inner)`, `(inner) // D`, or
|
|
246
|
+
* `(inner) * N // D`. Terminal (like `half`): use {@link sumRolls} to combine with other rolls.
|
|
247
|
+
*/
|
|
248
|
+
declare class ScaleRollBuilder extends RollBuilder {
|
|
249
|
+
private readonly innerRoll;
|
|
250
|
+
private readonly numerator;
|
|
251
|
+
private readonly denominator;
|
|
252
|
+
private readonly rounding;
|
|
253
|
+
constructor(innerRoll: RollBuilder, numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil");
|
|
254
|
+
hasHiddenState(): boolean;
|
|
255
|
+
get lastConfig(): RollConfig;
|
|
256
|
+
getSubRollConfigs(): readonly RollConfig[];
|
|
257
|
+
toExpression(): string;
|
|
258
|
+
toAST(): ExpressionNode;
|
|
259
|
+
toPMF(eps?: number): PMF;
|
|
260
|
+
copy(): ScaleRollBuilder;
|
|
261
|
+
}
|
|
221
262
|
declare class MaxOfRollBuilder extends RollBuilder {
|
|
222
263
|
private readonly innerRoll;
|
|
223
264
|
private readonly count;
|
|
@@ -306,6 +347,14 @@ declare class PooledRollBuilder extends RollBuilder {
|
|
|
306
347
|
scaleDice(scale: number): RollBuilder;
|
|
307
348
|
times(count: number): PooledRollBuilder;
|
|
308
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Combine several rolls into one additive payload whose PMF is their convolution and whose
|
|
352
|
+
* expression is them joined with ` + `. Unlike `a.plus(b)`, this preserves parts that carry
|
|
353
|
+
* hidden state (e.g. `roll.scaleResult(1, 2)` / `roll.half()`), so per-damage-type resistance
|
|
354
|
+
* and vulnerability survive into both the distribution and the rendered expression.
|
|
355
|
+
* Empty parts collapse to `0`; a single part is returned unwrapped.
|
|
356
|
+
*/
|
|
357
|
+
declare function sumRolls(parts: readonly RollBuilder[]): RollBuilder;
|
|
309
358
|
|
|
310
359
|
type ActionEffect = RollBuilder;
|
|
311
360
|
declare class AttackBuilder implements CheckBuilder {
|
|
@@ -377,4 +426,4 @@ declare const flat: (n: number) => RollBuilder;
|
|
|
377
426
|
declare const roll: RollFactory;
|
|
378
427
|
declare const builderPMFCache: LRUCache<string, PMF>;
|
|
379
428
|
|
|
380
|
-
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory,
|
|
429
|
+
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, RollType, SaveBuilder, type SaveOutcome, type SaveResolution, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
|
package/dist/builder/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { P as PMF,
|
|
1
|
+
import { b as RollType, P as PMF, l as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-D5VRghZI.js';
|
|
2
2
|
|
|
3
3
|
type RollFactory = {
|
|
4
4
|
(count: number, sides?: number, modifier?: number): RollBuilder;
|
|
@@ -14,7 +14,6 @@ type RollFactory = {
|
|
|
14
14
|
d100(): RollBuilder;
|
|
15
15
|
flat(n: number): RollBuilder;
|
|
16
16
|
};
|
|
17
|
-
type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
|
|
18
17
|
type KeepMode = "highest" | "lowest";
|
|
19
18
|
type RollConfig = {
|
|
20
19
|
count: number;
|
|
@@ -99,7 +98,7 @@ declare class DCBuilder extends RollBuilder {
|
|
|
99
98
|
toPMF(eps?: number): PMF;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode;
|
|
101
|
+
type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode | ScaleNode;
|
|
103
102
|
type DieNode = {
|
|
104
103
|
type: "die";
|
|
105
104
|
sides: number;
|
|
@@ -143,6 +142,22 @@ type MaxOfNode = {
|
|
|
143
142
|
count: number;
|
|
144
143
|
child: ExpressionNode;
|
|
145
144
|
};
|
|
145
|
+
/**
|
|
146
|
+
* Scale the child's result by `numerator / denominator`, then round.
|
|
147
|
+
*
|
|
148
|
+
* Unlike {@link HalfNode} (a fixed `// 2` with floor), this is a general, composable
|
|
149
|
+
* multiplier/divider — the building block for damage-type resistance (`1/2`, floor),
|
|
150
|
+
* vulnerability (`2/1`), and similar per-source transforms. It renders as
|
|
151
|
+
* `N * (child)` when the denominator is 1, `(child) // D` when the numerator is 1,
|
|
152
|
+
* and `(child) * N // D` otherwise.
|
|
153
|
+
*/
|
|
154
|
+
type ScaleNode = {
|
|
155
|
+
type: "scale";
|
|
156
|
+
numerator: number;
|
|
157
|
+
denominator: number;
|
|
158
|
+
rounding: "floor" | "round" | "ceil";
|
|
159
|
+
child: ExpressionNode;
|
|
160
|
+
};
|
|
146
161
|
|
|
147
162
|
declare const defaultConfig: RollConfig;
|
|
148
163
|
declare class RollBuilder {
|
|
@@ -203,6 +218,13 @@ declare class RollBuilder {
|
|
|
203
218
|
get rollType(): RollType;
|
|
204
219
|
get baseReroll(): number;
|
|
205
220
|
half(): HalfRollBuilder;
|
|
221
|
+
/**
|
|
222
|
+
* Scale this roll's result by `numerator / denominator`, rounding each outcome.
|
|
223
|
+
* A general, composable form of {@link half} — used to model damage-type resistance
|
|
224
|
+
* (`scaleResult(1, 2)` → `(expr) // 2`) and vulnerability (`scaleResult(2)` → `2 * (expr)`).
|
|
225
|
+
* Compose several of these (and plain rolls) into one payload with {@link sumRolls}.
|
|
226
|
+
*/
|
|
227
|
+
scaleResult(numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil"): ScaleRollBuilder;
|
|
206
228
|
maxOf(count: number): MaxOfRollBuilder;
|
|
207
229
|
ac(_targetAC: number): ACBuilder;
|
|
208
230
|
dc(_saveDC: number): DCBuilder;
|
|
@@ -218,6 +240,25 @@ declare class HalfRollBuilder extends RollBuilder {
|
|
|
218
240
|
toPMF(eps?: number): PMF;
|
|
219
241
|
copy(): HalfRollBuilder;
|
|
220
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* A roll whose result is scaled by `numerator / denominator` and rounded — the composable
|
|
245
|
+
* generalization of {@link HalfRollBuilder}. Renders as `N * (inner)`, `(inner) // D`, or
|
|
246
|
+
* `(inner) * N // D`. Terminal (like `half`): use {@link sumRolls} to combine with other rolls.
|
|
247
|
+
*/
|
|
248
|
+
declare class ScaleRollBuilder extends RollBuilder {
|
|
249
|
+
private readonly innerRoll;
|
|
250
|
+
private readonly numerator;
|
|
251
|
+
private readonly denominator;
|
|
252
|
+
private readonly rounding;
|
|
253
|
+
constructor(innerRoll: RollBuilder, numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil");
|
|
254
|
+
hasHiddenState(): boolean;
|
|
255
|
+
get lastConfig(): RollConfig;
|
|
256
|
+
getSubRollConfigs(): readonly RollConfig[];
|
|
257
|
+
toExpression(): string;
|
|
258
|
+
toAST(): ExpressionNode;
|
|
259
|
+
toPMF(eps?: number): PMF;
|
|
260
|
+
copy(): ScaleRollBuilder;
|
|
261
|
+
}
|
|
221
262
|
declare class MaxOfRollBuilder extends RollBuilder {
|
|
222
263
|
private readonly innerRoll;
|
|
223
264
|
private readonly count;
|
|
@@ -306,6 +347,14 @@ declare class PooledRollBuilder extends RollBuilder {
|
|
|
306
347
|
scaleDice(scale: number): RollBuilder;
|
|
307
348
|
times(count: number): PooledRollBuilder;
|
|
308
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Combine several rolls into one additive payload whose PMF is their convolution and whose
|
|
352
|
+
* expression is them joined with ` + `. Unlike `a.plus(b)`, this preserves parts that carry
|
|
353
|
+
* hidden state (e.g. `roll.scaleResult(1, 2)` / `roll.half()`), so per-damage-type resistance
|
|
354
|
+
* and vulnerability survive into both the distribution and the rendered expression.
|
|
355
|
+
* Empty parts collapse to `0`; a single part is returned unwrapped.
|
|
356
|
+
*/
|
|
357
|
+
declare function sumRolls(parts: readonly RollBuilder[]): RollBuilder;
|
|
309
358
|
|
|
310
359
|
type ActionEffect = RollBuilder;
|
|
311
360
|
declare class AttackBuilder implements CheckBuilder {
|
|
@@ -377,4 +426,4 @@ declare const flat: (n: number) => RollBuilder;
|
|
|
377
426
|
declare const roll: RollFactory;
|
|
378
427
|
declare const builderPMFCache: LRUCache<string, PMF>;
|
|
379
428
|
|
|
380
|
-
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory,
|
|
429
|
+
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, RollType, SaveBuilder, type SaveOutcome, type SaveResolution, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
|
package/dist/builder/index.js
CHANGED
|
@@ -42,6 +42,7 @@ var LRUCache = class {
|
|
|
42
42
|
|
|
43
43
|
// src/common/types.ts
|
|
44
44
|
var EPS = 1e-12;
|
|
45
|
+
var MISS_NONE_OUTCOME = "missNone";
|
|
45
46
|
|
|
46
47
|
// src/pmf/query.ts
|
|
47
48
|
var _DiceQuery = class _DiceQuery {
|
|
@@ -104,6 +105,29 @@ var _DiceQuery = class _DiceQuery {
|
|
|
104
105
|
this._combinedWithAttr = normalized;
|
|
105
106
|
return normalized;
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Per-label `damage value → probability mass` series for the combined,
|
|
110
|
+
* attribution-carrying distribution — the provenance core of the stacked
|
|
111
|
+
* damage-attribution chart. Convenience for
|
|
112
|
+
* `combinedWithAttribution().attributionByValue()`; see
|
|
113
|
+
* {@link PMF.attributionByValue}.
|
|
114
|
+
*/
|
|
115
|
+
attributionByValue() {
|
|
116
|
+
return this.combinedWithAttribution().attributionByValue();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* How many of the independent single PMFs can produce the given outcome
|
|
120
|
+
* label. Useful for "all of them succeeded" style probabilities where the
|
|
121
|
+
* exponent is the number of contributing attacks (see
|
|
122
|
+
* {@link DiceQuery.probExactlyK}).
|
|
123
|
+
*/
|
|
124
|
+
countSinglesWith(label) {
|
|
125
|
+
let count = 0;
|
|
126
|
+
for (const single of this.singles) {
|
|
127
|
+
if (single.hasOutcome(label)) count++;
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
107
131
|
/**
|
|
108
132
|
* Returns the expected damage across all possible outcomes.
|
|
109
133
|
*
|
|
@@ -1387,6 +1411,20 @@ var _PMF = class _PMF {
|
|
|
1387
1411
|
static delta(value, epsilon = EPS) {
|
|
1388
1412
|
return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
|
|
1389
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Point mass at damage 0 tagged with the canonical `missNone` outcome.
|
|
1416
|
+
*
|
|
1417
|
+
* Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
|
|
1418
|
+
* builder's attack-resolution vocabulary. This uses the `missNone`
|
|
1419
|
+
* {@link OutcomeType} that the attribution charts and outcome stats key on,
|
|
1420
|
+
* so it is the correct "clean miss / no damage" delta for provenance-aware
|
|
1421
|
+
* mixtures feeding those consumers.
|
|
1422
|
+
*/
|
|
1423
|
+
static missNone(epsilon = EPS) {
|
|
1424
|
+
const m = /* @__PURE__ */ new Map();
|
|
1425
|
+
m.set(0, { p: 1, count: { [MISS_NONE_OUTCOME]: 1 }, attr: {} });
|
|
1426
|
+
return new _PMF(m, epsilon, false, "missNone");
|
|
1427
|
+
}
|
|
1390
1428
|
// This creates a single bin at value 0, but with weight 0.
|
|
1391
1429
|
static emptyMass() {
|
|
1392
1430
|
return _PMF.zero().scaleMass(0);
|
|
@@ -1908,6 +1946,49 @@ var _PMF = class _PMF {
|
|
|
1908
1946
|
`${this.identifier}+scaled(${branch.identifier},${probability})`
|
|
1909
1947
|
);
|
|
1910
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Redistributes probability mass to model an effect that only occurs with
|
|
1951
|
+
* probability `frequency` — a conditional attack, an on-hit rider, or a
|
|
1952
|
+
* sub-one AoE target fraction.
|
|
1953
|
+
*
|
|
1954
|
+
* Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
|
|
1955
|
+
* per-label `count`, AND per-label `attr` — and the freed mass is moved into
|
|
1956
|
+
* the miss bin at damage 0, tagged with the canonical `missNone` outcome.
|
|
1957
|
+
* Total probability mass is preserved.
|
|
1958
|
+
*
|
|
1959
|
+
* Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
|
|
1960
|
+
* attribution (`attr`) intact, so a frequency-scaled PMF still renders
|
|
1961
|
+
* correctly in the damage-attribution charts.
|
|
1962
|
+
*
|
|
1963
|
+
* `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
|
|
1964
|
+
* collapses all mass into the miss bin. The miss outcome is assumed to be
|
|
1965
|
+
* encoded at damage value 0.
|
|
1966
|
+
*
|
|
1967
|
+
* @param frequency Probability in [0, 1] that the effect occurs.
|
|
1968
|
+
*/
|
|
1969
|
+
applyHitFrequency(frequency) {
|
|
1970
|
+
if (!Number.isFinite(frequency) || frequency >= 1) return this;
|
|
1971
|
+
const freq = Math.max(0, frequency);
|
|
1972
|
+
const pMiss = this.pAt(0);
|
|
1973
|
+
const pHit = 1 - pMiss;
|
|
1974
|
+
const newMissMass = pMiss + (1 - freq) * pHit;
|
|
1975
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1976
|
+
newMap.set(0, {
|
|
1977
|
+
p: newMissMass,
|
|
1978
|
+
count: { [MISS_NONE_OUTCOME]: newMissMass },
|
|
1979
|
+
attr: {}
|
|
1980
|
+
});
|
|
1981
|
+
for (const [damage, bin] of this.map) {
|
|
1982
|
+
if (damage <= 0) continue;
|
|
1983
|
+
newMap.set(damage, _PMF.scaleBin(bin, freq));
|
|
1984
|
+
}
|
|
1985
|
+
return new _PMF(
|
|
1986
|
+
newMap,
|
|
1987
|
+
this.epsilon,
|
|
1988
|
+
false,
|
|
1989
|
+
`freq(${this.identifier},${freq})`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1911
1992
|
scaleMass(factor) {
|
|
1912
1993
|
if (factor === 1) return this;
|
|
1913
1994
|
const scaledMap = /* @__PURE__ */ new Map();
|
|
@@ -2135,6 +2216,39 @@ var _PMF = class _PMF {
|
|
|
2135
2216
|
pAt(x) {
|
|
2136
2217
|
return this.map.get(x)?.p ?? 0;
|
|
2137
2218
|
}
|
|
2219
|
+
/**
|
|
2220
|
+
* P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
|
|
2221
|
+
* Assumes a miss is encoded as the damage-0 bin (the convention used across
|
|
2222
|
+
* attack/save PMFs). The dual of {@link missProbability}.
|
|
2223
|
+
*/
|
|
2224
|
+
hitProbability() {
|
|
2225
|
+
return 1 - this.pAt(0);
|
|
2226
|
+
}
|
|
2227
|
+
/** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
|
|
2228
|
+
missProbability() {
|
|
2229
|
+
return this.pAt(0);
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
|
|
2233
|
+
* damage buckets, aggregating probability mass (and `count`/`attr`
|
|
2234
|
+
* provenance) into each bucket's start value. Returns this PMF unchanged when
|
|
2235
|
+
* its integer support already fits within `maxBuckets`.
|
|
2236
|
+
*
|
|
2237
|
+
* This is a lossy display/downsampling transform (bucket start replaces the
|
|
2238
|
+
* exact damage value) — use it for charting wide distributions, not for DPR
|
|
2239
|
+
* math.
|
|
2240
|
+
*/
|
|
2241
|
+
rebin(maxBuckets) {
|
|
2242
|
+
if (!(maxBuckets > 0)) return this;
|
|
2243
|
+
const support = this.support();
|
|
2244
|
+
if (support.length === 0) return this;
|
|
2245
|
+
const min = support[0];
|
|
2246
|
+
const max = support[support.length - 1];
|
|
2247
|
+
const range = max - min;
|
|
2248
|
+
if (range + 1 <= maxBuckets) return this;
|
|
2249
|
+
const binSize = Math.ceil((range + 1) / maxBuckets);
|
|
2250
|
+
return this.mapDamage((d2) => min + Math.floor((d2 - min) / binSize) * binSize);
|
|
2251
|
+
}
|
|
2138
2252
|
/** Dense integer support from min..max (inclusive).
|
|
2139
2253
|
* Useful for showing empty bars in charts.
|
|
2140
2254
|
*/
|
|
@@ -2210,6 +2324,56 @@ var _PMF = class _PMF {
|
|
|
2210
2324
|
}
|
|
2211
2325
|
return false;
|
|
2212
2326
|
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Split each damage value's probability mass across outcome labels, returning
|
|
2329
|
+
* per-label maps of `damage value → probability mass attributable to that
|
|
2330
|
+
* label`. Summing over labels at a given value recovers that value's `p`.
|
|
2331
|
+
*
|
|
2332
|
+
* Damage-bearing bins are split by `attr` weight (the share of damage each
|
|
2333
|
+
* outcome contributed); the clean-miss bin at 0 is split by `count` weight
|
|
2334
|
+
* (there is no damage to attribute). Attribution is computed on demand via
|
|
2335
|
+
* {@link withAttribution} when absent, so builder-generated PMFs work too.
|
|
2336
|
+
*
|
|
2337
|
+
* This is the provenance core of the stacked damage-attribution chart — the
|
|
2338
|
+
* caller only maps these series into its rendering format (colors, binning,
|
|
2339
|
+
* axis labels).
|
|
2340
|
+
*/
|
|
2341
|
+
attributionByValue() {
|
|
2342
|
+
const src = this.hasAttribution() ? this : this.withAttribution();
|
|
2343
|
+
const result = /* @__PURE__ */ new Map();
|
|
2344
|
+
const add = (label, damage, mass) => {
|
|
2345
|
+
if (!(mass > 0)) return;
|
|
2346
|
+
let series = result.get(label);
|
|
2347
|
+
if (!series) {
|
|
2348
|
+
series = /* @__PURE__ */ new Map();
|
|
2349
|
+
result.set(label, series);
|
|
2350
|
+
}
|
|
2351
|
+
series.set(damage, (series.get(damage) ?? 0) + mass);
|
|
2352
|
+
};
|
|
2353
|
+
for (const [damage, bin] of src.map) {
|
|
2354
|
+
const p = bin.p || 0;
|
|
2355
|
+
if (p <= 0) continue;
|
|
2356
|
+
const isMissBin = damage === 0;
|
|
2357
|
+
if (isMissBin) {
|
|
2358
|
+
let totalCount = 0;
|
|
2359
|
+
for (const k in bin.count) totalCount += bin.count[k] || 0;
|
|
2360
|
+
if (totalCount > 0) {
|
|
2361
|
+
const c = bin.count[MISS_NONE_OUTCOME] || 0;
|
|
2362
|
+
add(MISS_NONE_OUTCOME, damage, c / totalCount * p);
|
|
2363
|
+
}
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
let totalAttr = 0;
|
|
2367
|
+
if (bin.attr) for (const k in bin.attr) totalAttr += bin.attr[k] || 0;
|
|
2368
|
+
if (bin.attr && totalAttr > 0) {
|
|
2369
|
+
for (const k in bin.attr) {
|
|
2370
|
+
if (k === MISS_NONE_OUTCOME) continue;
|
|
2371
|
+
add(k, damage, (bin.attr[k] || 0) / totalAttr * p);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return result;
|
|
2376
|
+
}
|
|
2213
2377
|
tailProbGE(t) {
|
|
2214
2378
|
let s = 0;
|
|
2215
2379
|
for (const [x, bin] of this) {
|
|
@@ -3928,6 +4092,15 @@ var RollBuilder = class _RollBuilder {
|
|
|
3928
4092
|
half() {
|
|
3929
4093
|
return new HalfRollBuilder(this);
|
|
3930
4094
|
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Scale this roll's result by `numerator / denominator`, rounding each outcome.
|
|
4097
|
+
* A general, composable form of {@link half} — used to model damage-type resistance
|
|
4098
|
+
* (`scaleResult(1, 2)` → `(expr) // 2`) and vulnerability (`scaleResult(2)` → `2 * (expr)`).
|
|
4099
|
+
* Compose several of these (and plain rolls) into one payload with {@link sumRolls}.
|
|
4100
|
+
*/
|
|
4101
|
+
scaleResult(numerator, denominator = 1, rounding = "floor") {
|
|
4102
|
+
return new ScaleRollBuilder(this, numerator, denominator, rounding);
|
|
4103
|
+
}
|
|
3931
4104
|
// Create a "max of N rolls" version of this roll for crit damage with keep operations
|
|
3932
4105
|
maxOf(count) {
|
|
3933
4106
|
return new MaxOfRollBuilder(this, count);
|
|
@@ -3982,6 +4155,50 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
|
|
|
3982
4155
|
return new _HalfRollBuilder(this.innerRoll.copy());
|
|
3983
4156
|
}
|
|
3984
4157
|
};
|
|
4158
|
+
var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
|
|
4159
|
+
constructor(innerRoll, numerator, denominator = 1, rounding = "floor") {
|
|
4160
|
+
super(0);
|
|
4161
|
+
this.innerRoll = innerRoll;
|
|
4162
|
+
this.numerator = numerator;
|
|
4163
|
+
this.denominator = denominator;
|
|
4164
|
+
this.rounding = rounding;
|
|
4165
|
+
}
|
|
4166
|
+
hasHiddenState() {
|
|
4167
|
+
return this.innerRoll.hasHiddenState();
|
|
4168
|
+
}
|
|
4169
|
+
get lastConfig() {
|
|
4170
|
+
return this.innerRoll.lastConfig;
|
|
4171
|
+
}
|
|
4172
|
+
getSubRollConfigs() {
|
|
4173
|
+
return this.innerRoll.getSubRollConfigs();
|
|
4174
|
+
}
|
|
4175
|
+
toExpression() {
|
|
4176
|
+
const inner = this.innerRoll.toExpression();
|
|
4177
|
+
if (this.denominator === 1) return `${this.numerator} * (${inner})`;
|
|
4178
|
+
if (this.numerator === 1) return `(${inner}) // ${this.denominator}`;
|
|
4179
|
+
return `(${inner}) * ${this.numerator} // ${this.denominator}`;
|
|
4180
|
+
}
|
|
4181
|
+
toAST() {
|
|
4182
|
+
return {
|
|
4183
|
+
type: "scale",
|
|
4184
|
+
numerator: this.numerator,
|
|
4185
|
+
denominator: this.denominator,
|
|
4186
|
+
rounding: this.rounding,
|
|
4187
|
+
child: this.innerRoll.toAST()
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
toPMF(eps = 0) {
|
|
4191
|
+
return pmfFromRollBuilder(this, eps);
|
|
4192
|
+
}
|
|
4193
|
+
copy() {
|
|
4194
|
+
return new _ScaleRollBuilder(
|
|
4195
|
+
this.innerRoll.copy(),
|
|
4196
|
+
this.numerator,
|
|
4197
|
+
this.denominator,
|
|
4198
|
+
this.rounding
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
};
|
|
3985
4202
|
var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
|
|
3986
4203
|
constructor(innerRoll, count, diceCount, diceSides) {
|
|
3987
4204
|
super(0);
|
|
@@ -4280,6 +4497,49 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
|
|
|
4280
4497
|
return new _PooledRollBuilder(sumNode, newExpr);
|
|
4281
4498
|
}
|
|
4282
4499
|
};
|
|
4500
|
+
var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder {
|
|
4501
|
+
constructor(parts) {
|
|
4502
|
+
super(0);
|
|
4503
|
+
this.parts = parts;
|
|
4504
|
+
}
|
|
4505
|
+
hasHiddenState() {
|
|
4506
|
+
return true;
|
|
4507
|
+
}
|
|
4508
|
+
getSubRollConfigs() {
|
|
4509
|
+
return [];
|
|
4510
|
+
}
|
|
4511
|
+
toAST() {
|
|
4512
|
+
return {
|
|
4513
|
+
type: "add",
|
|
4514
|
+
children: this.parts.map((p) => ({
|
|
4515
|
+
node: p.toAST(),
|
|
4516
|
+
sign: 1
|
|
4517
|
+
}))
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
toExpression() {
|
|
4521
|
+
const exprs = this.parts.map((p) => p.toExpression()).filter((e) => e && e !== "0");
|
|
4522
|
+
if (exprs.length === 0) return "0";
|
|
4523
|
+
let result = exprs[0];
|
|
4524
|
+
for (let i = 1; i < exprs.length; i++) {
|
|
4525
|
+
const e = exprs[i];
|
|
4526
|
+
result += e.startsWith("-") ? ` - ${e.substring(1)}` : ` + ${e}`;
|
|
4527
|
+
}
|
|
4528
|
+
return result.replace(/\+ -/g, "-");
|
|
4529
|
+
}
|
|
4530
|
+
toPMF(eps = 0) {
|
|
4531
|
+
return pmfFromRollBuilder(this, eps);
|
|
4532
|
+
}
|
|
4533
|
+
copy() {
|
|
4534
|
+
return new _CompositeSumRollBuilder(this.parts.map((p) => p.copy()));
|
|
4535
|
+
}
|
|
4536
|
+
};
|
|
4537
|
+
function sumRolls(parts) {
|
|
4538
|
+
const meaningful = parts.filter((p) => p !== void 0);
|
|
4539
|
+
if (meaningful.length === 0) return new RollBuilder(0);
|
|
4540
|
+
if (meaningful.length === 1) return meaningful[0];
|
|
4541
|
+
return new CompositeSumRollBuilder(meaningful);
|
|
4542
|
+
}
|
|
4283
4543
|
|
|
4284
4544
|
// src/builder/factory.ts
|
|
4285
4545
|
var rollFn = (count, sidesOrDie, modifier) => {
|
|
@@ -4510,6 +4770,11 @@ function resolve(node, eps = defaultEps) {
|
|
|
4510
4770
|
if (count === 1) return childPMF;
|
|
4511
4771
|
return computeMaxOfPMF(childPMF, count, eps);
|
|
4512
4772
|
}
|
|
4773
|
+
case "scale": {
|
|
4774
|
+
const childPMF = resolve(node.child, eps);
|
|
4775
|
+
const denom = node.denominator === 0 ? 1 : node.denominator;
|
|
4776
|
+
return childPMF.scaleDamage(node.numerator / denom, node.rounding);
|
|
4777
|
+
}
|
|
4513
4778
|
}
|
|
4514
4779
|
})();
|
|
4515
4780
|
builderPMFCache.set(cacheKey, result);
|
|
@@ -4581,6 +4846,7 @@ function findDie(node) {
|
|
|
4581
4846
|
case "d20Roll":
|
|
4582
4847
|
case "half":
|
|
4583
4848
|
case "maxOf":
|
|
4849
|
+
case "scale":
|
|
4584
4850
|
return findDie(node.child);
|
|
4585
4851
|
case "keep":
|
|
4586
4852
|
return findDie(node.child.child);
|
|
@@ -4765,6 +5031,8 @@ function getASTSignature(node) {
|
|
|
4765
5031
|
return `half{ch:${getASTSignature(node.child)}}`;
|
|
4766
5032
|
case "maxOf":
|
|
4767
5033
|
return `maxOf{c:${node.count},ch:${getASTSignature(node.child)}}`;
|
|
5034
|
+
case "scale":
|
|
5035
|
+
return `scale{n:${node.numerator},d:${node.denominator},r:${node.rounding},ch:${getASTSignature(node.child)}}`;
|
|
4768
5036
|
case "add": {
|
|
4769
5037
|
let constantValue = 0;
|
|
4770
5038
|
const otherChildrenSigs = [];
|
|
@@ -5201,6 +5469,6 @@ RollBuilder.prototype.dc = function(saveDC) {
|
|
|
5201
5469
|
return new DCBuilder(this).dc(saveDC);
|
|
5202
5470
|
};
|
|
5203
5471
|
|
|
5204
|
-
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
|
|
5472
|
+
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
|
|
5205
5473
|
//# sourceMappingURL=index.js.map
|
|
5206
5474
|
//# sourceMappingURL=index.js.map
|