@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.
@@ -1,4 +1,4 @@
1
- import { P as PMF, C as CritConfig, i as DiceQuery, L as LRUCache } from '../pmf-CYaEbY7m.cjs';
1
+ import { P as PMF, C as CritConfig, i as DiceQuery, L as LRUCache } from '../pmf-BC1poIqF.cjs';
2
2
 
3
3
  type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode;
4
4
  type DieNode = {
@@ -139,6 +139,7 @@ declare class RollBuilder {
139
139
  scaleDice(scale: number): RollBuilder;
140
140
  doubleDice(): RollBuilder;
141
141
  alwaysHits(): AlwaysHitBuilder;
142
+ alwaysCrits(): AlwaysCritBuilder;
142
143
  copy(): RollBuilder;
143
144
  d4: () => RollBuilder;
144
145
  d6: () => RollBuilder;
@@ -198,18 +199,30 @@ declare class AlwaysHitBuilder extends RollBuilder {
198
199
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
199
200
  get critThreshold(): number;
200
201
  critOn(critThreshold: number): AlwaysHitBuilder;
202
+ alwaysCrits(): AlwaysCritBuilder;
201
203
  toExpression(): string;
202
204
  toPMF(): PMF;
203
205
  copy(): AlwaysHitBuilder;
204
206
  }
205
207
  declare class AlwaysCritBuilder extends RollBuilder {
206
- readonly attackConfig: CritConfig;
207
- constructor(baseRoll: RollBuilder, attackConfig?: CritConfig);
208
+ readonly attackConfig: CritConfig & {
209
+ ac?: number;
210
+ };
211
+ readonly fromAlwaysHit: boolean;
212
+ constructor(baseRoll: RollBuilder, attackConfig?: CritConfig & {
213
+ ac?: number;
214
+ }, fromAlwaysHit?: boolean);
215
+ onHit(val: number): AttackBuilder;
216
+ onHit(val: RollBuilder): AttackBuilder;
217
+ onHit(count: number, die: RollBuilder): AttackBuilder;
218
+ onHit(count: number, sides: number): AttackBuilder;
219
+ onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
220
+ onHit(count: number, sides: number, modifier: number): AttackBuilder;
208
221
  get critThreshold(): number;
209
- critOn(critThreshold: number): AlwaysHitBuilder;
222
+ critOn(critThreshold: number): AlwaysCritBuilder;
210
223
  toExpression(): string;
211
224
  toPMF(): PMF;
212
- copy(): AlwaysHitBuilder;
225
+ copy(): AlwaysCritBuilder;
213
226
  }
214
227
 
215
228
  type ActionEffect = RollBuilder;
@@ -260,6 +273,7 @@ declare class ACBuilder extends RollBuilder {
260
273
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
261
274
  get critThreshold(): number;
262
275
  critOn(threshold: number): ACBuilder;
276
+ alwaysCrits(): AlwaysCritBuilder;
263
277
  toExpression(): string;
264
278
  toPMF(eps?: number): PMF;
265
279
  copy(): ACBuilder;
@@ -1,4 +1,4 @@
1
- import { P as PMF, C as CritConfig, i as DiceQuery, L as LRUCache } from '../pmf-CYaEbY7m.js';
1
+ import { P as PMF, C as CritConfig, i as DiceQuery, L as LRUCache } from '../pmf-BC1poIqF.js';
2
2
 
3
3
  type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode;
4
4
  type DieNode = {
@@ -139,6 +139,7 @@ declare class RollBuilder {
139
139
  scaleDice(scale: number): RollBuilder;
140
140
  doubleDice(): RollBuilder;
141
141
  alwaysHits(): AlwaysHitBuilder;
142
+ alwaysCrits(): AlwaysCritBuilder;
142
143
  copy(): RollBuilder;
143
144
  d4: () => RollBuilder;
144
145
  d6: () => RollBuilder;
@@ -198,18 +199,30 @@ declare class AlwaysHitBuilder extends RollBuilder {
198
199
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
199
200
  get critThreshold(): number;
200
201
  critOn(critThreshold: number): AlwaysHitBuilder;
202
+ alwaysCrits(): AlwaysCritBuilder;
201
203
  toExpression(): string;
202
204
  toPMF(): PMF;
203
205
  copy(): AlwaysHitBuilder;
204
206
  }
205
207
  declare class AlwaysCritBuilder extends RollBuilder {
206
- readonly attackConfig: CritConfig;
207
- constructor(baseRoll: RollBuilder, attackConfig?: CritConfig);
208
+ readonly attackConfig: CritConfig & {
209
+ ac?: number;
210
+ };
211
+ readonly fromAlwaysHit: boolean;
212
+ constructor(baseRoll: RollBuilder, attackConfig?: CritConfig & {
213
+ ac?: number;
214
+ }, fromAlwaysHit?: boolean);
215
+ onHit(val: number): AttackBuilder;
216
+ onHit(val: RollBuilder): AttackBuilder;
217
+ onHit(count: number, die: RollBuilder): AttackBuilder;
218
+ onHit(count: number, sides: number): AttackBuilder;
219
+ onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
220
+ onHit(count: number, sides: number, modifier: number): AttackBuilder;
208
221
  get critThreshold(): number;
209
- critOn(critThreshold: number): AlwaysHitBuilder;
222
+ critOn(critThreshold: number): AlwaysCritBuilder;
210
223
  toExpression(): string;
211
224
  toPMF(): PMF;
212
- copy(): AlwaysHitBuilder;
225
+ copy(): AlwaysCritBuilder;
213
226
  }
214
227
 
215
228
  type ActionEffect = RollBuilder;
@@ -260,6 +273,7 @@ declare class ACBuilder extends RollBuilder {
260
273
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
261
274
  get critThreshold(): number;
262
275
  critOn(threshold: number): ACBuilder;
276
+ alwaysCrits(): AlwaysCritBuilder;
263
277
  toExpression(): string;
264
278
  toPMF(eps?: number): PMF;
265
279
  copy(): ACBuilder;
@@ -52,6 +52,7 @@ var _DiceQuery = class _DiceQuery {
52
52
  constructor(singles, combined, eps = EPS) {
53
53
  __publicField(this, "singles");
54
54
  __publicField(this, "combined");
55
+ __publicField(this, "_combinedWithAttr");
55
56
  this.singles = Array.isArray(singles) ? singles : [singles];
56
57
  if (this.singles.some((s) => s === void 0)) {
57
58
  throw new Error("DiceQuery contains undefined singles");
@@ -59,6 +60,35 @@ var _DiceQuery = class _DiceQuery {
59
60
  const c = combined ?? PMF.convolveMany(this.singles);
60
61
  this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
61
62
  }
63
+ /**
64
+ * Returns a new PMF with damage attribution metadata populated.
65
+ *
66
+ * This method computes attribution on-demand for builder-generated PMFs,
67
+ * enabling them to work with damage attribution charts. The `attr` field
68
+ * tracks how much damage each outcome type contributes at each damage value.
69
+ *
70
+ * For each bin at damage D: sum(attr.values()) ≈ D × P(damage = D)
71
+ *
72
+ * Performance: Cached after first call. Adds minimal overhead vs `combined`.
73
+ *
74
+ * @returns PMF with attr field populated for damage attribution charts
75
+ *
76
+ * @example
77
+ * const attack = d20.plus(5).ac(15).onHit(d(2,6).plus(3)).onCrit(d(2,6))
78
+ * const query = attack.toQuery()
79
+ * const pmf = query.combinedWithAttribution()
80
+ * // Now pmf can be used with toDamageAttributionChartSeries()
81
+ */
82
+ combinedWithAttribution() {
83
+ if (this._combinedWithAttr) {
84
+ return this._combinedWithAttr;
85
+ }
86
+ const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
87
+ const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
88
+ const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
89
+ this._combinedWithAttr = normalized;
90
+ return normalized;
91
+ }
62
92
  /**
63
93
  * Returns the expected damage across all possible outcomes.
64
94
  *
@@ -1403,9 +1433,9 @@ var _PMF = class _PMF {
1403
1433
  let totalWeight = items.reduce((s, { weight }) => s + weight, 0);
1404
1434
  if (Math.abs(totalWeight) <= eps) totalWeight = 0;
1405
1435
  if (Math.abs(1 - totalWeight) <= eps) totalWeight = 1;
1406
- if (totalWeight > 1 + eps) {
1436
+ if (totalWeight > 1 + EPS) {
1407
1437
  throw new Error(
1408
- `PMF.exclusive: total weight ${totalWeight.toFixed(6)} exceeds 1.`
1438
+ `PMF.exclusive: total weight ${totalWeight} exceeds 1. (epsilon: ${eps})`
1409
1439
  );
1410
1440
  }
1411
1441
  let out = _PMF.empty(eps);
@@ -1447,6 +1477,44 @@ var _PMF = class _PMF {
1447
1477
  }
1448
1478
  return out;
1449
1479
  }
1480
+ /**
1481
+ * Adds damage attribution metadata to this PMF based on existing count metadata.
1482
+ * For each bin, sets attr[outcome] = damage × count[outcome].
1483
+ *
1484
+ * This enables damage attribution charts to work with builder-generated PMFs.
1485
+ * The parser generates attr automatically, but builder PMFs only have count.
1486
+ *
1487
+ * @returns New PMF with attr field populated in each bin
1488
+ */
1489
+ withAttribution() {
1490
+ for (const [damage, bin] of this.map) {
1491
+ if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
1492
+ return this;
1493
+ }
1494
+ if (damage > 0) break;
1495
+ }
1496
+ const newMap = /* @__PURE__ */ new Map();
1497
+ for (const [damage, bin] of this.map) {
1498
+ const attr = {};
1499
+ for (const outcome in bin.count) {
1500
+ const probability = bin.count[outcome];
1501
+ if (probability > 0) {
1502
+ attr[outcome] = damage * probability;
1503
+ }
1504
+ }
1505
+ newMap.set(damage, {
1506
+ p: bin.p,
1507
+ count: { ...bin.count },
1508
+ attr: Object.keys(attr).length > 0 ? attr : void 0
1509
+ });
1510
+ }
1511
+ return new _PMF(
1512
+ newMap,
1513
+ this.epsilon,
1514
+ this.normalized,
1515
+ `${this.identifier}~attr`
1516
+ );
1517
+ }
1450
1518
  /**
1451
1519
  * General-purpose N-way mixture.
1452
1520
  * weights: Array of [weight, PMF].
@@ -2669,6 +2737,9 @@ var RollBuilder = class _RollBuilder {
2669
2737
  alwaysHits() {
2670
2738
  return new AlwaysHitBuilder(this);
2671
2739
  }
2740
+ alwaysCrits() {
2741
+ return new AlwaysCritBuilder(this);
2742
+ }
2672
2743
  copy() {
2673
2744
  return new this.constructor(this.getSubRollConfigs());
2674
2745
  }
@@ -2960,7 +3031,7 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
2960
3031
  child: sumChild
2961
3032
  };
2962
3033
  }
2963
- } catch (e) {
3034
+ } catch {
2964
3035
  }
2965
3036
  throw new Error(
2966
3037
  `MaxOfRollBuilder.toAST(): Unsupported innerRoll configuration`
@@ -2995,6 +3066,9 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
2995
3066
  const newConfig = { critThreshold };
2996
3067
  return new _AlwaysHitBuilder(this, newConfig);
2997
3068
  }
3069
+ alwaysCrits() {
3070
+ return new AlwaysCritBuilder(this, void 0, true);
3071
+ }
2998
3072
  // Legacy expressions
2999
3073
  toExpression() {
3000
3074
  const configs = this.getSubRollConfigs();
@@ -3012,23 +3086,28 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
3012
3086
  return new _AlwaysHitBuilder(baseCopy, newConfig);
3013
3087
  }
3014
3088
  };
3015
- var AlwaysCritBuilder = class extends RollBuilder {
3016
- constructor(baseRoll, attackConfig) {
3089
+ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
3090
+ constructor(baseRoll, attackConfig, fromAlwaysHit = false) {
3017
3091
  super(baseRoll.getSubRollConfigs());
3018
3092
  __publicField(this, "attackConfig");
3093
+ __publicField(this, "fromAlwaysHit");
3019
3094
  if (attackConfig) {
3020
3095
  this.attackConfig = { ...attackConfig };
3021
3096
  } else {
3022
3097
  this.attackConfig = { critThreshold: 20 };
3023
3098
  }
3099
+ this.fromAlwaysHit = fromAlwaysHit || baseRoll instanceof AlwaysHitBuilder;
3100
+ }
3101
+ onHit(...args) {
3102
+ const damageRoll = RollBuilder.fromArgs(...args);
3103
+ return new AttackBuilder(this, damageRoll);
3024
3104
  }
3025
3105
  get critThreshold() {
3026
3106
  return this.attackConfig.critThreshold;
3027
3107
  }
3028
- // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
3029
3108
  critOn(critThreshold) {
3030
- const newConfig = { critThreshold };
3031
- return new AlwaysHitBuilder(this, newConfig);
3109
+ const newConfig = { critThreshold, ac: this.attackConfig.ac };
3110
+ return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
3032
3111
  }
3033
3112
  // Legacy expressions
3034
3113
  toExpression() {
@@ -3043,8 +3122,8 @@ var AlwaysCritBuilder = class extends RollBuilder {
3043
3122
  copy() {
3044
3123
  const baseCopy = new RollBuilder(this.getSubRollConfigs());
3045
3124
  const critThreshold = this.critThreshold;
3046
- const newConfig = { critThreshold };
3047
- return new AlwaysHitBuilder(baseCopy, newConfig);
3125
+ const newConfig = { critThreshold, ac: this.attackConfig.ac };
3126
+ return new _AlwaysCritBuilder(baseCopy, newConfig, this.fromAlwaysHit);
3048
3127
  }
3049
3128
  };
3050
3129
 
@@ -3580,9 +3659,9 @@ var AttackBuilder = class _AttackBuilder {
3580
3659
  crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
3581
3660
  }
3582
3661
  const critThreshold = this.check.critThreshold;
3583
- if (critThreshold < 15 || critThreshold > 20) {
3662
+ if (critThreshold < 1 || critThreshold > 20) {
3584
3663
  throw new Error(
3585
- `Invalid crit threshold: ${critThreshold}. Must be between 15 and 20.`
3664
+ `Invalid crit threshold: ${critThreshold}. Must be between 1 and 20.`
3586
3665
  );
3587
3666
  }
3588
3667
  if (critThreshold === 20) {
@@ -3599,13 +3678,34 @@ var AttackBuilder = class _AttackBuilder {
3599
3678
  return `${checkPart} * ${effectPart}`;
3600
3679
  }
3601
3680
  resolveProbabilities(check, eps = 0) {
3602
- if (check instanceof AlwaysCritBuilder) {
3603
- return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
3604
- }
3605
3681
  const rollType = check.rollType;
3606
3682
  const rerollOne = check.baseReroll > 0;
3607
3683
  const critThreshold = check.critThreshold;
3608
3684
  const d202 = d20RollPMF(rollType, rerollOne);
3685
+ if (check instanceof AlwaysCritBuilder) {
3686
+ if (check.fromAlwaysHit) {
3687
+ return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
3688
+ }
3689
+ const ac2 = check.attackConfig.ac ?? 0;
3690
+ const staticMod2 = this.check.modifier;
3691
+ const bonusDicePMFs2 = this.check.getBonusDicePMFs(this.check, eps);
3692
+ const bonusPMF2 = bonusDicePMFs2.length ? PMF.convolveMany(bonusDicePMFs2, eps) : PMF.delta(0, eps);
3693
+ let pcrit2 = 0;
3694
+ let pmiss2 = 0;
3695
+ for (const [r, rec] of d202) {
3696
+ const pr = typeof rec === "number" ? rec : rec.p;
3697
+ if (pr <= 0) continue;
3698
+ if (r === 1) {
3699
+ pmiss2 += pr;
3700
+ continue;
3701
+ }
3702
+ const need = ac2 - staticMod2 - r;
3703
+ const pBonusHit = bonusPMF2.tailProbGE(need);
3704
+ pcrit2 += pr * pBonusHit;
3705
+ pmiss2 += pr * (1 - pBonusHit);
3706
+ }
3707
+ return { pSuccess: pcrit2, pHit: 0, pCrit: pcrit2, pMiss: pmiss2 };
3708
+ }
3609
3709
  if (check instanceof AlwaysHitBuilder) {
3610
3710
  let pCrit = 0;
3611
3711
  for (const [r, rec] of d202) {
@@ -3627,14 +3727,14 @@ var AttackBuilder = class _AttackBuilder {
3627
3727
  for (const [r, rec] of d202) {
3628
3728
  const pr = typeof rec === "number" ? rec : rec.p;
3629
3729
  if (pr <= 0) continue;
3630
- if (r >= critThreshold) {
3631
- pcrit += pr;
3632
- continue;
3633
- }
3634
3730
  if (r === 1) {
3635
3731
  pmiss += pr;
3636
3732
  continue;
3637
3733
  }
3734
+ if (r >= critThreshold) {
3735
+ pcrit += pr;
3736
+ continue;
3737
+ }
3638
3738
  const need = ac - staticMod - r;
3639
3739
  const pBonusHit = bonusPMF.tailProbGE(need);
3640
3740
  phit += pr * pBonusHit;
@@ -3715,6 +3815,16 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
3715
3815
  };
3716
3816
  return new _ACBuilder(this, this.attackConfig.ac, newConfig);
3717
3817
  }
3818
+ alwaysCrits() {
3819
+ return new AlwaysCritBuilder(
3820
+ this,
3821
+ {
3822
+ critThreshold: this.attackConfig.critThreshold,
3823
+ ac: this.attackConfig.ac
3824
+ },
3825
+ false
3826
+ );
3827
+ }
3718
3828
  // Legacy expressions
3719
3829
  toExpression() {
3720
3830
  const configs = this.getSubRollConfigs();