@yipe/dice 0.2.23 → 0.4.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.
@@ -1,12 +1,8 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
-
5
1
  // src/common/lru-cache.ts
6
2
  var LRUCache = class {
7
3
  constructor(maxSize = 1e3) {
8
4
  this.maxSize = maxSize;
9
- __publicField(this, "cache", /* @__PURE__ */ new Map());
5
+ this.cache = /* @__PURE__ */ new Map();
10
6
  }
11
7
  get(key) {
12
8
  const value = this.cache.get(key);
@@ -46,19 +42,35 @@ var LRUCache = class {
46
42
 
47
43
  // src/common/types.ts
48
44
  var EPS = 1e-12;
45
+ var MISS_NONE_OUTCOME = "missNone";
49
46
 
50
47
  // src/pmf/query.ts
51
48
  var _DiceQuery = class _DiceQuery {
52
49
  constructor(singles, combined, eps = EPS) {
53
- __publicField(this, "singles");
54
- __publicField(this, "combined");
55
- __publicField(this, "_combinedWithAttr");
56
50
  this.singles = Array.isArray(singles) ? singles : [singles];
57
51
  if (this.singles.some((s) => s === void 0)) {
58
52
  throw new Error("DiceQuery contains undefined singles");
59
53
  }
60
- const c = combined ?? PMF.convolveMany(this.singles);
61
- this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
54
+ this._eps = eps;
55
+ this._combinedProvided = combined !== void 0;
56
+ if (combined !== void 0) {
57
+ this._combined = Math.abs(combined.mass() - 1) <= eps ? combined : combined.normalize();
58
+ }
59
+ }
60
+ /**
61
+ * The combined damage distribution of all single PMFs (their convolution),
62
+ * normalized to total probability 1.
63
+ *
64
+ * Computed lazily on first access and cached. Queries that only need
65
+ * additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance},
66
+ * {@link DiceQuery.stddev} — never trigger this convolution.
67
+ */
68
+ get combined() {
69
+ if (this._combined === void 0) {
70
+ const c = PMF.convolveMany(this.singles);
71
+ this._combined = Math.abs(c.mass() - 1) <= this._eps ? c : c.normalize();
72
+ }
73
+ return this._combined;
62
74
  }
63
75
  /**
64
76
  * Returns a new PMF with damage attribution metadata populated.
@@ -83,12 +95,39 @@ var _DiceQuery = class _DiceQuery {
83
95
  if (this._combinedWithAttr) {
84
96
  return this._combinedWithAttr;
85
97
  }
98
+ if (this.singles.every((pmf) => pmf.hasAttribution())) {
99
+ this._combinedWithAttr = this.combined;
100
+ return this._combinedWithAttr;
101
+ }
86
102
  const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
87
103
  const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
88
104
  const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
89
105
  this._combinedWithAttr = normalized;
90
106
  return normalized;
91
107
  }
108
+ /**
109
+ * Per-label `damage value → probability mass` series for the combined,
110
+ * attribution-carrying distribution — the provenance core of the stacked
111
+ * damage-attribution chart. Convenience for
112
+ * `combinedWithAttribution().attributionByValue()`; see
113
+ * {@link PMF.attributionByValue}.
114
+ */
115
+ attributionByValue() {
116
+ return this.combinedWithAttribution().attributionByValue();
117
+ }
118
+ /**
119
+ * How many of the independent single PMFs can produce the given outcome
120
+ * label. Useful for "all of them succeeded" style probabilities where the
121
+ * exponent is the number of contributing attacks (see
122
+ * {@link DiceQuery.probExactlyK}).
123
+ */
124
+ countSinglesWith(label) {
125
+ let count = 0;
126
+ for (const single of this.singles) {
127
+ if (single.hasOutcome(label)) count++;
128
+ }
129
+ return count;
130
+ }
92
131
  /**
93
132
  * Returns the expected damage across all possible outcomes.
94
133
  *
@@ -96,11 +135,18 @@ var _DiceQuery = class _DiceQuery {
96
135
  * Use case: "What's my average damage per round?"
97
136
  */
98
137
  mean() {
99
- let totalSum = 0;
100
- for (const [damageValue, probabilityBin] of this.combined) {
101
- totalSum += damageValue * probabilityBin.p;
138
+ if (this._combinedProvided) {
139
+ let m = 0;
140
+ for (const [damageValue, bin] of this.combined) m += damageValue * bin.p;
141
+ return m;
142
+ }
143
+ let totalMean = 0;
144
+ for (const single of this.singles) {
145
+ const mass = single.mass();
146
+ if (mass <= 0) continue;
147
+ totalMean += Math.abs(mass - 1) <= this._eps ? single.mean() : single.mean() / mass;
102
148
  }
103
- return totalSum;
149
+ return totalMean;
104
150
  }
105
151
  /**
106
152
  * Returns the variance of the damage distribution.
@@ -110,13 +156,33 @@ var _DiceQuery = class _DiceQuery {
110
156
  * High variance means higher risk/reward. Lower variance means more consistent damage.
111
157
  */
112
158
  variance() {
113
- const meanValue = this.mean();
114
- let varianceSum = 0;
115
- for (const [damageValue, probabilityBin] of this.combined) {
116
- const deviationFromMean = damageValue - meanValue;
117
- varianceSum += deviationFromMean * deviationFromMean * probabilityBin.p;
159
+ if (this._combinedProvided) {
160
+ const mu = this.mean();
161
+ let v = 0;
162
+ for (const [damageValue, bin] of this.combined) {
163
+ const dev = damageValue - mu;
164
+ v += dev * dev * bin.p;
165
+ }
166
+ return v;
167
+ }
168
+ let totalVariance = 0;
169
+ for (const single of this.singles) {
170
+ const mass = single.mass();
171
+ if (mass <= 0) continue;
172
+ if (Math.abs(mass - 1) <= this._eps) {
173
+ totalVariance += single.variance();
174
+ } else {
175
+ let mu = 0;
176
+ for (const [d2, b] of single) mu += d2 * (b.p / mass);
177
+ let v = 0;
178
+ for (const [d2, b] of single) {
179
+ const dev = d2 - mu;
180
+ v += dev * dev * (b.p / mass);
181
+ }
182
+ totalVariance += v;
183
+ }
118
184
  }
119
- return varianceSum;
185
+ return totalVariance;
120
186
  }
121
187
  /**
122
188
  * Returns the standard deviation of the damage distribution.
@@ -128,6 +194,10 @@ var _DiceQuery = class _DiceQuery {
128
194
  stddev() {
129
195
  return Math.sqrt(this.variance());
130
196
  }
197
+ /** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */
198
+ stdev() {
199
+ return this.stddev();
200
+ }
131
201
  /**
132
202
  * Returns the Cumulative Distribution Function.
133
203
  */
@@ -218,20 +288,49 @@ var _DiceQuery = class _DiceQuery {
218
288
  return this.combined.max();
219
289
  }
220
290
  singleProb(diceIndex, label) {
291
+ const single = this.singles[diceIndex];
221
292
  let probabilitySum = 0;
222
- for (const [, probabilityBin] of this.singles[diceIndex]) {
293
+ for (const [, probabilityBin] of single) {
223
294
  probabilitySum += probabilityBin.count[label] || 0;
224
295
  }
225
- return probabilitySum;
296
+ const mass = single.mass();
297
+ return mass > 0 ? probabilitySum / mass : 0;
298
+ }
299
+ /**
300
+ * Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it
301
+ * carries ANY of `labels`", over the n independent singles.
302
+ *
303
+ * Each single's per-event success probability is the Poisson-binomial
304
+ * marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne),
305
+ * computed exactly once. The binomial DP then runs once to produce the whole
306
+ * distribution, so the array-label paths of probExactlyK / probAtLeastK /
307
+ * probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and
308
+ * re-running the DP per requested k.
309
+ */
310
+ countDistribution(labels) {
311
+ const n = this.singles.length;
312
+ const successProbabilities = this.singles.map(
313
+ (single) => new _DiceQuery([single]).probabilityOf(labels)
314
+ );
315
+ const dist = new Array(n + 1).fill(0);
316
+ dist[0] = 1;
317
+ for (const successProb of successProbabilities) {
318
+ for (let outcomeCount = n; outcomeCount >= 1; outcomeCount--) {
319
+ dist[outcomeCount] = dist[outcomeCount] * (1 - successProb) + dist[outcomeCount - 1] * successProb;
320
+ }
321
+ dist[0] *= 1 - successProb;
322
+ }
323
+ return dist;
226
324
  }
227
325
  probAtLeastK(labels, k) {
228
326
  const L = Array.isArray(labels) ? [...new Set(labels)] : [labels];
229
327
  const n = this.singles.length;
230
328
  if (k <= 0) return 1;
231
329
  if (k > n) return 0;
330
+ const dist = this.countDistribution(L);
232
331
  let tail = 0;
233
332
  for (let i = k; i <= n; i++) {
234
- tail += this.probExactlyK(L, i);
333
+ tail += dist[i];
235
334
  }
236
335
  if (tail < 0) return 0;
237
336
  if (tail > 1) return 1;
@@ -263,9 +362,12 @@ var _DiceQuery = class _DiceQuery {
263
362
  for (const label of labels) {
264
363
  combinedProbability += this.singleProb(diceIndex, label);
265
364
  }
365
+ if (combinedProbability < 0) combinedProbability = 0;
366
+ else if (combinedProbability > 1) combinedProbability = 1;
266
367
  productOfNonOccurrence *= 1 - combinedProbability;
267
368
  }
268
- return 1 - productOfNonOccurrence;
369
+ const result = 1 - productOfNonOccurrence;
370
+ return result < 0 ? 0 : result > 1 ? 1 : result;
269
371
  }
270
372
  /**
271
373
  * Computes binomial probabilities for exactly 0, 1, 2, ..., maxK occurrences of a label.
@@ -306,7 +408,7 @@ var _DiceQuery = class _DiceQuery {
306
408
  * Array examples:
307
409
  * - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed
308
410
  * - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds
309
- * - probExactlyK(['miss', 'missNone'], 0) = probability no attacks miss
411
+ * - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss
310
412
  *
311
413
  * Use cases:
312
414
  * - "What's the chance exactly one of my attacks hits?"
@@ -321,19 +423,8 @@ var _DiceQuery = class _DiceQuery {
321
423
  const probabilityArray = this.computeBinomialProbabilities(labels, k);
322
424
  return probabilityArray[k];
323
425
  }
324
- const successProbabilities = this.singles.map((single) => {
325
- const singleQuery = new _DiceQuery([single]);
326
- return singleQuery.probabilityOf(labels);
327
- });
328
- const binomialProbs = new Array(k + 1).fill(0);
329
- binomialProbs[0] = 1;
330
- for (const successProb of successProbabilities) {
331
- for (let outcomeCount = k; outcomeCount >= 1; outcomeCount--) {
332
- binomialProbs[outcomeCount] = binomialProbs[outcomeCount] * (1 - successProb) + binomialProbs[outcomeCount - 1] * successProb;
333
- }
334
- binomialProbs[0] *= 1 - successProb;
335
- }
336
- return binomialProbs[k];
426
+ const dist = this.countDistribution(labels);
427
+ return k >= 0 && k < dist.length ? dist[k] : 0;
337
428
  }
338
429
  /**
339
430
  * Returns the probability that AT MOST K attacks result in the specified outcome(s).
@@ -341,7 +432,7 @@ var _DiceQuery = class _DiceQuery {
341
432
  * Single label examples:
342
433
  * - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1)
343
434
  * - probAtMostK('crit', 0) = probability no attacks crit
344
- * - probAtMostK('miss', 2) = probability at most 2 attacks miss
435
+ * - probAtMostK('missDamage', 2) = probability at most 2 attacks miss
345
436
  *
346
437
  * Array examples:
347
438
  * - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds
@@ -362,9 +453,11 @@ var _DiceQuery = class _DiceQuery {
362
453
  }
363
454
  return cumulativeSum2;
364
455
  }
456
+ const dist = this.countDistribution(labels);
457
+ const upper = Math.min(k, dist.length - 1);
365
458
  let cumulativeSum = 0;
366
- for (let outcomeCount = 0; outcomeCount <= k; outcomeCount++) {
367
- cumulativeSum += this.probExactlyK(labels, outcomeCount);
459
+ for (let outcomeCount = 0; outcomeCount <= upper; outcomeCount++) {
460
+ cumulativeSum += dist[outcomeCount];
368
461
  }
369
462
  return cumulativeSum;
370
463
  }
@@ -410,7 +503,7 @@ var _DiceQuery = class _DiceQuery {
410
503
  *
411
504
  * Array examples:
412
505
  * - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds
413
- * - damageStatsFrom(['miss', 'missNone']) = damage range when at least one attack misses
506
+ * - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses
414
507
  *
415
508
  * Tactical Use Cases:
416
509
  * - "Given that I don't completely whiff (99% of turns), what damage should I expect?"
@@ -428,6 +521,12 @@ var _DiceQuery = class _DiceQuery {
428
521
  * This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which
429
522
  * occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats.
430
523
  *
524
+ * KNOWN LIMITATION (multi-attack, single label): the returned `count` is an
525
+ * EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and
526
+ * `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than
527
+ * E[dmg | the label occurs]. For a single attack both are the plain
528
+ * conditional figures. Use {@link probAtLeastOne} for the scenario probability.
529
+ *
431
530
  * @example
432
531
  * // High-level tactical planning
433
532
  * const successStats = query.damageStatsFrom('hit')
@@ -532,7 +631,8 @@ var _DiceQuery = class _DiceQuery {
532
631
  };
533
632
  }
534
633
  /**
535
- * Returns the probability that a result includes ANY of the specified labels.
634
+ * Returns the probability that at least one attack carries ANY of the
635
+ * specified labels (the marginal P(≥1) across the independent attacks).
536
636
  *
537
637
  * Examples:
538
638
  * - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs)
@@ -541,25 +641,15 @@ var _DiceQuery = class _DiceQuery {
541
641
  * Use cases:
542
642
  * - "What's the chance my resolution includes a success label?"
543
643
  * - "How likely am I to get any hits or crits across all attacks?"
644
+ *
645
+ * Note: this must NOT be computed by summing `combined` bin probabilities. A
646
+ * single combined damage total is reachable by many outcome combinations and
647
+ * a bin can hold several labels at once, so summing `bin.p` over bins that
648
+ * contain a label over-counts. The correct marginal is the Poisson-binomial
649
+ * complement over the per-attack probabilities, i.e. {@link probAtLeastOne}.
544
650
  */
545
651
  probabilityOf(labels) {
546
- if (typeof labels === "string") {
547
- labels = [labels];
548
- }
549
- let totalProbability = 0;
550
- for (const [, probabilityBin] of this.combined) {
551
- let binHasAnyLabel = false;
552
- for (const label of labels) {
553
- if (probabilityBin.count[label] && probabilityBin.count[label] > 0) {
554
- binHasAnyLabel = true;
555
- break;
556
- }
557
- }
558
- if (binHasAnyLabel) {
559
- totalProbability += probabilityBin.p;
560
- }
561
- }
562
- return totalProbability;
652
+ return this.probAtLeastOne(labels);
563
653
  }
564
654
  /**
565
655
  * Returns the probability of missing (any type of miss).
@@ -619,10 +709,6 @@ var _DiceQuery = class _DiceQuery {
619
709
  */
620
710
  toStackedChartData(labels = [], epsilon = EPS) {
621
711
  const damageValues = this.combined.support();
622
- damageValues.map((dmg) => {
623
- const bin = this.combined.map.get(dmg);
624
- return labels.reduce((sum, lab) => sum + (bin.count[lab] || 0), 0);
625
- });
626
712
  const datasets = labels.map((outcomeLabel) => ({
627
713
  label: outcomeLabel,
628
714
  data: damageValues.map((dmg) => {
@@ -1059,6 +1145,16 @@ var _DiceQuery = class _DiceQuery {
1059
1145
  * Snapshot of the distribution in the exact shape the UI consumes.
1060
1146
  * - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
1061
1147
  * - damageRange is conditional on the outcome occurring
1148
+ *
1149
+ * The outcome probabilities use the correct Poisson-binomial marginals
1150
+ * (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)),
1151
+ * so they are always valid probabilities in [0,1].
1152
+ *
1153
+ * KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from
1154
+ * the combined PMF's `count`, which the convolution accumulates as an EXPECTED
1155
+ * COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label]
1156
+ * rather than a clean conditional expectation. It is correct for a single
1157
+ * attack.
1062
1158
  */
1063
1159
  snapshot(order) {
1064
1160
  const discovered = /* @__PURE__ */ new Set();
@@ -1080,10 +1176,8 @@ var _DiceQuery = class _DiceQuery {
1080
1176
  );
1081
1177
  }
1082
1178
  const rows = this.toLabeledTable(outcomes);
1083
- const totals = /* @__PURE__ */ new Map();
1084
1179
  const rangeAcc = /* @__PURE__ */ new Map();
1085
1180
  for (const ot of outcomes) {
1086
- totals.set(ot, 0);
1087
1181
  rangeAcc.set(ot, { sum: 0, mass: 0 });
1088
1182
  }
1089
1183
  for (const row of rows) {
@@ -1091,7 +1185,6 @@ var _DiceQuery = class _DiceQuery {
1091
1185
  for (const ot of outcomes) {
1092
1186
  const p = row[ot] || 0;
1093
1187
  if (p <= 0) continue;
1094
- totals.set(ot, (totals.get(ot) || 0) + p);
1095
1188
  const r = rangeAcc.get(ot);
1096
1189
  r.sum += dmg * p;
1097
1190
  r.mass += p;
@@ -1099,15 +1192,14 @@ var _DiceQuery = class _DiceQuery {
1099
1192
  if (r.max === void 0 || dmg > r.max) r.max = dmg;
1100
1193
  }
1101
1194
  }
1195
+ const n = this.singles.length;
1102
1196
  const outcomeMap = /* @__PURE__ */ new Map();
1103
1197
  for (const ot of outcomes) {
1104
- const total = totals.get(ot) || 0;
1105
1198
  const r = rangeAcc.get(ot);
1106
1199
  const avg = r.mass > 0 ? r.sum / r.mass : 0;
1107
1200
  outcomeMap.set(ot, {
1108
- atLeastOneProbability: total,
1109
- allProbability: total,
1110
- // single aggregate PMF: same value
1201
+ atLeastOneProbability: this.probAtLeastOne(ot),
1202
+ allProbability: this.probAtLeastK(ot, n),
1111
1203
  damageRange: { min: r.min ?? 0, avg, max: r.max ?? 0 }
1112
1204
  });
1113
1205
  }
@@ -1292,11 +1384,11 @@ var _DiceQuery = class _DiceQuery {
1292
1384
  return [a, b, any, none];
1293
1385
  }
1294
1386
  };
1295
- __publicField(_DiceQuery, "DEFAULT_OUTCOMES", [
1387
+ _DiceQuery.DEFAULT_OUTCOMES = [
1296
1388
  "hit",
1297
1389
  "crit",
1298
1390
  "missNone"
1299
- ]);
1391
+ ];
1300
1392
  var DiceQuery = _DiceQuery;
1301
1393
  var pmfCache = new LRUCache(1e3);
1302
1394
  var _PMF = class _PMF {
@@ -1306,14 +1398,6 @@ var _PMF = class _PMF {
1306
1398
  this.normalized = normalized;
1307
1399
  this.identifier = identifier;
1308
1400
  this._preservedProvenance = _preservedProvenance;
1309
- // Cached computed values
1310
- __publicField(this, "_support");
1311
- __publicField(this, "_min");
1312
- __publicField(this, "_max");
1313
- __publicField(this, "_totalMass");
1314
- __publicField(this, "_mean");
1315
- __publicField(this, "_variance");
1316
- __publicField(this, "_stdev");
1317
1401
  }
1318
1402
  static empty(epsilon = EPS, identifier = "empty") {
1319
1403
  return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
@@ -1327,6 +1411,20 @@ var _PMF = class _PMF {
1327
1411
  static delta(value, epsilon = EPS) {
1328
1412
  return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
1329
1413
  }
1414
+ /**
1415
+ * Point mass at damage 0 tagged with the canonical `missNone` outcome.
1416
+ *
1417
+ * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
1418
+ * builder's attack-resolution vocabulary. This uses the `missNone`
1419
+ * {@link OutcomeType} that the attribution charts and outcome stats key on,
1420
+ * so it is the correct "clean miss / no damage" delta for provenance-aware
1421
+ * mixtures feeding those consumers.
1422
+ */
1423
+ static missNone(epsilon = EPS) {
1424
+ const m = /* @__PURE__ */ new Map();
1425
+ m.set(0, { p: 1, count: { [MISS_NONE_OUTCOME]: 1 }, attr: {} });
1426
+ return new _PMF(m, epsilon, false, "missNone");
1427
+ }
1330
1428
  // This creates a single bin at value 0, but with weight 0.
1331
1429
  static emptyMass() {
1332
1430
  return _PMF.zero().scaleMass(0);
@@ -1353,8 +1451,14 @@ var _PMF = class _PMF {
1353
1451
  if (p === 1) return successPMF.scaleMass(1);
1354
1452
  const eps = successPMF.epsilon ?? failurePMF.epsilon;
1355
1453
  const id = `branch(${failurePMF.identifier}*${q.toFixed(6)} + ${successPMF.identifier}*${p.toFixed(6)})`;
1356
- const out = _PMF.empty(eps, id).addScaled(failurePMF, q).addScaled(successPMF, p);
1357
- return out;
1454
+ const resultMap = /* @__PURE__ */ new Map();
1455
+ for (const [damageValue, bin] of failurePMF.map) {
1456
+ _PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, q));
1457
+ }
1458
+ for (const [damageValue, bin] of successPMF.map) {
1459
+ _PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, p));
1460
+ }
1461
+ return new _PMF(resultMap, eps, false, id);
1358
1462
  }
1359
1463
  /**
1360
1464
  * withProbability()
@@ -1402,8 +1506,8 @@ var _PMF = class _PMF {
1402
1506
  * @param fallback PMF to apply when this PMF is *not* selected.
1403
1507
  * @returns A new PMF representing the weighted mixture of this PMF and the fallback.
1404
1508
  */
1405
- gate(p, zero) {
1406
- return _PMF.branch(this, zero, p);
1509
+ gate(p, fallback) {
1510
+ return _PMF.branch(this, fallback, p);
1407
1511
  }
1408
1512
  /**
1409
1513
  * PMF.exclusive()
@@ -1486,13 +1590,23 @@ var _PMF = class _PMF {
1486
1590
  *
1487
1591
  * @returns New PMF with attr field populated in each bin
1488
1592
  */
1489
- withAttribution() {
1593
+ /**
1594
+ * Returns true if this PMF already carries damage attribution metadata.
1595
+ *
1596
+ * Only the first positive-damage bin is inspected (parser-generated PMFs
1597
+ * populate `attr` uniformly), so this is O(1) in practice.
1598
+ */
1599
+ hasAttribution() {
1490
1600
  for (const [damage, bin] of this.map) {
1491
1601
  if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
1492
- return this;
1602
+ return true;
1493
1603
  }
1494
1604
  if (damage > 0) break;
1495
1605
  }
1606
+ return false;
1607
+ }
1608
+ withAttribution() {
1609
+ if (this.hasAttribution()) return this;
1496
1610
  const newMap = /* @__PURE__ */ new Map();
1497
1611
  for (const [damage, bin] of this.map) {
1498
1612
  const attr = {};
@@ -1609,7 +1723,7 @@ var _PMF = class _PMF {
1609
1723
  */
1610
1724
  replicate(n) {
1611
1725
  if (!Number.isInteger(n) || n <= 0) {
1612
- throw new Error("combineN(n): n must be a positive integer");
1726
+ throw new Error("replicate(n): n must be a positive integer");
1613
1727
  }
1614
1728
  if (n === 1) return [this];
1615
1729
  return Array.from({ length: n }, () => this);
@@ -1641,7 +1755,6 @@ var _PMF = class _PMF {
1641
1755
  if (normalizationFactor === 0) return this;
1642
1756
  const normalizedMap = /* @__PURE__ */ new Map();
1643
1757
  for (const [damageValue, probabilityBin] of this.map) {
1644
- const normalizedProbability = probabilityBin.p / normalizationFactor;
1645
1758
  const normalizedCount = {};
1646
1759
  for (const labelKey in probabilityBin.count) {
1647
1760
  normalizedCount[labelKey] = probabilityBin.count[labelKey] / normalizationFactor;
@@ -1654,7 +1767,7 @@ var _PMF = class _PMF {
1654
1767
  }
1655
1768
  }
1656
1769
  normalizedMap.set(damageValue, {
1657
- p: normalizedProbability,
1770
+ p: probabilityBin.p / normalizationFactor,
1658
1771
  count: normalizedCount,
1659
1772
  attr: normalizedAttributes
1660
1773
  });
@@ -1677,22 +1790,23 @@ var _PMF = class _PMF {
1677
1790
  for (const [damageValue, probabilityBin] of this.map) {
1678
1791
  const shouldKeep = probabilityBin.p >= eps || keepFinalBin && damageValue === maxKey;
1679
1792
  if (!shouldKeep) continue;
1680
- for (const labelKey in probabilityBin.count) {
1681
- if (Math.abs(probabilityBin.count[labelKey] || 0) < eps) {
1682
- delete probabilityBin.count[labelKey];
1793
+ const cleanedBin = _PMF.cloneBin(probabilityBin);
1794
+ for (const labelKey in cleanedBin.count) {
1795
+ if (Math.abs(cleanedBin.count[labelKey] || 0) < eps) {
1796
+ delete cleanedBin.count[labelKey];
1683
1797
  }
1684
1798
  }
1685
- if (probabilityBin.attr) {
1686
- for (const labelKey in probabilityBin.attr) {
1687
- if (Math.abs(probabilityBin.attr[labelKey] || 0) < eps) {
1688
- delete probabilityBin.attr[labelKey];
1799
+ if (cleanedBin.attr) {
1800
+ for (const labelKey in cleanedBin.attr) {
1801
+ if (Math.abs(cleanedBin.attr[labelKey] || 0) < eps) {
1802
+ delete cleanedBin.attr[labelKey];
1689
1803
  }
1690
1804
  }
1691
- if (Object.keys(probabilityBin.attr).length === 0) {
1692
- probabilityBin.attr = void 0;
1805
+ if (Object.keys(cleanedBin.attr).length === 0) {
1806
+ cleanedBin.attr = void 0;
1693
1807
  }
1694
1808
  }
1695
- compactedMap.set(damageValue, probabilityBin);
1809
+ compactedMap.set(damageValue, cleanedBin);
1696
1810
  }
1697
1811
  return new _PMF(compactedMap, eps, this.normalized, this.identifier);
1698
1812
  }
@@ -1759,14 +1873,33 @@ var _PMF = class _PMF {
1759
1873
  }
1760
1874
  return this._stdev;
1761
1875
  }
1876
+ /** Deep-copies a Bin, cloning its count and (optional) attr maps. */
1877
+ static cloneBin(bin) {
1878
+ return {
1879
+ p: bin.p,
1880
+ count: { ...bin.count },
1881
+ attr: bin.attr ? { ...bin.attr } : void 0
1882
+ };
1883
+ }
1884
+ /** Returns a new Bin with p, count, and attr all multiplied by `factor`. */
1885
+ static scaleBin(bin, factor) {
1886
+ const count = {};
1887
+ for (const k in bin.count) {
1888
+ count[k] = bin.count[k] * factor;
1889
+ }
1890
+ let attr;
1891
+ if (bin.attr) {
1892
+ attr = {};
1893
+ for (const k in bin.attr) {
1894
+ attr[k] = bin.attr[k] * factor;
1895
+ }
1896
+ }
1897
+ return { p: bin.p * factor, count, attr };
1898
+ }
1762
1899
  static mergeInto(destinationMap, damageValue, binToAdd) {
1763
1900
  const existingBin = destinationMap.get(damageValue);
1764
1901
  if (!existingBin) {
1765
- destinationMap.set(damageValue, {
1766
- p: binToAdd.p,
1767
- count: { ...binToAdd.count },
1768
- attr: binToAdd.attr ? { ...binToAdd.attr } : void 0
1769
- });
1902
+ destinationMap.set(damageValue, _PMF.cloneBin(binToAdd));
1770
1903
  return;
1771
1904
  }
1772
1905
  existingBin.p += binToAdd.p;
@@ -1797,29 +1930,14 @@ var _PMF = class _PMF {
1797
1930
  if (probability === 0) return this;
1798
1931
  const resultMap = /* @__PURE__ */ new Map();
1799
1932
  for (const [dmg, bin] of this.map) {
1800
- resultMap.set(dmg, {
1801
- p: bin.p,
1802
- count: { ...bin.count },
1803
- attr: bin.attr ? { ...bin.attr } : void 0
1804
- });
1933
+ resultMap.set(dmg, _PMF.cloneBin(bin));
1805
1934
  }
1806
1935
  for (const [damageValue, probabilityBin] of branch.map) {
1807
- const scaledCount = {};
1808
- for (const k in probabilityBin.count) {
1809
- scaledCount[k] = probability * probabilityBin.count[k];
1810
- }
1811
- let scaledAttributes;
1812
- if (probabilityBin.attr) {
1813
- scaledAttributes = {};
1814
- for (const k in probabilityBin.attr) {
1815
- scaledAttributes[k] = probability * probabilityBin.attr[k];
1816
- }
1817
- }
1818
- _PMF.mergeInto(resultMap, damageValue, {
1819
- p: probability * probabilityBin.p,
1820
- count: scaledCount,
1821
- attr: scaledAttributes
1822
- });
1936
+ _PMF.mergeInto(
1937
+ resultMap,
1938
+ damageValue,
1939
+ _PMF.scaleBin(probabilityBin, probability)
1940
+ );
1823
1941
  }
1824
1942
  return new _PMF(
1825
1943
  resultMap,
@@ -1828,26 +1946,54 @@ var _PMF = class _PMF {
1828
1946
  `${this.identifier}+scaled(${branch.identifier},${probability})`
1829
1947
  );
1830
1948
  }
1949
+ /**
1950
+ * Redistributes probability mass to model an effect that only occurs with
1951
+ * probability `frequency` — a conditional attack, an on-hit rider, or a
1952
+ * sub-one AoE target fraction.
1953
+ *
1954
+ * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
1955
+ * per-label `count`, AND per-label `attr` — and the freed mass is moved into
1956
+ * the miss bin at damage 0, tagged with the canonical `missNone` outcome.
1957
+ * Total probability mass is preserved.
1958
+ *
1959
+ * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
1960
+ * attribution (`attr`) intact, so a frequency-scaled PMF still renders
1961
+ * correctly in the damage-attribution charts.
1962
+ *
1963
+ * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
1964
+ * collapses all mass into the miss bin. The miss outcome is assumed to be
1965
+ * encoded at damage value 0.
1966
+ *
1967
+ * @param frequency Probability in [0, 1] that the effect occurs.
1968
+ */
1969
+ applyHitFrequency(frequency) {
1970
+ if (!Number.isFinite(frequency) || frequency >= 1) return this;
1971
+ const freq = Math.max(0, frequency);
1972
+ const pMiss = this.pAt(0);
1973
+ const pHit = 1 - pMiss;
1974
+ const newMissMass = pMiss + (1 - freq) * pHit;
1975
+ const newMap = /* @__PURE__ */ new Map();
1976
+ newMap.set(0, {
1977
+ p: newMissMass,
1978
+ count: { [MISS_NONE_OUTCOME]: newMissMass },
1979
+ attr: {}
1980
+ });
1981
+ for (const [damage, bin] of this.map) {
1982
+ if (damage <= 0) continue;
1983
+ newMap.set(damage, _PMF.scaleBin(bin, freq));
1984
+ }
1985
+ return new _PMF(
1986
+ newMap,
1987
+ this.epsilon,
1988
+ false,
1989
+ `freq(${this.identifier},${freq})`
1990
+ );
1991
+ }
1831
1992
  scaleMass(factor) {
1832
1993
  if (factor === 1) return this;
1833
1994
  const scaledMap = /* @__PURE__ */ new Map();
1834
1995
  for (const [damageValue, probabilityBin] of this.map) {
1835
- const scaledCount = {};
1836
- for (const labelKey in probabilityBin.count) {
1837
- scaledCount[labelKey] = probabilityBin.count[labelKey] * factor;
1838
- }
1839
- let scaledAttributes;
1840
- if (probabilityBin.attr) {
1841
- scaledAttributes = {};
1842
- for (const labelKey in probabilityBin.attr) {
1843
- scaledAttributes[labelKey] = probabilityBin.attr[labelKey] * factor;
1844
- }
1845
- }
1846
- scaledMap.set(damageValue, {
1847
- p: probabilityBin.p * factor,
1848
- count: scaledCount,
1849
- attr: scaledAttributes
1850
- });
1996
+ scaledMap.set(damageValue, _PMF.scaleBin(probabilityBin, factor));
1851
1997
  }
1852
1998
  return new _PMF(
1853
1999
  scaledMap,
@@ -1860,11 +2006,11 @@ var _PMF = class _PMF {
1860
2006
  const transformedMap = /* @__PURE__ */ new Map();
1861
2007
  for (const [originalDamage, probabilityBin] of this.map) {
1862
2008
  const transformedDamage = damageTransformFunction(originalDamage);
1863
- _PMF.mergeInto(transformedMap, transformedDamage, {
1864
- p: probabilityBin.p,
1865
- count: { ...probabilityBin.count },
1866
- attr: probabilityBin.attr ? { ...probabilityBin.attr } : void 0
1867
- });
2009
+ _PMF.mergeInto(
2010
+ transformedMap,
2011
+ transformedDamage,
2012
+ _PMF.cloneBin(probabilityBin)
2013
+ );
1868
2014
  }
1869
2015
  return new _PMF(
1870
2016
  transformedMap,
@@ -1879,14 +2025,21 @@ var _PMF = class _PMF {
1879
2025
  }
1880
2026
  getPMFCombineCacheKey(p1, p2, eps, raw) {
1881
2027
  const [id1, id2] = [p1.identifier, p2.identifier].sort();
1882
- const fp = (x) => {
1883
- const m = x.mass().toFixed(12);
1884
- const n = x.map.size;
2028
+ return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
2029
+ }
2030
+ /**
2031
+ * A small content fingerprint (mass + bin count + face sum) so convolution
2032
+ * cache keys change if the underlying numbers do. Memoized because a PMF is
2033
+ * immutable once constructed — this avoids re-summing every key on each
2034
+ * convolve() call (including cache hits).
2035
+ */
2036
+ fingerprint() {
2037
+ if (this._fingerprint === void 0) {
1885
2038
  let faceSum = 0;
1886
- for (const k of x.map.keys()) faceSum += k;
1887
- return `${m}|${n}|${faceSum}`;
1888
- };
1889
- return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${fp(p1)}|${fp(p2)}`;
2039
+ for (const k of this.map.keys()) faceSum += k;
2040
+ this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
2041
+ }
2042
+ return this._fingerprint;
1890
2043
  }
1891
2044
  convolve(other, eps, raw = false) {
1892
2045
  const epsilon = eps ?? this.epsilon;
@@ -1899,25 +2052,34 @@ var _PMF = class _PMF {
1899
2052
  if (cached) return cached;
1900
2053
  const combinedMap = /* @__PURE__ */ new Map();
1901
2054
  for (const [aVal, aBin] of A.map) {
2055
+ const ap = aBin.p;
2056
+ const aCount = aBin.count;
2057
+ const aAttr = aBin.attr;
1902
2058
  for (const [bVal, bBin] of B.map) {
1903
- const p = aBin.p * bBin.p;
2059
+ const bp = bBin.p;
1904
2060
  const dmg = aVal + bVal;
1905
- const count = {};
1906
- for (const k in aBin.count)
1907
- count[k] = (count[k] || 0) + aBin.count[k] * bBin.p;
2061
+ let dest = combinedMap.get(dmg);
2062
+ if (dest === void 0) {
2063
+ dest = { p: 0, count: {} };
2064
+ combinedMap.set(dmg, dest);
2065
+ }
2066
+ dest.p += ap * bp;
2067
+ const dc = dest.count;
2068
+ for (const k in aCount) dc[k] = (dc[k] || 0) + aCount[k] * bp;
1908
2069
  for (const k in bBin.count)
1909
- count[k] = (count[k] || 0) + bBin.count[k] * aBin.p;
1910
- let attr;
1911
- if (aBin.attr || bBin.attr) {
1912
- attr = {};
1913
- if (aBin.attr)
1914
- for (const k in aBin.attr)
1915
- attr[k] = (attr[k] || 0) + aBin.attr[k] * bBin.p;
2070
+ dc[k] = (dc[k] || 0) + bBin.count[k] * ap;
2071
+ if (aAttr || bBin.attr) {
2072
+ let da = dest.attr;
2073
+ if (da === void 0) {
2074
+ da = {};
2075
+ dest.attr = da;
2076
+ }
2077
+ if (aAttr)
2078
+ for (const k in aAttr) da[k] = (da[k] || 0) + aAttr[k] * bp;
1916
2079
  if (bBin.attr)
1917
2080
  for (const k in bBin.attr)
1918
- attr[k] = (attr[k] || 0) + bBin.attr[k] * aBin.p;
2081
+ da[k] = (da[k] || 0) + bBin.attr[k] * ap;
1919
2082
  }
1920
- _PMF.mergeInto(combinedMap, dmg, { p, count, attr });
1921
2083
  }
1922
2084
  }
1923
2085
  let result = new _PMF(
@@ -1928,10 +2090,10 @@ var _PMF = class _PMF {
1928
2090
  );
1929
2091
  const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
1930
2092
  const mGot = result.mass();
1931
- if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
2093
+ if (mExp !== 0 && mGot !== 0 && Math.abs(mGot - mExp) > epsilon) {
1932
2094
  result = result.scaleMass(mExp / mGot);
1933
2095
  }
1934
- if (!raw && Math.abs(result.mass() - 1) > epsilon)
2096
+ if (!raw && mGot !== 0 && Math.abs(result.mass() - 1) > epsilon)
1935
2097
  result = result.normalize();
1936
2098
  pmfCache?.set(cacheKey, result);
1937
2099
  return result;
@@ -1940,27 +2102,6 @@ var _PMF = class _PMF {
1940
2102
  combineRaw(other, eps) {
1941
2103
  return this.convolve(other, eps, true);
1942
2104
  }
1943
- // Collapse repeated identical PMFs using power() and return a sorted list
1944
- // Temporarily disabled for now. This was used for a performance optimization, but can lose data provenance.
1945
- // private static collapseIdentical(pmfList: PMF[], eps: number): PMF[] {
1946
- // const grouped = new Map<string, { pmf: PMF; count: number }>();
1947
- // for (const pmf of pmfList) {
1948
- // const id = pmf.identifier;
1949
- // const g = grouped.get(id);
1950
- // if (g) g.count++;
1951
- // else grouped.set(id, { pmf, count: 1 });
1952
- // }
1953
- // if (grouped.size >= pmfList.length) return pmfList;
1954
- // const collapsed: PMF[] = [];
1955
- // for (const { pmf, count } of grouped.values()) {
1956
- // collapsed.push(count > 1 ? pmf.power(count, eps) : pmf);
1957
- // }
1958
- // // Stable order for better cache locality
1959
- // collapsed.sort((a, b) =>
1960
- // a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0
1961
- // );
1962
- // return collapsed;
1963
- // }
1964
2105
  // Reduce a list of PMFs by left-folding convolve() with the given eps
1965
2106
  static reduceConvolveLeft(pmfList, eps) {
1966
2107
  let result = pmfList[0];
@@ -1984,12 +2125,23 @@ var _PMF = class _PMF {
1984
2125
  if (pmfList.length === 1) return pmfList[0];
1985
2126
  return _PMF.reduceConvolveLeft(pmfList, eps);
1986
2127
  }
2128
+ /**
2129
+ * Returns a plain, JSON-serializable representation of this PMF.
2130
+ *
2131
+ * Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces
2132
+ * the expected output (no double-encoding). Use {@link PMF.fromJSON} to
2133
+ * reconstruct, or {@link PMF.toJSONString} if you need the string directly.
2134
+ */
1987
2135
  toJSON() {
1988
- return JSON.stringify({
2136
+ return {
1989
2137
  bins: [...this.map.entries()],
1990
2138
  normalized: this.normalized,
1991
2139
  identifier: this.identifier
1992
- });
2140
+ };
2141
+ }
2142
+ /** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */
2143
+ toJSONString() {
2144
+ return JSON.stringify(this);
1993
2145
  }
1994
2146
  static fromJSON(jsonData) {
1995
2147
  return new _PMF(
@@ -2060,11 +2212,43 @@ var _PMF = class _PMF {
2060
2212
  }
2061
2213
  return new _PMF(prunedMap, epsRel, false, `prune(${this.identifier})`);
2062
2214
  }
2063
- /** NEW - REVIEW IF THESE ARE USEFUL OR DUPLCIATIVE? */
2064
2215
  /** Probability mass at exactly x. */
2065
2216
  pAt(x) {
2066
2217
  return this.map.get(x)?.p ?? 0;
2067
2218
  }
2219
+ /**
2220
+ * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
2221
+ * Assumes a miss is encoded as the damage-0 bin (the convention used across
2222
+ * attack/save PMFs). The dual of {@link missProbability}.
2223
+ */
2224
+ hitProbability() {
2225
+ return 1 - this.pAt(0);
2226
+ }
2227
+ /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
2228
+ missProbability() {
2229
+ return this.pAt(0);
2230
+ }
2231
+ /**
2232
+ * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
2233
+ * damage buckets, aggregating probability mass (and `count`/`attr`
2234
+ * provenance) into each bucket's start value. Returns this PMF unchanged when
2235
+ * its integer support already fits within `maxBuckets`.
2236
+ *
2237
+ * This is a lossy display/downsampling transform (bucket start replaces the
2238
+ * exact damage value) — use it for charting wide distributions, not for DPR
2239
+ * math.
2240
+ */
2241
+ rebin(maxBuckets) {
2242
+ if (!(maxBuckets > 0)) return this;
2243
+ const support = this.support();
2244
+ if (support.length === 0) return this;
2245
+ const min = support[0];
2246
+ const max = support[support.length - 1];
2247
+ const range = max - min;
2248
+ if (range + 1 <= maxBuckets) return this;
2249
+ const binSize = Math.ceil((range + 1) / maxBuckets);
2250
+ return this.mapDamage((d2) => min + Math.floor((d2 - min) / binSize) * binSize);
2251
+ }
2068
2252
  /** Dense integer support from min..max (inclusive).
2069
2253
  * Useful for showing empty bars in charts.
2070
2254
  */
@@ -2140,11 +2324,60 @@ var _PMF = class _PMF {
2140
2324
  }
2141
2325
  return false;
2142
2326
  }
2327
+ /**
2328
+ * Split each damage value's probability mass across outcome labels, returning
2329
+ * per-label maps of `damage value → probability mass attributable to that
2330
+ * label`. Summing over labels at a given value recovers that value's `p`.
2331
+ *
2332
+ * Damage-bearing bins are split by `attr` weight (the share of damage each
2333
+ * outcome contributed); the clean-miss bin at 0 is split by `count` weight
2334
+ * (there is no damage to attribute). Attribution is computed on demand via
2335
+ * {@link withAttribution} when absent, so builder-generated PMFs work too.
2336
+ *
2337
+ * This is the provenance core of the stacked damage-attribution chart — the
2338
+ * caller only maps these series into its rendering format (colors, binning,
2339
+ * axis labels).
2340
+ */
2341
+ attributionByValue() {
2342
+ const src = this.hasAttribution() ? this : this.withAttribution();
2343
+ const result = /* @__PURE__ */ new Map();
2344
+ const add = (label, damage, mass) => {
2345
+ if (!(mass > 0)) return;
2346
+ let series = result.get(label);
2347
+ if (!series) {
2348
+ series = /* @__PURE__ */ new Map();
2349
+ result.set(label, series);
2350
+ }
2351
+ series.set(damage, (series.get(damage) ?? 0) + mass);
2352
+ };
2353
+ for (const [damage, bin] of src.map) {
2354
+ const p = bin.p || 0;
2355
+ if (p <= 0) continue;
2356
+ const isMissBin = damage === 0;
2357
+ if (isMissBin) {
2358
+ let totalCount = 0;
2359
+ for (const k in bin.count) totalCount += bin.count[k] || 0;
2360
+ if (totalCount > 0) {
2361
+ const c = bin.count[MISS_NONE_OUTCOME] || 0;
2362
+ add(MISS_NONE_OUTCOME, damage, c / totalCount * p);
2363
+ }
2364
+ continue;
2365
+ }
2366
+ let totalAttr = 0;
2367
+ if (bin.attr) for (const k in bin.attr) totalAttr += bin.attr[k] || 0;
2368
+ if (bin.attr && totalAttr > 0) {
2369
+ for (const k in bin.attr) {
2370
+ if (k === MISS_NONE_OUTCOME) continue;
2371
+ add(k, damage, (bin.attr[k] || 0) / totalAttr * p);
2372
+ }
2373
+ }
2374
+ }
2375
+ return result;
2376
+ }
2143
2377
  tailProbGE(t) {
2144
2378
  let s = 0;
2145
- for (const [x, rec] of this) {
2146
- const p = typeof rec === "number" ? rec : rec.p;
2147
- if (p > 0 && x >= t) s += p;
2379
+ for (const [x, bin] of this) {
2380
+ if (bin.p > 0 && x >= t) s += bin.p;
2148
2381
  }
2149
2382
  return s;
2150
2383
  }
@@ -2205,6 +2438,11 @@ var _PMF = class _PMF {
2205
2438
  * - pAny: Probability that at least one success occurred
2206
2439
  */
2207
2440
  static firstSuccessWeights(pSuccess, pSpecial, n) {
2441
+ if (!Number.isFinite(pSuccess) || !Number.isFinite(pSpecial) || pSuccess < 0 || pSuccess > 1 || pSpecial < 0 || pSpecial - pSuccess > EPS) {
2442
+ throw new Error(
2443
+ `firstSuccessWeights: require 0 <= pSpecial <= pSuccess <= 1 (got pSuccess=${pSuccess}, pSpecial=${pSpecial})`
2444
+ );
2445
+ }
2208
2446
  const pFail = 1 - pSuccess;
2209
2447
  const pFailAll = Math.pow(pFail, n);
2210
2448
  const pAny = 1 - pFailAll;
@@ -2220,13 +2458,12 @@ var _PMF = class _PMF {
2220
2458
  const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
2221
2459
  const probs = /* @__PURE__ */ new Map();
2222
2460
  const counts = /* @__PURE__ */ new Map();
2223
- for (const [v, rec] of this) {
2224
- if (Math.abs(rec.p) < eps) continue;
2461
+ for (const [v, bin] of this) {
2462
+ if (Math.abs(bin.p) < eps) continue;
2225
2463
  const u = round(f(v));
2226
- probs.set(u, (probs.get(u) ?? 0) + rec.p);
2464
+ probs.set(u, (probs.get(u) ?? 0) + bin.p);
2227
2465
  if (preserveCounts) {
2228
- const rec2 = this.map.get(v);
2229
- const src = typeof rec2 === "number" ? void 0 : rec2?.count;
2466
+ const src = bin.count;
2230
2467
  if (src) {
2231
2468
  const dest = counts.get(u) ?? {};
2232
2469
  for (const k in src) {
@@ -2279,17 +2516,15 @@ var _PMF = class _PMF {
2279
2516
  }
2280
2517
  };
2281
2518
  // Unique ID generator for anonymous PMFs to avoid cache key collisions
2282
- __publicField(_PMF, "__anonIdCounter", 1);
2519
+ _PMF.__anonIdCounter = 1;
2283
2520
  var PMF = _PMF;
2284
2521
 
2285
2522
  // src/pmf/mixture.ts
2286
2523
  var Mixture = class _Mixture {
2287
2524
  constructor(eps = EPS) {
2288
- __publicField(this, "totals", /* @__PURE__ */ new Map());
2525
+ this.totals = /* @__PURE__ */ new Map();
2289
2526
  // raw mass per outcome (pre-normalization)
2290
- __publicField(this, "labelMass", /* @__PURE__ */ new Map());
2291
- // raw mass per outcome per label
2292
- __publicField(this, "eps");
2527
+ this.labelMass = /* @__PURE__ */ new Map();
2293
2528
  this.eps = Number.isFinite(eps) ? eps : EPS;
2294
2529
  }
2295
2530
  /** Remove all accumulated state. */
@@ -2313,9 +2548,8 @@ var Mixture = class _Mixture {
2313
2548
  */
2314
2549
  add(label, pmf, weight = 1) {
2315
2550
  if (!Number.isFinite(weight) || weight <= 0) return this;
2316
- for (const [v, binOrNumber] of pmf) {
2317
- const isNumber = typeof binOrNumber === "number";
2318
- const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
2551
+ for (const [v, bin] of pmf) {
2552
+ const p = bin.p;
2319
2553
  if (p <= 0) continue;
2320
2554
  const add = weight * p;
2321
2555
  if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
@@ -2403,14 +2637,28 @@ var Mixture = class _Mixture {
2403
2637
  }
2404
2638
  };
2405
2639
 
2640
+ // src/common/errors.ts
2641
+ var DiceParseError = class _DiceParseError extends Error {
2642
+ constructor(message, options) {
2643
+ super(message);
2644
+ this.name = "DiceParseError";
2645
+ this.expression = options?.expression;
2646
+ this.cause = options?.cause;
2647
+ Object.setPrototypeOf(this, _DiceParseError.prototype);
2648
+ }
2649
+ };
2650
+
2406
2651
  // src/parser/dice.ts
2652
+ var MAX_BINARY_OUTCOMES = 1e8;
2407
2653
  var Dice = class _Dice {
2408
2654
  constructor(x = 0) {
2409
- __publicField(this, "faces", {});
2410
- __publicField(this, "privateData", {});
2411
- __publicField(this, "outcomeData", {});
2412
- __publicField(this, "hasHitDistributionCalculated", false);
2413
- __publicField(this, "identifier");
2655
+ this.faces = {};
2656
+ this.privateData = {};
2657
+ // Partial: the object starts empty and gains keys as outcomes are recorded,
2658
+ // so the type must not claim every OutcomeType is present. (Previously typed
2659
+ // as a full Record via an `as` cast, which lied about missing keys.)
2660
+ this.outcomeData = {};
2661
+ this.hasHitDistributionCalculated = false;
2414
2662
  if (x <= 0) return;
2415
2663
  for (let i = 1; i <= x; i++) {
2416
2664
  this.faces[i] = 1;
@@ -2461,27 +2709,27 @@ var Dice = class _Dice {
2461
2709
  // TODO this can be private later if we change how testing works
2462
2710
  calculateHitDistribution() {
2463
2711
  const hitValues = {};
2712
+ const subtractedOutcomes = [
2713
+ this.outcomeData.crit,
2714
+ this.outcomeData.missNone,
2715
+ this.outcomeData.missDamage,
2716
+ this.outcomeData.saveHalf,
2717
+ this.outcomeData.saveFail,
2718
+ this.outcomeData.pc
2719
+ ];
2464
2720
  for (const [face, totalCount] of Object.entries(this.faces)) {
2465
2721
  const numFace = Number(face);
2466
2722
  let hitCount = totalCount;
2467
- for (const outcomeType of [
2468
- "crit",
2469
- "missNone",
2470
- "missDamage",
2471
- "saveHalf",
2472
- "saveFail",
2473
- "pc"
2474
- ]) {
2475
- const distribution = this.getOutcomeDistribution(outcomeType);
2476
- if (distribution && distribution[numFace]) {
2477
- hitCount -= distribution[numFace];
2723
+ for (const distribution of subtractedOutcomes) {
2724
+ const outcomeCount = distribution?.[numFace];
2725
+ if (outcomeCount) {
2726
+ hitCount -= outcomeCount;
2478
2727
  }
2479
2728
  }
2480
2729
  if (numFace === 0) {
2481
2730
  hitCount = 0;
2482
2731
  }
2483
2732
  if (hitCount < 0) {
2484
- console.error("hitCount is <=0?", face, totalCount, hitCount);
2485
2733
  hitCount = 0;
2486
2734
  }
2487
2735
  hitValues[numFace] = hitCount;
@@ -2500,13 +2748,18 @@ var Dice = class _Dice {
2500
2748
  const result = diceConstructor ? diceConstructor() : new _Dice();
2501
2749
  const isScalar = typeof other === "number";
2502
2750
  const keys1 = this.keys();
2751
+ const keys2 = isScalar ? [] : other.keys();
2752
+ if (!isScalar && keys1.length * keys2.length > MAX_BINARY_OUTCOMES) {
2753
+ throw new DiceParseError(
2754
+ `Dice operation over ${keys1.length}\xD7${keys2.length} face pairs exceeds the maximum of ${MAX_BINARY_OUTCOMES}`
2755
+ );
2756
+ }
2503
2757
  for (const key1 of keys1) {
2504
2758
  const value1 = this.faces[key1];
2505
2759
  if (isScalar) {
2506
2760
  const resultKey = op(key1, other);
2507
2761
  result.increment(resultKey, value1);
2508
2762
  } else {
2509
- const keys2 = other.keys();
2510
2763
  for (const key2 of keys2) {
2511
2764
  const value2 = other.faces[key2];
2512
2765
  const resultKey = op(key1, key2);
@@ -2528,7 +2781,7 @@ var Dice = class _Dice {
2528
2781
  result.outcomeData = { ...this.outcomeData };
2529
2782
  return result;
2530
2783
  }
2531
- // PUBLIC FUNTIONS
2784
+ // PUBLIC FUNCTIONS
2532
2785
  getFaceEntries() {
2533
2786
  return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
2534
2787
  }
@@ -2658,10 +2911,12 @@ var Dice = class _Dice {
2658
2911
  }
2659
2912
  reroll(toReroll) {
2660
2913
  const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
2661
- const removed = this.removeFaces(rerollDice.keys());
2914
+ const rerollKeys = rerollDice.keys();
2915
+ const rerollSet = new Set(rerollKeys);
2916
+ const removed = this.removeFaces(rerollKeys);
2662
2917
  let result = new _Dice();
2663
2918
  for (const face of this.keys()) {
2664
- const wasRerolled = rerollDice.keys().includes(face);
2919
+ const wasRerolled = rerollSet.has(face);
2665
2920
  result = result.combine(removed);
2666
2921
  if (wasRerolled) {
2667
2922
  result = result.combine(this);
@@ -2729,17 +2984,10 @@ var Dice = class _Dice {
2729
2984
  const missDistro = this.getOutcomeDistribution("missDamage") || {};
2730
2985
  const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
2731
2986
  const pcDistro = this.getOutcomeDistribution("pc") || {};
2732
- let isSaveHalf = false;
2733
- for (const halfDamage of Object.keys(saveDistro).map(Number)) {
2734
- const fullDamage = halfDamage * 2;
2735
- if (fullDamage > 0 && hitDistro[fullDamage]) {
2736
- isSaveHalf = true;
2737
- break;
2738
- }
2739
- }
2987
+ const isSaveHalf = Object.keys(saveDistro).length > 0;
2740
2988
  const isDCCheck = this.privateData.isDCCheck === true;
2741
2989
  const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
2742
- for (const [faceStr, faceCountRaw] of Object.entries(this.getFaceMap())) {
2990
+ for (const [faceStr, faceCountRaw] of Object.entries(this.faces)) {
2743
2991
  const face = Number(faceStr);
2744
2992
  const faceCount = Number(faceCountRaw);
2745
2993
  if (faceCount <= 0) continue;
@@ -2813,14 +3061,14 @@ var Dice = class _Dice {
2813
3061
  map.set(face, bin);
2814
3062
  }
2815
3063
  const identifier = this.identifier || "ERROR";
2816
- if (identifier === "ERROR") {
2817
- console.error("Dice identifier is undefined", this);
2818
- }
2819
3064
  return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
2820
3065
  }
2821
3066
  };
2822
3067
 
2823
3068
  // src/parser/parser.ts
3069
+ var MAX_DIE_SIDES = 1e6;
3070
+ var MAX_DICE_COUNT = 1e4;
3071
+ var MAX_KEEP_OUTCOMES = 1e6;
2824
3072
  var parseCache = new LRUCache(1e3);
2825
3073
  function parse(expression, n = 0) {
2826
3074
  const cleaned = expression.replace(/ /g, "").toLowerCase();
@@ -2830,20 +3078,21 @@ function parse(expression, n = 0) {
2830
3078
  if (cached) return cached;
2831
3079
  }
2832
3080
  const chars = [...cleaned];
2833
- let result = void 0;
3081
+ let result;
2834
3082
  try {
2835
3083
  result = parseExpression(chars, n);
2836
3084
  } catch (error) {
2837
- throw new Error(`Cannot parse dice expression [${expression}]: ${error}`);
2838
- }
2839
- try {
2840
- result.privateData = result.privateData || {};
2841
- result.identifier = cleaned;
2842
- } catch {
3085
+ throw new DiceParseError(
3086
+ `Cannot parse dice expression [${expression}]: ${error}`,
3087
+ { expression, cause: error }
3088
+ );
2843
3089
  }
3090
+ result.privateData = result.privateData || {};
3091
+ result.identifier = cleaned;
2844
3092
  if (chars.length > 0) {
2845
- throw new Error(
2846
- `Unexpected token: '${chars[0]}' from expression: '${expression}'`
3093
+ throw new DiceParseError(
3094
+ `Unexpected token: '${chars[0]}' from expression: '${expression}'`,
3095
+ { expression }
2847
3096
  );
2848
3097
  }
2849
3098
  const resultPMF = result.toPMF(-1);
@@ -2997,7 +3246,7 @@ function multiplyDiceByDice(d1, d2) {
2997
3246
  if (typeof d1 === "number") d1 = Dice.scalar(d1);
2998
3247
  if (typeof d2 === "number") d2 = Dice.scalar(d2);
2999
3248
  const result = new Dice();
3000
- const faces = {};
3249
+ const faces = /* @__PURE__ */ new Map();
3001
3250
  let normalizationFactor = 1;
3002
3251
  for (const key of d1.keys()) {
3003
3252
  let face;
@@ -3005,17 +3254,21 @@ function multiplyDiceByDice(d1, d2) {
3005
3254
  continue;
3006
3255
  }
3007
3256
  if (d2.privateData.keep) {
3257
+ const faceCount = d2.keys().length;
3258
+ if (Math.pow(faceCount, key) > MAX_KEEP_OUTCOMES) {
3259
+ throw new DiceParseError(
3260
+ `Keep enumeration of ${faceCount}^${key} outcomes exceeds the maximum of ${MAX_KEEP_OUTCOMES}`
3261
+ );
3262
+ }
3008
3263
  const repeat = Array(key).fill(d2);
3009
3264
  face = opDice(repeat, d2.privateData.keep);
3010
3265
  } else {
3011
3266
  face = multiplyDice(key, d2);
3012
3267
  }
3013
3268
  normalizationFactor *= face.total();
3014
- faces[key] = face;
3269
+ faces.set(key, face);
3015
3270
  }
3016
- for (const key of Object.keys(faces)) {
3017
- const k = parseFloat(key);
3018
- const face = faces[k];
3271
+ for (const [k, face] of faces) {
3019
3272
  const count = d1.get(k);
3020
3273
  result.combineInPlace(
3021
3274
  face.normalize(count * normalizationFactor / face.total())
@@ -3025,6 +3278,11 @@ function multiplyDiceByDice(d1, d2) {
3025
3278
  return result;
3026
3279
  }
3027
3280
  function multiplyDice(n, d2) {
3281
+ if (n > MAX_DICE_COUNT) {
3282
+ throw new DiceParseError(
3283
+ `Dice count ${n} exceeds the maximum of ${MAX_DICE_COUNT}`
3284
+ );
3285
+ }
3028
3286
  if (n === 0) return new Dice(0);
3029
3287
  if (n === 1) return d2;
3030
3288
  const half = Math.floor(n / 2);
@@ -3107,9 +3365,14 @@ function parseDice(s, n) {
3107
3365
  return;
3108
3366
  }
3109
3367
  const sides = parseNumber(s, n);
3368
+ if (sides > MAX_DIE_SIDES) {
3369
+ throw new DiceParseError(
3370
+ `Die size ${sides} exceeds the maximum of ${MAX_DIE_SIDES}`
3371
+ );
3372
+ }
3110
3373
  let result = new Dice(sides);
3111
3374
  if (rerollOne) {
3112
- result = result.deleteFace(1).combine(result);
3375
+ result = result.reroll(1);
3113
3376
  }
3114
3377
  return result;
3115
3378
  }
@@ -3321,15 +3584,14 @@ var configComplexityScore = (config) => {
3321
3584
  };
3322
3585
  var RollBuilder = class _RollBuilder {
3323
3586
  constructor(countOrConfigs = 1) {
3324
- __publicField(this, "subRollConfigs");
3325
3587
  // --- Dice Shortcut Methods ---
3326
- __publicField(this, "d4", () => this.d(4));
3327
- __publicField(this, "d6", () => this.d(6));
3328
- __publicField(this, "d8", () => this.d(8));
3329
- __publicField(this, "d10", () => this.d(10));
3330
- __publicField(this, "d12", () => this.d(12));
3331
- __publicField(this, "d20", () => this.d(20));
3332
- __publicField(this, "d100", () => this.d(100));
3588
+ this.d4 = () => this.d(4);
3589
+ this.d6 = () => this.d(6);
3590
+ this.d8 = () => this.d(8);
3591
+ this.d10 = () => this.d(10);
3592
+ this.d12 = () => this.d(12);
3593
+ this.d20 = () => this.d(20);
3594
+ this.d100 = () => this.d(100);
3333
3595
  if (typeof countOrConfigs === "number") {
3334
3596
  const count = countOrConfigs;
3335
3597
  if (isNaN(count)) throw new Error("Invalid NaN value for count");
@@ -3836,10 +4098,10 @@ var RollBuilder = class _RollBuilder {
3836
4098
  }
3837
4099
  // These methods are implemented via prototype augmentation in ac.ts and dc.ts
3838
4100
  // They are declared here to provide proper TypeScript types
3839
- ac(targetAC) {
4101
+ ac(_targetAC) {
3840
4102
  throw new Error("ac() should be implemented via prototype augmentation");
3841
4103
  }
3842
- dc(saveDC) {
4104
+ dc(_saveDC) {
3843
4105
  throw new Error("dc() should be implemented via prototype augmentation");
3844
4106
  }
3845
4107
  };
@@ -3956,7 +4218,6 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
3956
4218
  );
3957
4219
  }
3958
4220
  super(baseRoll.getSubRollConfigs());
3959
- __publicField(this, "attackConfig");
3960
4221
  if (attackConfig) {
3961
4222
  this.attackConfig = { ...attackConfig };
3962
4223
  } else {
@@ -4006,8 +4267,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4006
4267
  );
4007
4268
  }
4008
4269
  super(baseRoll.getSubRollConfigs());
4009
- __publicField(this, "attackConfig");
4010
- __publicField(this, "fromAlwaysHit");
4011
4270
  if (attackConfig) {
4012
4271
  this.attackConfig = { ...attackConfig };
4013
4272
  } else {
@@ -4049,8 +4308,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4049
4308
  var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
4050
4309
  constructor(expression) {
4051
4310
  super([]);
4052
- __publicField(this, "cachedPMF");
4053
- __publicField(this, "originalExpression");
4054
4311
  this.originalExpression = expression;
4055
4312
  this.cachedPMF = parse(expression, 0);
4056
4313
  }
@@ -4300,7 +4557,11 @@ function astFromRollConfigs(configs) {
4300
4557
  if (trials === 1) {
4301
4558
  node = perTrial;
4302
4559
  } else {
4303
- node = { type: "maxOf", count: trials, child: perTrial };
4560
+ node = {
4561
+ type: "maxOf",
4562
+ count: trials,
4563
+ child: perTrial
4564
+ };
4304
4565
  }
4305
4566
  } else if (trials === baseCount) {
4306
4567
  const base = { type: "sum", count: trials, child: node };
@@ -4505,7 +4766,7 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
4505
4766
  const support = pmf.support();
4506
4767
  const out = /* @__PURE__ */ new Map();
4507
4768
  if (count <= 6 && support.length <= 20) {
4508
- let dfs = function(rollsLeft, currentMax, probability) {
4769
+ let dfs2 = function(rollsLeft, currentMax, probability) {
4509
4770
  if (rollsLeft === 0) {
4510
4771
  out.set(currentMax, (out.get(currentMax) || 0) + probability);
4511
4772
  return;
@@ -4514,17 +4775,18 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
4514
4775
  const p = pmf.pAt(value);
4515
4776
  if (p > 0) {
4516
4777
  const newMax = Math.max(currentMax, value);
4517
- dfs(rollsLeft - 1, newMax, probability * p);
4778
+ dfs2(rollsLeft - 1, newMax, probability * p);
4518
4779
  }
4519
4780
  }
4520
4781
  };
4521
- dfs(count, -Infinity, 1);
4782
+ dfs2(count, -Infinity, 1);
4522
4783
  } else {
4523
4784
  const sortedSupport = [...support].sort((a, b) => a - b);
4785
+ let runningCdf = 0;
4524
4786
  for (const value of sortedSupport) {
4525
- const cdfAtValue = pmf.cdfAt(value);
4526
- const cdfAtValueMinus1 = value > sortedSupport[0] ? pmf.cdfAt(value - 1) : 0;
4527
- const probMax = Math.pow(cdfAtValue, count) - Math.pow(cdfAtValueMinus1, count);
4787
+ const prevCdf = runningCdf;
4788
+ runningCdf += pmf.pAt(value);
4789
+ const probMax = Math.pow(runningCdf, count) - Math.pow(prevCdf, count);
4528
4790
  if (probMax > eps) {
4529
4791
  out.set(value, probMax);
4530
4792
  }
@@ -4551,7 +4813,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
4551
4813
  }
4552
4814
  }
4553
4815
  let state = /* @__PURE__ */ new Map();
4554
- const keyOf = (used, r) => `${used}|${r}`;
4816
+ const stride = total + 1;
4817
+ const keyOf = (used, r) => used * stride + r;
4555
4818
  state.set(keyOf(0, total), /* @__PURE__ */ new Map([[0, 1]]));
4556
4819
  const valuesDesc = highest ? [...sortedSupport].sort((a, b) => b - a) : [...sortedSupport].sort((a, b) => a - b);
4557
4820
  const binomPMF = (r, p) => {
@@ -4600,9 +4863,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
4600
4863
  const pCond = Math.min(1, p / q);
4601
4864
  const next = /* @__PURE__ */ new Map();
4602
4865
  for (const [k, m] of state) {
4603
- const [usedStr, rStr] = k.split("|");
4604
- const used = parseInt(usedStr, 10);
4605
- const r = parseInt(rStr, 10);
4866
+ const used = Math.floor(k / stride);
4867
+ const r = k - used * stride;
4606
4868
  if (r === 0) {
4607
4869
  const destKey = keyOf(used, 0);
4608
4870
  const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
@@ -4773,8 +5035,8 @@ var AttackBuilder = class _AttackBuilder {
4773
5035
  const bonusPMF2 = bonusDicePMFs2.length ? PMF.convolveMany(bonusDicePMFs2, eps) : PMF.delta(0, eps);
4774
5036
  let pcrit2 = 0;
4775
5037
  let pmiss2 = 0;
4776
- for (const [r, rec] of d202) {
4777
- const pr = typeof rec === "number" ? rec : rec.p;
5038
+ for (const [r, bin] of d202) {
5039
+ const pr = bin.p;
4778
5040
  if (pr <= 0) continue;
4779
5041
  if (r === 1) {
4780
5042
  pmiss2 += pr;
@@ -4789,8 +5051,8 @@ var AttackBuilder = class _AttackBuilder {
4789
5051
  }
4790
5052
  if (check instanceof AlwaysHitBuilder) {
4791
5053
  let pCrit = 0;
4792
- for (const [r, rec] of d202) {
4793
- const pr = typeof rec === "number" ? rec : rec.p;
5054
+ for (const [r, bin] of d202) {
5055
+ const pr = bin.p;
4794
5056
  if (pr <= 0) continue;
4795
5057
  if (r >= critThreshold) pCrit += pr;
4796
5058
  }
@@ -4805,8 +5067,8 @@ var AttackBuilder = class _AttackBuilder {
4805
5067
  let pcrit = 0;
4806
5068
  let phit = 0;
4807
5069
  let pmiss = 0;
4808
- for (const [r, rec] of d202) {
4809
- const pr = typeof rec === "number" ? rec : rec.p;
5070
+ for (const [r, bin] of d202) {
5071
+ const pr = bin.p;
4810
5072
  if (pr <= 0) continue;
4811
5073
  if (r === 1) {
4812
5074
  pmiss += pr;
@@ -4886,7 +5148,6 @@ var AttackBuilder = class _AttackBuilder {
4886
5148
  var ACBuilder = class _ACBuilder extends RollBuilder {
4887
5149
  constructor(baseRoll, ac, attackConfig) {
4888
5150
  super(baseRoll.getSubRollConfigs());
4889
- __publicField(this, "attackConfig");
4890
5151
  if (attackConfig) {
4891
5152
  this.attackConfig = { ...attackConfig, ac };
4892
5153
  } else {
@@ -5015,8 +5276,8 @@ function resolveProbabilities(check) {
5015
5276
  const baseReroll = check.baseReroll;
5016
5277
  const die = d20RollPMF(d20Type, baseReroll > 0);
5017
5278
  const faceP = /* @__PURE__ */ new Map();
5018
- for (const [r, rec] of die) {
5019
- const pr = typeof rec === "number" ? rec : rec.p;
5279
+ for (const [r, bin] of die) {
5280
+ const pr = bin.p;
5020
5281
  if (pr > 0) faceP.set(r, pr);
5021
5282
  }
5022
5283
  const eps = 0;
@@ -5037,7 +5298,6 @@ function resolveProbabilities(check) {
5037
5298
  var DCBuilder = class _DCBuilder extends RollBuilder {
5038
5299
  constructor(baseRoll, saveConfig) {
5039
5300
  super(baseRoll.getSubRollConfigs());
5040
- __publicField(this, "saveConfig");
5041
5301
  this.saveConfig = saveConfig ? { ...saveConfig } : { dc: 10 };
5042
5302
  }
5043
5303
  dc(saveDC) {
@@ -5086,8 +5346,8 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5086
5346
  );
5087
5347
  const bonusPMF = bonusDicePMFs.length ? PMF.convolveMany(bonusDicePMFs, eps) : PMF.delta(0, eps);
5088
5348
  let psuccess = 0;
5089
- for (const [r, rec] of d202) {
5090
- const pr = typeof rec === "number" ? rec : rec.p;
5349
+ for (const [r, bin] of d202) {
5350
+ const pr = bin.p;
5091
5351
  if (pr <= 0) continue;
5092
5352
  const need = saveDC - staticMod - r;
5093
5353
  psuccess += pr * bonusPMF.tailProbGE(need);