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