@yipe/dice 0.9.0 → 0.10.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/dist/builder/ac.d.ts.map +1 -1
- package/dist/builder/ast.d.ts +32 -2
- package/dist/builder/ast.d.ts.map +1 -1
- package/dist/builder/attack.d.ts.map +1 -1
- package/dist/builder/dc.d.ts.map +1 -1
- package/dist/builder/example.d.ts +4 -4
- package/dist/builder/example.d.ts.map +1 -1
- package/dist/builder/index.cjs +221 -84
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.js +221 -84
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/roll.d.ts +5 -1
- package/dist/builder/roll.d.ts.map +1 -1
- package/dist/builder/save.d.ts.map +1 -1
- package/dist/index.cjs +64 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +64 -15
- package/dist/index.js.map +1 -1
- package/dist/parser/dice.d.ts +14 -0
- package/dist/parser/dice.d.ts.map +1 -1
- package/dist/pmf/pmf.d.ts +10 -4
- package/dist/pmf/pmf.d.ts.map +1 -1
- package/package.json +2 -2
- package/CHANGELOG.md +0 -456
package/dist/builder/index.cjs
CHANGED
|
@@ -1476,7 +1476,7 @@ var _PMF = class _PMF {
|
|
|
1476
1476
|
const id = this.identifier;
|
|
1477
1477
|
let key = `${id}`;
|
|
1478
1478
|
for (let i = 1; i < n; i++) key += `+${id}`;
|
|
1479
|
-
return `${key}@${eps}`;
|
|
1479
|
+
return `${key}@${eps}|${this.fingerprint()}`;
|
|
1480
1480
|
}
|
|
1481
1481
|
/**
|
|
1482
1482
|
* Efficiently computes this PMF convolved with itself `n` times.
|
|
@@ -1827,16 +1827,27 @@ var _PMF = class _PMF {
|
|
|
1827
1827
|
return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
|
|
1828
1828
|
}
|
|
1829
1829
|
/**
|
|
1830
|
-
* A
|
|
1831
|
-
* cache keys change
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1830
|
+
* A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus
|
|
1831
|
+
* the `normalized` flag, so convolution/power cache keys change whenever the underlying
|
|
1832
|
+
* numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can
|
|
1833
|
+
* keep the same identifier, support, mass, and face sum while differing in per-bin
|
|
1834
|
+
* probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate --
|
|
1835
|
+
* that previously let `power()` return one PMF's cached result for a different PMF. Memoized
|
|
1836
|
+
* because a PMF is immutable once constructed -- this avoids re-deriving the key on every
|
|
1837
|
+
* convolve()/power() call (including cache hits). Bin order is sorted by damage value (and
|
|
1838
|
+
* label keys sorted within each bin) so two equal-content PMFs built via different code paths
|
|
1839
|
+
* fingerprint identically regardless of Map insertion order.
|
|
1834
1840
|
*/
|
|
1835
1841
|
fingerprint() {
|
|
1836
1842
|
if (this._fingerprint === void 0) {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1843
|
+
const bins = [...this.map.entries()].sort((a, b) => a[0] - b[0]);
|
|
1844
|
+
const parts = [];
|
|
1845
|
+
for (const [damageValue, bin] of bins) {
|
|
1846
|
+
const countStr = Object.keys(bin.count).sort().map((k) => `${k}:${bin.count[k]}`).join(",");
|
|
1847
|
+
const attrStr = bin.attr ? Object.keys(bin.attr).sort().map((k) => `${k}:${bin.attr[k]}`).join(",") : "";
|
|
1848
|
+
parts.push(`${damageValue}:${bin.p}[${countStr}]{${attrStr}}`);
|
|
1849
|
+
}
|
|
1850
|
+
this._fingerprint = `${this.normalized ? 1 : 0}|${parts.join(";")}`;
|
|
1840
1851
|
}
|
|
1841
1852
|
return this._fingerprint;
|
|
1842
1853
|
}
|
|
@@ -2068,11 +2079,13 @@ var _PMF = class _PMF {
|
|
|
2068
2079
|
/** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
|
|
2069
2080
|
quantile(p) {
|
|
2070
2081
|
if (this.map.size === 0) return 0;
|
|
2082
|
+
const totalMass = this.mass();
|
|
2083
|
+
if (totalMass <= 0) return 0;
|
|
2071
2084
|
const s = this.support().sort((a, b) => a - b);
|
|
2072
2085
|
let acc = 0;
|
|
2073
2086
|
for (const x of s) {
|
|
2074
2087
|
acc += this.pAt(x);
|
|
2075
|
-
if (acc >= p) return x;
|
|
2088
|
+
if (acc / totalMass >= p) return x;
|
|
2076
2089
|
}
|
|
2077
2090
|
return s[s.length - 1];
|
|
2078
2091
|
}
|
|
@@ -3051,6 +3064,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
|
|
|
3051
3064
|
finalResult = finalResult.combine(dice);
|
|
3052
3065
|
return { newNorm: currentNorm * normValue, updatedResult: finalResult };
|
|
3053
3066
|
}
|
|
3067
|
+
function subtractCounts(a, b) {
|
|
3068
|
+
const result = new Dice();
|
|
3069
|
+
for (const [key, value] of a.getFaceEntries()) result.increment(key, value);
|
|
3070
|
+
for (const [key, value] of b.getFaceEntries()) result.increment(key, -value);
|
|
3071
|
+
return result;
|
|
3072
|
+
}
|
|
3054
3073
|
function parseExpression(arr, n) {
|
|
3055
3074
|
const result = (() => {
|
|
3056
3075
|
const res = parseArgument(arr, n);
|
|
@@ -3058,8 +3077,29 @@ function parseExpression(arr, n) {
|
|
|
3058
3077
|
})();
|
|
3059
3078
|
let op = parseOperation(arr);
|
|
3060
3079
|
let finalResult = result;
|
|
3080
|
+
let baseDieMeta = result.privateData?.checkDie && !result.privateData.checkDie.rerollOne ? result.privateData.checkDie : void 0;
|
|
3081
|
+
let bonusOnly = Dice.scalar(0);
|
|
3061
3082
|
while (op != null) {
|
|
3062
3083
|
const arg = !op.unary ? parseArgument(arr, n) : finalResult;
|
|
3084
|
+
let acAlreadyApplied = false;
|
|
3085
|
+
if (baseDieMeta) {
|
|
3086
|
+
if (op === Dice.prototype.addNonZero) {
|
|
3087
|
+
bonusOnly = bonusOnly.add(arg);
|
|
3088
|
+
} else if (op === Dice.prototype.subtract) {
|
|
3089
|
+
bonusOnly = bonusOnly.subtract(arg);
|
|
3090
|
+
} else if (op === Dice.prototype.ac && typeof arg === "number") {
|
|
3091
|
+
const natMaxSlice = bonusOnly.add(baseDieMeta.sides);
|
|
3092
|
+
const restSlice = subtractCounts(finalResult, natMaxSlice);
|
|
3093
|
+
const gatedNatMaxSlice = natMaxSlice.ac(arg);
|
|
3094
|
+
finalResult = restSlice.ac(arg).combine(gatedNatMaxSlice);
|
|
3095
|
+
finalResult.privateData.checkDie = baseDieMeta;
|
|
3096
|
+
finalResult.privateData.natMaxCritSlice = gatedNatMaxSlice;
|
|
3097
|
+
acAlreadyApplied = true;
|
|
3098
|
+
baseDieMeta = void 0;
|
|
3099
|
+
} else {
|
|
3100
|
+
baseDieMeta = void 0;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3063
3103
|
let crit;
|
|
3064
3104
|
let critNorm = 1;
|
|
3065
3105
|
if (arr[0] === "x" || arr[0] === "c") {
|
|
@@ -3070,11 +3110,17 @@ function parseExpression(arr, n) {
|
|
|
3070
3110
|
assertToken(arr, "i");
|
|
3071
3111
|
assertToken(arr, "t");
|
|
3072
3112
|
const count = isXcrit ? parseNumber(arr, n) : 1;
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3113
|
+
const trackedCritSlice = finalResult.privateData?.natMaxCritSlice;
|
|
3114
|
+
if (count === 1 && trackedCritSlice) {
|
|
3115
|
+
crit = trackedCritSlice;
|
|
3116
|
+
finalResult = subtractCounts(finalResult, trackedCritSlice);
|
|
3117
|
+
} else {
|
|
3118
|
+
crit = new Dice();
|
|
3119
|
+
for (let i = 0; i < count; i++) {
|
|
3120
|
+
const max = finalResult.maxFace();
|
|
3121
|
+
crit.setFace(max, finalResult.get(max));
|
|
3122
|
+
finalResult = finalResult.deleteFace(max);
|
|
3123
|
+
}
|
|
3078
3124
|
}
|
|
3079
3125
|
critNorm = crit.total();
|
|
3080
3126
|
crit = op.call(crit, parseBinaryArgument(arg, arr, n));
|
|
@@ -3125,7 +3171,9 @@ function parseExpression(arr, n) {
|
|
|
3125
3171
|
missNorm = miss && missNorm ? miss.total() / missNorm : 1;
|
|
3126
3172
|
}
|
|
3127
3173
|
let norm = finalResult.total();
|
|
3128
|
-
|
|
3174
|
+
if (!acAlreadyApplied) {
|
|
3175
|
+
finalResult = op.call(finalResult, arg);
|
|
3176
|
+
}
|
|
3129
3177
|
norm = norm ? finalResult.total() / norm : 1;
|
|
3130
3178
|
if (crit) {
|
|
3131
3179
|
const result2 = combineDiceWithNormalization(
|
|
@@ -3316,6 +3364,7 @@ function parseDice(s, n) {
|
|
|
3316
3364
|
if (rerollOne) {
|
|
3317
3365
|
result = result.reroll(1);
|
|
3318
3366
|
}
|
|
3367
|
+
result.privateData.checkDie = { sides, rerollOne };
|
|
3319
3368
|
return result;
|
|
3320
3369
|
}
|
|
3321
3370
|
function peek(arr, expected) {
|
|
@@ -3507,6 +3556,12 @@ function d20PMF(rerollOne) {
|
|
|
3507
3556
|
}
|
|
3508
3557
|
|
|
3509
3558
|
// src/builder/roll.ts
|
|
3559
|
+
function validateScaleInt(scale) {
|
|
3560
|
+
const scaleInt = Math.floor(scale);
|
|
3561
|
+
if (scaleInt !== scale) throw new Error("Scale must be an integer");
|
|
3562
|
+
if (scaleInt <= 0) throw new Error("Scale must be > 0");
|
|
3563
|
+
return scaleInt;
|
|
3564
|
+
}
|
|
3510
3565
|
var rollPMFCache = new LRUCache(4e3);
|
|
3511
3566
|
function clearRollCache() {
|
|
3512
3567
|
rollPMFCache.clear();
|
|
@@ -3712,7 +3767,8 @@ var RollBuilder = class _RollBuilder {
|
|
|
3712
3767
|
newConfigs[newConfigs.length - 1].explode = count;
|
|
3713
3768
|
return this.create(newConfigs);
|
|
3714
3769
|
}
|
|
3715
|
-
/** Apply per-die minimum value (
|
|
3770
|
+
/** Apply per-die minimum value (floors each die roll at `val`, e.g. `minimum(3)` treats a 1 or
|
|
3771
|
+
* 2 as a 3 -- the 2024 Great Weapon Fighting style). */
|
|
3716
3772
|
minimum(val) {
|
|
3717
3773
|
if (val !== void 0 && isNaN(val))
|
|
3718
3774
|
throw new Error("Invalid NaN value for minimum");
|
|
@@ -3720,7 +3776,7 @@ var RollBuilder = class _RollBuilder {
|
|
|
3720
3776
|
if (val === 0) return this;
|
|
3721
3777
|
if (val < 0) throw new Error("Minimum value must be >= 0");
|
|
3722
3778
|
const newConfigs = this.getSubRollConfigs();
|
|
3723
|
-
newConfigs[newConfigs.length - 1].minimum = val
|
|
3779
|
+
newConfigs[newConfigs.length - 1].minimum = val;
|
|
3724
3780
|
return this.create(newConfigs);
|
|
3725
3781
|
}
|
|
3726
3782
|
bestOf(count) {
|
|
@@ -3821,9 +3877,7 @@ var RollBuilder = class _RollBuilder {
|
|
|
3821
3877
|
return this.create(configs);
|
|
3822
3878
|
}
|
|
3823
3879
|
scaleDice(scale) {
|
|
3824
|
-
const scaleInt =
|
|
3825
|
-
if (scaleInt !== scale) throw new Error("Scale must be an integer");
|
|
3826
|
-
if (scaleInt <= 0) throw new Error("Scale must be > 0");
|
|
3880
|
+
const scaleInt = validateScaleInt(scale);
|
|
3827
3881
|
const newConfigs = this.getSubRollConfigs().map((config) => {
|
|
3828
3882
|
if (!config.sides || config.sides <= 0) return config;
|
|
3829
3883
|
return { ...config, count: config.count * scaleInt };
|
|
@@ -3946,12 +4000,16 @@ var RollBuilder = class _RollBuilder {
|
|
|
3946
4000
|
}
|
|
3947
4001
|
configToSingleExpressionWithoutModifier(config, isRootDie) {
|
|
3948
4002
|
if (!config.sides || config.sides <= 0) return "";
|
|
4003
|
+
if (config.explode && Number.isFinite(config.explode) && config.explode > 0) {
|
|
4004
|
+
throw new Error(
|
|
4005
|
+
`toExpression() cannot represent an exploding die (d${config.sides} explode(${config.explode})): the string grammar has no explode syntax. Use the builder's own PMF (.toPMF()/.pmf) instead of round-tripping through toExpression()/parse().`
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
3949
4008
|
let baseDie = `d${config.sides}`;
|
|
4009
|
+
const rerollClause = config.reroll > 0 ? config.reroll === 1 ? " reroll 1" : ` reroll d${config.reroll}` : "";
|
|
3950
4010
|
if (config.reroll > 0) {
|
|
3951
|
-
if (config.minimum > 0 && config.explode > 0) ; else
|
|
3952
|
-
|
|
3953
|
-
} else {
|
|
3954
|
-
for (let i = 1; i <= config.reroll; i++) baseDie += ` reroll ${i}`;
|
|
4011
|
+
if (config.minimum > 0 && config.explode > 0) ; else {
|
|
4012
|
+
baseDie += rerollClause;
|
|
3955
4013
|
}
|
|
3956
4014
|
}
|
|
3957
4015
|
if (config.minimum > 0) {
|
|
@@ -3961,9 +4019,7 @@ var RollBuilder = class _RollBuilder {
|
|
|
3961
4019
|
baseDie = `${config.minimum}>${baseDie}`;
|
|
3962
4020
|
}
|
|
3963
4021
|
if (config.reroll > 0 && config.explode > 0) {
|
|
3964
|
-
|
|
3965
|
-
baseDie += ` reroll ${i}`;
|
|
3966
|
-
}
|
|
4022
|
+
baseDie += rerollClause;
|
|
3967
4023
|
}
|
|
3968
4024
|
}
|
|
3969
4025
|
if (baseDie === "d20 reroll 1" && config.minimum <= 1) baseDie = "hd20";
|
|
@@ -3981,10 +4037,14 @@ var RollBuilder = class _RollBuilder {
|
|
|
3981
4037
|
case "flat":
|
|
3982
4038
|
if (config.keep) {
|
|
3983
4039
|
const mode = config.keep.mode === "highest" ? "kh" : "kl";
|
|
4040
|
+
const baseCount = Math.max(1, Math.floor(Math.abs(config.count || 1)));
|
|
4041
|
+
const trials = Math.max(1, Math.floor(config.keep.total));
|
|
4042
|
+
const isMaxOfShape = config.keep.count === 1 && config.keep.mode === "highest";
|
|
4043
|
+
const innerCount = trials === baseCount && !isMaxOfShape ? 1 : baseCount;
|
|
3984
4044
|
const baseDieExpression = this.configToSingleExpressionWithoutModifier(
|
|
3985
4045
|
{
|
|
3986
4046
|
...config,
|
|
3987
|
-
count:
|
|
4047
|
+
count: innerCount,
|
|
3988
4048
|
modifier: 0,
|
|
3989
4049
|
rollType: "flat",
|
|
3990
4050
|
keep: void 0
|
|
@@ -4016,7 +4076,19 @@ var RollBuilder = class _RollBuilder {
|
|
|
4016
4076
|
}
|
|
4017
4077
|
}
|
|
4018
4078
|
if (config.bestOf && config.count && config.bestOf < config.count) {
|
|
4019
|
-
|
|
4079
|
+
const pool = Math.max(1, Math.floor(Math.abs(config.count)));
|
|
4080
|
+
const baseDieExpression = this.configToSingleExpressionWithoutModifier(
|
|
4081
|
+
{
|
|
4082
|
+
...config,
|
|
4083
|
+
count: 1,
|
|
4084
|
+
modifier: 0,
|
|
4085
|
+
bestOf: 0,
|
|
4086
|
+
keep: void 0,
|
|
4087
|
+
rollType: "flat"
|
|
4088
|
+
},
|
|
4089
|
+
false
|
|
4090
|
+
);
|
|
4091
|
+
mainExpression = `${pool}kh${Math.floor(config.bestOf)}(${baseDieExpression})`;
|
|
4020
4092
|
}
|
|
4021
4093
|
break;
|
|
4022
4094
|
}
|
|
@@ -4119,6 +4191,12 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
|
|
|
4119
4191
|
toPMF(eps = 0) {
|
|
4120
4192
|
return pmfFromRollBuilder(this, eps);
|
|
4121
4193
|
}
|
|
4194
|
+
// Scale the dice, keep the same // 2 (half) transform applied on top -- delegating to the
|
|
4195
|
+
// base class's `create()`-based scaleDice would drop the halving entirely, e.g. a doubled-dice
|
|
4196
|
+
// crit on a resisted hit payload silently losing the resistance.
|
|
4197
|
+
scaleDice(scale) {
|
|
4198
|
+
return new _HalfRollBuilder(this.innerRoll.scaleDice(scale));
|
|
4199
|
+
}
|
|
4122
4200
|
copy() {
|
|
4123
4201
|
return new _HalfRollBuilder(this.innerRoll.copy());
|
|
4124
4202
|
}
|
|
@@ -4145,9 +4223,16 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
|
|
|
4145
4223
|
}
|
|
4146
4224
|
toExpression() {
|
|
4147
4225
|
const inner = this.innerRoll.toExpression();
|
|
4148
|
-
|
|
4149
|
-
if (
|
|
4150
|
-
|
|
4226
|
+
const denominator = this.denominator === 0 ? 1 : this.denominator;
|
|
4227
|
+
if (denominator === 1) return `${this.numerator} ** (${inner})`;
|
|
4228
|
+
if (this.rounding === "round") {
|
|
4229
|
+
throw new Error(
|
|
4230
|
+
`toExpression() cannot represent scaleResult(${this.numerator}, ${this.denominator}, "round"): the string grammar has only floor (//) and ceil (/) division. Use the builder's own PMF (.toPMF()/.pmf) instead.`
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
const div = this.rounding === "ceil" ? "/" : "//";
|
|
4234
|
+
if (this.numerator === 1) return `(${inner}) ${div} ${denominator}`;
|
|
4235
|
+
return `(${inner}) ** ${this.numerator} ${div} ${denominator}`;
|
|
4151
4236
|
}
|
|
4152
4237
|
toAST() {
|
|
4153
4238
|
return {
|
|
@@ -4161,6 +4246,17 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
|
|
|
4161
4246
|
toPMF(eps = 0) {
|
|
4162
4247
|
return pmfFromRollBuilder(this, eps);
|
|
4163
4248
|
}
|
|
4249
|
+
// Scale the dice, keep the same numerator/denominator/rounding transform applied on top --
|
|
4250
|
+
// delegating to the base class's `create()`-based scaleDice would drop the scale entirely,
|
|
4251
|
+
// e.g. a doubled-dice crit on a vulnerable hit payload silently losing the vulnerability.
|
|
4252
|
+
scaleDice(scale) {
|
|
4253
|
+
return new _ScaleRollBuilder(
|
|
4254
|
+
this.innerRoll.scaleDice(scale),
|
|
4255
|
+
this.numerator,
|
|
4256
|
+
this.denominator,
|
|
4257
|
+
this.rounding
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4164
4260
|
copy() {
|
|
4165
4261
|
return new _ScaleRollBuilder(
|
|
4166
4262
|
this.innerRoll.copy(),
|
|
@@ -4233,6 +4329,18 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
|
|
|
4233
4329
|
toPMF(eps = 0) {
|
|
4234
4330
|
return pmfFromRollBuilder(this, eps);
|
|
4235
4331
|
}
|
|
4332
|
+
// Scale the dice INSIDE each trial (e.g. maxOf(2, 1d12) -> maxOf(2, 2d12)), keeping the same
|
|
4333
|
+
// trial count -- delegating to the base class's `create()`-based scaleDice would collapse
|
|
4334
|
+
// straight to plain dice, losing the "take the highest of N trials" semantics entirely.
|
|
4335
|
+
scaleDice(scale) {
|
|
4336
|
+
const scaleInt = validateScaleInt(scale);
|
|
4337
|
+
return new _MaxOfRollBuilder(
|
|
4338
|
+
this.innerRoll.scaleDice(scaleInt),
|
|
4339
|
+
this.count,
|
|
4340
|
+
this.diceCount ? this.diceCount * scaleInt : void 0,
|
|
4341
|
+
this.diceSides
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4236
4344
|
copy() {
|
|
4237
4345
|
return new _MaxOfRollBuilder(this.innerRoll.copy(), this.count);
|
|
4238
4346
|
}
|
|
@@ -4279,9 +4387,7 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
|
|
|
4279
4387
|
return new RollBuilder(configs).toExpression();
|
|
4280
4388
|
}
|
|
4281
4389
|
toPMF() {
|
|
4282
|
-
|
|
4283
|
-
const rerollOne = this.baseReroll > 0;
|
|
4284
|
-
return d20RollPMF(rollType, rerollOne);
|
|
4390
|
+
return resolveRootD20(this);
|
|
4285
4391
|
}
|
|
4286
4392
|
copy() {
|
|
4287
4393
|
const baseCopy = new RollBuilder(this.getSubRollConfigs());
|
|
@@ -4329,9 +4435,7 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
4329
4435
|
return new RollBuilder(configs).toExpression();
|
|
4330
4436
|
}
|
|
4331
4437
|
toPMF() {
|
|
4332
|
-
|
|
4333
|
-
const rerollOne = this.baseReroll > 0;
|
|
4334
|
-
return d20RollPMF(rollType, rerollOne);
|
|
4438
|
+
return resolveRootD20(this);
|
|
4335
4439
|
}
|
|
4336
4440
|
copy() {
|
|
4337
4441
|
const baseCopy = new RollBuilder(this.getSubRollConfigs());
|
|
@@ -4521,6 +4625,17 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
|
|
|
4521
4625
|
toPMF(eps = 0) {
|
|
4522
4626
|
return pmfFromRollBuilder(this, eps);
|
|
4523
4627
|
}
|
|
4628
|
+
// Scaling a composite (mixed damage types, e.g. base + resisted) must scale each PART's own
|
|
4629
|
+
// dice while preserving its own half/scale wrapper -- delegating to the base class's
|
|
4630
|
+
// `create()`-based scaleDice would lose every part's transform, collapsing straight to plain
|
|
4631
|
+
// dice. This is what auto-crit doubling (attack.ts's `hitEffect.copy().doubleDice()`) relies
|
|
4632
|
+
// on for a mixed-resistance hit payload.
|
|
4633
|
+
scaleDice(scale) {
|
|
4634
|
+
validateScaleInt(scale);
|
|
4635
|
+
return new _CompositeSumRollBuilder(
|
|
4636
|
+
this.parts.map((p) => p.scaleDice(scale))
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
4524
4639
|
copy() {
|
|
4525
4640
|
return new _CompositeSumRollBuilder(this.parts.map((p) => p.copy()));
|
|
4526
4641
|
}
|
|
@@ -4598,6 +4713,15 @@ var builderPMFCache = new LRUCache(1e3);
|
|
|
4598
4713
|
// src/builder/ast.ts
|
|
4599
4714
|
var defaultEps = 0;
|
|
4600
4715
|
var singleDiePMFCache = new LRUCache(1e3);
|
|
4716
|
+
function dieNodeFromConfig(cfg) {
|
|
4717
|
+
return {
|
|
4718
|
+
type: "die",
|
|
4719
|
+
sides: cfg.sides,
|
|
4720
|
+
reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
|
|
4721
|
+
minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
|
|
4722
|
+
explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4601
4725
|
function astFromRollConfigs(configs) {
|
|
4602
4726
|
if (!configs || configs.length === 0) return void 0;
|
|
4603
4727
|
const children = [];
|
|
@@ -4607,13 +4731,9 @@ function astFromRollConfigs(configs) {
|
|
|
4607
4731
|
const count = Math.abs(cfg.count || 0);
|
|
4608
4732
|
constantSum += cfg.modifier || 0;
|
|
4609
4733
|
if ((cfg.sides || 0) <= 0) continue;
|
|
4610
|
-
const
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
|
|
4614
|
-
minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
|
|
4615
|
-
explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
|
|
4616
|
-
};
|
|
4734
|
+
const isSynthesizedBestOf = !cfg.keep && cfg.bestOf > 0 && cfg.bestOf < count;
|
|
4735
|
+
const effectiveKeep = isSynthesizedBestOf ? { total: count, count: Math.floor(cfg.bestOf), mode: "highest" } : cfg.keep;
|
|
4736
|
+
const die = dieNodeFromConfig(cfg);
|
|
4617
4737
|
let node = die;
|
|
4618
4738
|
let appliedRollType = false;
|
|
4619
4739
|
if (cfg.rollType && cfg.rollType !== "flat") {
|
|
@@ -4631,11 +4751,11 @@ function astFromRollConfigs(configs) {
|
|
|
4631
4751
|
}
|
|
4632
4752
|
appliedRollType = true;
|
|
4633
4753
|
}
|
|
4634
|
-
if (cfg.rollType === "flat" &&
|
|
4754
|
+
if (cfg.rollType === "flat" && effectiveKeep && effectiveKeep.total > 0) {
|
|
4635
4755
|
const baseCount = Math.max(1, Math.floor(Math.abs(count || 1)));
|
|
4636
|
-
const trials = Math.max(1, Math.floor(
|
|
4637
|
-
const k = Math.max(0, Math.floor(
|
|
4638
|
-
if (k === 1 &&
|
|
4756
|
+
const trials = Math.max(1, Math.floor(effectiveKeep.total));
|
|
4757
|
+
const k = Math.max(0, Math.floor(effectiveKeep.count));
|
|
4758
|
+
if (k === 1 && effectiveKeep.mode === "highest" && !isSynthesizedBestOf) {
|
|
4639
4759
|
const perTrial = {
|
|
4640
4760
|
type: "sum",
|
|
4641
4761
|
count: baseCount,
|
|
@@ -4654,7 +4774,7 @@ function astFromRollConfigs(configs) {
|
|
|
4654
4774
|
const base = { type: "sum", count: trials, child: node };
|
|
4655
4775
|
node = {
|
|
4656
4776
|
type: "keep",
|
|
4657
|
-
mode:
|
|
4777
|
+
mode: effectiveKeep.mode,
|
|
4658
4778
|
count: k,
|
|
4659
4779
|
child: base
|
|
4660
4780
|
};
|
|
@@ -4674,7 +4794,7 @@ function astFromRollConfigs(configs) {
|
|
|
4674
4794
|
};
|
|
4675
4795
|
node = {
|
|
4676
4796
|
type: "keep",
|
|
4677
|
-
mode:
|
|
4797
|
+
mode: effectiveKeep.mode,
|
|
4678
4798
|
count: k,
|
|
4679
4799
|
child: trialPool
|
|
4680
4800
|
};
|
|
@@ -4748,8 +4868,8 @@ function resolve(node, eps = defaultEps) {
|
|
|
4748
4868
|
}
|
|
4749
4869
|
case "d20Roll": {
|
|
4750
4870
|
const childDie = findDie(node.child);
|
|
4751
|
-
|
|
4752
|
-
return
|
|
4871
|
+
if (!childDie) return d20RollPMF(node.rollType, false);
|
|
4872
|
+
return resolveD20Roll(childDie, node.rollType);
|
|
4753
4873
|
}
|
|
4754
4874
|
case "half": {
|
|
4755
4875
|
const childPMF = resolve(node.child, eps);
|
|
@@ -4775,6 +4895,37 @@ function pmfFromRollBuilder(rb, eps = defaultEps) {
|
|
|
4775
4895
|
const ast = rb.toAST();
|
|
4776
4896
|
return resolve(ast, eps);
|
|
4777
4897
|
}
|
|
4898
|
+
var d20RollLiftCache = new LRUCache(500);
|
|
4899
|
+
function resolveD20Roll(die, rollType) {
|
|
4900
|
+
const base = resolveSingleDie(die, defaultEps);
|
|
4901
|
+
const type = rollType || "flat";
|
|
4902
|
+
if (type === "flat") return base;
|
|
4903
|
+
const cacheKey = `${getASTSignature(die)}|${type}`;
|
|
4904
|
+
const cached = d20RollLiftCache.get(cacheKey);
|
|
4905
|
+
if (cached) return cached;
|
|
4906
|
+
const support = [...base.support()].sort((a, b) => a - b);
|
|
4907
|
+
const out = /* @__PURE__ */ new Map();
|
|
4908
|
+
let cum = 0;
|
|
4909
|
+
let prevLifted = 0;
|
|
4910
|
+
for (const k of support) {
|
|
4911
|
+
cum += base.pAt(k);
|
|
4912
|
+
const curLifted = type === "advantage" ? cum * cum : type === "elven accuracy" ? cum * cum * cum : 1 - (1 - cum) * (1 - cum);
|
|
4913
|
+
const pk = curLifted - prevLifted;
|
|
4914
|
+
if (pk > 0) out.set(k, pk);
|
|
4915
|
+
prevLifted = curLifted;
|
|
4916
|
+
}
|
|
4917
|
+
const result = PMF.fromMap(out, defaultEps);
|
|
4918
|
+
d20RollLiftCache.set(cacheKey, result);
|
|
4919
|
+
return result;
|
|
4920
|
+
}
|
|
4921
|
+
function resolveRootD20(check) {
|
|
4922
|
+
const rootConfig = check.getRootDieConfig();
|
|
4923
|
+
const rollType = check.rollType;
|
|
4924
|
+
if (!rootConfig || !(rootConfig.sides > 0)) {
|
|
4925
|
+
return d20RollPMF(rollType, check.baseReroll > 0);
|
|
4926
|
+
}
|
|
4927
|
+
return resolveD20Roll(dieNodeFromConfig(rootConfig), rollType);
|
|
4928
|
+
}
|
|
4778
4929
|
function resolveSingleDie(die, eps = defaultEps) {
|
|
4779
4930
|
const signature = getASTSignature(die);
|
|
4780
4931
|
const cacheKey = `${signature}_${eps}`;
|
|
@@ -4808,20 +4959,12 @@ function resolveSingleDie(die, eps = defaultEps) {
|
|
|
4808
4959
|
for (const v of pmf.support()) {
|
|
4809
4960
|
if (v !== maxFace) nonMax.set(v, pmf.pAt(v));
|
|
4810
4961
|
}
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
let tail = PMF.delta(0, eps);
|
|
4816
|
-
const addOnce = pmf;
|
|
4817
|
-
for (let t = 1; t <= times; t++) {
|
|
4818
|
-
tail = tail.convolve(addOnce, eps);
|
|
4962
|
+
const nonMaxPMF = PMF.fromMap(nonMax, eps);
|
|
4963
|
+
let chain = pmf;
|
|
4964
|
+
for (let remaining = 1; remaining <= times - 1; remaining++) {
|
|
4965
|
+
chain = PMF.branch(chain.mapDamage((v) => v + maxFace), nonMaxPMF, pMax);
|
|
4819
4966
|
}
|
|
4820
|
-
const exploded = PMF.branch(
|
|
4821
|
-
tail.mapDamage((v) => v + maxFace),
|
|
4822
|
-
nonMaxPMF,
|
|
4823
|
-
pMax
|
|
4824
|
-
);
|
|
4967
|
+
const exploded = PMF.branch(chain.mapDamage((v) => v + maxFace), nonMaxPMF, pMax);
|
|
4825
4968
|
pmf = exploded;
|
|
4826
4969
|
}
|
|
4827
4970
|
singleDiePMFCache.set(cacheKey, pmf);
|
|
@@ -5120,10 +5263,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5120
5263
|
return `${checkPart} * ${effectPart}`;
|
|
5121
5264
|
}
|
|
5122
5265
|
resolveProbabilities(check, eps = 0) {
|
|
5123
|
-
const rollType = check.rollType;
|
|
5124
|
-
const rerollOne = check.baseReroll > 0;
|
|
5125
5266
|
const critThreshold = check.critThreshold;
|
|
5126
|
-
const d202 =
|
|
5267
|
+
const d202 = resolveRootD20(check);
|
|
5127
5268
|
if (check instanceof AlwaysCritBuilder) {
|
|
5128
5269
|
if (check.fromAlwaysHit) {
|
|
5129
5270
|
return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
|
|
@@ -5173,13 +5314,17 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5173
5314
|
pmiss += pr;
|
|
5174
5315
|
continue;
|
|
5175
5316
|
}
|
|
5176
|
-
if (r
|
|
5317
|
+
if (r === 20) {
|
|
5177
5318
|
pcrit += pr;
|
|
5178
5319
|
continue;
|
|
5179
5320
|
}
|
|
5180
5321
|
const need = ac - staticMod - r;
|
|
5181
5322
|
const pBonusHit = bonusPMF.tailProbGE(need);
|
|
5182
|
-
|
|
5323
|
+
if (r >= critThreshold) {
|
|
5324
|
+
pcrit += pr * pBonusHit;
|
|
5325
|
+
} else {
|
|
5326
|
+
phit += pr * pBonusHit;
|
|
5327
|
+
}
|
|
5183
5328
|
pmiss += pr * (1 - pBonusHit);
|
|
5184
5329
|
}
|
|
5185
5330
|
const psuccess = phit + pcrit;
|
|
@@ -5327,9 +5472,7 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
|
|
|
5327
5472
|
}
|
|
5328
5473
|
toPMF(eps = 0) {
|
|
5329
5474
|
const ac = this.attackConfig.ac;
|
|
5330
|
-
const
|
|
5331
|
-
const rerollOne = this.baseReroll > 0;
|
|
5332
|
-
const d202 = d20RollPMF(rollType, rerollOne);
|
|
5475
|
+
const d202 = resolveRootD20(this);
|
|
5333
5476
|
const staticMod = this.modifier;
|
|
5334
5477
|
const bonusPMFs = this.getBonusDicePMFs(this, eps);
|
|
5335
5478
|
const parts = [d202, ...bonusPMFs];
|
|
@@ -5439,21 +5582,17 @@ var SaveBuilder = class _SaveBuilder {
|
|
|
5439
5582
|
function resolveProbabilities(check) {
|
|
5440
5583
|
const saveBonus = check.modifier;
|
|
5441
5584
|
const dc = check.saveDC;
|
|
5442
|
-
const
|
|
5443
|
-
const
|
|
5444
|
-
const die = d20RollPMF(d20Type, baseReroll > 0);
|
|
5585
|
+
const eps = 0;
|
|
5586
|
+
const die = resolveRootD20(check);
|
|
5445
5587
|
const faceP = /* @__PURE__ */ new Map();
|
|
5446
5588
|
for (const [r, bin] of die) {
|
|
5447
5589
|
const pr = bin.p;
|
|
5448
5590
|
if (pr > 0) faceP.set(r, pr);
|
|
5449
5591
|
}
|
|
5450
|
-
const eps = 0;
|
|
5451
5592
|
const bonusDicePMFs = check.getBonusDicePMFs(check, eps);
|
|
5452
5593
|
const bonusPMF = bonusDicePMFs.length > 0 ? PMF.convolveMany(bonusDicePMFs, eps) : PMF.zero(eps);
|
|
5453
5594
|
let pSuccess = 0;
|
|
5454
|
-
for (
|
|
5455
|
-
const pr = faceP.get(r);
|
|
5456
|
-
if (!pr) continue;
|
|
5595
|
+
for (const [r, pr] of faceP) {
|
|
5457
5596
|
const need = dc - saveBonus - r;
|
|
5458
5597
|
pSuccess += pr * bonusPMF.tailProbGE(need);
|
|
5459
5598
|
}
|
|
@@ -5523,9 +5662,7 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
|
5523
5662
|
if (cached) return cached;
|
|
5524
5663
|
}
|
|
5525
5664
|
const saveDC = this.saveDC;
|
|
5526
|
-
const
|
|
5527
|
-
const rerollOne = this.baseReroll > 0;
|
|
5528
|
-
const d202 = d20RollPMF(rollType, rerollOne);
|
|
5665
|
+
const d202 = resolveRootD20(this);
|
|
5529
5666
|
const staticMod = this.modifier;
|
|
5530
5667
|
const bonusDicePMFs = this.getBonusDiceConfigs().map(
|
|
5531
5668
|
(cfg) => pmfFromRollBuilder(RollBuilder.fromConfigs([cfg]), eps)
|