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