@yipe/dice 0.10.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.
- package/dist/builder/ast.d.ts.map +1 -1
- package/dist/builder/attack.d.ts +19 -0
- package/dist/builder/attack.d.ts.map +1 -1
- package/dist/builder/index.cjs +393 -37
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.js +393 -38
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/nodes.d.ts +1 -0
- package/dist/builder/nodes.d.ts.map +1 -1
- package/dist/builder/roll.d.ts +8 -0
- package/dist/builder/roll.d.ts.map +1 -1
- package/dist/builder/types.d.ts +1 -0
- package/dist/builder/types.d.ts.map +1 -1
- package/dist/common/bounce.d.ts +45 -0
- package/dist/common/bounce.d.ts.map +1 -1
- package/dist/common/types.d.ts +33 -0
- package/dist/common/types.d.ts.map +1 -1
- package/dist/index.cjs +171 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +168 -1
- package/dist/index.js.map +1 -1
- package/dist/pmf/pmf.d.ts +12 -0
- package/dist/pmf/pmf.d.ts.map +1 -1
- package/dist/turn/plan.d.ts +29 -6
- package/dist/turn/plan.d.ts.map +1 -1
- package/dist/turn/state.d.ts +16 -8
- package/dist/turn/state.d.ts.map +1 -1
- package/dist/turn/turn.d.ts +35 -0
- package/dist/turn/turn.d.ts.map +1 -1
- package/dist/turn/types.d.ts +32 -10
- package/dist/turn/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/builder/index.cjs
CHANGED
|
@@ -1788,6 +1788,30 @@ var _PMF = class _PMF {
|
|
|
1788
1788
|
`freq(${this.identifier},${freq})`
|
|
1789
1789
|
);
|
|
1790
1790
|
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Splits this PMF into two complementary PMFs by an arbitrary per-damage-value factor in
|
|
1793
|
+
* `[0, 1]` — bin `d`'s mass, `count`, and `attr` split `factor(d)` / `1 - factor(d)` between the
|
|
1794
|
+
* two results (via the same proportional scaling {@link applyHitFrequency} uses, `scaleBin`), so
|
|
1795
|
+
* `a.add(b)` recovers this PMF exactly and both halves stay chart-attributable. Unlike
|
|
1796
|
+
* {@link applyHitFrequency}, mass is NOT redistributed to a miss bin at 0 — each bin stays at its
|
|
1797
|
+
* own damage value in whichever half it lands in. `factor` outside `[0, 1]` is clamped.
|
|
1798
|
+
*
|
|
1799
|
+
* Built for `dice-match` trigger slicing: splitting a hit/crit sub-PMF into "matched" and
|
|
1800
|
+
* "did not match" halves by the exact per-damage-value match probability.
|
|
1801
|
+
*/
|
|
1802
|
+
splitByFactor(factor) {
|
|
1803
|
+
const a = /* @__PURE__ */ new Map();
|
|
1804
|
+
const b = /* @__PURE__ */ new Map();
|
|
1805
|
+
for (const [damage, bin] of this.map) {
|
|
1806
|
+
const f = Math.min(1, Math.max(0, factor(damage)));
|
|
1807
|
+
if (f > 0) a.set(damage, _PMF.scaleBin(bin, f));
|
|
1808
|
+
if (f < 1) b.set(damage, _PMF.scaleBin(bin, 1 - f));
|
|
1809
|
+
}
|
|
1810
|
+
return [
|
|
1811
|
+
new _PMF(a, this.epsilon, false, `split+(${this.identifier})`),
|
|
1812
|
+
new _PMF(b, this.epsilon, false, `split-(${this.identifier})`)
|
|
1813
|
+
];
|
|
1814
|
+
}
|
|
1791
1815
|
scaleMass(factor) {
|
|
1792
1816
|
if (factor === 1) return this;
|
|
1793
1817
|
const scaledMap = /* @__PURE__ */ new Map();
|
|
@@ -2474,6 +2498,84 @@ var _PMF = class _PMF {
|
|
|
2474
2498
|
_PMF.__anonIdCounter = 1;
|
|
2475
2499
|
var PMF = _PMF;
|
|
2476
2500
|
|
|
2501
|
+
// src/common/bounce.ts
|
|
2502
|
+
function faceWeights(faces, minimum = 0, reroll = 0) {
|
|
2503
|
+
const f = Math.max(0, Math.floor(faces));
|
|
2504
|
+
if (f <= 0) return [];
|
|
2505
|
+
let weights = new Array(f).fill(1 / f);
|
|
2506
|
+
const r = Math.max(0, Math.min(Math.floor(reroll), f));
|
|
2507
|
+
if (r > 0) {
|
|
2508
|
+
const rerollMass = r / f;
|
|
2509
|
+
const uniformReroll = rerollMass / f;
|
|
2510
|
+
weights = weights.map((_, i) => (i < r ? 0 : 1 / f) + uniformReroll);
|
|
2511
|
+
}
|
|
2512
|
+
const minV = Math.max(0, Math.floor(minimum));
|
|
2513
|
+
if (minV > 1) {
|
|
2514
|
+
const collapsed = new Array(f).fill(0);
|
|
2515
|
+
for (let v = 1; v <= f; v++) {
|
|
2516
|
+
const target = Math.min(f, Math.max(v, minV));
|
|
2517
|
+
collapsed[target - 1] += weights[v - 1];
|
|
2518
|
+
}
|
|
2519
|
+
weights = collapsed;
|
|
2520
|
+
}
|
|
2521
|
+
return weights;
|
|
2522
|
+
}
|
|
2523
|
+
function diceSumDistribution(dice, weights) {
|
|
2524
|
+
let dist = /* @__PURE__ */ new Map([[0, 1]]);
|
|
2525
|
+
for (let die = 0; die < dice; die++) {
|
|
2526
|
+
const next = /* @__PURE__ */ new Map();
|
|
2527
|
+
for (const [sum, mass] of dist) {
|
|
2528
|
+
for (let face = 1; face <= weights.length; face++) {
|
|
2529
|
+
const w = weights[face - 1] ?? 0;
|
|
2530
|
+
if (w <= 0) continue;
|
|
2531
|
+
const s = sum + face;
|
|
2532
|
+
next.set(s, (next.get(s) ?? 0) + mass * w);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
dist = next;
|
|
2536
|
+
}
|
|
2537
|
+
return dist;
|
|
2538
|
+
}
|
|
2539
|
+
function sumAllDistinctDistribution(dice, weights) {
|
|
2540
|
+
const faceCount = weights.length;
|
|
2541
|
+
let dp = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, 1]])]]);
|
|
2542
|
+
for (let face = 1; face <= faceCount; face++) {
|
|
2543
|
+
const w = weights[face - 1] ?? 0;
|
|
2544
|
+
const next = /* @__PURE__ */ new Map();
|
|
2545
|
+
for (const [count, sumMap] of dp) next.set(count, new Map(sumMap));
|
|
2546
|
+
if (w > 0) {
|
|
2547
|
+
for (const [count, sumMap] of dp) {
|
|
2548
|
+
const nextCount = count + 1;
|
|
2549
|
+
if (nextCount > dice) continue;
|
|
2550
|
+
const target = next.get(nextCount) ?? /* @__PURE__ */ new Map();
|
|
2551
|
+
for (const [sum, mass] of sumMap) {
|
|
2552
|
+
const s = sum + face;
|
|
2553
|
+
target.set(s, (target.get(s) ?? 0) + mass * w);
|
|
2554
|
+
}
|
|
2555
|
+
next.set(nextCount, target);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
dp = next;
|
|
2559
|
+
}
|
|
2560
|
+
let factorial = 1;
|
|
2561
|
+
for (let i = 2; i <= dice; i++) factorial *= i;
|
|
2562
|
+
const chosen = dp.get(dice) ?? /* @__PURE__ */ new Map();
|
|
2563
|
+
const result = /* @__PURE__ */ new Map();
|
|
2564
|
+
for (const [sum, mass] of chosen) result.set(sum, mass * factorial);
|
|
2565
|
+
return result;
|
|
2566
|
+
}
|
|
2567
|
+
function jointSumAndMatch(dice, weights) {
|
|
2568
|
+
if (dice <= 1) return /* @__PURE__ */ new Map();
|
|
2569
|
+
const total = diceSumDistribution(dice, weights);
|
|
2570
|
+
const distinct = sumAllDistinctDistribution(dice, weights);
|
|
2571
|
+
const result = /* @__PURE__ */ new Map();
|
|
2572
|
+
for (const [sum, mass] of total) {
|
|
2573
|
+
const matchMass = Math.max(0, mass - (distinct.get(sum) ?? 0));
|
|
2574
|
+
if (matchMass > 0) result.set(sum, matchMass);
|
|
2575
|
+
}
|
|
2576
|
+
return result;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2477
2579
|
// src/pmf/mixture.ts
|
|
2478
2580
|
var Mixture = class _Mixture {
|
|
2479
2581
|
constructor(eps = EPS) {
|
|
@@ -3572,16 +3674,17 @@ var defaultConfig = {
|
|
|
3572
3674
|
modifier: 0,
|
|
3573
3675
|
reroll: 0,
|
|
3574
3676
|
explode: 0,
|
|
3677
|
+
explodePoolBudget: 0,
|
|
3575
3678
|
minimum: 0,
|
|
3576
3679
|
bestOf: 0,
|
|
3577
3680
|
keep: void 0,
|
|
3578
3681
|
rollType: "flat"
|
|
3579
3682
|
};
|
|
3580
3683
|
var rollConfigsEqual = (a, b) => {
|
|
3581
|
-
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;
|
|
3684
|
+
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;
|
|
3582
3685
|
};
|
|
3583
3686
|
var configComplexityScore = (config) => {
|
|
3584
|
-
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);
|
|
3687
|
+
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);
|
|
3585
3688
|
};
|
|
3586
3689
|
var RollBuilder = class _RollBuilder {
|
|
3587
3690
|
constructor(countOrConfigs = 1) {
|
|
@@ -3763,10 +3866,35 @@ var RollBuilder = class _RollBuilder {
|
|
|
3763
3866
|
if (count === void 0) return this;
|
|
3764
3867
|
if (count === 0) return this;
|
|
3765
3868
|
if (count < 0) throw new Error("Explode count must be >= 0");
|
|
3869
|
+
if (this.lastConfig.explodePoolBudget > 0) {
|
|
3870
|
+
throw new Error(
|
|
3871
|
+
"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."
|
|
3872
|
+
);
|
|
3873
|
+
}
|
|
3766
3874
|
const newConfigs = this.getSubRollConfigs();
|
|
3767
3875
|
newConfigs[newConfigs.length - 1].explode = count;
|
|
3768
3876
|
return this.create(newConfigs);
|
|
3769
3877
|
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Set a pool-wide exploding-dice budget: at most `budget` extra dice may be added across the
|
|
3880
|
+
* WHOLE pool (shared), as opposed to {@link explode}'s per-die cap (`n` dice each individually
|
|
3881
|
+
* allowed up to `explode(k)` extra dice). `budget` must be a finite non-negative integer —
|
|
3882
|
+
* unlike `explode()`, `Infinity` is not accepted (it would make the pool-wide DP non-terminating).
|
|
3883
|
+
*/
|
|
3884
|
+
explodePool(budget) {
|
|
3885
|
+
if (isNaN(budget)) throw new Error("Invalid NaN value for explodePool budget");
|
|
3886
|
+
if (!Number.isFinite(budget)) throw new Error("explodePool budget must be finite");
|
|
3887
|
+
if (budget < 0) throw new Error("explodePool budget must be >= 0");
|
|
3888
|
+
if (budget === 0) return this;
|
|
3889
|
+
if (this.lastConfig.explode > 0) {
|
|
3890
|
+
throw new Error(
|
|
3891
|
+
"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."
|
|
3892
|
+
);
|
|
3893
|
+
}
|
|
3894
|
+
const newConfigs = this.getSubRollConfigs();
|
|
3895
|
+
newConfigs[newConfigs.length - 1].explodePoolBudget = Math.floor(budget);
|
|
3896
|
+
return this.create(newConfigs);
|
|
3897
|
+
}
|
|
3770
3898
|
/** Apply per-die minimum value (floors each die roll at `val`, e.g. `minimum(3)` treats a 1 or
|
|
3771
3899
|
* 2 as a 3 -- the 2024 Great Weapon Fighting style). */
|
|
3772
3900
|
minimum(val) {
|
|
@@ -4005,6 +4133,11 @@ var RollBuilder = class _RollBuilder {
|
|
|
4005
4133
|
`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().`
|
|
4006
4134
|
);
|
|
4007
4135
|
}
|
|
4136
|
+
if (config.explodePoolBudget && config.explodePoolBudget > 0) {
|
|
4137
|
+
throw new Error(
|
|
4138
|
+
`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().`
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4008
4141
|
let baseDie = `d${config.sides}`;
|
|
4009
4142
|
const rerollClause = config.reroll > 0 ? config.reroll === 1 ? " reroll 1" : ` reroll d${config.reroll}` : "";
|
|
4010
4143
|
if (config.reroll > 0) {
|
|
@@ -4503,6 +4636,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
|
|
|
4503
4636
|
explode(_count = Infinity) {
|
|
4504
4637
|
throw new Error("Cannot set explode on a pooled roll.");
|
|
4505
4638
|
}
|
|
4639
|
+
explodePool(_budget) {
|
|
4640
|
+
throw new Error("Cannot set explodePool on a pooled roll.");
|
|
4641
|
+
}
|
|
4506
4642
|
minimum(_val) {
|
|
4507
4643
|
throw new Error("Cannot set minimum on a pooled roll.");
|
|
4508
4644
|
}
|
|
@@ -4752,6 +4888,11 @@ function astFromRollConfigs(configs) {
|
|
|
4752
4888
|
appliedRollType = true;
|
|
4753
4889
|
}
|
|
4754
4890
|
if (cfg.rollType === "flat" && effectiveKeep && effectiveKeep.total > 0) {
|
|
4891
|
+
if (cfg.explodePoolBudget > 0) {
|
|
4892
|
+
throw new Error(
|
|
4893
|
+
"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."
|
|
4894
|
+
);
|
|
4895
|
+
}
|
|
4755
4896
|
const baseCount = Math.max(1, Math.floor(Math.abs(count || 1)));
|
|
4756
4897
|
const trials = Math.max(1, Math.floor(effectiveKeep.total));
|
|
4757
4898
|
const k = Math.max(0, Math.floor(effectiveKeep.count));
|
|
@@ -4802,7 +4943,17 @@ function astFromRollConfigs(configs) {
|
|
|
4802
4943
|
}
|
|
4803
4944
|
} else {
|
|
4804
4945
|
const c = appliedRollType ? 1 : Math.max(1, count || 1);
|
|
4805
|
-
|
|
4946
|
+
if (cfg.explodePoolBudget > 0 && appliedRollType) {
|
|
4947
|
+
throw new Error(
|
|
4948
|
+
"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."
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
node = {
|
|
4952
|
+
type: "sum",
|
|
4953
|
+
count: c,
|
|
4954
|
+
child: node,
|
|
4955
|
+
explodePoolBudget: cfg.explodePoolBudget > 0 ? cfg.explodePoolBudget : void 0
|
|
4956
|
+
};
|
|
4806
4957
|
}
|
|
4807
4958
|
children.push({ node, sign });
|
|
4808
4959
|
}
|
|
@@ -4833,6 +4984,13 @@ function resolve(node, eps = defaultEps) {
|
|
|
4833
4984
|
const base = resolve(node.child, eps);
|
|
4834
4985
|
const n = Math.max(0, Math.floor(node.count));
|
|
4835
4986
|
if (n === 0) return PMF.delta(0, eps);
|
|
4987
|
+
if (node.explodePoolBudget && Number.isFinite(node.explodePoolBudget) && node.explodePoolBudget > 0) {
|
|
4988
|
+
const die = findDie(node.child);
|
|
4989
|
+
if (!die) {
|
|
4990
|
+
throw new Error("explodePool() requires the pool's child to be a plain die.");
|
|
4991
|
+
}
|
|
4992
|
+
return resolveExplodingPool(base, die.sides, n, Math.floor(node.explodePoolBudget), eps);
|
|
4993
|
+
}
|
|
4836
4994
|
if (n === 1) return base;
|
|
4837
4995
|
return base.power(n, eps);
|
|
4838
4996
|
}
|
|
@@ -4970,6 +5128,31 @@ function resolveSingleDie(die, eps = defaultEps) {
|
|
|
4970
5128
|
singleDiePMFCache.set(cacheKey, pmf);
|
|
4971
5129
|
return pmf;
|
|
4972
5130
|
}
|
|
5131
|
+
function resolveExplodingPool(diePMF, maxFace, count, budget, eps) {
|
|
5132
|
+
const pMax = diePMF.pAt(maxFace);
|
|
5133
|
+
const nonMax = /* @__PURE__ */ new Map();
|
|
5134
|
+
for (const v of diePMF.support()) {
|
|
5135
|
+
if (v !== maxFace) nonMax.set(v, diePMF.pAt(v));
|
|
5136
|
+
}
|
|
5137
|
+
if (nonMax.size === 0) {
|
|
5138
|
+
return PMF.delta((count + budget) * maxFace, eps);
|
|
5139
|
+
}
|
|
5140
|
+
const nonMaxPMF = PMF.fromMap(nonMax, eps);
|
|
5141
|
+
const memo = /* @__PURE__ */ new Map();
|
|
5142
|
+
const f = (p, b) => {
|
|
5143
|
+
if (p === 0) return PMF.delta(0, eps);
|
|
5144
|
+
if (b === 0) return diePMF.power(p, eps);
|
|
5145
|
+
const key = `${p},${b}`;
|
|
5146
|
+
const cached = memo.get(key);
|
|
5147
|
+
if (cached) return cached;
|
|
5148
|
+
const maxBranch = f(p, b - 1).mapDamage((v) => v + maxFace);
|
|
5149
|
+
const nonMaxBranch = nonMaxPMF.convolve(f(p - 1, b), eps);
|
|
5150
|
+
const result = PMF.branch(maxBranch, nonMaxBranch, pMax);
|
|
5151
|
+
memo.set(key, result);
|
|
5152
|
+
return result;
|
|
5153
|
+
};
|
|
5154
|
+
return f(count, budget);
|
|
5155
|
+
}
|
|
4973
5156
|
function findDie(node) {
|
|
4974
5157
|
switch (node.type) {
|
|
4975
5158
|
case "die":
|
|
@@ -5154,7 +5337,7 @@ function getASTSignature(node) {
|
|
|
5154
5337
|
return `d{${parts.join(",")}}`;
|
|
5155
5338
|
}
|
|
5156
5339
|
case "sum":
|
|
5157
|
-
return `sum{c:${node.count},ch:${getASTSignature(node.child)}}`;
|
|
5340
|
+
return `sum{c:${node.count},b:${node.explodePoolBudget || 0},ch:${getASTSignature(node.child)}}`;
|
|
5158
5341
|
case "d20Roll":
|
|
5159
5342
|
return `d20{t:${node.rollType},ch:${getASTSignature(node.child)}}`;
|
|
5160
5343
|
case "keep":
|
|
@@ -5375,6 +5558,55 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5375
5558
|
weights: { hit: phit, crit: pcrit, miss: pmiss }
|
|
5376
5559
|
};
|
|
5377
5560
|
}
|
|
5561
|
+
/**
|
|
5562
|
+
* For `dice-match` trigger slicing (`turn/types.ts`'s `HasDiceMatchInfo`): the exact
|
|
5563
|
+
* per-damage-value match probability for the hit and crit branches, or `null` per branch when no
|
|
5564
|
+
* descriptor is available — a string-parsed effect, a wrapped transform whose PMF isn't fully
|
|
5565
|
+
* captured by its `RollConfig`s (half/scale/maxOf/pooled — `cacheKey()` returns `null` for
|
|
5566
|
+
* exactly these), a `keep`/`bestOf` pool (ambiguous "the dice" under crit doubling), a
|
|
5567
|
+
* pool-wide exploding budget (composition with match not yet threaded through here), a
|
|
5568
|
+
* multi-die-type pool, a single die (can never match), or (crit) `noCrit()`.
|
|
5569
|
+
*
|
|
5570
|
+
* The crit branch is built from the SAME crit-effect selection `resolve()` uses (an explicit
|
|
5571
|
+
* `onCrit` roll, or the hit dice auto-doubled via `copy().doubleDice()`), so its descriptor
|
|
5572
|
+
* reflects the crit branch's REAL doubled pool, not the hit pool re-used blindly.
|
|
5573
|
+
*/
|
|
5574
|
+
diceMatchInfo(_eps = EPS) {
|
|
5575
|
+
const hit = this.matchInfoForEffect(this.hitEffect);
|
|
5576
|
+
let critEffect;
|
|
5577
|
+
if (this.critEffect === null) {
|
|
5578
|
+
critEffect = void 0;
|
|
5579
|
+
} else if (this.critEffect) {
|
|
5580
|
+
critEffect = this.critEffect;
|
|
5581
|
+
} else if (this.hitEffect instanceof ParsedRollBuilder) {
|
|
5582
|
+
critEffect = void 0;
|
|
5583
|
+
} else {
|
|
5584
|
+
critEffect = this.hitEffect?.copy().doubleDice();
|
|
5585
|
+
}
|
|
5586
|
+
const crit = this.matchInfoForEffect(critEffect);
|
|
5587
|
+
return { hit, crit };
|
|
5588
|
+
}
|
|
5589
|
+
matchInfoForEffect(effect) {
|
|
5590
|
+
if (!effect || effect instanceof ParsedRollBuilder) return null;
|
|
5591
|
+
if (effect.cacheKey() === null) return null;
|
|
5592
|
+
const diceConfigs = effect.getSubRollConfigs().filter((c) => c.sides > 0);
|
|
5593
|
+
if (diceConfigs.length !== 1) return null;
|
|
5594
|
+
const config = diceConfigs[0];
|
|
5595
|
+
if (config.keep || config.bestOf > 0) return null;
|
|
5596
|
+
if (config.explodePoolBudget > 0) return null;
|
|
5597
|
+
if (config.count <= 1) return { matchProbabilityByDamage: /* @__PURE__ */ new Map() };
|
|
5598
|
+
const weights = faceWeights(config.sides, config.minimum, config.reroll);
|
|
5599
|
+
const totalDist = diceSumDistribution(config.count, weights);
|
|
5600
|
+
const joint = jointSumAndMatch(config.count, weights);
|
|
5601
|
+
const modifier = effect.modifier;
|
|
5602
|
+
const matchProbabilityByDamage = /* @__PURE__ */ new Map();
|
|
5603
|
+
for (const [sum, totalMass] of totalDist) {
|
|
5604
|
+
if (totalMass <= 0) continue;
|
|
5605
|
+
const matchMass = joint.get(sum) ?? 0;
|
|
5606
|
+
matchProbabilityByDamage.set(sum + modifier, matchMass / totalMass);
|
|
5607
|
+
}
|
|
5608
|
+
return { matchProbabilityByDamage };
|
|
5609
|
+
}
|
|
5378
5610
|
/**
|
|
5379
5611
|
* A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
|
|
5380
5612
|
* effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
|
|
@@ -5699,14 +5931,15 @@ var TurnSpecError = class extends Error {
|
|
|
5699
5931
|
this.name = "TurnSpecError";
|
|
5700
5932
|
}
|
|
5701
5933
|
};
|
|
5702
|
-
var MAX_TRIGGER_GROUPS =
|
|
5934
|
+
var MAX_TRIGGER_GROUPS = 9;
|
|
5703
5935
|
|
|
5704
5936
|
// src/turn/plan.ts
|
|
5705
|
-
var
|
|
5937
|
+
var READS_GROUP = {
|
|
5706
5938
|
"first-hit": true,
|
|
5707
5939
|
"any-crit": true,
|
|
5708
5940
|
"any-miss": true,
|
|
5709
|
-
"every-hit": true
|
|
5941
|
+
"every-hit": true,
|
|
5942
|
+
"dice-match": true
|
|
5710
5943
|
};
|
|
5711
5944
|
function toPMF(damage, eps, id = "") {
|
|
5712
5945
|
const parts = Array.isArray(damage) ? damage : [damage];
|
|
@@ -5750,20 +5983,49 @@ function critPMF(rider, base, eps) {
|
|
|
5750
5983
|
if (doubled.length === 0) return base;
|
|
5751
5984
|
return PMF.convolveMany(doubled, eps);
|
|
5752
5985
|
}
|
|
5753
|
-
function
|
|
5986
|
+
function diceMatchInfoOf(source, eps) {
|
|
5987
|
+
const capable = source;
|
|
5988
|
+
if (typeof capable.diceMatchInfo === "function") {
|
|
5989
|
+
return capable.diceMatchInfo(eps);
|
|
5990
|
+
}
|
|
5991
|
+
return { hit: null, crit: null };
|
|
5992
|
+
}
|
|
5993
|
+
function sliceSource(pmf, matchInfo) {
|
|
5754
5994
|
const labels = pmf.outcomes();
|
|
5755
5995
|
if (!labels.includes("hit") && !labels.includes("crit")) return null;
|
|
5756
5996
|
const missParts = ["missNone", "missDamage"].filter((label) => labels.includes(label)).map((label) => pmf.filterOutcome(label));
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5997
|
+
const hit = labels.includes("hit") ? pmf.filterOutcome("hit") : PMF.emptyMass();
|
|
5998
|
+
const crit = labels.includes("crit") ? pmf.filterOutcome("crit") : PMF.emptyMass();
|
|
5999
|
+
const miss = missParts.length ? missParts.reduce((all, part) => all.add(part)) : PMF.emptyMass();
|
|
6000
|
+
let hitMatch = null;
|
|
6001
|
+
let hitNoMatch = null;
|
|
6002
|
+
let critMatch = null;
|
|
6003
|
+
let critNoMatch = null;
|
|
6004
|
+
if (matchInfo?.hit) {
|
|
6005
|
+
const info = matchInfo.hit;
|
|
6006
|
+
const [m, nm] = hit.splitByFactor((d2) => info.matchProbabilityByDamage.get(d2) ?? 0);
|
|
6007
|
+
hitMatch = m;
|
|
6008
|
+
hitNoMatch = nm;
|
|
6009
|
+
}
|
|
6010
|
+
if (matchInfo?.crit) {
|
|
6011
|
+
const info = matchInfo.crit;
|
|
6012
|
+
const [m, nm] = crit.splitByFactor((d2) => info.matchProbabilityByDamage.get(d2) ?? 0);
|
|
6013
|
+
critMatch = m;
|
|
6014
|
+
critNoMatch = nm;
|
|
6015
|
+
}
|
|
6016
|
+
return { hit, crit, miss, hitMatch, hitNoMatch, critMatch, critNoMatch };
|
|
5762
6017
|
}
|
|
5763
6018
|
function buildPlan(spec, eps = EPS) {
|
|
5764
6019
|
const fail = (code, id, message) => {
|
|
5765
6020
|
throw new TurnSpecError(code, id, message);
|
|
5766
6021
|
};
|
|
6022
|
+
const riders = spec.riders ?? [];
|
|
6023
|
+
const matchNeededSourceIds = /* @__PURE__ */ new Set();
|
|
6024
|
+
for (const rider of riders) {
|
|
6025
|
+
if (rider.on === "dice-match") {
|
|
6026
|
+
for (const sourceId of rider.of) matchNeededSourceIds.add(sourceId);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
5767
6029
|
const attackIds = [];
|
|
5768
6030
|
const attackPMFs = [];
|
|
5769
6031
|
const attackSlices = [];
|
|
@@ -5773,11 +6035,11 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5773
6035
|
const id = hasWrapper ? named.id : `attack ${index + 1}`;
|
|
5774
6036
|
const source = hasWrapper ? named.source : entry;
|
|
5775
6037
|
const pmf = toPMF(source, eps, id);
|
|
6038
|
+
const matchInfo = matchNeededSourceIds.has(id) ? diceMatchInfoOf(source, eps) : null;
|
|
5776
6039
|
attackIds.push(id);
|
|
5777
6040
|
attackPMFs.push(pmf);
|
|
5778
|
-
attackSlices.push(sliceSource(pmf));
|
|
6041
|
+
attackSlices.push(sliceSource(pmf, matchInfo));
|
|
5779
6042
|
});
|
|
5780
|
-
const riders = spec.riders ?? [];
|
|
5781
6043
|
const riderIds = riders.map((rider, index) => rider.id ?? `rider ${index + 1}`);
|
|
5782
6044
|
const seen = /* @__PURE__ */ new Set();
|
|
5783
6045
|
for (const id of [...attackIds, ...riderIds]) {
|
|
@@ -5786,6 +6048,17 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5786
6048
|
}
|
|
5787
6049
|
const attackIndexById = new Map(attackIds.map((id, index) => [id, index]));
|
|
5788
6050
|
const riderIndexById = new Map(riderIds.map((id, index) => [id, index]));
|
|
6051
|
+
const checkMatchable = (riderId, sourceId, slices) => {
|
|
6052
|
+
const missingHit = slices.hit.mass() > 0 && slices.hitMatch === null;
|
|
6053
|
+
const missingCrit = slices.crit.mass() > 0 && slices.critMatch === null;
|
|
6054
|
+
if (missingHit || missingCrit) {
|
|
6055
|
+
fail(
|
|
6056
|
+
"no-dice-descriptor",
|
|
6057
|
+
sourceId,
|
|
6058
|
+
`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.`
|
|
6059
|
+
);
|
|
6060
|
+
}
|
|
6061
|
+
};
|
|
5789
6062
|
const sourceIdsByRider = riders.map((rider, index) => {
|
|
5790
6063
|
const id = riderIds[index];
|
|
5791
6064
|
if (rider.on === "not-fired") {
|
|
@@ -5810,7 +6083,7 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5810
6083
|
}
|
|
5811
6084
|
return [target];
|
|
5812
6085
|
}
|
|
5813
|
-
const of = [...new Set(rider.of ?? attackIds)];
|
|
6086
|
+
const of = [...new Set(rider.on === "dice-match" ? rider.of : rider.of ?? attackIds)];
|
|
5814
6087
|
if (of.length === 0) {
|
|
5815
6088
|
fail("unknown-id", id, `Rider "${id}" has no sources.`);
|
|
5816
6089
|
}
|
|
@@ -5834,8 +6107,11 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5834
6107
|
`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.`
|
|
5835
6108
|
);
|
|
5836
6109
|
}
|
|
6110
|
+
const damageSource = isAttack ? void 0 : riders[riderIndex].damage;
|
|
6111
|
+
const singleDamageSource = damageSource !== void 0 && !Array.isArray(damageSource) ? damageSource : void 0;
|
|
5837
6112
|
const slices = isAttack ? attackSlices[attackIndexById.get(sourceId)] : sliceSource(
|
|
5838
|
-
toPMF(
|
|
6113
|
+
toPMF(damageSource, eps, sourceId),
|
|
6114
|
+
matchNeededSourceIds.has(sourceId) && singleDamageSource !== void 0 ? diceMatchInfoOf(singleDamageSource, eps) : null
|
|
5839
6115
|
);
|
|
5840
6116
|
if (!slices) {
|
|
5841
6117
|
fail(
|
|
@@ -5843,6 +6119,8 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5843
6119
|
sourceId,
|
|
5844
6120
|
`Rider "${id}" triggers on "${sourceId}", which has no hit/crit outcomes.`
|
|
5845
6121
|
);
|
|
6122
|
+
} else if (rider.on === "dice-match") {
|
|
6123
|
+
checkMatchable(id, sourceId, slices);
|
|
5846
6124
|
}
|
|
5847
6125
|
}
|
|
5848
6126
|
return [...of];
|
|
@@ -5888,7 +6166,7 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5888
6166
|
const perHitGroups = /* @__PURE__ */ new Map();
|
|
5889
6167
|
for (const index of order) {
|
|
5890
6168
|
const rider = riders[index];
|
|
5891
|
-
if (!
|
|
6169
|
+
if (!READS_GROUP[rider.on]) continue;
|
|
5892
6170
|
const group = groupOf(sourceIdsByRider[index]);
|
|
5893
6171
|
readsByRider.set(index, group);
|
|
5894
6172
|
if (rider.on === "every-hit") perHitGroups.set(riderIds[index], group);
|
|
@@ -5918,21 +6196,34 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5918
6196
|
if (!payloads) return slices;
|
|
5919
6197
|
let hit = slices.hit;
|
|
5920
6198
|
let crit = slices.crit;
|
|
6199
|
+
let hitMatch = slices.hitMatch;
|
|
6200
|
+
let hitNoMatch = slices.hitNoMatch;
|
|
6201
|
+
let critMatch = slices.critMatch;
|
|
6202
|
+
let critNoMatch = slices.critNoMatch;
|
|
5921
6203
|
for (const payload of payloads) {
|
|
5922
6204
|
hit = hit.convolve(payload.hit, eps, true);
|
|
5923
6205
|
crit = crit.convolve(payload.crit, eps, true);
|
|
6206
|
+
if (hitMatch) hitMatch = hitMatch.convolve(payload.hit, eps, true);
|
|
6207
|
+
if (hitNoMatch) hitNoMatch = hitNoMatch.convolve(payload.hit, eps, true);
|
|
6208
|
+
if (critMatch) critMatch = critMatch.convolve(payload.crit, eps, true);
|
|
6209
|
+
if (critNoMatch) critNoMatch = critNoMatch.convolve(payload.crit, eps, true);
|
|
5924
6210
|
}
|
|
5925
|
-
return { hit, crit, miss: slices.miss };
|
|
6211
|
+
return { hit, crit, miss: slices.miss, hitMatch, hitNoMatch, critMatch, critNoMatch };
|
|
6212
|
+
};
|
|
6213
|
+
const emptySlices = {
|
|
6214
|
+
hit: PMF.emptyMass(),
|
|
6215
|
+
crit: PMF.emptyMass(),
|
|
6216
|
+
miss: PMF.emptyMass(),
|
|
6217
|
+
hitMatch: null,
|
|
6218
|
+
hitNoMatch: null,
|
|
6219
|
+
critMatch: null,
|
|
6220
|
+
critNoMatch: null
|
|
5926
6221
|
};
|
|
5927
6222
|
const steps = attackIds.map((id, index) => ({
|
|
5928
6223
|
id,
|
|
5929
6224
|
trigger: null,
|
|
5930
6225
|
slices: withPerHit(
|
|
5931
|
-
attackSlices[index] ?? {
|
|
5932
|
-
hit: attackPMFs[index],
|
|
5933
|
-
crit: PMF.emptyMass(),
|
|
5934
|
-
miss: PMF.emptyMass()
|
|
5935
|
-
},
|
|
6226
|
+
attackSlices[index] ?? { ...emptySlices, hit: attackPMFs[index] },
|
|
5936
6227
|
id
|
|
5937
6228
|
),
|
|
5938
6229
|
damage: null,
|
|
@@ -5947,7 +6238,9 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5947
6238
|
if (rider.on === "every-hit") continue;
|
|
5948
6239
|
const id = riderIds[index];
|
|
5949
6240
|
const hit = toPMF(rider.damage, eps, id);
|
|
5950
|
-
const
|
|
6241
|
+
const singleRiderDamage = !Array.isArray(rider.damage) ? rider.damage : void 0;
|
|
6242
|
+
const matchInfo = matchNeededSourceIds.has(id) && singleRiderDamage !== void 0 ? diceMatchInfoOf(singleRiderDamage, eps) : null;
|
|
6243
|
+
const slices = sliceSource(hit, matchInfo);
|
|
5951
6244
|
if (slices && rider.critDamage !== void 0) {
|
|
5952
6245
|
fail(
|
|
5953
6246
|
"unused-crit-damage",
|
|
@@ -5969,6 +6262,15 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5969
6262
|
stepIndexByRider.set(index, steps.length - 1);
|
|
5970
6263
|
riderSteps.set(id, steps.length - 1);
|
|
5971
6264
|
}
|
|
6265
|
+
const groupLastReadStep = new Array(groupSources.length).fill(-1);
|
|
6266
|
+
steps.forEach((step, stepIndex) => {
|
|
6267
|
+
if (step.reads !== -1) {
|
|
6268
|
+
groupLastReadStep[step.reads] = Math.max(groupLastReadStep[step.reads], stepIndex);
|
|
6269
|
+
}
|
|
6270
|
+
});
|
|
6271
|
+
for (const group of perHitGroups.values()) {
|
|
6272
|
+
groupLastReadStep[group] = steps.length;
|
|
6273
|
+
}
|
|
5972
6274
|
return {
|
|
5973
6275
|
steps,
|
|
5974
6276
|
groupCount: groupSources.length,
|
|
@@ -5976,7 +6278,8 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5976
6278
|
attackIds,
|
|
5977
6279
|
riderIds,
|
|
5978
6280
|
riderSteps,
|
|
5979
|
-
perHitGroups
|
|
6281
|
+
perHitGroups,
|
|
6282
|
+
groupLastReadStep
|
|
5980
6283
|
};
|
|
5981
6284
|
}
|
|
5982
6285
|
|
|
@@ -5986,18 +6289,36 @@ var FIRST_HIT = 1;
|
|
|
5986
6289
|
var FIRST_CRIT = 2;
|
|
5987
6290
|
var CRIT_BIT = 2;
|
|
5988
6291
|
var MISS_BIT = 1;
|
|
6292
|
+
var MATCH_BIT = 16;
|
|
5989
6293
|
var START_CODE = FIRST_NONE << 2;
|
|
5990
|
-
function advance(code, outcome) {
|
|
6294
|
+
function advance(code, outcome, matched = false) {
|
|
5991
6295
|
if (outcome === "miss") return code | MISS_BIT;
|
|
5992
|
-
const first = code >> 2;
|
|
6296
|
+
const first = code >> 2 & 3;
|
|
5993
6297
|
const withCrit = outcome === "crit" ? code | CRIT_BIT : code;
|
|
5994
|
-
|
|
6298
|
+
const withMatch = matched ? withCrit | MATCH_BIT : withCrit;
|
|
6299
|
+
if (first !== FIRST_NONE) return withMatch;
|
|
5995
6300
|
const nextFirst = outcome === "crit" ? FIRST_CRIT : FIRST_HIT;
|
|
5996
|
-
return nextFirst << 2 |
|
|
6301
|
+
return nextFirst << 2 | withMatch & (MATCH_BIT | CRIT_BIT | MISS_BIT);
|
|
5997
6302
|
}
|
|
5998
6303
|
|
|
5999
6304
|
// src/turn/turn.ts
|
|
6000
|
-
|
|
6305
|
+
function stepDraws(slices) {
|
|
6306
|
+
const draws = [];
|
|
6307
|
+
if (slices.hitMatch) {
|
|
6308
|
+
draws.push({ outcome: "hit", matched: true, slice: slices.hitMatch });
|
|
6309
|
+
draws.push({ outcome: "hit", matched: false, slice: slices.hitNoMatch });
|
|
6310
|
+
} else {
|
|
6311
|
+
draws.push({ outcome: "hit", matched: false, slice: slices.hit });
|
|
6312
|
+
}
|
|
6313
|
+
if (slices.critMatch) {
|
|
6314
|
+
draws.push({ outcome: "crit", matched: true, slice: slices.critMatch });
|
|
6315
|
+
draws.push({ outcome: "crit", matched: false, slice: slices.critNoMatch });
|
|
6316
|
+
} else {
|
|
6317
|
+
draws.push({ outcome: "crit", matched: false, slice: slices.crit });
|
|
6318
|
+
}
|
|
6319
|
+
draws.push({ outcome: "miss", matched: false, slice: slices.miss });
|
|
6320
|
+
return draws;
|
|
6321
|
+
}
|
|
6001
6322
|
function fireMode(step, codes, firedByStep) {
|
|
6002
6323
|
const trigger = step.trigger;
|
|
6003
6324
|
if (!trigger) return "hit";
|
|
@@ -6005,7 +6326,7 @@ function fireMode(step, codes, firedByStep) {
|
|
|
6005
6326
|
return firedByStep[step.negates] === null ? "hit" : null;
|
|
6006
6327
|
}
|
|
6007
6328
|
const code = codes[step.reads];
|
|
6008
|
-
const first = code >> 2;
|
|
6329
|
+
const first = code >> 2 & 3;
|
|
6009
6330
|
switch (trigger.on) {
|
|
6010
6331
|
case "first-hit":
|
|
6011
6332
|
if (first === FIRST_NONE) return null;
|
|
@@ -6014,6 +6335,8 @@ function fireMode(step, codes, firedByStep) {
|
|
|
6014
6335
|
return (code & CRIT_BIT) !== 0 ? "crit" : null;
|
|
6015
6336
|
case "any-miss":
|
|
6016
6337
|
return (code & MISS_BIT) !== 0 ? "hit" : null;
|
|
6338
|
+
case "dice-match":
|
|
6339
|
+
return (code & MATCH_BIT) !== 0 ? "hit" : null;
|
|
6017
6340
|
default:
|
|
6018
6341
|
return null;
|
|
6019
6342
|
}
|
|
@@ -6141,6 +6464,20 @@ var Turn = class _Turn {
|
|
|
6141
6464
|
riders.push({ ...options, damage, on: "not-fired", of: target });
|
|
6142
6465
|
return new _Turn(this.declaredAttacks, riders, this.eps);
|
|
6143
6466
|
}
|
|
6467
|
+
/**
|
|
6468
|
+
* Fires once if any of `of`'s named sources' own damage dice matched (showed a
|
|
6469
|
+
* duplicate value) on hit or crit — Chromatic Orb's bounce. Unlike the other
|
|
6470
|
+
* `onX` triggers, `of` is required: "the dice matched" has no coherent meaning
|
|
6471
|
+
* defaulted across every declared attack. Each named source must expose a
|
|
6472
|
+
* dice-match descriptor (an `AttackBuilder`-shaped source does); naming one
|
|
6473
|
+
* that doesn't is a `TurnSpecError("no-dice-descriptor", ...)`.
|
|
6474
|
+
*
|
|
6475
|
+
* Most callers want {@link bounce} instead of calling this directly — it
|
|
6476
|
+
* builds the whole depth-capped chain of attack-shaped riders.
|
|
6477
|
+
*/
|
|
6478
|
+
onDiceMatch(of, damage, options = {}) {
|
|
6479
|
+
return this.rider({ ...options, damage, on: "dice-match", of });
|
|
6480
|
+
}
|
|
6144
6481
|
/**
|
|
6145
6482
|
* The exact joint distribution: mass 1, outcome-labelled. Resolved once and
|
|
6146
6483
|
* cached.
|
|
@@ -6213,8 +6550,14 @@ var Turn = class _Turn {
|
|
|
6213
6550
|
let states = /* @__PURE__ */ new Map([[String.fromCharCode(), start]]);
|
|
6214
6551
|
plan.steps.forEach((step, stepIndex) => {
|
|
6215
6552
|
const next = /* @__PURE__ */ new Map();
|
|
6553
|
+
const liveGroups = [];
|
|
6554
|
+
for (let g = 0; g < plan.groupCount; g++) {
|
|
6555
|
+
if (plan.groupLastReadStep[g] > stepIndex) liveGroups.push(g);
|
|
6556
|
+
}
|
|
6216
6557
|
const merge = (state) => {
|
|
6217
|
-
|
|
6558
|
+
let codesKey = "";
|
|
6559
|
+
for (const g of liveGroups) codesKey += String.fromCharCode(state.codes[g]);
|
|
6560
|
+
const key = codesKey + "" + state.fired.map((mode) => mode === null ? "-" : "+").join("");
|
|
6218
6561
|
const existing = next.get(key);
|
|
6219
6562
|
if (existing) existing.pmf = existing.pmf.add(state.pmf);
|
|
6220
6563
|
else next.set(key, state);
|
|
@@ -6240,13 +6583,12 @@ var Turn = class _Turn {
|
|
|
6240
6583
|
});
|
|
6241
6584
|
continue;
|
|
6242
6585
|
}
|
|
6243
|
-
for (const outcome of
|
|
6244
|
-
const slice = step.slices[outcome];
|
|
6586
|
+
for (const { outcome, matched, slice } of stepDraws(step.slices)) {
|
|
6245
6587
|
const sliceMass = slice.mass();
|
|
6246
6588
|
if (sliceMass <= eps) continue;
|
|
6247
6589
|
const codes = [...state.codes];
|
|
6248
6590
|
for (const group of step.updates) {
|
|
6249
|
-
codes[group] = advance(codes[group], outcome);
|
|
6591
|
+
codes[group] = advance(codes[group], outcome, matched);
|
|
6250
6592
|
}
|
|
6251
6593
|
merge({
|
|
6252
6594
|
codes,
|
|
@@ -6270,7 +6612,7 @@ var Turn = class _Turn {
|
|
|
6270
6612
|
}
|
|
6271
6613
|
}
|
|
6272
6614
|
for (const [id, group] of plan.perHitGroups) {
|
|
6273
|
-
if (state.codes[group] >> 2 !== FIRST_NONE) {
|
|
6615
|
+
if ((state.codes[group] >> 2 & 3) !== FIRST_NONE) {
|
|
6274
6616
|
fireMass.set(id, fireMass.get(id) + mass);
|
|
6275
6617
|
}
|
|
6276
6618
|
}
|
|
@@ -6294,6 +6636,19 @@ function turn(attacks = [], eps = EPS) {
|
|
|
6294
6636
|
eps
|
|
6295
6637
|
);
|
|
6296
6638
|
}
|
|
6639
|
+
function bounce({ source, max }) {
|
|
6640
|
+
if (!Number.isInteger(max) || max < 0) {
|
|
6641
|
+
throw new RangeError(`bounce({ max }) needs a non-negative integer, got ${max}.`);
|
|
6642
|
+
}
|
|
6643
|
+
let result = turn(source);
|
|
6644
|
+
let previousId = result.attackIds[0];
|
|
6645
|
+
for (let i = 0; i < max; i++) {
|
|
6646
|
+
const riderId = `bounce ${i + 1}`;
|
|
6647
|
+
result = result.onDiceMatch([previousId], source, { id: riderId });
|
|
6648
|
+
previousId = riderId;
|
|
6649
|
+
}
|
|
6650
|
+
return result;
|
|
6651
|
+
}
|
|
6297
6652
|
|
|
6298
6653
|
exports.ACBuilder = ACBuilder;
|
|
6299
6654
|
exports.AlwaysCritBuilder = AlwaysCritBuilder;
|
|
@@ -6310,6 +6665,7 @@ exports.SaveBuilder = SaveBuilder;
|
|
|
6310
6665
|
exports.ScaleRollBuilder = ScaleRollBuilder;
|
|
6311
6666
|
exports.Turn = Turn;
|
|
6312
6667
|
exports.TurnSpecError = TurnSpecError;
|
|
6668
|
+
exports.bounce = bounce;
|
|
6313
6669
|
exports.builderPMFCache = builderPMFCache;
|
|
6314
6670
|
exports.clearAttackCache = clearAttackCache;
|
|
6315
6671
|
exports.clearDCCache = clearDCCache;
|