@yipe/dice 0.2.15 → 0.2.17
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/index.cjs +129 -19
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +19 -5
- package/dist/builder/index.d.ts +19 -5
- package/dist/builder/index.js +129 -19
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +70 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +70 -2
- package/dist/index.js.map +1 -1
- package/dist/{pmf-CYaEbY7m.d.cts → pmf-BC1poIqF.d.cts} +31 -0
- package/dist/{pmf-CYaEbY7m.d.ts → pmf-BC1poIqF.d.ts} +31 -0
- package/package.json +1 -1
package/dist/builder/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
54
54
|
constructor(singles, combined, eps = EPS) {
|
|
55
55
|
__publicField(this, "singles");
|
|
56
56
|
__publicField(this, "combined");
|
|
57
|
+
__publicField(this, "_combinedWithAttr");
|
|
57
58
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
58
59
|
if (this.singles.some((s) => s === void 0)) {
|
|
59
60
|
throw new Error("DiceQuery contains undefined singles");
|
|
@@ -61,6 +62,35 @@ var _DiceQuery = class _DiceQuery {
|
|
|
61
62
|
const c = combined ?? PMF.convolveMany(this.singles);
|
|
62
63
|
this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns a new PMF with damage attribution metadata populated.
|
|
67
|
+
*
|
|
68
|
+
* This method computes attribution on-demand for builder-generated PMFs,
|
|
69
|
+
* enabling them to work with damage attribution charts. The `attr` field
|
|
70
|
+
* tracks how much damage each outcome type contributes at each damage value.
|
|
71
|
+
*
|
|
72
|
+
* For each bin at damage D: sum(attr.values()) ≈ D × P(damage = D)
|
|
73
|
+
*
|
|
74
|
+
* Performance: Cached after first call. Adds minimal overhead vs `combined`.
|
|
75
|
+
*
|
|
76
|
+
* @returns PMF with attr field populated for damage attribution charts
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const attack = d20.plus(5).ac(15).onHit(d(2,6).plus(3)).onCrit(d(2,6))
|
|
80
|
+
* const query = attack.toQuery()
|
|
81
|
+
* const pmf = query.combinedWithAttribution()
|
|
82
|
+
* // Now pmf can be used with toDamageAttributionChartSeries()
|
|
83
|
+
*/
|
|
84
|
+
combinedWithAttribution() {
|
|
85
|
+
if (this._combinedWithAttr) {
|
|
86
|
+
return this._combinedWithAttr;
|
|
87
|
+
}
|
|
88
|
+
const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
|
|
89
|
+
const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
|
|
90
|
+
const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
|
|
91
|
+
this._combinedWithAttr = normalized;
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
64
94
|
/**
|
|
65
95
|
* Returns the expected damage across all possible outcomes.
|
|
66
96
|
*
|
|
@@ -1405,9 +1435,9 @@ var _PMF = class _PMF {
|
|
|
1405
1435
|
let totalWeight = items.reduce((s, { weight }) => s + weight, 0);
|
|
1406
1436
|
if (Math.abs(totalWeight) <= eps) totalWeight = 0;
|
|
1407
1437
|
if (Math.abs(1 - totalWeight) <= eps) totalWeight = 1;
|
|
1408
|
-
if (totalWeight > 1 +
|
|
1438
|
+
if (totalWeight > 1 + EPS) {
|
|
1409
1439
|
throw new Error(
|
|
1410
|
-
`PMF.exclusive: total weight ${totalWeight
|
|
1440
|
+
`PMF.exclusive: total weight ${totalWeight} exceeds 1. (epsilon: ${eps})`
|
|
1411
1441
|
);
|
|
1412
1442
|
}
|
|
1413
1443
|
let out = _PMF.empty(eps);
|
|
@@ -1449,6 +1479,44 @@ var _PMF = class _PMF {
|
|
|
1449
1479
|
}
|
|
1450
1480
|
return out;
|
|
1451
1481
|
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Adds damage attribution metadata to this PMF based on existing count metadata.
|
|
1484
|
+
* For each bin, sets attr[outcome] = damage × count[outcome].
|
|
1485
|
+
*
|
|
1486
|
+
* This enables damage attribution charts to work with builder-generated PMFs.
|
|
1487
|
+
* The parser generates attr automatically, but builder PMFs only have count.
|
|
1488
|
+
*
|
|
1489
|
+
* @returns New PMF with attr field populated in each bin
|
|
1490
|
+
*/
|
|
1491
|
+
withAttribution() {
|
|
1492
|
+
for (const [damage, bin] of this.map) {
|
|
1493
|
+
if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
|
|
1494
|
+
return this;
|
|
1495
|
+
}
|
|
1496
|
+
if (damage > 0) break;
|
|
1497
|
+
}
|
|
1498
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1499
|
+
for (const [damage, bin] of this.map) {
|
|
1500
|
+
const attr = {};
|
|
1501
|
+
for (const outcome in bin.count) {
|
|
1502
|
+
const probability = bin.count[outcome];
|
|
1503
|
+
if (probability > 0) {
|
|
1504
|
+
attr[outcome] = damage * probability;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
newMap.set(damage, {
|
|
1508
|
+
p: bin.p,
|
|
1509
|
+
count: { ...bin.count },
|
|
1510
|
+
attr: Object.keys(attr).length > 0 ? attr : void 0
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return new _PMF(
|
|
1514
|
+
newMap,
|
|
1515
|
+
this.epsilon,
|
|
1516
|
+
this.normalized,
|
|
1517
|
+
`${this.identifier}~attr`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1452
1520
|
/**
|
|
1453
1521
|
* General-purpose N-way mixture.
|
|
1454
1522
|
* weights: Array of [weight, PMF].
|
|
@@ -2671,6 +2739,9 @@ var RollBuilder = class _RollBuilder {
|
|
|
2671
2739
|
alwaysHits() {
|
|
2672
2740
|
return new AlwaysHitBuilder(this);
|
|
2673
2741
|
}
|
|
2742
|
+
alwaysCrits() {
|
|
2743
|
+
return new AlwaysCritBuilder(this);
|
|
2744
|
+
}
|
|
2674
2745
|
copy() {
|
|
2675
2746
|
return new this.constructor(this.getSubRollConfigs());
|
|
2676
2747
|
}
|
|
@@ -2962,7 +3033,7 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
|
|
|
2962
3033
|
child: sumChild
|
|
2963
3034
|
};
|
|
2964
3035
|
}
|
|
2965
|
-
} catch
|
|
3036
|
+
} catch {
|
|
2966
3037
|
}
|
|
2967
3038
|
throw new Error(
|
|
2968
3039
|
`MaxOfRollBuilder.toAST(): Unsupported innerRoll configuration`
|
|
@@ -2997,6 +3068,9 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
|
|
|
2997
3068
|
const newConfig = { critThreshold };
|
|
2998
3069
|
return new _AlwaysHitBuilder(this, newConfig);
|
|
2999
3070
|
}
|
|
3071
|
+
alwaysCrits() {
|
|
3072
|
+
return new AlwaysCritBuilder(this, void 0, true);
|
|
3073
|
+
}
|
|
3000
3074
|
// Legacy expressions
|
|
3001
3075
|
toExpression() {
|
|
3002
3076
|
const configs = this.getSubRollConfigs();
|
|
@@ -3014,23 +3088,28 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
|
|
|
3014
3088
|
return new _AlwaysHitBuilder(baseCopy, newConfig);
|
|
3015
3089
|
}
|
|
3016
3090
|
};
|
|
3017
|
-
var AlwaysCritBuilder = class extends RollBuilder {
|
|
3018
|
-
constructor(baseRoll, attackConfig) {
|
|
3091
|
+
var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
3092
|
+
constructor(baseRoll, attackConfig, fromAlwaysHit = false) {
|
|
3019
3093
|
super(baseRoll.getSubRollConfigs());
|
|
3020
3094
|
__publicField(this, "attackConfig");
|
|
3095
|
+
__publicField(this, "fromAlwaysHit");
|
|
3021
3096
|
if (attackConfig) {
|
|
3022
3097
|
this.attackConfig = { ...attackConfig };
|
|
3023
3098
|
} else {
|
|
3024
3099
|
this.attackConfig = { critThreshold: 20 };
|
|
3025
3100
|
}
|
|
3101
|
+
this.fromAlwaysHit = fromAlwaysHit || baseRoll instanceof AlwaysHitBuilder;
|
|
3102
|
+
}
|
|
3103
|
+
onHit(...args) {
|
|
3104
|
+
const damageRoll = RollBuilder.fromArgs(...args);
|
|
3105
|
+
return new AttackBuilder(this, damageRoll);
|
|
3026
3106
|
}
|
|
3027
3107
|
get critThreshold() {
|
|
3028
3108
|
return this.attackConfig.critThreshold;
|
|
3029
3109
|
}
|
|
3030
|
-
// TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
|
|
3031
3110
|
critOn(critThreshold) {
|
|
3032
|
-
const newConfig = { critThreshold };
|
|
3033
|
-
return new
|
|
3111
|
+
const newConfig = { critThreshold, ac: this.attackConfig.ac };
|
|
3112
|
+
return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
|
|
3034
3113
|
}
|
|
3035
3114
|
// Legacy expressions
|
|
3036
3115
|
toExpression() {
|
|
@@ -3045,8 +3124,8 @@ var AlwaysCritBuilder = class extends RollBuilder {
|
|
|
3045
3124
|
copy() {
|
|
3046
3125
|
const baseCopy = new RollBuilder(this.getSubRollConfigs());
|
|
3047
3126
|
const critThreshold = this.critThreshold;
|
|
3048
|
-
const newConfig = { critThreshold };
|
|
3049
|
-
return new
|
|
3127
|
+
const newConfig = { critThreshold, ac: this.attackConfig.ac };
|
|
3128
|
+
return new _AlwaysCritBuilder(baseCopy, newConfig, this.fromAlwaysHit);
|
|
3050
3129
|
}
|
|
3051
3130
|
};
|
|
3052
3131
|
|
|
@@ -3582,9 +3661,9 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3582
3661
|
crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
|
|
3583
3662
|
}
|
|
3584
3663
|
const critThreshold = this.check.critThreshold;
|
|
3585
|
-
if (critThreshold <
|
|
3664
|
+
if (critThreshold < 1 || critThreshold > 20) {
|
|
3586
3665
|
throw new Error(
|
|
3587
|
-
`Invalid crit threshold: ${critThreshold}. Must be between
|
|
3666
|
+
`Invalid crit threshold: ${critThreshold}. Must be between 1 and 20.`
|
|
3588
3667
|
);
|
|
3589
3668
|
}
|
|
3590
3669
|
if (critThreshold === 20) {
|
|
@@ -3601,13 +3680,34 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3601
3680
|
return `${checkPart} * ${effectPart}`;
|
|
3602
3681
|
}
|
|
3603
3682
|
resolveProbabilities(check, eps = 0) {
|
|
3604
|
-
if (check instanceof AlwaysCritBuilder) {
|
|
3605
|
-
return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
|
|
3606
|
-
}
|
|
3607
3683
|
const rollType = check.rollType;
|
|
3608
3684
|
const rerollOne = check.baseReroll > 0;
|
|
3609
3685
|
const critThreshold = check.critThreshold;
|
|
3610
3686
|
const d202 = d20RollPMF(rollType, rerollOne);
|
|
3687
|
+
if (check instanceof AlwaysCritBuilder) {
|
|
3688
|
+
if (check.fromAlwaysHit) {
|
|
3689
|
+
return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
|
|
3690
|
+
}
|
|
3691
|
+
const ac2 = check.attackConfig.ac ?? 0;
|
|
3692
|
+
const staticMod2 = this.check.modifier;
|
|
3693
|
+
const bonusDicePMFs2 = this.check.getBonusDicePMFs(this.check, eps);
|
|
3694
|
+
const bonusPMF2 = bonusDicePMFs2.length ? PMF.convolveMany(bonusDicePMFs2, eps) : PMF.delta(0, eps);
|
|
3695
|
+
let pcrit2 = 0;
|
|
3696
|
+
let pmiss2 = 0;
|
|
3697
|
+
for (const [r, rec] of d202) {
|
|
3698
|
+
const pr = typeof rec === "number" ? rec : rec.p;
|
|
3699
|
+
if (pr <= 0) continue;
|
|
3700
|
+
if (r === 1) {
|
|
3701
|
+
pmiss2 += pr;
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
const need = ac2 - staticMod2 - r;
|
|
3705
|
+
const pBonusHit = bonusPMF2.tailProbGE(need);
|
|
3706
|
+
pcrit2 += pr * pBonusHit;
|
|
3707
|
+
pmiss2 += pr * (1 - pBonusHit);
|
|
3708
|
+
}
|
|
3709
|
+
return { pSuccess: pcrit2, pHit: 0, pCrit: pcrit2, pMiss: pmiss2 };
|
|
3710
|
+
}
|
|
3611
3711
|
if (check instanceof AlwaysHitBuilder) {
|
|
3612
3712
|
let pCrit = 0;
|
|
3613
3713
|
for (const [r, rec] of d202) {
|
|
@@ -3629,14 +3729,14 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3629
3729
|
for (const [r, rec] of d202) {
|
|
3630
3730
|
const pr = typeof rec === "number" ? rec : rec.p;
|
|
3631
3731
|
if (pr <= 0) continue;
|
|
3632
|
-
if (r >= critThreshold) {
|
|
3633
|
-
pcrit += pr;
|
|
3634
|
-
continue;
|
|
3635
|
-
}
|
|
3636
3732
|
if (r === 1) {
|
|
3637
3733
|
pmiss += pr;
|
|
3638
3734
|
continue;
|
|
3639
3735
|
}
|
|
3736
|
+
if (r >= critThreshold) {
|
|
3737
|
+
pcrit += pr;
|
|
3738
|
+
continue;
|
|
3739
|
+
}
|
|
3640
3740
|
const need = ac - staticMod - r;
|
|
3641
3741
|
const pBonusHit = bonusPMF.tailProbGE(need);
|
|
3642
3742
|
phit += pr * pBonusHit;
|
|
@@ -3717,6 +3817,16 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
|
|
|
3717
3817
|
};
|
|
3718
3818
|
return new _ACBuilder(this, this.attackConfig.ac, newConfig);
|
|
3719
3819
|
}
|
|
3820
|
+
alwaysCrits() {
|
|
3821
|
+
return new AlwaysCritBuilder(
|
|
3822
|
+
this,
|
|
3823
|
+
{
|
|
3824
|
+
critThreshold: this.attackConfig.critThreshold,
|
|
3825
|
+
ac: this.attackConfig.ac
|
|
3826
|
+
},
|
|
3827
|
+
false
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3720
3830
|
// Legacy expressions
|
|
3721
3831
|
toExpression() {
|
|
3722
3832
|
const configs = this.getSubRollConfigs();
|