@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.
@@ -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 + eps) {
1438
+ if (totalWeight > 1 + EPS) {
1409
1439
  throw new Error(
1410
- `PMF.exclusive: total weight ${totalWeight.toFixed(6)} exceeds 1.`
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 (e) {
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 AlwaysHitBuilder(this, newConfig);
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 AlwaysHitBuilder(baseCopy, newConfig);
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 < 15 || critThreshold > 20) {
3664
+ if (critThreshold < 1 || critThreshold > 20) {
3586
3665
  throw new Error(
3587
- `Invalid crit threshold: ${critThreshold}. Must be between 15 and 20.`
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();