@yipe/dice 0.9.0 → 0.11.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.
Files changed (41) hide show
  1. package/dist/builder/ac.d.ts.map +1 -1
  2. package/dist/builder/ast.d.ts +32 -2
  3. package/dist/builder/ast.d.ts.map +1 -1
  4. package/dist/builder/attack.d.ts +19 -0
  5. package/dist/builder/attack.d.ts.map +1 -1
  6. package/dist/builder/dc.d.ts.map +1 -1
  7. package/dist/builder/example.d.ts +4 -4
  8. package/dist/builder/example.d.ts.map +1 -1
  9. package/dist/builder/index.cjs +614 -121
  10. package/dist/builder/index.cjs.map +1 -1
  11. package/dist/builder/index.js +614 -122
  12. package/dist/builder/index.js.map +1 -1
  13. package/dist/builder/nodes.d.ts +1 -0
  14. package/dist/builder/nodes.d.ts.map +1 -1
  15. package/dist/builder/roll.d.ts +13 -1
  16. package/dist/builder/roll.d.ts.map +1 -1
  17. package/dist/builder/save.d.ts.map +1 -1
  18. package/dist/builder/types.d.ts +1 -0
  19. package/dist/builder/types.d.ts.map +1 -1
  20. package/dist/common/bounce.d.ts +45 -0
  21. package/dist/common/bounce.d.ts.map +1 -1
  22. package/dist/common/types.d.ts +33 -0
  23. package/dist/common/types.d.ts.map +1 -1
  24. package/dist/index.cjs +235 -15
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +232 -16
  27. package/dist/index.js.map +1 -1
  28. package/dist/parser/dice.d.ts +14 -0
  29. package/dist/parser/dice.d.ts.map +1 -1
  30. package/dist/pmf/pmf.d.ts +22 -4
  31. package/dist/pmf/pmf.d.ts.map +1 -1
  32. package/dist/turn/plan.d.ts +29 -6
  33. package/dist/turn/plan.d.ts.map +1 -1
  34. package/dist/turn/state.d.ts +16 -8
  35. package/dist/turn/state.d.ts.map +1 -1
  36. package/dist/turn/turn.d.ts +35 -0
  37. package/dist/turn/turn.d.ts.map +1 -1
  38. package/dist/turn/types.d.ts +32 -10
  39. package/dist/turn/types.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/CHANGELOG.md +0 -456
@@ -1474,7 +1474,7 @@ var _PMF = class _PMF {
1474
1474
  const id = this.identifier;
1475
1475
  let key = `${id}`;
1476
1476
  for (let i = 1; i < n; i++) key += `+${id}`;
1477
- return `${key}@${eps}`;
1477
+ return `${key}@${eps}|${this.fingerprint()}`;
1478
1478
  }
1479
1479
  /**
1480
1480
  * Efficiently computes this PMF convolved with itself `n` times.
@@ -1786,6 +1786,30 @@ var _PMF = class _PMF {
1786
1786
  `freq(${this.identifier},${freq})`
1787
1787
  );
1788
1788
  }
1789
+ /**
1790
+ * Splits this PMF into two complementary PMFs by an arbitrary per-damage-value factor in
1791
+ * `[0, 1]` — bin `d`'s mass, `count`, and `attr` split `factor(d)` / `1 - factor(d)` between the
1792
+ * two results (via the same proportional scaling {@link applyHitFrequency} uses, `scaleBin`), so
1793
+ * `a.add(b)` recovers this PMF exactly and both halves stay chart-attributable. Unlike
1794
+ * {@link applyHitFrequency}, mass is NOT redistributed to a miss bin at 0 — each bin stays at its
1795
+ * own damage value in whichever half it lands in. `factor` outside `[0, 1]` is clamped.
1796
+ *
1797
+ * Built for `dice-match` trigger slicing: splitting a hit/crit sub-PMF into "matched" and
1798
+ * "did not match" halves by the exact per-damage-value match probability.
1799
+ */
1800
+ splitByFactor(factor) {
1801
+ const a = /* @__PURE__ */ new Map();
1802
+ const b = /* @__PURE__ */ new Map();
1803
+ for (const [damage, bin] of this.map) {
1804
+ const f = Math.min(1, Math.max(0, factor(damage)));
1805
+ if (f > 0) a.set(damage, _PMF.scaleBin(bin, f));
1806
+ if (f < 1) b.set(damage, _PMF.scaleBin(bin, 1 - f));
1807
+ }
1808
+ return [
1809
+ new _PMF(a, this.epsilon, false, `split+(${this.identifier})`),
1810
+ new _PMF(b, this.epsilon, false, `split-(${this.identifier})`)
1811
+ ];
1812
+ }
1789
1813
  scaleMass(factor) {
1790
1814
  if (factor === 1) return this;
1791
1815
  const scaledMap = /* @__PURE__ */ new Map();
@@ -1825,16 +1849,27 @@ var _PMF = class _PMF {
1825
1849
  return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
1826
1850
  }
1827
1851
  /**
1828
- * A small content fingerprint (mass + bin count + face sum) so convolution
1829
- * cache keys change if the underlying numbers do. Memoized because a PMF is
1830
- * immutable once constructed — this avoids re-summing every key on each
1831
- * convolve() call (including cache hits).
1852
+ * A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus
1853
+ * the `normalized` flag, so convolution/power cache keys change whenever the underlying
1854
+ * numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can
1855
+ * keep the same identifier, support, mass, and face sum while differing in per-bin
1856
+ * probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate --
1857
+ * that previously let `power()` return one PMF's cached result for a different PMF. Memoized
1858
+ * because a PMF is immutable once constructed -- this avoids re-deriving the key on every
1859
+ * convolve()/power() call (including cache hits). Bin order is sorted by damage value (and
1860
+ * label keys sorted within each bin) so two equal-content PMFs built via different code paths
1861
+ * fingerprint identically regardless of Map insertion order.
1832
1862
  */
1833
1863
  fingerprint() {
1834
1864
  if (this._fingerprint === void 0) {
1835
- let faceSum = 0;
1836
- for (const k of this.map.keys()) faceSum += k;
1837
- this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
1865
+ const bins = [...this.map.entries()].sort((a, b) => a[0] - b[0]);
1866
+ const parts = [];
1867
+ for (const [damageValue, bin] of bins) {
1868
+ const countStr = Object.keys(bin.count).sort().map((k) => `${k}:${bin.count[k]}`).join(",");
1869
+ const attrStr = bin.attr ? Object.keys(bin.attr).sort().map((k) => `${k}:${bin.attr[k]}`).join(",") : "";
1870
+ parts.push(`${damageValue}:${bin.p}[${countStr}]{${attrStr}}`);
1871
+ }
1872
+ this._fingerprint = `${this.normalized ? 1 : 0}|${parts.join(";")}`;
1838
1873
  }
1839
1874
  return this._fingerprint;
1840
1875
  }
@@ -2066,11 +2101,13 @@ var _PMF = class _PMF {
2066
2101
  /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
2067
2102
  quantile(p) {
2068
2103
  if (this.map.size === 0) return 0;
2104
+ const totalMass = this.mass();
2105
+ if (totalMass <= 0) return 0;
2069
2106
  const s = this.support().sort((a, b) => a - b);
2070
2107
  let acc = 0;
2071
2108
  for (const x of s) {
2072
2109
  acc += this.pAt(x);
2073
- if (acc >= p) return x;
2110
+ if (acc / totalMass >= p) return x;
2074
2111
  }
2075
2112
  return s[s.length - 1];
2076
2113
  }
@@ -2459,6 +2496,84 @@ var _PMF = class _PMF {
2459
2496
  _PMF.__anonIdCounter = 1;
2460
2497
  var PMF = _PMF;
2461
2498
 
2499
+ // src/common/bounce.ts
2500
+ function faceWeights(faces, minimum = 0, reroll = 0) {
2501
+ const f = Math.max(0, Math.floor(faces));
2502
+ if (f <= 0) return [];
2503
+ let weights = new Array(f).fill(1 / f);
2504
+ const r = Math.max(0, Math.min(Math.floor(reroll), f));
2505
+ if (r > 0) {
2506
+ const rerollMass = r / f;
2507
+ const uniformReroll = rerollMass / f;
2508
+ weights = weights.map((_, i) => (i < r ? 0 : 1 / f) + uniformReroll);
2509
+ }
2510
+ const minV = Math.max(0, Math.floor(minimum));
2511
+ if (minV > 1) {
2512
+ const collapsed = new Array(f).fill(0);
2513
+ for (let v = 1; v <= f; v++) {
2514
+ const target = Math.min(f, Math.max(v, minV));
2515
+ collapsed[target - 1] += weights[v - 1];
2516
+ }
2517
+ weights = collapsed;
2518
+ }
2519
+ return weights;
2520
+ }
2521
+ function diceSumDistribution(dice, weights) {
2522
+ let dist = /* @__PURE__ */ new Map([[0, 1]]);
2523
+ for (let die = 0; die < dice; die++) {
2524
+ const next = /* @__PURE__ */ new Map();
2525
+ for (const [sum, mass] of dist) {
2526
+ for (let face = 1; face <= weights.length; face++) {
2527
+ const w = weights[face - 1] ?? 0;
2528
+ if (w <= 0) continue;
2529
+ const s = sum + face;
2530
+ next.set(s, (next.get(s) ?? 0) + mass * w);
2531
+ }
2532
+ }
2533
+ dist = next;
2534
+ }
2535
+ return dist;
2536
+ }
2537
+ function sumAllDistinctDistribution(dice, weights) {
2538
+ const faceCount = weights.length;
2539
+ let dp = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, 1]])]]);
2540
+ for (let face = 1; face <= faceCount; face++) {
2541
+ const w = weights[face - 1] ?? 0;
2542
+ const next = /* @__PURE__ */ new Map();
2543
+ for (const [count, sumMap] of dp) next.set(count, new Map(sumMap));
2544
+ if (w > 0) {
2545
+ for (const [count, sumMap] of dp) {
2546
+ const nextCount = count + 1;
2547
+ if (nextCount > dice) continue;
2548
+ const target = next.get(nextCount) ?? /* @__PURE__ */ new Map();
2549
+ for (const [sum, mass] of sumMap) {
2550
+ const s = sum + face;
2551
+ target.set(s, (target.get(s) ?? 0) + mass * w);
2552
+ }
2553
+ next.set(nextCount, target);
2554
+ }
2555
+ }
2556
+ dp = next;
2557
+ }
2558
+ let factorial = 1;
2559
+ for (let i = 2; i <= dice; i++) factorial *= i;
2560
+ const chosen = dp.get(dice) ?? /* @__PURE__ */ new Map();
2561
+ const result = /* @__PURE__ */ new Map();
2562
+ for (const [sum, mass] of chosen) result.set(sum, mass * factorial);
2563
+ return result;
2564
+ }
2565
+ function jointSumAndMatch(dice, weights) {
2566
+ if (dice <= 1) return /* @__PURE__ */ new Map();
2567
+ const total = diceSumDistribution(dice, weights);
2568
+ const distinct = sumAllDistinctDistribution(dice, weights);
2569
+ const result = /* @__PURE__ */ new Map();
2570
+ for (const [sum, mass] of total) {
2571
+ const matchMass = Math.max(0, mass - (distinct.get(sum) ?? 0));
2572
+ if (matchMass > 0) result.set(sum, matchMass);
2573
+ }
2574
+ return result;
2575
+ }
2576
+
2462
2577
  // src/pmf/mixture.ts
2463
2578
  var Mixture = class _Mixture {
2464
2579
  constructor(eps = EPS) {
@@ -3049,6 +3164,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
3049
3164
  finalResult = finalResult.combine(dice);
3050
3165
  return { newNorm: currentNorm * normValue, updatedResult: finalResult };
3051
3166
  }
3167
+ function subtractCounts(a, b) {
3168
+ const result = new Dice();
3169
+ for (const [key, value] of a.getFaceEntries()) result.increment(key, value);
3170
+ for (const [key, value] of b.getFaceEntries()) result.increment(key, -value);
3171
+ return result;
3172
+ }
3052
3173
  function parseExpression(arr, n) {
3053
3174
  const result = (() => {
3054
3175
  const res = parseArgument(arr, n);
@@ -3056,8 +3177,29 @@ function parseExpression(arr, n) {
3056
3177
  })();
3057
3178
  let op = parseOperation(arr);
3058
3179
  let finalResult = result;
3180
+ let baseDieMeta = result.privateData?.checkDie && !result.privateData.checkDie.rerollOne ? result.privateData.checkDie : void 0;
3181
+ let bonusOnly = Dice.scalar(0);
3059
3182
  while (op != null) {
3060
3183
  const arg = !op.unary ? parseArgument(arr, n) : finalResult;
3184
+ let acAlreadyApplied = false;
3185
+ if (baseDieMeta) {
3186
+ if (op === Dice.prototype.addNonZero) {
3187
+ bonusOnly = bonusOnly.add(arg);
3188
+ } else if (op === Dice.prototype.subtract) {
3189
+ bonusOnly = bonusOnly.subtract(arg);
3190
+ } else if (op === Dice.prototype.ac && typeof arg === "number") {
3191
+ const natMaxSlice = bonusOnly.add(baseDieMeta.sides);
3192
+ const restSlice = subtractCounts(finalResult, natMaxSlice);
3193
+ const gatedNatMaxSlice = natMaxSlice.ac(arg);
3194
+ finalResult = restSlice.ac(arg).combine(gatedNatMaxSlice);
3195
+ finalResult.privateData.checkDie = baseDieMeta;
3196
+ finalResult.privateData.natMaxCritSlice = gatedNatMaxSlice;
3197
+ acAlreadyApplied = true;
3198
+ baseDieMeta = void 0;
3199
+ } else {
3200
+ baseDieMeta = void 0;
3201
+ }
3202
+ }
3061
3203
  let crit;
3062
3204
  let critNorm = 1;
3063
3205
  if (arr[0] === "x" || arr[0] === "c") {
@@ -3068,11 +3210,17 @@ function parseExpression(arr, n) {
3068
3210
  assertToken(arr, "i");
3069
3211
  assertToken(arr, "t");
3070
3212
  const count = isXcrit ? parseNumber(arr, n) : 1;
3071
- crit = new Dice();
3072
- for (let i = 0; i < count; i++) {
3073
- const max = finalResult.maxFace();
3074
- crit.setFace(max, finalResult.get(max));
3075
- finalResult = finalResult.deleteFace(max);
3213
+ const trackedCritSlice = finalResult.privateData?.natMaxCritSlice;
3214
+ if (count === 1 && trackedCritSlice) {
3215
+ crit = trackedCritSlice;
3216
+ finalResult = subtractCounts(finalResult, trackedCritSlice);
3217
+ } else {
3218
+ crit = new Dice();
3219
+ for (let i = 0; i < count; i++) {
3220
+ const max = finalResult.maxFace();
3221
+ crit.setFace(max, finalResult.get(max));
3222
+ finalResult = finalResult.deleteFace(max);
3223
+ }
3076
3224
  }
3077
3225
  critNorm = crit.total();
3078
3226
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
@@ -3123,7 +3271,9 @@ function parseExpression(arr, n) {
3123
3271
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3124
3272
  }
3125
3273
  let norm = finalResult.total();
3126
- finalResult = op.call(finalResult, arg);
3274
+ if (!acAlreadyApplied) {
3275
+ finalResult = op.call(finalResult, arg);
3276
+ }
3127
3277
  norm = norm ? finalResult.total() / norm : 1;
3128
3278
  if (crit) {
3129
3279
  const result2 = combineDiceWithNormalization(
@@ -3314,6 +3464,7 @@ function parseDice(s, n) {
3314
3464
  if (rerollOne) {
3315
3465
  result = result.reroll(1);
3316
3466
  }
3467
+ result.privateData.checkDie = { sides, rerollOne };
3317
3468
  return result;
3318
3469
  }
3319
3470
  function peek(arr, expected) {
@@ -3505,6 +3656,12 @@ function d20PMF(rerollOne) {
3505
3656
  }
3506
3657
 
3507
3658
  // src/builder/roll.ts
3659
+ function validateScaleInt(scale) {
3660
+ const scaleInt = Math.floor(scale);
3661
+ if (scaleInt !== scale) throw new Error("Scale must be an integer");
3662
+ if (scaleInt <= 0) throw new Error("Scale must be > 0");
3663
+ return scaleInt;
3664
+ }
3508
3665
  var rollPMFCache = new LRUCache(4e3);
3509
3666
  function clearRollCache() {
3510
3667
  rollPMFCache.clear();
@@ -3515,16 +3672,17 @@ var defaultConfig = {
3515
3672
  modifier: 0,
3516
3673
  reroll: 0,
3517
3674
  explode: 0,
3675
+ explodePoolBudget: 0,
3518
3676
  minimum: 0,
3519
3677
  bestOf: 0,
3520
3678
  keep: void 0,
3521
3679
  rollType: "flat"
3522
3680
  };
3523
3681
  var rollConfigsEqual = (a, b) => {
3524
- return a.count === b.count && a.sides === b.sides && a.modifier === b.modifier && a.reroll === b.reroll && a.explode === b.explode && a.minimum === b.minimum && a.bestOf === b.bestOf && a.keep === b.keep && a.rollType === b.rollType;
3682
+ return a.count === b.count && a.sides === b.sides && a.modifier === b.modifier && a.reroll === b.reroll && a.explode === b.explode && a.explodePoolBudget === b.explodePoolBudget && a.minimum === b.minimum && a.bestOf === b.bestOf && a.keep === b.keep && a.rollType === b.rollType;
3525
3683
  };
3526
3684
  var configComplexityScore = (config) => {
3527
- return (config.reroll > 0 ? 1 : 0) + (config.explode > 0 ? 1 : 0) + (config.minimum > 0 ? 1 : 0) + (config.bestOf > 0 ? 1 : 0) + (config.keep !== void 0 ? 1 : 0) + (config.rollType !== "flat" ? 1 : 0);
3685
+ return (config.reroll > 0 ? 1 : 0) + (config.explode > 0 ? 1 : 0) + (config.explodePoolBudget > 0 ? 1 : 0) + (config.minimum > 0 ? 1 : 0) + (config.bestOf > 0 ? 1 : 0) + (config.keep !== void 0 ? 1 : 0) + (config.rollType !== "flat" ? 1 : 0);
3528
3686
  };
3529
3687
  var RollBuilder = class _RollBuilder {
3530
3688
  constructor(countOrConfigs = 1) {
@@ -3706,11 +3864,37 @@ var RollBuilder = class _RollBuilder {
3706
3864
  if (count === void 0) return this;
3707
3865
  if (count === 0) return this;
3708
3866
  if (count < 0) throw new Error("Explode count must be >= 0");
3867
+ if (this.lastConfig.explodePoolBudget > 0) {
3868
+ throw new Error(
3869
+ "Cannot set explode() on a config that already has a pool-wide explodePool() budget \u2014 the two exploding-dice semantics (per-die vs pool-wide) are mutually exclusive on one config."
3870
+ );
3871
+ }
3709
3872
  const newConfigs = this.getSubRollConfigs();
3710
3873
  newConfigs[newConfigs.length - 1].explode = count;
3711
3874
  return this.create(newConfigs);
3712
3875
  }
3713
- /** Apply per-die minimum value (min > 0). */
3876
+ /**
3877
+ * Set a pool-wide exploding-dice budget: at most `budget` extra dice may be added across the
3878
+ * WHOLE pool (shared), as opposed to {@link explode}'s per-die cap (`n` dice each individually
3879
+ * allowed up to `explode(k)` extra dice). `budget` must be a finite non-negative integer —
3880
+ * unlike `explode()`, `Infinity` is not accepted (it would make the pool-wide DP non-terminating).
3881
+ */
3882
+ explodePool(budget) {
3883
+ if (isNaN(budget)) throw new Error("Invalid NaN value for explodePool budget");
3884
+ if (!Number.isFinite(budget)) throw new Error("explodePool budget must be finite");
3885
+ if (budget < 0) throw new Error("explodePool budget must be >= 0");
3886
+ if (budget === 0) return this;
3887
+ if (this.lastConfig.explode > 0) {
3888
+ throw new Error(
3889
+ "Cannot set explodePool() on a config that already has a per-die explode() cap \u2014 the two exploding-dice semantics (per-die vs pool-wide) are mutually exclusive on one config."
3890
+ );
3891
+ }
3892
+ const newConfigs = this.getSubRollConfigs();
3893
+ newConfigs[newConfigs.length - 1].explodePoolBudget = Math.floor(budget);
3894
+ return this.create(newConfigs);
3895
+ }
3896
+ /** Apply per-die minimum value (floors each die roll at `val`, e.g. `minimum(3)` treats a 1 or
3897
+ * 2 as a 3 -- the 2024 Great Weapon Fighting style). */
3714
3898
  minimum(val) {
3715
3899
  if (val !== void 0 && isNaN(val))
3716
3900
  throw new Error("Invalid NaN value for minimum");
@@ -3718,7 +3902,7 @@ var RollBuilder = class _RollBuilder {
3718
3902
  if (val === 0) return this;
3719
3903
  if (val < 0) throw new Error("Minimum value must be >= 0");
3720
3904
  const newConfigs = this.getSubRollConfigs();
3721
- newConfigs[newConfigs.length - 1].minimum = val + 1;
3905
+ newConfigs[newConfigs.length - 1].minimum = val;
3722
3906
  return this.create(newConfigs);
3723
3907
  }
3724
3908
  bestOf(count) {
@@ -3819,9 +4003,7 @@ var RollBuilder = class _RollBuilder {
3819
4003
  return this.create(configs);
3820
4004
  }
3821
4005
  scaleDice(scale) {
3822
- const scaleInt = Math.floor(scale);
3823
- if (scaleInt !== scale) throw new Error("Scale must be an integer");
3824
- if (scaleInt <= 0) throw new Error("Scale must be > 0");
4006
+ const scaleInt = validateScaleInt(scale);
3825
4007
  const newConfigs = this.getSubRollConfigs().map((config) => {
3826
4008
  if (!config.sides || config.sides <= 0) return config;
3827
4009
  return { ...config, count: config.count * scaleInt };
@@ -3944,12 +4126,21 @@ var RollBuilder = class _RollBuilder {
3944
4126
  }
3945
4127
  configToSingleExpressionWithoutModifier(config, isRootDie) {
3946
4128
  if (!config.sides || config.sides <= 0) return "";
4129
+ if (config.explode && Number.isFinite(config.explode) && config.explode > 0) {
4130
+ throw new Error(
4131
+ `toExpression() cannot represent an exploding die (d${config.sides} explode(${config.explode})): the string grammar has no explode syntax. Use the builder's own PMF (.toPMF()/.pmf) instead of round-tripping through toExpression()/parse().`
4132
+ );
4133
+ }
4134
+ if (config.explodePoolBudget && config.explodePoolBudget > 0) {
4135
+ throw new Error(
4136
+ `toExpression() cannot represent a pool-wide exploding-dice budget (d${config.sides} explodePool(${config.explodePoolBudget})): the string grammar has no explode syntax. Use the builder's own PMF (.toPMF()/.pmf) instead of round-tripping through toExpression()/parse().`
4137
+ );
4138
+ }
3947
4139
  let baseDie = `d${config.sides}`;
4140
+ const rerollClause = config.reroll > 0 ? config.reroll === 1 ? " reroll 1" : ` reroll d${config.reroll}` : "";
3948
4141
  if (config.reroll > 0) {
3949
- if (config.minimum > 0 && config.explode > 0) ; else if (config.minimum > 0) {
3950
- for (let i = config.reroll; i >= 1; i--) baseDie += ` reroll ${i}`;
3951
- } else {
3952
- for (let i = 1; i <= config.reroll; i++) baseDie += ` reroll ${i}`;
4142
+ if (config.minimum > 0 && config.explode > 0) ; else {
4143
+ baseDie += rerollClause;
3953
4144
  }
3954
4145
  }
3955
4146
  if (config.minimum > 0) {
@@ -3959,9 +4150,7 @@ var RollBuilder = class _RollBuilder {
3959
4150
  baseDie = `${config.minimum}>${baseDie}`;
3960
4151
  }
3961
4152
  if (config.reroll > 0 && config.explode > 0) {
3962
- for (let i = 1; i <= config.reroll; i++) {
3963
- baseDie += ` reroll ${i}`;
3964
- }
4153
+ baseDie += rerollClause;
3965
4154
  }
3966
4155
  }
3967
4156
  if (baseDie === "d20 reroll 1" && config.minimum <= 1) baseDie = "hd20";
@@ -3979,10 +4168,14 @@ var RollBuilder = class _RollBuilder {
3979
4168
  case "flat":
3980
4169
  if (config.keep) {
3981
4170
  const mode = config.keep.mode === "highest" ? "kh" : "kl";
4171
+ const baseCount = Math.max(1, Math.floor(Math.abs(config.count || 1)));
4172
+ const trials = Math.max(1, Math.floor(config.keep.total));
4173
+ const isMaxOfShape = config.keep.count === 1 && config.keep.mode === "highest";
4174
+ const innerCount = trials === baseCount && !isMaxOfShape ? 1 : baseCount;
3982
4175
  const baseDieExpression = this.configToSingleExpressionWithoutModifier(
3983
4176
  {
3984
4177
  ...config,
3985
- count: config.count,
4178
+ count: innerCount,
3986
4179
  modifier: 0,
3987
4180
  rollType: "flat",
3988
4181
  keep: void 0
@@ -4014,7 +4207,19 @@ var RollBuilder = class _RollBuilder {
4014
4207
  }
4015
4208
  }
4016
4209
  if (config.bestOf && config.count && config.bestOf < config.count) {
4017
- mainExpression += `kh${config.bestOf}`;
4210
+ const pool = Math.max(1, Math.floor(Math.abs(config.count)));
4211
+ const baseDieExpression = this.configToSingleExpressionWithoutModifier(
4212
+ {
4213
+ ...config,
4214
+ count: 1,
4215
+ modifier: 0,
4216
+ bestOf: 0,
4217
+ keep: void 0,
4218
+ rollType: "flat"
4219
+ },
4220
+ false
4221
+ );
4222
+ mainExpression = `${pool}kh${Math.floor(config.bestOf)}(${baseDieExpression})`;
4018
4223
  }
4019
4224
  break;
4020
4225
  }
@@ -4117,6 +4322,12 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
4117
4322
  toPMF(eps = 0) {
4118
4323
  return pmfFromRollBuilder(this, eps);
4119
4324
  }
4325
+ // Scale the dice, keep the same // 2 (half) transform applied on top -- delegating to the
4326
+ // base class's `create()`-based scaleDice would drop the halving entirely, e.g. a doubled-dice
4327
+ // crit on a resisted hit payload silently losing the resistance.
4328
+ scaleDice(scale) {
4329
+ return new _HalfRollBuilder(this.innerRoll.scaleDice(scale));
4330
+ }
4120
4331
  copy() {
4121
4332
  return new _HalfRollBuilder(this.innerRoll.copy());
4122
4333
  }
@@ -4143,9 +4354,16 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4143
4354
  }
4144
4355
  toExpression() {
4145
4356
  const inner = this.innerRoll.toExpression();
4146
- if (this.denominator === 1) return `${this.numerator} * (${inner})`;
4147
- if (this.numerator === 1) return `(${inner}) // ${this.denominator}`;
4148
- return `(${inner}) * ${this.numerator} // ${this.denominator}`;
4357
+ const denominator = this.denominator === 0 ? 1 : this.denominator;
4358
+ if (denominator === 1) return `${this.numerator} ** (${inner})`;
4359
+ if (this.rounding === "round") {
4360
+ throw new Error(
4361
+ `toExpression() cannot represent scaleResult(${this.numerator}, ${this.denominator}, "round"): the string grammar has only floor (//) and ceil (/) division. Use the builder's own PMF (.toPMF()/.pmf) instead.`
4362
+ );
4363
+ }
4364
+ const div = this.rounding === "ceil" ? "/" : "//";
4365
+ if (this.numerator === 1) return `(${inner}) ${div} ${denominator}`;
4366
+ return `(${inner}) ** ${this.numerator} ${div} ${denominator}`;
4149
4367
  }
4150
4368
  toAST() {
4151
4369
  return {
@@ -4159,6 +4377,17 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4159
4377
  toPMF(eps = 0) {
4160
4378
  return pmfFromRollBuilder(this, eps);
4161
4379
  }
4380
+ // Scale the dice, keep the same numerator/denominator/rounding transform applied on top --
4381
+ // delegating to the base class's `create()`-based scaleDice would drop the scale entirely,
4382
+ // e.g. a doubled-dice crit on a vulnerable hit payload silently losing the vulnerability.
4383
+ scaleDice(scale) {
4384
+ return new _ScaleRollBuilder(
4385
+ this.innerRoll.scaleDice(scale),
4386
+ this.numerator,
4387
+ this.denominator,
4388
+ this.rounding
4389
+ );
4390
+ }
4162
4391
  copy() {
4163
4392
  return new _ScaleRollBuilder(
4164
4393
  this.innerRoll.copy(),
@@ -4231,6 +4460,18 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
4231
4460
  toPMF(eps = 0) {
4232
4461
  return pmfFromRollBuilder(this, eps);
4233
4462
  }
4463
+ // Scale the dice INSIDE each trial (e.g. maxOf(2, 1d12) -> maxOf(2, 2d12)), keeping the same
4464
+ // trial count -- delegating to the base class's `create()`-based scaleDice would collapse
4465
+ // straight to plain dice, losing the "take the highest of N trials" semantics entirely.
4466
+ scaleDice(scale) {
4467
+ const scaleInt = validateScaleInt(scale);
4468
+ return new _MaxOfRollBuilder(
4469
+ this.innerRoll.scaleDice(scaleInt),
4470
+ this.count,
4471
+ this.diceCount ? this.diceCount * scaleInt : void 0,
4472
+ this.diceSides
4473
+ );
4474
+ }
4234
4475
  copy() {
4235
4476
  return new _MaxOfRollBuilder(this.innerRoll.copy(), this.count);
4236
4477
  }
@@ -4277,9 +4518,7 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
4277
4518
  return new RollBuilder(configs).toExpression();
4278
4519
  }
4279
4520
  toPMF() {
4280
- const rollType = this.rollType;
4281
- const rerollOne = this.baseReroll > 0;
4282
- return d20RollPMF(rollType, rerollOne);
4521
+ return resolveRootD20(this);
4283
4522
  }
4284
4523
  copy() {
4285
4524
  const baseCopy = new RollBuilder(this.getSubRollConfigs());
@@ -4327,9 +4566,7 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4327
4566
  return new RollBuilder(configs).toExpression();
4328
4567
  }
4329
4568
  toPMF() {
4330
- const rollType = this.rollType;
4331
- const rerollOne = this.baseReroll > 0;
4332
- return d20RollPMF(rollType, rerollOne);
4569
+ return resolveRootD20(this);
4333
4570
  }
4334
4571
  copy() {
4335
4572
  const baseCopy = new RollBuilder(this.getSubRollConfigs());
@@ -4397,6 +4634,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
4397
4634
  explode(_count = Infinity) {
4398
4635
  throw new Error("Cannot set explode on a pooled roll.");
4399
4636
  }
4637
+ explodePool(_budget) {
4638
+ throw new Error("Cannot set explodePool on a pooled roll.");
4639
+ }
4400
4640
  minimum(_val) {
4401
4641
  throw new Error("Cannot set minimum on a pooled roll.");
4402
4642
  }
@@ -4519,6 +4759,17 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
4519
4759
  toPMF(eps = 0) {
4520
4760
  return pmfFromRollBuilder(this, eps);
4521
4761
  }
4762
+ // Scaling a composite (mixed damage types, e.g. base + resisted) must scale each PART's own
4763
+ // dice while preserving its own half/scale wrapper -- delegating to the base class's
4764
+ // `create()`-based scaleDice would lose every part's transform, collapsing straight to plain
4765
+ // dice. This is what auto-crit doubling (attack.ts's `hitEffect.copy().doubleDice()`) relies
4766
+ // on for a mixed-resistance hit payload.
4767
+ scaleDice(scale) {
4768
+ validateScaleInt(scale);
4769
+ return new _CompositeSumRollBuilder(
4770
+ this.parts.map((p) => p.scaleDice(scale))
4771
+ );
4772
+ }
4522
4773
  copy() {
4523
4774
  return new _CompositeSumRollBuilder(this.parts.map((p) => p.copy()));
4524
4775
  }
@@ -4596,6 +4847,15 @@ var builderPMFCache = new LRUCache(1e3);
4596
4847
  // src/builder/ast.ts
4597
4848
  var defaultEps = 0;
4598
4849
  var singleDiePMFCache = new LRUCache(1e3);
4850
+ function dieNodeFromConfig(cfg) {
4851
+ return {
4852
+ type: "die",
4853
+ sides: cfg.sides,
4854
+ reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
4855
+ minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
4856
+ explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
4857
+ };
4858
+ }
4599
4859
  function astFromRollConfigs(configs) {
4600
4860
  if (!configs || configs.length === 0) return void 0;
4601
4861
  const children = [];
@@ -4605,13 +4865,9 @@ function astFromRollConfigs(configs) {
4605
4865
  const count = Math.abs(cfg.count || 0);
4606
4866
  constantSum += cfg.modifier || 0;
4607
4867
  if ((cfg.sides || 0) <= 0) continue;
4608
- const die = {
4609
- type: "die",
4610
- sides: cfg.sides,
4611
- reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
4612
- minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
4613
- explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
4614
- };
4868
+ const isSynthesizedBestOf = !cfg.keep && cfg.bestOf > 0 && cfg.bestOf < count;
4869
+ const effectiveKeep = isSynthesizedBestOf ? { total: count, count: Math.floor(cfg.bestOf), mode: "highest" } : cfg.keep;
4870
+ const die = dieNodeFromConfig(cfg);
4615
4871
  let node = die;
4616
4872
  let appliedRollType = false;
4617
4873
  if (cfg.rollType && cfg.rollType !== "flat") {
@@ -4629,11 +4885,16 @@ function astFromRollConfigs(configs) {
4629
4885
  }
4630
4886
  appliedRollType = true;
4631
4887
  }
4632
- if (cfg.rollType === "flat" && cfg.keep && cfg.keep.total > 0) {
4888
+ if (cfg.rollType === "flat" && effectiveKeep && effectiveKeep.total > 0) {
4889
+ if (cfg.explodePoolBudget > 0) {
4890
+ throw new Error(
4891
+ "explodePool() cannot be combined with keep()/bestOf() on the same config \u2014 the match/keep pool is ambiguous once dice can be added mid-resolution. Use explodePool() on a plain (non-keep) pool."
4892
+ );
4893
+ }
4633
4894
  const baseCount = Math.max(1, Math.floor(Math.abs(count || 1)));
4634
- const trials = Math.max(1, Math.floor(cfg.keep.total));
4635
- const k = Math.max(0, Math.floor(cfg.keep.count));
4636
- if (k === 1 && cfg.keep.mode === "highest") {
4895
+ const trials = Math.max(1, Math.floor(effectiveKeep.total));
4896
+ const k = Math.max(0, Math.floor(effectiveKeep.count));
4897
+ if (k === 1 && effectiveKeep.mode === "highest" && !isSynthesizedBestOf) {
4637
4898
  const perTrial = {
4638
4899
  type: "sum",
4639
4900
  count: baseCount,
@@ -4652,7 +4913,7 @@ function astFromRollConfigs(configs) {
4652
4913
  const base = { type: "sum", count: trials, child: node };
4653
4914
  node = {
4654
4915
  type: "keep",
4655
- mode: cfg.keep.mode,
4916
+ mode: effectiveKeep.mode,
4656
4917
  count: k,
4657
4918
  child: base
4658
4919
  };
@@ -4672,7 +4933,7 @@ function astFromRollConfigs(configs) {
4672
4933
  };
4673
4934
  node = {
4674
4935
  type: "keep",
4675
- mode: cfg.keep.mode,
4936
+ mode: effectiveKeep.mode,
4676
4937
  count: k,
4677
4938
  child: trialPool
4678
4939
  };
@@ -4680,7 +4941,17 @@ function astFromRollConfigs(configs) {
4680
4941
  }
4681
4942
  } else {
4682
4943
  const c = appliedRollType ? 1 : Math.max(1, count || 1);
4683
- node = { type: "sum", count: c, child: node };
4944
+ if (cfg.explodePoolBudget > 0 && appliedRollType) {
4945
+ throw new Error(
4946
+ "explodePool() cannot be combined with advantage/disadvantage/elven-accuracy on the same config \u2014 pool-wide explosion is for damage dice pools, not d20 rolls."
4947
+ );
4948
+ }
4949
+ node = {
4950
+ type: "sum",
4951
+ count: c,
4952
+ child: node,
4953
+ explodePoolBudget: cfg.explodePoolBudget > 0 ? cfg.explodePoolBudget : void 0
4954
+ };
4684
4955
  }
4685
4956
  children.push({ node, sign });
4686
4957
  }
@@ -4711,6 +4982,13 @@ function resolve(node, eps = defaultEps) {
4711
4982
  const base = resolve(node.child, eps);
4712
4983
  const n = Math.max(0, Math.floor(node.count));
4713
4984
  if (n === 0) return PMF.delta(0, eps);
4985
+ if (node.explodePoolBudget && Number.isFinite(node.explodePoolBudget) && node.explodePoolBudget > 0) {
4986
+ const die = findDie(node.child);
4987
+ if (!die) {
4988
+ throw new Error("explodePool() requires the pool's child to be a plain die.");
4989
+ }
4990
+ return resolveExplodingPool(base, die.sides, n, Math.floor(node.explodePoolBudget), eps);
4991
+ }
4714
4992
  if (n === 1) return base;
4715
4993
  return base.power(n, eps);
4716
4994
  }
@@ -4746,8 +5024,8 @@ function resolve(node, eps = defaultEps) {
4746
5024
  }
4747
5025
  case "d20Roll": {
4748
5026
  const childDie = findDie(node.child);
4749
- const rerollOne = !!childDie && (childDie.reroll || 0) >= 1;
4750
- return d20RollPMF(node.rollType, rerollOne);
5027
+ if (!childDie) return d20RollPMF(node.rollType, false);
5028
+ return resolveD20Roll(childDie, node.rollType);
4751
5029
  }
4752
5030
  case "half": {
4753
5031
  const childPMF = resolve(node.child, eps);
@@ -4773,6 +5051,37 @@ function pmfFromRollBuilder(rb, eps = defaultEps) {
4773
5051
  const ast = rb.toAST();
4774
5052
  return resolve(ast, eps);
4775
5053
  }
5054
+ var d20RollLiftCache = new LRUCache(500);
5055
+ function resolveD20Roll(die, rollType) {
5056
+ const base = resolveSingleDie(die, defaultEps);
5057
+ const type = rollType || "flat";
5058
+ if (type === "flat") return base;
5059
+ const cacheKey = `${getASTSignature(die)}|${type}`;
5060
+ const cached = d20RollLiftCache.get(cacheKey);
5061
+ if (cached) return cached;
5062
+ const support = [...base.support()].sort((a, b) => a - b);
5063
+ const out = /* @__PURE__ */ new Map();
5064
+ let cum = 0;
5065
+ let prevLifted = 0;
5066
+ for (const k of support) {
5067
+ cum += base.pAt(k);
5068
+ const curLifted = type === "advantage" ? cum * cum : type === "elven accuracy" ? cum * cum * cum : 1 - (1 - cum) * (1 - cum);
5069
+ const pk = curLifted - prevLifted;
5070
+ if (pk > 0) out.set(k, pk);
5071
+ prevLifted = curLifted;
5072
+ }
5073
+ const result = PMF.fromMap(out, defaultEps);
5074
+ d20RollLiftCache.set(cacheKey, result);
5075
+ return result;
5076
+ }
5077
+ function resolveRootD20(check) {
5078
+ const rootConfig = check.getRootDieConfig();
5079
+ const rollType = check.rollType;
5080
+ if (!rootConfig || !(rootConfig.sides > 0)) {
5081
+ return d20RollPMF(rollType, check.baseReroll > 0);
5082
+ }
5083
+ return resolveD20Roll(dieNodeFromConfig(rootConfig), rollType);
5084
+ }
4776
5085
  function resolveSingleDie(die, eps = defaultEps) {
4777
5086
  const signature = getASTSignature(die);
4778
5087
  const cacheKey = `${signature}_${eps}`;
@@ -4806,25 +5115,42 @@ function resolveSingleDie(die, eps = defaultEps) {
4806
5115
  for (const v of pmf.support()) {
4807
5116
  if (v !== maxFace) nonMax.set(v, pmf.pAt(v));
4808
5117
  }
4809
- let nonMaxPMF = PMF.fromMap(nonMax, eps);
4810
- if (Math.abs(nonMaxPMF.mass() - (1 - pMax)) > eps) {
4811
- nonMaxPMF = nonMaxPMF.scaleMass(1 - pMax);
5118
+ const nonMaxPMF = PMF.fromMap(nonMax, eps);
5119
+ let chain = pmf;
5120
+ for (let remaining = 1; remaining <= times - 1; remaining++) {
5121
+ chain = PMF.branch(chain.mapDamage((v) => v + maxFace), nonMaxPMF, pMax);
4812
5122
  }
4813
- let tail = PMF.delta(0, eps);
4814
- const addOnce = pmf;
4815
- for (let t = 1; t <= times; t++) {
4816
- tail = tail.convolve(addOnce, eps);
4817
- }
4818
- const exploded = PMF.branch(
4819
- tail.mapDamage((v) => v + maxFace),
4820
- nonMaxPMF,
4821
- pMax
4822
- );
5123
+ const exploded = PMF.branch(chain.mapDamage((v) => v + maxFace), nonMaxPMF, pMax);
4823
5124
  pmf = exploded;
4824
5125
  }
4825
5126
  singleDiePMFCache.set(cacheKey, pmf);
4826
5127
  return pmf;
4827
5128
  }
5129
+ function resolveExplodingPool(diePMF, maxFace, count, budget, eps) {
5130
+ const pMax = diePMF.pAt(maxFace);
5131
+ const nonMax = /* @__PURE__ */ new Map();
5132
+ for (const v of diePMF.support()) {
5133
+ if (v !== maxFace) nonMax.set(v, diePMF.pAt(v));
5134
+ }
5135
+ if (nonMax.size === 0) {
5136
+ return PMF.delta((count + budget) * maxFace, eps);
5137
+ }
5138
+ const nonMaxPMF = PMF.fromMap(nonMax, eps);
5139
+ const memo = /* @__PURE__ */ new Map();
5140
+ const f = (p, b) => {
5141
+ if (p === 0) return PMF.delta(0, eps);
5142
+ if (b === 0) return diePMF.power(p, eps);
5143
+ const key = `${p},${b}`;
5144
+ const cached = memo.get(key);
5145
+ if (cached) return cached;
5146
+ const maxBranch = f(p, b - 1).mapDamage((v) => v + maxFace);
5147
+ const nonMaxBranch = nonMaxPMF.convolve(f(p - 1, b), eps);
5148
+ const result = PMF.branch(maxBranch, nonMaxBranch, pMax);
5149
+ memo.set(key, result);
5150
+ return result;
5151
+ };
5152
+ return f(count, budget);
5153
+ }
4828
5154
  function findDie(node) {
4829
5155
  switch (node.type) {
4830
5156
  case "die":
@@ -5009,7 +5335,7 @@ function getASTSignature(node) {
5009
5335
  return `d{${parts.join(",")}}`;
5010
5336
  }
5011
5337
  case "sum":
5012
- return `sum{c:${node.count},ch:${getASTSignature(node.child)}}`;
5338
+ return `sum{c:${node.count},b:${node.explodePoolBudget || 0},ch:${getASTSignature(node.child)}}`;
5013
5339
  case "d20Roll":
5014
5340
  return `d20{t:${node.rollType},ch:${getASTSignature(node.child)}}`;
5015
5341
  case "keep":
@@ -5118,10 +5444,8 @@ var AttackBuilder = class _AttackBuilder {
5118
5444
  return `${checkPart} * ${effectPart}`;
5119
5445
  }
5120
5446
  resolveProbabilities(check, eps = 0) {
5121
- const rollType = check.rollType;
5122
- const rerollOne = check.baseReroll > 0;
5123
5447
  const critThreshold = check.critThreshold;
5124
- const d202 = d20RollPMF(rollType, rerollOne);
5448
+ const d202 = resolveRootD20(check);
5125
5449
  if (check instanceof AlwaysCritBuilder) {
5126
5450
  if (check.fromAlwaysHit) {
5127
5451
  return { pSuccess: 1, pHit: 0, pCrit: 1, pMiss: 0 };
@@ -5171,13 +5495,17 @@ var AttackBuilder = class _AttackBuilder {
5171
5495
  pmiss += pr;
5172
5496
  continue;
5173
5497
  }
5174
- if (r >= critThreshold) {
5498
+ if (r === 20) {
5175
5499
  pcrit += pr;
5176
5500
  continue;
5177
5501
  }
5178
5502
  const need = ac - staticMod - r;
5179
5503
  const pBonusHit = bonusPMF.tailProbGE(need);
5180
- phit += pr * pBonusHit;
5504
+ if (r >= critThreshold) {
5505
+ pcrit += pr * pBonusHit;
5506
+ } else {
5507
+ phit += pr * pBonusHit;
5508
+ }
5181
5509
  pmiss += pr * (1 - pBonusHit);
5182
5510
  }
5183
5511
  const psuccess = phit + pcrit;
@@ -5228,6 +5556,55 @@ var AttackBuilder = class _AttackBuilder {
5228
5556
  weights: { hit: phit, crit: pcrit, miss: pmiss }
5229
5557
  };
5230
5558
  }
5559
+ /**
5560
+ * For `dice-match` trigger slicing (`turn/types.ts`'s `HasDiceMatchInfo`): the exact
5561
+ * per-damage-value match probability for the hit and crit branches, or `null` per branch when no
5562
+ * descriptor is available — a string-parsed effect, a wrapped transform whose PMF isn't fully
5563
+ * captured by its `RollConfig`s (half/scale/maxOf/pooled — `cacheKey()` returns `null` for
5564
+ * exactly these), a `keep`/`bestOf` pool (ambiguous "the dice" under crit doubling), a
5565
+ * pool-wide exploding budget (composition with match not yet threaded through here), a
5566
+ * multi-die-type pool, a single die (can never match), or (crit) `noCrit()`.
5567
+ *
5568
+ * The crit branch is built from the SAME crit-effect selection `resolve()` uses (an explicit
5569
+ * `onCrit` roll, or the hit dice auto-doubled via `copy().doubleDice()`), so its descriptor
5570
+ * reflects the crit branch's REAL doubled pool, not the hit pool re-used blindly.
5571
+ */
5572
+ diceMatchInfo(_eps = EPS) {
5573
+ const hit = this.matchInfoForEffect(this.hitEffect);
5574
+ let critEffect;
5575
+ if (this.critEffect === null) {
5576
+ critEffect = void 0;
5577
+ } else if (this.critEffect) {
5578
+ critEffect = this.critEffect;
5579
+ } else if (this.hitEffect instanceof ParsedRollBuilder) {
5580
+ critEffect = void 0;
5581
+ } else {
5582
+ critEffect = this.hitEffect?.copy().doubleDice();
5583
+ }
5584
+ const crit = this.matchInfoForEffect(critEffect);
5585
+ return { hit, crit };
5586
+ }
5587
+ matchInfoForEffect(effect) {
5588
+ if (!effect || effect instanceof ParsedRollBuilder) return null;
5589
+ if (effect.cacheKey() === null) return null;
5590
+ const diceConfigs = effect.getSubRollConfigs().filter((c) => c.sides > 0);
5591
+ if (diceConfigs.length !== 1) return null;
5592
+ const config = diceConfigs[0];
5593
+ if (config.keep || config.bestOf > 0) return null;
5594
+ if (config.explodePoolBudget > 0) return null;
5595
+ if (config.count <= 1) return { matchProbabilityByDamage: /* @__PURE__ */ new Map() };
5596
+ const weights = faceWeights(config.sides, config.minimum, config.reroll);
5597
+ const totalDist = diceSumDistribution(config.count, weights);
5598
+ const joint = jointSumAndMatch(config.count, weights);
5599
+ const modifier = effect.modifier;
5600
+ const matchProbabilityByDamage = /* @__PURE__ */ new Map();
5601
+ for (const [sum, totalMass] of totalDist) {
5602
+ if (totalMass <= 0) continue;
5603
+ const matchMass = joint.get(sum) ?? 0;
5604
+ matchProbabilityByDamage.set(sum + modifier, matchMass / totalMass);
5605
+ }
5606
+ return { matchProbabilityByDamage };
5607
+ }
5231
5608
  /**
5232
5609
  * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
5233
5610
  * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
@@ -5325,9 +5702,7 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
5325
5702
  }
5326
5703
  toPMF(eps = 0) {
5327
5704
  const ac = this.attackConfig.ac;
5328
- const rollType = this.rollType;
5329
- const rerollOne = this.baseReroll > 0;
5330
- const d202 = d20RollPMF(rollType, rerollOne);
5705
+ const d202 = resolveRootD20(this);
5331
5706
  const staticMod = this.modifier;
5332
5707
  const bonusPMFs = this.getBonusDicePMFs(this, eps);
5333
5708
  const parts = [d202, ...bonusPMFs];
@@ -5437,21 +5812,17 @@ var SaveBuilder = class _SaveBuilder {
5437
5812
  function resolveProbabilities(check) {
5438
5813
  const saveBonus = check.modifier;
5439
5814
  const dc = check.saveDC;
5440
- const d20Type = check.rollType;
5441
- const baseReroll = check.baseReroll;
5442
- const die = d20RollPMF(d20Type, baseReroll > 0);
5815
+ const eps = 0;
5816
+ const die = resolveRootD20(check);
5443
5817
  const faceP = /* @__PURE__ */ new Map();
5444
5818
  for (const [r, bin] of die) {
5445
5819
  const pr = bin.p;
5446
5820
  if (pr > 0) faceP.set(r, pr);
5447
5821
  }
5448
- const eps = 0;
5449
5822
  const bonusDicePMFs = check.getBonusDicePMFs(check, eps);
5450
5823
  const bonusPMF = bonusDicePMFs.length > 0 ? PMF.convolveMany(bonusDicePMFs, eps) : PMF.zero(eps);
5451
5824
  let pSuccess = 0;
5452
- for (let r = 1; r <= 20; r++) {
5453
- const pr = faceP.get(r);
5454
- if (!pr) continue;
5825
+ for (const [r, pr] of faceP) {
5455
5826
  const need = dc - saveBonus - r;
5456
5827
  pSuccess += pr * bonusPMF.tailProbGE(need);
5457
5828
  }
@@ -5521,9 +5892,7 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5521
5892
  if (cached) return cached;
5522
5893
  }
5523
5894
  const saveDC = this.saveDC;
5524
- const rollType = this.rollType;
5525
- const rerollOne = this.baseReroll > 0;
5526
- const d202 = d20RollPMF(rollType, rerollOne);
5895
+ const d202 = resolveRootD20(this);
5527
5896
  const staticMod = this.modifier;
5528
5897
  const bonusDicePMFs = this.getBonusDiceConfigs().map(
5529
5898
  (cfg) => pmfFromRollBuilder(RollBuilder.fromConfigs([cfg]), eps)
@@ -5560,14 +5929,15 @@ var TurnSpecError = class extends Error {
5560
5929
  this.name = "TurnSpecError";
5561
5930
  }
5562
5931
  };
5563
- var MAX_TRIGGER_GROUPS = 4;
5932
+ var MAX_TRIGGER_GROUPS = 9;
5564
5933
 
5565
5934
  // src/turn/plan.ts
5566
- var IS_HIT_TRIGGER = {
5935
+ var READS_GROUP = {
5567
5936
  "first-hit": true,
5568
5937
  "any-crit": true,
5569
5938
  "any-miss": true,
5570
- "every-hit": true
5939
+ "every-hit": true,
5940
+ "dice-match": true
5571
5941
  };
5572
5942
  function toPMF(damage, eps, id = "") {
5573
5943
  const parts = Array.isArray(damage) ? damage : [damage];
@@ -5611,20 +5981,49 @@ function critPMF(rider, base, eps) {
5611
5981
  if (doubled.length === 0) return base;
5612
5982
  return PMF.convolveMany(doubled, eps);
5613
5983
  }
5614
- function sliceSource(pmf) {
5984
+ function diceMatchInfoOf(source, eps) {
5985
+ const capable = source;
5986
+ if (typeof capable.diceMatchInfo === "function") {
5987
+ return capable.diceMatchInfo(eps);
5988
+ }
5989
+ return { hit: null, crit: null };
5990
+ }
5991
+ function sliceSource(pmf, matchInfo) {
5615
5992
  const labels = pmf.outcomes();
5616
5993
  if (!labels.includes("hit") && !labels.includes("crit")) return null;
5617
5994
  const missParts = ["missNone", "missDamage"].filter((label) => labels.includes(label)).map((label) => pmf.filterOutcome(label));
5618
- return {
5619
- hit: labels.includes("hit") ? pmf.filterOutcome("hit") : PMF.emptyMass(),
5620
- crit: labels.includes("crit") ? pmf.filterOutcome("crit") : PMF.emptyMass(),
5621
- miss: missParts.length ? missParts.reduce((all, part) => all.add(part)) : PMF.emptyMass()
5622
- };
5995
+ const hit = labels.includes("hit") ? pmf.filterOutcome("hit") : PMF.emptyMass();
5996
+ const crit = labels.includes("crit") ? pmf.filterOutcome("crit") : PMF.emptyMass();
5997
+ const miss = missParts.length ? missParts.reduce((all, part) => all.add(part)) : PMF.emptyMass();
5998
+ let hitMatch = null;
5999
+ let hitNoMatch = null;
6000
+ let critMatch = null;
6001
+ let critNoMatch = null;
6002
+ if (matchInfo?.hit) {
6003
+ const info = matchInfo.hit;
6004
+ const [m, nm] = hit.splitByFactor((d2) => info.matchProbabilityByDamage.get(d2) ?? 0);
6005
+ hitMatch = m;
6006
+ hitNoMatch = nm;
6007
+ }
6008
+ if (matchInfo?.crit) {
6009
+ const info = matchInfo.crit;
6010
+ const [m, nm] = crit.splitByFactor((d2) => info.matchProbabilityByDamage.get(d2) ?? 0);
6011
+ critMatch = m;
6012
+ critNoMatch = nm;
6013
+ }
6014
+ return { hit, crit, miss, hitMatch, hitNoMatch, critMatch, critNoMatch };
5623
6015
  }
5624
6016
  function buildPlan(spec, eps = EPS) {
5625
6017
  const fail = (code, id, message) => {
5626
6018
  throw new TurnSpecError(code, id, message);
5627
6019
  };
6020
+ const riders = spec.riders ?? [];
6021
+ const matchNeededSourceIds = /* @__PURE__ */ new Set();
6022
+ for (const rider of riders) {
6023
+ if (rider.on === "dice-match") {
6024
+ for (const sourceId of rider.of) matchNeededSourceIds.add(sourceId);
6025
+ }
6026
+ }
5628
6027
  const attackIds = [];
5629
6028
  const attackPMFs = [];
5630
6029
  const attackSlices = [];
@@ -5634,11 +6033,11 @@ function buildPlan(spec, eps = EPS) {
5634
6033
  const id = hasWrapper ? named.id : `attack ${index + 1}`;
5635
6034
  const source = hasWrapper ? named.source : entry;
5636
6035
  const pmf = toPMF(source, eps, id);
6036
+ const matchInfo = matchNeededSourceIds.has(id) ? diceMatchInfoOf(source, eps) : null;
5637
6037
  attackIds.push(id);
5638
6038
  attackPMFs.push(pmf);
5639
- attackSlices.push(sliceSource(pmf));
6039
+ attackSlices.push(sliceSource(pmf, matchInfo));
5640
6040
  });
5641
- const riders = spec.riders ?? [];
5642
6041
  const riderIds = riders.map((rider, index) => rider.id ?? `rider ${index + 1}`);
5643
6042
  const seen = /* @__PURE__ */ new Set();
5644
6043
  for (const id of [...attackIds, ...riderIds]) {
@@ -5647,6 +6046,17 @@ function buildPlan(spec, eps = EPS) {
5647
6046
  }
5648
6047
  const attackIndexById = new Map(attackIds.map((id, index) => [id, index]));
5649
6048
  const riderIndexById = new Map(riderIds.map((id, index) => [id, index]));
6049
+ const checkMatchable = (riderId, sourceId, slices) => {
6050
+ const missingHit = slices.hit.mass() > 0 && slices.hitMatch === null;
6051
+ const missingCrit = slices.crit.mass() > 0 && slices.critMatch === null;
6052
+ if (missingHit || missingCrit) {
6053
+ fail(
6054
+ "no-dice-descriptor",
6055
+ sourceId,
6056
+ `Rider "${riderId}" reads "${sourceId}" for "dice-match", but "${sourceId}" has no dice descriptor to match against \u2014 a bare PMF, a string-parsed expression, or a keep()/bestOf() pool (ambiguous "the dice" under crit doubling) cannot be matched.`
6057
+ );
6058
+ }
6059
+ };
5650
6060
  const sourceIdsByRider = riders.map((rider, index) => {
5651
6061
  const id = riderIds[index];
5652
6062
  if (rider.on === "not-fired") {
@@ -5671,7 +6081,7 @@ function buildPlan(spec, eps = EPS) {
5671
6081
  }
5672
6082
  return [target];
5673
6083
  }
5674
- const of = [...new Set(rider.of ?? attackIds)];
6084
+ const of = [...new Set(rider.on === "dice-match" ? rider.of : rider.of ?? attackIds)];
5675
6085
  if (of.length === 0) {
5676
6086
  fail("unknown-id", id, `Rider "${id}" has no sources.`);
5677
6087
  }
@@ -5695,8 +6105,11 @@ function buildPlan(spec, eps = EPS) {
5695
6105
  `Rider "${id}" triggers on "${sourceId}", an every-hit rider. Those are folded into their own sources rather than resolved separately, so they cannot be triggered on \u2014 point at the attacks instead.`
5696
6106
  );
5697
6107
  }
6108
+ const damageSource = isAttack ? void 0 : riders[riderIndex].damage;
6109
+ const singleDamageSource = damageSource !== void 0 && !Array.isArray(damageSource) ? damageSource : void 0;
5698
6110
  const slices = isAttack ? attackSlices[attackIndexById.get(sourceId)] : sliceSource(
5699
- toPMF(riders[riderIndex].damage, eps, sourceId)
6111
+ toPMF(damageSource, eps, sourceId),
6112
+ matchNeededSourceIds.has(sourceId) && singleDamageSource !== void 0 ? diceMatchInfoOf(singleDamageSource, eps) : null
5700
6113
  );
5701
6114
  if (!slices) {
5702
6115
  fail(
@@ -5704,6 +6117,8 @@ function buildPlan(spec, eps = EPS) {
5704
6117
  sourceId,
5705
6118
  `Rider "${id}" triggers on "${sourceId}", which has no hit/crit outcomes.`
5706
6119
  );
6120
+ } else if (rider.on === "dice-match") {
6121
+ checkMatchable(id, sourceId, slices);
5707
6122
  }
5708
6123
  }
5709
6124
  return [...of];
@@ -5749,7 +6164,7 @@ function buildPlan(spec, eps = EPS) {
5749
6164
  const perHitGroups = /* @__PURE__ */ new Map();
5750
6165
  for (const index of order) {
5751
6166
  const rider = riders[index];
5752
- if (!IS_HIT_TRIGGER[rider.on]) continue;
6167
+ if (!READS_GROUP[rider.on]) continue;
5753
6168
  const group = groupOf(sourceIdsByRider[index]);
5754
6169
  readsByRider.set(index, group);
5755
6170
  if (rider.on === "every-hit") perHitGroups.set(riderIds[index], group);
@@ -5779,21 +6194,34 @@ function buildPlan(spec, eps = EPS) {
5779
6194
  if (!payloads) return slices;
5780
6195
  let hit = slices.hit;
5781
6196
  let crit = slices.crit;
6197
+ let hitMatch = slices.hitMatch;
6198
+ let hitNoMatch = slices.hitNoMatch;
6199
+ let critMatch = slices.critMatch;
6200
+ let critNoMatch = slices.critNoMatch;
5782
6201
  for (const payload of payloads) {
5783
6202
  hit = hit.convolve(payload.hit, eps, true);
5784
6203
  crit = crit.convolve(payload.crit, eps, true);
6204
+ if (hitMatch) hitMatch = hitMatch.convolve(payload.hit, eps, true);
6205
+ if (hitNoMatch) hitNoMatch = hitNoMatch.convolve(payload.hit, eps, true);
6206
+ if (critMatch) critMatch = critMatch.convolve(payload.crit, eps, true);
6207
+ if (critNoMatch) critNoMatch = critNoMatch.convolve(payload.crit, eps, true);
5785
6208
  }
5786
- return { hit, crit, miss: slices.miss };
6209
+ return { hit, crit, miss: slices.miss, hitMatch, hitNoMatch, critMatch, critNoMatch };
6210
+ };
6211
+ const emptySlices = {
6212
+ hit: PMF.emptyMass(),
6213
+ crit: PMF.emptyMass(),
6214
+ miss: PMF.emptyMass(),
6215
+ hitMatch: null,
6216
+ hitNoMatch: null,
6217
+ critMatch: null,
6218
+ critNoMatch: null
5787
6219
  };
5788
6220
  const steps = attackIds.map((id, index) => ({
5789
6221
  id,
5790
6222
  trigger: null,
5791
6223
  slices: withPerHit(
5792
- attackSlices[index] ?? {
5793
- hit: attackPMFs[index],
5794
- crit: PMF.emptyMass(),
5795
- miss: PMF.emptyMass()
5796
- },
6224
+ attackSlices[index] ?? { ...emptySlices, hit: attackPMFs[index] },
5797
6225
  id
5798
6226
  ),
5799
6227
  damage: null,
@@ -5808,7 +6236,9 @@ function buildPlan(spec, eps = EPS) {
5808
6236
  if (rider.on === "every-hit") continue;
5809
6237
  const id = riderIds[index];
5810
6238
  const hit = toPMF(rider.damage, eps, id);
5811
- const slices = sliceSource(hit);
6239
+ const singleRiderDamage = !Array.isArray(rider.damage) ? rider.damage : void 0;
6240
+ const matchInfo = matchNeededSourceIds.has(id) && singleRiderDamage !== void 0 ? diceMatchInfoOf(singleRiderDamage, eps) : null;
6241
+ const slices = sliceSource(hit, matchInfo);
5812
6242
  if (slices && rider.critDamage !== void 0) {
5813
6243
  fail(
5814
6244
  "unused-crit-damage",
@@ -5830,6 +6260,15 @@ function buildPlan(spec, eps = EPS) {
5830
6260
  stepIndexByRider.set(index, steps.length - 1);
5831
6261
  riderSteps.set(id, steps.length - 1);
5832
6262
  }
6263
+ const groupLastReadStep = new Array(groupSources.length).fill(-1);
6264
+ steps.forEach((step, stepIndex) => {
6265
+ if (step.reads !== -1) {
6266
+ groupLastReadStep[step.reads] = Math.max(groupLastReadStep[step.reads], stepIndex);
6267
+ }
6268
+ });
6269
+ for (const group of perHitGroups.values()) {
6270
+ groupLastReadStep[group] = steps.length;
6271
+ }
5833
6272
  return {
5834
6273
  steps,
5835
6274
  groupCount: groupSources.length,
@@ -5837,7 +6276,8 @@ function buildPlan(spec, eps = EPS) {
5837
6276
  attackIds,
5838
6277
  riderIds,
5839
6278
  riderSteps,
5840
- perHitGroups
6279
+ perHitGroups,
6280
+ groupLastReadStep
5841
6281
  };
5842
6282
  }
5843
6283
 
@@ -5847,18 +6287,36 @@ var FIRST_HIT = 1;
5847
6287
  var FIRST_CRIT = 2;
5848
6288
  var CRIT_BIT = 2;
5849
6289
  var MISS_BIT = 1;
6290
+ var MATCH_BIT = 16;
5850
6291
  var START_CODE = FIRST_NONE << 2;
5851
- function advance(code, outcome) {
6292
+ function advance(code, outcome, matched = false) {
5852
6293
  if (outcome === "miss") return code | MISS_BIT;
5853
- const first = code >> 2;
6294
+ const first = code >> 2 & 3;
5854
6295
  const withCrit = outcome === "crit" ? code | CRIT_BIT : code;
5855
- if (first !== FIRST_NONE) return withCrit;
6296
+ const withMatch = matched ? withCrit | MATCH_BIT : withCrit;
6297
+ if (first !== FIRST_NONE) return withMatch;
5856
6298
  const nextFirst = outcome === "crit" ? FIRST_CRIT : FIRST_HIT;
5857
- return nextFirst << 2 | withCrit & (CRIT_BIT | MISS_BIT);
6299
+ return nextFirst << 2 | withMatch & (MATCH_BIT | CRIT_BIT | MISS_BIT);
5858
6300
  }
5859
6301
 
5860
6302
  // src/turn/turn.ts
5861
- var OUTCOMES = ["hit", "crit", "miss"];
6303
+ function stepDraws(slices) {
6304
+ const draws = [];
6305
+ if (slices.hitMatch) {
6306
+ draws.push({ outcome: "hit", matched: true, slice: slices.hitMatch });
6307
+ draws.push({ outcome: "hit", matched: false, slice: slices.hitNoMatch });
6308
+ } else {
6309
+ draws.push({ outcome: "hit", matched: false, slice: slices.hit });
6310
+ }
6311
+ if (slices.critMatch) {
6312
+ draws.push({ outcome: "crit", matched: true, slice: slices.critMatch });
6313
+ draws.push({ outcome: "crit", matched: false, slice: slices.critNoMatch });
6314
+ } else {
6315
+ draws.push({ outcome: "crit", matched: false, slice: slices.crit });
6316
+ }
6317
+ draws.push({ outcome: "miss", matched: false, slice: slices.miss });
6318
+ return draws;
6319
+ }
5862
6320
  function fireMode(step, codes, firedByStep) {
5863
6321
  const trigger = step.trigger;
5864
6322
  if (!trigger) return "hit";
@@ -5866,7 +6324,7 @@ function fireMode(step, codes, firedByStep) {
5866
6324
  return firedByStep[step.negates] === null ? "hit" : null;
5867
6325
  }
5868
6326
  const code = codes[step.reads];
5869
- const first = code >> 2;
6327
+ const first = code >> 2 & 3;
5870
6328
  switch (trigger.on) {
5871
6329
  case "first-hit":
5872
6330
  if (first === FIRST_NONE) return null;
@@ -5875,6 +6333,8 @@ function fireMode(step, codes, firedByStep) {
5875
6333
  return (code & CRIT_BIT) !== 0 ? "crit" : null;
5876
6334
  case "any-miss":
5877
6335
  return (code & MISS_BIT) !== 0 ? "hit" : null;
6336
+ case "dice-match":
6337
+ return (code & MATCH_BIT) !== 0 ? "hit" : null;
5878
6338
  default:
5879
6339
  return null;
5880
6340
  }
@@ -6002,6 +6462,20 @@ var Turn = class _Turn {
6002
6462
  riders.push({ ...options, damage, on: "not-fired", of: target });
6003
6463
  return new _Turn(this.declaredAttacks, riders, this.eps);
6004
6464
  }
6465
+ /**
6466
+ * Fires once if any of `of`'s named sources' own damage dice matched (showed a
6467
+ * duplicate value) on hit or crit — Chromatic Orb's bounce. Unlike the other
6468
+ * `onX` triggers, `of` is required: "the dice matched" has no coherent meaning
6469
+ * defaulted across every declared attack. Each named source must expose a
6470
+ * dice-match descriptor (an `AttackBuilder`-shaped source does); naming one
6471
+ * that doesn't is a `TurnSpecError("no-dice-descriptor", ...)`.
6472
+ *
6473
+ * Most callers want {@link bounce} instead of calling this directly — it
6474
+ * builds the whole depth-capped chain of attack-shaped riders.
6475
+ */
6476
+ onDiceMatch(of, damage, options = {}) {
6477
+ return this.rider({ ...options, damage, on: "dice-match", of });
6478
+ }
6005
6479
  /**
6006
6480
  * The exact joint distribution: mass 1, outcome-labelled. Resolved once and
6007
6481
  * cached.
@@ -6074,8 +6548,14 @@ var Turn = class _Turn {
6074
6548
  let states = /* @__PURE__ */ new Map([[String.fromCharCode(), start]]);
6075
6549
  plan.steps.forEach((step, stepIndex) => {
6076
6550
  const next = /* @__PURE__ */ new Map();
6551
+ const liveGroups = [];
6552
+ for (let g = 0; g < plan.groupCount; g++) {
6553
+ if (plan.groupLastReadStep[g] > stepIndex) liveGroups.push(g);
6554
+ }
6077
6555
  const merge = (state) => {
6078
- const key = String.fromCharCode(...state.codes) + "" + state.fired.map((mode) => mode === null ? "-" : "+").join("");
6556
+ let codesKey = "";
6557
+ for (const g of liveGroups) codesKey += String.fromCharCode(state.codes[g]);
6558
+ const key = codesKey + "" + state.fired.map((mode) => mode === null ? "-" : "+").join("");
6079
6559
  const existing = next.get(key);
6080
6560
  if (existing) existing.pmf = existing.pmf.add(state.pmf);
6081
6561
  else next.set(key, state);
@@ -6101,13 +6581,12 @@ var Turn = class _Turn {
6101
6581
  });
6102
6582
  continue;
6103
6583
  }
6104
- for (const outcome of OUTCOMES) {
6105
- const slice = step.slices[outcome];
6584
+ for (const { outcome, matched, slice } of stepDraws(step.slices)) {
6106
6585
  const sliceMass = slice.mass();
6107
6586
  if (sliceMass <= eps) continue;
6108
6587
  const codes = [...state.codes];
6109
6588
  for (const group of step.updates) {
6110
- codes[group] = advance(codes[group], outcome);
6589
+ codes[group] = advance(codes[group], outcome, matched);
6111
6590
  }
6112
6591
  merge({
6113
6592
  codes,
@@ -6131,7 +6610,7 @@ var Turn = class _Turn {
6131
6610
  }
6132
6611
  }
6133
6612
  for (const [id, group] of plan.perHitGroups) {
6134
- if (state.codes[group] >> 2 !== FIRST_NONE) {
6613
+ if ((state.codes[group] >> 2 & 3) !== FIRST_NONE) {
6135
6614
  fireMass.set(id, fireMass.get(id) + mass);
6136
6615
  }
6137
6616
  }
@@ -6155,7 +6634,20 @@ function turn(attacks = [], eps = EPS) {
6155
6634
  eps
6156
6635
  );
6157
6636
  }
6637
+ function bounce({ source, max }) {
6638
+ if (!Number.isInteger(max) || max < 0) {
6639
+ throw new RangeError(`bounce({ max }) needs a non-negative integer, got ${max}.`);
6640
+ }
6641
+ let result = turn(source);
6642
+ let previousId = result.attackIds[0];
6643
+ for (let i = 0; i < max; i++) {
6644
+ const riderId = `bounce ${i + 1}`;
6645
+ result = result.onDiceMatch([previousId], source, { id: riderId });
6646
+ previousId = riderId;
6647
+ }
6648
+ return result;
6649
+ }
6158
6650
 
6159
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MAX_TRIGGER_GROUPS, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, Turn, TurnSpecError, builderPMFCache, clearAttackCache, clearDCCache, clearRollCache, clearSaveCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls, turn };
6651
+ export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MAX_TRIGGER_GROUPS, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, Turn, TurnSpecError, bounce, builderPMFCache, clearAttackCache, clearDCCache, clearRollCache, clearSaveCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls, turn };
6160
6652
  //# sourceMappingURL=index.js.map
6161
6653
  //# sourceMappingURL=index.js.map