@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.js
CHANGED
|
@@ -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();
|
|
@@ -2472,6 +2496,84 @@ var _PMF = class _PMF {
|
|
|
2472
2496
|
_PMF.__anonIdCounter = 1;
|
|
2473
2497
|
var PMF = _PMF;
|
|
2474
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
|
+
|
|
2475
2577
|
// src/pmf/mixture.ts
|
|
2476
2578
|
var Mixture = class _Mixture {
|
|
2477
2579
|
constructor(eps = EPS) {
|
|
@@ -3570,16 +3672,17 @@ var defaultConfig = {
|
|
|
3570
3672
|
modifier: 0,
|
|
3571
3673
|
reroll: 0,
|
|
3572
3674
|
explode: 0,
|
|
3675
|
+
explodePoolBudget: 0,
|
|
3573
3676
|
minimum: 0,
|
|
3574
3677
|
bestOf: 0,
|
|
3575
3678
|
keep: void 0,
|
|
3576
3679
|
rollType: "flat"
|
|
3577
3680
|
};
|
|
3578
3681
|
var rollConfigsEqual = (a, b) => {
|
|
3579
|
-
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;
|
|
3580
3683
|
};
|
|
3581
3684
|
var configComplexityScore = (config) => {
|
|
3582
|
-
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);
|
|
3583
3686
|
};
|
|
3584
3687
|
var RollBuilder = class _RollBuilder {
|
|
3585
3688
|
constructor(countOrConfigs = 1) {
|
|
@@ -3761,10 +3864,35 @@ var RollBuilder = class _RollBuilder {
|
|
|
3761
3864
|
if (count === void 0) return this;
|
|
3762
3865
|
if (count === 0) return this;
|
|
3763
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
|
+
}
|
|
3764
3872
|
const newConfigs = this.getSubRollConfigs();
|
|
3765
3873
|
newConfigs[newConfigs.length - 1].explode = count;
|
|
3766
3874
|
return this.create(newConfigs);
|
|
3767
3875
|
}
|
|
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
|
+
}
|
|
3768
3896
|
/** Apply per-die minimum value (floors each die roll at `val`, e.g. `minimum(3)` treats a 1 or
|
|
3769
3897
|
* 2 as a 3 -- the 2024 Great Weapon Fighting style). */
|
|
3770
3898
|
minimum(val) {
|
|
@@ -4003,6 +4131,11 @@ var RollBuilder = class _RollBuilder {
|
|
|
4003
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().`
|
|
4004
4132
|
);
|
|
4005
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
|
+
}
|
|
4006
4139
|
let baseDie = `d${config.sides}`;
|
|
4007
4140
|
const rerollClause = config.reroll > 0 ? config.reroll === 1 ? " reroll 1" : ` reroll d${config.reroll}` : "";
|
|
4008
4141
|
if (config.reroll > 0) {
|
|
@@ -4501,6 +4634,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
|
|
|
4501
4634
|
explode(_count = Infinity) {
|
|
4502
4635
|
throw new Error("Cannot set explode on a pooled roll.");
|
|
4503
4636
|
}
|
|
4637
|
+
explodePool(_budget) {
|
|
4638
|
+
throw new Error("Cannot set explodePool on a pooled roll.");
|
|
4639
|
+
}
|
|
4504
4640
|
minimum(_val) {
|
|
4505
4641
|
throw new Error("Cannot set minimum on a pooled roll.");
|
|
4506
4642
|
}
|
|
@@ -4750,6 +4886,11 @@ function astFromRollConfigs(configs) {
|
|
|
4750
4886
|
appliedRollType = true;
|
|
4751
4887
|
}
|
|
4752
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
|
+
}
|
|
4753
4894
|
const baseCount = Math.max(1, Math.floor(Math.abs(count || 1)));
|
|
4754
4895
|
const trials = Math.max(1, Math.floor(effectiveKeep.total));
|
|
4755
4896
|
const k = Math.max(0, Math.floor(effectiveKeep.count));
|
|
@@ -4800,7 +4941,17 @@ function astFromRollConfigs(configs) {
|
|
|
4800
4941
|
}
|
|
4801
4942
|
} else {
|
|
4802
4943
|
const c = appliedRollType ? 1 : Math.max(1, count || 1);
|
|
4803
|
-
|
|
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
|
+
};
|
|
4804
4955
|
}
|
|
4805
4956
|
children.push({ node, sign });
|
|
4806
4957
|
}
|
|
@@ -4831,6 +4982,13 @@ function resolve(node, eps = defaultEps) {
|
|
|
4831
4982
|
const base = resolve(node.child, eps);
|
|
4832
4983
|
const n = Math.max(0, Math.floor(node.count));
|
|
4833
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
|
+
}
|
|
4834
4992
|
if (n === 1) return base;
|
|
4835
4993
|
return base.power(n, eps);
|
|
4836
4994
|
}
|
|
@@ -4968,6 +5126,31 @@ function resolveSingleDie(die, eps = defaultEps) {
|
|
|
4968
5126
|
singleDiePMFCache.set(cacheKey, pmf);
|
|
4969
5127
|
return pmf;
|
|
4970
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
|
+
}
|
|
4971
5154
|
function findDie(node) {
|
|
4972
5155
|
switch (node.type) {
|
|
4973
5156
|
case "die":
|
|
@@ -5152,7 +5335,7 @@ function getASTSignature(node) {
|
|
|
5152
5335
|
return `d{${parts.join(",")}}`;
|
|
5153
5336
|
}
|
|
5154
5337
|
case "sum":
|
|
5155
|
-
return `sum{c:${node.count},ch:${getASTSignature(node.child)}}`;
|
|
5338
|
+
return `sum{c:${node.count},b:${node.explodePoolBudget || 0},ch:${getASTSignature(node.child)}}`;
|
|
5156
5339
|
case "d20Roll":
|
|
5157
5340
|
return `d20{t:${node.rollType},ch:${getASTSignature(node.child)}}`;
|
|
5158
5341
|
case "keep":
|
|
@@ -5373,6 +5556,55 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5373
5556
|
weights: { hit: phit, crit: pcrit, miss: pmiss }
|
|
5374
5557
|
};
|
|
5375
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
|
+
}
|
|
5376
5608
|
/**
|
|
5377
5609
|
* A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
|
|
5378
5610
|
* effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
|
|
@@ -5697,14 +5929,15 @@ var TurnSpecError = class extends Error {
|
|
|
5697
5929
|
this.name = "TurnSpecError";
|
|
5698
5930
|
}
|
|
5699
5931
|
};
|
|
5700
|
-
var MAX_TRIGGER_GROUPS =
|
|
5932
|
+
var MAX_TRIGGER_GROUPS = 9;
|
|
5701
5933
|
|
|
5702
5934
|
// src/turn/plan.ts
|
|
5703
|
-
var
|
|
5935
|
+
var READS_GROUP = {
|
|
5704
5936
|
"first-hit": true,
|
|
5705
5937
|
"any-crit": true,
|
|
5706
5938
|
"any-miss": true,
|
|
5707
|
-
"every-hit": true
|
|
5939
|
+
"every-hit": true,
|
|
5940
|
+
"dice-match": true
|
|
5708
5941
|
};
|
|
5709
5942
|
function toPMF(damage, eps, id = "") {
|
|
5710
5943
|
const parts = Array.isArray(damage) ? damage : [damage];
|
|
@@ -5748,20 +5981,49 @@ function critPMF(rider, base, eps) {
|
|
|
5748
5981
|
if (doubled.length === 0) return base;
|
|
5749
5982
|
return PMF.convolveMany(doubled, eps);
|
|
5750
5983
|
}
|
|
5751
|
-
function
|
|
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) {
|
|
5752
5992
|
const labels = pmf.outcomes();
|
|
5753
5993
|
if (!labels.includes("hit") && !labels.includes("crit")) return null;
|
|
5754
5994
|
const missParts = ["missNone", "missDamage"].filter((label) => labels.includes(label)).map((label) => pmf.filterOutcome(label));
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
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 };
|
|
5760
6015
|
}
|
|
5761
6016
|
function buildPlan(spec, eps = EPS) {
|
|
5762
6017
|
const fail = (code, id, message) => {
|
|
5763
6018
|
throw new TurnSpecError(code, id, message);
|
|
5764
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
|
+
}
|
|
5765
6027
|
const attackIds = [];
|
|
5766
6028
|
const attackPMFs = [];
|
|
5767
6029
|
const attackSlices = [];
|
|
@@ -5771,11 +6033,11 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5771
6033
|
const id = hasWrapper ? named.id : `attack ${index + 1}`;
|
|
5772
6034
|
const source = hasWrapper ? named.source : entry;
|
|
5773
6035
|
const pmf = toPMF(source, eps, id);
|
|
6036
|
+
const matchInfo = matchNeededSourceIds.has(id) ? diceMatchInfoOf(source, eps) : null;
|
|
5774
6037
|
attackIds.push(id);
|
|
5775
6038
|
attackPMFs.push(pmf);
|
|
5776
|
-
attackSlices.push(sliceSource(pmf));
|
|
6039
|
+
attackSlices.push(sliceSource(pmf, matchInfo));
|
|
5777
6040
|
});
|
|
5778
|
-
const riders = spec.riders ?? [];
|
|
5779
6041
|
const riderIds = riders.map((rider, index) => rider.id ?? `rider ${index + 1}`);
|
|
5780
6042
|
const seen = /* @__PURE__ */ new Set();
|
|
5781
6043
|
for (const id of [...attackIds, ...riderIds]) {
|
|
@@ -5784,6 +6046,17 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5784
6046
|
}
|
|
5785
6047
|
const attackIndexById = new Map(attackIds.map((id, index) => [id, index]));
|
|
5786
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
|
+
};
|
|
5787
6060
|
const sourceIdsByRider = riders.map((rider, index) => {
|
|
5788
6061
|
const id = riderIds[index];
|
|
5789
6062
|
if (rider.on === "not-fired") {
|
|
@@ -5808,7 +6081,7 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5808
6081
|
}
|
|
5809
6082
|
return [target];
|
|
5810
6083
|
}
|
|
5811
|
-
const of = [...new Set(rider.of ?? attackIds)];
|
|
6084
|
+
const of = [...new Set(rider.on === "dice-match" ? rider.of : rider.of ?? attackIds)];
|
|
5812
6085
|
if (of.length === 0) {
|
|
5813
6086
|
fail("unknown-id", id, `Rider "${id}" has no sources.`);
|
|
5814
6087
|
}
|
|
@@ -5832,8 +6105,11 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5832
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.`
|
|
5833
6106
|
);
|
|
5834
6107
|
}
|
|
6108
|
+
const damageSource = isAttack ? void 0 : riders[riderIndex].damage;
|
|
6109
|
+
const singleDamageSource = damageSource !== void 0 && !Array.isArray(damageSource) ? damageSource : void 0;
|
|
5835
6110
|
const slices = isAttack ? attackSlices[attackIndexById.get(sourceId)] : sliceSource(
|
|
5836
|
-
toPMF(
|
|
6111
|
+
toPMF(damageSource, eps, sourceId),
|
|
6112
|
+
matchNeededSourceIds.has(sourceId) && singleDamageSource !== void 0 ? diceMatchInfoOf(singleDamageSource, eps) : null
|
|
5837
6113
|
);
|
|
5838
6114
|
if (!slices) {
|
|
5839
6115
|
fail(
|
|
@@ -5841,6 +6117,8 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5841
6117
|
sourceId,
|
|
5842
6118
|
`Rider "${id}" triggers on "${sourceId}", which has no hit/crit outcomes.`
|
|
5843
6119
|
);
|
|
6120
|
+
} else if (rider.on === "dice-match") {
|
|
6121
|
+
checkMatchable(id, sourceId, slices);
|
|
5844
6122
|
}
|
|
5845
6123
|
}
|
|
5846
6124
|
return [...of];
|
|
@@ -5886,7 +6164,7 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5886
6164
|
const perHitGroups = /* @__PURE__ */ new Map();
|
|
5887
6165
|
for (const index of order) {
|
|
5888
6166
|
const rider = riders[index];
|
|
5889
|
-
if (!
|
|
6167
|
+
if (!READS_GROUP[rider.on]) continue;
|
|
5890
6168
|
const group = groupOf(sourceIdsByRider[index]);
|
|
5891
6169
|
readsByRider.set(index, group);
|
|
5892
6170
|
if (rider.on === "every-hit") perHitGroups.set(riderIds[index], group);
|
|
@@ -5916,21 +6194,34 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5916
6194
|
if (!payloads) return slices;
|
|
5917
6195
|
let hit = slices.hit;
|
|
5918
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;
|
|
5919
6201
|
for (const payload of payloads) {
|
|
5920
6202
|
hit = hit.convolve(payload.hit, eps, true);
|
|
5921
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);
|
|
5922
6208
|
}
|
|
5923
|
-
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
|
|
5924
6219
|
};
|
|
5925
6220
|
const steps = attackIds.map((id, index) => ({
|
|
5926
6221
|
id,
|
|
5927
6222
|
trigger: null,
|
|
5928
6223
|
slices: withPerHit(
|
|
5929
|
-
attackSlices[index] ?? {
|
|
5930
|
-
hit: attackPMFs[index],
|
|
5931
|
-
crit: PMF.emptyMass(),
|
|
5932
|
-
miss: PMF.emptyMass()
|
|
5933
|
-
},
|
|
6224
|
+
attackSlices[index] ?? { ...emptySlices, hit: attackPMFs[index] },
|
|
5934
6225
|
id
|
|
5935
6226
|
),
|
|
5936
6227
|
damage: null,
|
|
@@ -5945,7 +6236,9 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5945
6236
|
if (rider.on === "every-hit") continue;
|
|
5946
6237
|
const id = riderIds[index];
|
|
5947
6238
|
const hit = toPMF(rider.damage, eps, id);
|
|
5948
|
-
const
|
|
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);
|
|
5949
6242
|
if (slices && rider.critDamage !== void 0) {
|
|
5950
6243
|
fail(
|
|
5951
6244
|
"unused-crit-damage",
|
|
@@ -5967,6 +6260,15 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5967
6260
|
stepIndexByRider.set(index, steps.length - 1);
|
|
5968
6261
|
riderSteps.set(id, steps.length - 1);
|
|
5969
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
|
+
}
|
|
5970
6272
|
return {
|
|
5971
6273
|
steps,
|
|
5972
6274
|
groupCount: groupSources.length,
|
|
@@ -5974,7 +6276,8 @@ function buildPlan(spec, eps = EPS) {
|
|
|
5974
6276
|
attackIds,
|
|
5975
6277
|
riderIds,
|
|
5976
6278
|
riderSteps,
|
|
5977
|
-
perHitGroups
|
|
6279
|
+
perHitGroups,
|
|
6280
|
+
groupLastReadStep
|
|
5978
6281
|
};
|
|
5979
6282
|
}
|
|
5980
6283
|
|
|
@@ -5984,18 +6287,36 @@ var FIRST_HIT = 1;
|
|
|
5984
6287
|
var FIRST_CRIT = 2;
|
|
5985
6288
|
var CRIT_BIT = 2;
|
|
5986
6289
|
var MISS_BIT = 1;
|
|
6290
|
+
var MATCH_BIT = 16;
|
|
5987
6291
|
var START_CODE = FIRST_NONE << 2;
|
|
5988
|
-
function advance(code, outcome) {
|
|
6292
|
+
function advance(code, outcome, matched = false) {
|
|
5989
6293
|
if (outcome === "miss") return code | MISS_BIT;
|
|
5990
|
-
const first = code >> 2;
|
|
6294
|
+
const first = code >> 2 & 3;
|
|
5991
6295
|
const withCrit = outcome === "crit" ? code | CRIT_BIT : code;
|
|
5992
|
-
|
|
6296
|
+
const withMatch = matched ? withCrit | MATCH_BIT : withCrit;
|
|
6297
|
+
if (first !== FIRST_NONE) return withMatch;
|
|
5993
6298
|
const nextFirst = outcome === "crit" ? FIRST_CRIT : FIRST_HIT;
|
|
5994
|
-
return nextFirst << 2 |
|
|
6299
|
+
return nextFirst << 2 | withMatch & (MATCH_BIT | CRIT_BIT | MISS_BIT);
|
|
5995
6300
|
}
|
|
5996
6301
|
|
|
5997
6302
|
// src/turn/turn.ts
|
|
5998
|
-
|
|
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
|
+
}
|
|
5999
6320
|
function fireMode(step, codes, firedByStep) {
|
|
6000
6321
|
const trigger = step.trigger;
|
|
6001
6322
|
if (!trigger) return "hit";
|
|
@@ -6003,7 +6324,7 @@ function fireMode(step, codes, firedByStep) {
|
|
|
6003
6324
|
return firedByStep[step.negates] === null ? "hit" : null;
|
|
6004
6325
|
}
|
|
6005
6326
|
const code = codes[step.reads];
|
|
6006
|
-
const first = code >> 2;
|
|
6327
|
+
const first = code >> 2 & 3;
|
|
6007
6328
|
switch (trigger.on) {
|
|
6008
6329
|
case "first-hit":
|
|
6009
6330
|
if (first === FIRST_NONE) return null;
|
|
@@ -6012,6 +6333,8 @@ function fireMode(step, codes, firedByStep) {
|
|
|
6012
6333
|
return (code & CRIT_BIT) !== 0 ? "crit" : null;
|
|
6013
6334
|
case "any-miss":
|
|
6014
6335
|
return (code & MISS_BIT) !== 0 ? "hit" : null;
|
|
6336
|
+
case "dice-match":
|
|
6337
|
+
return (code & MATCH_BIT) !== 0 ? "hit" : null;
|
|
6015
6338
|
default:
|
|
6016
6339
|
return null;
|
|
6017
6340
|
}
|
|
@@ -6139,6 +6462,20 @@ var Turn = class _Turn {
|
|
|
6139
6462
|
riders.push({ ...options, damage, on: "not-fired", of: target });
|
|
6140
6463
|
return new _Turn(this.declaredAttacks, riders, this.eps);
|
|
6141
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
|
+
}
|
|
6142
6479
|
/**
|
|
6143
6480
|
* The exact joint distribution: mass 1, outcome-labelled. Resolved once and
|
|
6144
6481
|
* cached.
|
|
@@ -6211,8 +6548,14 @@ var Turn = class _Turn {
|
|
|
6211
6548
|
let states = /* @__PURE__ */ new Map([[String.fromCharCode(), start]]);
|
|
6212
6549
|
plan.steps.forEach((step, stepIndex) => {
|
|
6213
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
|
+
}
|
|
6214
6555
|
const merge = (state) => {
|
|
6215
|
-
|
|
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("");
|
|
6216
6559
|
const existing = next.get(key);
|
|
6217
6560
|
if (existing) existing.pmf = existing.pmf.add(state.pmf);
|
|
6218
6561
|
else next.set(key, state);
|
|
@@ -6238,13 +6581,12 @@ var Turn = class _Turn {
|
|
|
6238
6581
|
});
|
|
6239
6582
|
continue;
|
|
6240
6583
|
}
|
|
6241
|
-
for (const outcome of
|
|
6242
|
-
const slice = step.slices[outcome];
|
|
6584
|
+
for (const { outcome, matched, slice } of stepDraws(step.slices)) {
|
|
6243
6585
|
const sliceMass = slice.mass();
|
|
6244
6586
|
if (sliceMass <= eps) continue;
|
|
6245
6587
|
const codes = [...state.codes];
|
|
6246
6588
|
for (const group of step.updates) {
|
|
6247
|
-
codes[group] = advance(codes[group], outcome);
|
|
6589
|
+
codes[group] = advance(codes[group], outcome, matched);
|
|
6248
6590
|
}
|
|
6249
6591
|
merge({
|
|
6250
6592
|
codes,
|
|
@@ -6268,7 +6610,7 @@ var Turn = class _Turn {
|
|
|
6268
6610
|
}
|
|
6269
6611
|
}
|
|
6270
6612
|
for (const [id, group] of plan.perHitGroups) {
|
|
6271
|
-
if (state.codes[group] >> 2 !== FIRST_NONE) {
|
|
6613
|
+
if ((state.codes[group] >> 2 & 3) !== FIRST_NONE) {
|
|
6272
6614
|
fireMass.set(id, fireMass.get(id) + mass);
|
|
6273
6615
|
}
|
|
6274
6616
|
}
|
|
@@ -6292,7 +6634,20 @@ function turn(attacks = [], eps = EPS) {
|
|
|
6292
6634
|
eps
|
|
6293
6635
|
);
|
|
6294
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
|
+
}
|
|
6295
6650
|
|
|
6296
|
-
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 };
|
|
6297
6652
|
//# sourceMappingURL=index.js.map
|
|
6298
6653
|
//# sourceMappingURL=index.js.map
|