@yipe/dice 0.8.0 → 0.9.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/CHANGELOG.md +456 -0
- package/README.md +169 -19
- package/dist/builder/dc.d.ts +8 -0
- package/dist/builder/dc.d.ts.map +1 -1
- package/dist/builder/index.cjs +735 -10
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.ts +1 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +729 -11
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/roll.d.ts +2 -0
- package/dist/builder/roll.d.ts.map +1 -1
- package/dist/builder/save.d.ts +9 -0
- package/dist/builder/save.d.ts.map +1 -1
- package/dist/index.cjs +114 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/rollType.d.ts +58 -0
- package/dist/parser/rollType.d.ts.map +1 -0
- package/dist/pmf/query.d.ts +26 -0
- package/dist/pmf/query.d.ts.map +1 -1
- package/dist/turn/index.d.ts +3 -0
- package/dist/turn/index.d.ts.map +1 -0
- package/dist/turn/plan.d.ts +55 -0
- package/dist/turn/plan.d.ts.map +1 -0
- package/dist/turn/state.d.ts +23 -0
- package/dist/turn/state.d.ts.map +1 -0
- package/dist/turn/turn.d.ts +149 -0
- package/dist/turn/turn.d.ts.map +1 -0
- package/dist/turn/types.d.ts +99 -0
- package/dist/turn/types.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/builder/index.cjs
CHANGED
|
@@ -117,6 +117,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
117
117
|
if (this._combinedWithAttr) {
|
|
118
118
|
return this._combinedWithAttr;
|
|
119
119
|
}
|
|
120
|
+
if (this._combinedProvided) {
|
|
121
|
+
this._combinedWithAttr = this.combined.withAttribution();
|
|
122
|
+
return this._combinedWithAttr;
|
|
123
|
+
}
|
|
120
124
|
if (this.singles.every((pmf) => pmf.hasAttribution())) {
|
|
121
125
|
this._combinedWithAttr = this.combined;
|
|
122
126
|
return this._combinedWithAttr;
|
|
@@ -887,6 +891,55 @@ var _DiceQuery = class _DiceQuery {
|
|
|
887
891
|
}
|
|
888
892
|
return out;
|
|
889
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Per-outcome probabilities and damage ranges, aggregated over the individual
|
|
896
|
+
* singles rather than read off the combined distribution.
|
|
897
|
+
*
|
|
898
|
+
* `damageRange` is the sum, over every single that can produce the outcome, of
|
|
899
|
+
* that single's own conditional damage range: "what this outcome contributes
|
|
900
|
+
* across the whole turn when every attack that can produce it does". Linear in
|
|
901
|
+
* the number of attacks by construction.
|
|
902
|
+
*
|
|
903
|
+
* Prefer this over {@link DiceQuery.snapshot} for a multi-attack query.
|
|
904
|
+
* `snapshot` reads `damageRange` off the combined PMF's `count`, which the
|
|
905
|
+
* convolution accumulates as an expected count, so its `avg` is size-biased
|
|
906
|
+
* for N≥2 (its own doc comment says so). The two agree for a single attack.
|
|
907
|
+
*
|
|
908
|
+
* Only outcomes that actually occur appear in the result.
|
|
909
|
+
*
|
|
910
|
+
* Like every `singles`-based helper on this class, it describes the singles
|
|
911
|
+
* and not an explicitly provided `combined`. `Turn.toQuery()` supplies one whose
|
|
912
|
+
* distribution also contains rider attacks that are absent from `singles`
|
|
913
|
+
* (an `otherwise([unarmed, unarmed])` flurry, say), so those attacks do not
|
|
914
|
+
* appear here. For rider-inclusive figures read the combined distribution
|
|
915
|
+
* directly: {@link DiceQuery.outcomeTotals}, {@link DiceQuery.outcomeDamageRanges}.
|
|
916
|
+
*
|
|
917
|
+
* @param outcomes Which outcomes to consider; defaults to every canonical one.
|
|
918
|
+
*/
|
|
919
|
+
outcomeStats(outcomes = ALL_OUTCOME_TYPES) {
|
|
920
|
+
const stats = /* @__PURE__ */ new Map();
|
|
921
|
+
const perSingle = this.singles.map((pmf) => new _DiceQuery([pmf], void 0, this._eps));
|
|
922
|
+
for (const outcome of outcomes) {
|
|
923
|
+
const atLeastOneProbability = this.probAtLeastOne(outcome);
|
|
924
|
+
if (atLeastOneProbability <= 0) continue;
|
|
925
|
+
const damageRange = { min: 0, avg: 0, max: 0 };
|
|
926
|
+
let contributors = 0;
|
|
927
|
+
for (const single of perSingle) {
|
|
928
|
+
if (single.probAtLeastOne(outcome) <= 0) continue;
|
|
929
|
+
contributors++;
|
|
930
|
+
const stat = single.damageStatsFrom(outcome);
|
|
931
|
+
damageRange.min += stat.min;
|
|
932
|
+
damageRange.avg += stat.avg;
|
|
933
|
+
damageRange.max += stat.max;
|
|
934
|
+
}
|
|
935
|
+
stats.set(outcome, {
|
|
936
|
+
atLeastOneProbability,
|
|
937
|
+
allProbability: contributors > 0 ? this.probExactlyK(outcome, contributors) : 0,
|
|
938
|
+
damageRange
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return stats;
|
|
942
|
+
}
|
|
890
943
|
/**
|
|
891
944
|
* Snapshot of the distribution in the exact shape the UI consumes.
|
|
892
945
|
* - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
|
|
@@ -3454,6 +3507,10 @@ function d20PMF(rerollOne) {
|
|
|
3454
3507
|
}
|
|
3455
3508
|
|
|
3456
3509
|
// src/builder/roll.ts
|
|
3510
|
+
var rollPMFCache = new LRUCache(4e3);
|
|
3511
|
+
function clearRollCache() {
|
|
3512
|
+
rollPMFCache.clear();
|
|
3513
|
+
}
|
|
3457
3514
|
var defaultConfig = {
|
|
3458
3515
|
count: 1,
|
|
3459
3516
|
sides: 0,
|
|
@@ -3866,8 +3923,16 @@ var RollBuilder = class _RollBuilder {
|
|
|
3866
3923
|
}
|
|
3867
3924
|
return result.replace(/\+ -/g, "-");
|
|
3868
3925
|
}
|
|
3926
|
+
// Main AST entry point. Cached by the cheap config key across identical rebuilds; see `rollPMFCache`.
|
|
3869
3927
|
toPMF(eps = 0) {
|
|
3870
|
-
|
|
3928
|
+
const key = this.cacheKey();
|
|
3929
|
+
if (key === null) return pmfFromRollBuilder(this, eps);
|
|
3930
|
+
const fullKey = `${key}*e${eps}`;
|
|
3931
|
+
const cached = rollPMFCache.get(fullKey);
|
|
3932
|
+
if (cached) return cached;
|
|
3933
|
+
const pmf = pmfFromRollBuilder(this, eps);
|
|
3934
|
+
rollPMFCache.set(fullKey, pmf);
|
|
3935
|
+
return pmf;
|
|
3871
3936
|
}
|
|
3872
3937
|
get pmf() {
|
|
3873
3938
|
return this.toPMF();
|
|
@@ -5127,11 +5192,11 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5127
5192
|
pMiss: pmiss
|
|
5128
5193
|
} = this.resolveProbabilities(this.check, eps);
|
|
5129
5194
|
const hitPMF = this.hitEffect ? this.hitEffect instanceof ParsedRollBuilder ? this.hitEffect.toPMF(eps) : pmfFromRollBuilder(this.hitEffect, eps) : PMF.delta(0, eps);
|
|
5130
|
-
let
|
|
5195
|
+
let critPMF2 = null;
|
|
5131
5196
|
let phit = pHit;
|
|
5132
5197
|
let pcrit = pCrit;
|
|
5133
5198
|
if (this.critEffect === null) {
|
|
5134
|
-
|
|
5199
|
+
critPMF2 = null;
|
|
5135
5200
|
phit += pcrit;
|
|
5136
5201
|
pcrit = 0;
|
|
5137
5202
|
} else {
|
|
@@ -5139,7 +5204,7 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5139
5204
|
if (this.critEffect) {
|
|
5140
5205
|
critBuilder = this.critEffect;
|
|
5141
5206
|
} else if (this.hitEffect instanceof ParsedRollBuilder) {
|
|
5142
|
-
|
|
5207
|
+
critPMF2 = null;
|
|
5143
5208
|
phit += pcrit;
|
|
5144
5209
|
pcrit = 0;
|
|
5145
5210
|
critBuilder = void 0;
|
|
@@ -5147,20 +5212,20 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
5147
5212
|
critBuilder = this.hitEffect?.copy().doubleDice();
|
|
5148
5213
|
}
|
|
5149
5214
|
if (critBuilder) {
|
|
5150
|
-
|
|
5215
|
+
critPMF2 = critBuilder instanceof ParsedRollBuilder ? critBuilder.toPMF(eps) : pmfFromRollBuilder(critBuilder, eps);
|
|
5151
5216
|
}
|
|
5152
5217
|
}
|
|
5153
5218
|
const missPMF = this.missEffect ? this.missEffect instanceof ParsedRollBuilder ? this.missEffect.toPMF(eps) : pmfFromRollBuilder(this.missEffect, eps) : PMF.delta(0, eps);
|
|
5154
5219
|
const mix = new Mixture(eps);
|
|
5155
5220
|
if (phit > 0) mix.add("hit", hitPMF, phit);
|
|
5156
|
-
if (
|
|
5221
|
+
if (critPMF2 && pcrit > 0) mix.add("crit", critPMF2, pcrit);
|
|
5157
5222
|
if (pmiss > 0)
|
|
5158
5223
|
mix.add(this.missEffect ? "missDamage" : "missNone", missPMF, pmiss);
|
|
5159
5224
|
return {
|
|
5160
5225
|
pmf: mix.buildPMF(eps) ?? PMF.delta(0, eps),
|
|
5161
5226
|
check: this.check.toPMF(eps) ?? PMF.delta(0, eps),
|
|
5162
5227
|
hit: hitPMF ?? PMF.delta(0, eps),
|
|
5163
|
-
crit:
|
|
5228
|
+
crit: critPMF2 ?? PMF.delta(0, eps),
|
|
5164
5229
|
miss: missPMF ?? PMF.delta(0, eps),
|
|
5165
5230
|
weights: { hit: phit, crit: pcrit, miss: pmiss }
|
|
5166
5231
|
};
|
|
@@ -5296,6 +5361,10 @@ RollBuilder.prototype.ac = function(targetAC) {
|
|
|
5296
5361
|
};
|
|
5297
5362
|
|
|
5298
5363
|
// src/builder/save.ts
|
|
5364
|
+
var savePMFCache = new LRUCache(4e3);
|
|
5365
|
+
function clearSaveCache() {
|
|
5366
|
+
savePMFCache.clear();
|
|
5367
|
+
}
|
|
5299
5368
|
var SaveBuilder = class _SaveBuilder {
|
|
5300
5369
|
constructor(check, failureEffect, saveOutcome = "normal") {
|
|
5301
5370
|
this.check = check;
|
|
@@ -5332,9 +5401,32 @@ var SaveBuilder = class _SaveBuilder {
|
|
|
5332
5401
|
weights: { success: psuccess, fail: pfail }
|
|
5333
5402
|
};
|
|
5334
5403
|
}
|
|
5335
|
-
|
|
5404
|
+
/**
|
|
5405
|
+
* A cheap, complete key for this save's resolved PMF, or `null` when it can't be cached soundly.
|
|
5406
|
+
* {@link resolve} reads exactly three things: the DC check (via `resolveProbabilities`), the failure
|
|
5407
|
+
* effect's PMF, and the save outcome — so composing their keys pins it. A `ParsedRollBuilder` failure
|
|
5408
|
+
* effect returns `null`, which correctly forces this uncached.
|
|
5409
|
+
*/
|
|
5410
|
+
cacheKey(eps) {
|
|
5411
|
+
const checkKey = this.check.cacheKey();
|
|
5412
|
+
if (checkKey === null) return null;
|
|
5413
|
+
let failKey = "";
|
|
5414
|
+
if (this.failureEffect) {
|
|
5415
|
+
const k = this.failureEffect.cacheKey();
|
|
5416
|
+
if (k === null) return null;
|
|
5417
|
+
failKey = k;
|
|
5418
|
+
}
|
|
5419
|
+
return `${checkKey}*F${failKey}*O${this.saveOutcome}*e${eps}`;
|
|
5420
|
+
}
|
|
5421
|
+
// By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
|
|
5336
5422
|
toPMF(eps = 0) {
|
|
5337
|
-
|
|
5423
|
+
const key = this.cacheKey(eps);
|
|
5424
|
+
if (key === null) return this.resolve(eps).pmf;
|
|
5425
|
+
const cached = savePMFCache.get(key);
|
|
5426
|
+
if (cached) return cached;
|
|
5427
|
+
const pmf = this.resolve(eps).pmf;
|
|
5428
|
+
savePMFCache.set(key, pmf);
|
|
5429
|
+
return pmf;
|
|
5338
5430
|
}
|
|
5339
5431
|
get pmf() {
|
|
5340
5432
|
return this.toPMF();
|
|
@@ -5370,6 +5462,10 @@ function resolveProbabilities(check) {
|
|
|
5370
5462
|
}
|
|
5371
5463
|
|
|
5372
5464
|
// src/builder/dc.ts
|
|
5465
|
+
var dcPMFCache = new LRUCache(4e3);
|
|
5466
|
+
function clearDCCache() {
|
|
5467
|
+
dcPMFCache.clear();
|
|
5468
|
+
}
|
|
5373
5469
|
var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
5374
5470
|
constructor(baseRoll, saveConfig) {
|
|
5375
5471
|
super(baseRoll.getSubRollConfigs());
|
|
@@ -5410,7 +5506,22 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
|
5410
5506
|
const expression = new RollBuilder(allConfigs).toExpression();
|
|
5411
5507
|
return `(${expression} DC ${this.saveConfig.dc})`;
|
|
5412
5508
|
}
|
|
5509
|
+
/**
|
|
5510
|
+
* The DC check's PMF is fully determined by the save DC plus everything `toPMF` below reads off the roll
|
|
5511
|
+
* configs (`rollType`, `baseReroll`, `modifier`, bonus dice) — all of which `super.cacheKey()` already
|
|
5512
|
+
* serializes. So extend the base key with the DC, mirroring {@link AlwaysHitBuilder.cacheKey}.
|
|
5513
|
+
*/
|
|
5514
|
+
cacheKey() {
|
|
5515
|
+
const base = super.cacheKey();
|
|
5516
|
+
return base === null ? null : `DC|${this.saveConfig.dc}|${base}`;
|
|
5517
|
+
}
|
|
5413
5518
|
toPMF(eps = 0) {
|
|
5519
|
+
const key = this.cacheKey();
|
|
5520
|
+
const fullKey = key === null ? null : `${key}*e${eps}`;
|
|
5521
|
+
if (fullKey !== null) {
|
|
5522
|
+
const cached = dcPMFCache.get(fullKey);
|
|
5523
|
+
if (cached) return cached;
|
|
5524
|
+
}
|
|
5414
5525
|
const saveDC = this.saveDC;
|
|
5415
5526
|
const rollType = this.rollType;
|
|
5416
5527
|
const rerollOne = this.baseReroll > 0;
|
|
@@ -5432,7 +5543,9 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
|
5432
5543
|
[0, psuccess > 0 ? psuccess : 0],
|
|
5433
5544
|
[1, pfail > 0 ? pfail : 0]
|
|
5434
5545
|
]);
|
|
5435
|
-
|
|
5546
|
+
const pmf = PMF.fromMap(m, eps);
|
|
5547
|
+
if (fullKey !== null) dcPMFCache.set(fullKey, pmf);
|
|
5548
|
+
return pmf;
|
|
5436
5549
|
}
|
|
5437
5550
|
};
|
|
5438
5551
|
RollBuilder.prototype.dc = function(saveDC) {
|
|
@@ -5440,20 +5553,631 @@ RollBuilder.prototype.dc = function(saveDC) {
|
|
|
5440
5553
|
return new DCBuilder(this).dc(saveDC);
|
|
5441
5554
|
};
|
|
5442
5555
|
|
|
5556
|
+
// src/turn/types.ts
|
|
5557
|
+
var TurnSpecError = class extends Error {
|
|
5558
|
+
constructor(code, id, message) {
|
|
5559
|
+
super(message);
|
|
5560
|
+
this.code = code;
|
|
5561
|
+
this.id = id;
|
|
5562
|
+
this.name = "TurnSpecError";
|
|
5563
|
+
}
|
|
5564
|
+
};
|
|
5565
|
+
var MAX_TRIGGER_GROUPS = 4;
|
|
5566
|
+
|
|
5567
|
+
// src/turn/plan.ts
|
|
5568
|
+
var IS_HIT_TRIGGER = {
|
|
5569
|
+
"first-hit": true,
|
|
5570
|
+
"any-crit": true,
|
|
5571
|
+
"any-miss": true,
|
|
5572
|
+
"every-hit": true
|
|
5573
|
+
};
|
|
5574
|
+
function toPMF(damage, eps, id = "") {
|
|
5575
|
+
const parts = Array.isArray(damage) ? damage : [damage];
|
|
5576
|
+
if (parts.length === 0) return PMF.delta(0, eps);
|
|
5577
|
+
const pmfs = parts.map((part) => {
|
|
5578
|
+
if (part instanceof PMF) return part;
|
|
5579
|
+
if (typeof part.toPMF !== "function") {
|
|
5580
|
+
throw new TurnSpecError(
|
|
5581
|
+
"not-an-attack",
|
|
5582
|
+
id,
|
|
5583
|
+
`"${id}" is neither a PMF nor a builder with toPMF().`
|
|
5584
|
+
);
|
|
5585
|
+
}
|
|
5586
|
+
const resolved = part.toPMF(eps);
|
|
5587
|
+
if (!(resolved instanceof PMF)) {
|
|
5588
|
+
throw new TurnSpecError(
|
|
5589
|
+
"not-an-attack",
|
|
5590
|
+
id,
|
|
5591
|
+
`"${id}" has a toPMF() that did not return a PMF.`
|
|
5592
|
+
);
|
|
5593
|
+
}
|
|
5594
|
+
return resolved;
|
|
5595
|
+
});
|
|
5596
|
+
return PMF.convolveMany(pmfs, eps);
|
|
5597
|
+
}
|
|
5598
|
+
function critPMF(rider, base, eps) {
|
|
5599
|
+
if (rider.critDamage !== void 0) return toPMF(rider.critDamage, eps);
|
|
5600
|
+
const parts = Array.isArray(rider.damage) ? rider.damage : [rider.damage];
|
|
5601
|
+
const doubled = [];
|
|
5602
|
+
for (const part of parts) {
|
|
5603
|
+
const doublable = part;
|
|
5604
|
+
if (part instanceof PMF || typeof doublable.doubleDice !== "function") {
|
|
5605
|
+
return base;
|
|
5606
|
+
}
|
|
5607
|
+
try {
|
|
5608
|
+
doubled.push(toPMF(doublable.doubleDice(), eps));
|
|
5609
|
+
} catch {
|
|
5610
|
+
return base;
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
if (doubled.length === 0) return base;
|
|
5614
|
+
return PMF.convolveMany(doubled, eps);
|
|
5615
|
+
}
|
|
5616
|
+
function sliceSource(pmf) {
|
|
5617
|
+
const labels = pmf.outcomes();
|
|
5618
|
+
if (!labels.includes("hit") && !labels.includes("crit")) return null;
|
|
5619
|
+
const missParts = ["missNone", "missDamage"].filter((label) => labels.includes(label)).map((label) => pmf.filterOutcome(label));
|
|
5620
|
+
return {
|
|
5621
|
+
hit: labels.includes("hit") ? pmf.filterOutcome("hit") : PMF.emptyMass(),
|
|
5622
|
+
crit: labels.includes("crit") ? pmf.filterOutcome("crit") : PMF.emptyMass(),
|
|
5623
|
+
miss: missParts.length ? missParts.reduce((all, part) => all.add(part)) : PMF.emptyMass()
|
|
5624
|
+
};
|
|
5625
|
+
}
|
|
5626
|
+
function buildPlan(spec, eps = EPS) {
|
|
5627
|
+
const fail = (code, id, message) => {
|
|
5628
|
+
throw new TurnSpecError(code, id, message);
|
|
5629
|
+
};
|
|
5630
|
+
const attackIds = [];
|
|
5631
|
+
const attackPMFs = [];
|
|
5632
|
+
const attackSlices = [];
|
|
5633
|
+
spec.attacks.forEach((entry, index) => {
|
|
5634
|
+
const named = entry;
|
|
5635
|
+
const hasWrapper = typeof named.id === "string" && named.source !== void 0;
|
|
5636
|
+
const id = hasWrapper ? named.id : `attack ${index + 1}`;
|
|
5637
|
+
const source = hasWrapper ? named.source : entry;
|
|
5638
|
+
const pmf = toPMF(source, eps, id);
|
|
5639
|
+
attackIds.push(id);
|
|
5640
|
+
attackPMFs.push(pmf);
|
|
5641
|
+
attackSlices.push(sliceSource(pmf));
|
|
5642
|
+
});
|
|
5643
|
+
const riders = spec.riders ?? [];
|
|
5644
|
+
const riderIds = riders.map((rider, index) => rider.id ?? `rider ${index + 1}`);
|
|
5645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5646
|
+
for (const id of [...attackIds, ...riderIds]) {
|
|
5647
|
+
if (seen.has(id)) fail("duplicate-id", id, `Duplicate id "${id}".`);
|
|
5648
|
+
seen.add(id);
|
|
5649
|
+
}
|
|
5650
|
+
const attackIndexById = new Map(attackIds.map((id, index) => [id, index]));
|
|
5651
|
+
const riderIndexById = new Map(riderIds.map((id, index) => [id, index]));
|
|
5652
|
+
const sourceIdsByRider = riders.map((rider, index) => {
|
|
5653
|
+
const id = riderIds[index];
|
|
5654
|
+
if (rider.on === "not-fired") {
|
|
5655
|
+
const target = rider.of;
|
|
5656
|
+
if (target === id) {
|
|
5657
|
+
fail("self-reference", id, `Rider "${id}" cannot depend on itself.`);
|
|
5658
|
+
}
|
|
5659
|
+
const targetIndex = riderIndexById.get(target);
|
|
5660
|
+
if (targetIndex === void 0) {
|
|
5661
|
+
fail(
|
|
5662
|
+
"unknown-id",
|
|
5663
|
+
target,
|
|
5664
|
+
`Rider "${id}" negates "${target}", which is not a rider in this turn.`
|
|
5665
|
+
);
|
|
5666
|
+
}
|
|
5667
|
+
if (riders[targetIndex].on === "every-hit") {
|
|
5668
|
+
fail(
|
|
5669
|
+
"not-an-attack",
|
|
5670
|
+
target,
|
|
5671
|
+
`Rider "${id}" negates "${target}", an every-hit rider, which can fire more than once and so has no single "did not fire" branch.`
|
|
5672
|
+
);
|
|
5673
|
+
}
|
|
5674
|
+
return [target];
|
|
5675
|
+
}
|
|
5676
|
+
const of = [...new Set(rider.of ?? attackIds)];
|
|
5677
|
+
if (of.length === 0) {
|
|
5678
|
+
fail("unknown-id", id, `Rider "${id}" has no sources.`);
|
|
5679
|
+
}
|
|
5680
|
+
for (const sourceId of of) {
|
|
5681
|
+
if (sourceId === id) {
|
|
5682
|
+
fail("self-reference", id, `Rider "${id}" cannot depend on itself.`);
|
|
5683
|
+
}
|
|
5684
|
+
const riderIndex = riderIndexById.get(sourceId);
|
|
5685
|
+
const isAttack = attackIndexById.has(sourceId);
|
|
5686
|
+
if (!isAttack && riderIndex === void 0) {
|
|
5687
|
+
fail(
|
|
5688
|
+
"unknown-id",
|
|
5689
|
+
sourceId,
|
|
5690
|
+
`Rider "${id}" depends on "${sourceId}", which is not in this turn.`
|
|
5691
|
+
);
|
|
5692
|
+
}
|
|
5693
|
+
if (riderIndex !== void 0 && riders[riderIndex].on === "every-hit") {
|
|
5694
|
+
fail(
|
|
5695
|
+
"not-an-attack",
|
|
5696
|
+
sourceId,
|
|
5697
|
+
`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.`
|
|
5698
|
+
);
|
|
5699
|
+
}
|
|
5700
|
+
const slices = isAttack ? attackSlices[attackIndexById.get(sourceId)] : sliceSource(
|
|
5701
|
+
toPMF(riders[riderIndex].damage, eps, sourceId)
|
|
5702
|
+
);
|
|
5703
|
+
if (!slices) {
|
|
5704
|
+
fail(
|
|
5705
|
+
"not-an-attack",
|
|
5706
|
+
sourceId,
|
|
5707
|
+
`Rider "${id}" triggers on "${sourceId}", which has no hit/crit outcomes.`
|
|
5708
|
+
);
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
return [...of];
|
|
5712
|
+
});
|
|
5713
|
+
const order = [];
|
|
5714
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
5715
|
+
const done = /* @__PURE__ */ new Set();
|
|
5716
|
+
const visit = (index) => {
|
|
5717
|
+
if (done.has(index)) return;
|
|
5718
|
+
const id = riderIds[index];
|
|
5719
|
+
if (visiting.has(index)) {
|
|
5720
|
+
fail("cycle", id, `Rider "${id}" is part of a dependency cycle.`);
|
|
5721
|
+
}
|
|
5722
|
+
visiting.add(index);
|
|
5723
|
+
for (const sourceId of sourceIdsByRider[index]) {
|
|
5724
|
+
const dependency = riderIndexById.get(sourceId);
|
|
5725
|
+
if (dependency !== void 0) visit(dependency);
|
|
5726
|
+
}
|
|
5727
|
+
visiting.delete(index);
|
|
5728
|
+
done.add(index);
|
|
5729
|
+
order.push(index);
|
|
5730
|
+
};
|
|
5731
|
+
riders.forEach((_, index) => visit(index));
|
|
5732
|
+
const groupIndexByKey = /* @__PURE__ */ new Map();
|
|
5733
|
+
const groupSources = [];
|
|
5734
|
+
const groupOf = (sourceIds) => {
|
|
5735
|
+
const key = JSON.stringify([...sourceIds].sort());
|
|
5736
|
+
const existing = groupIndexByKey.get(key);
|
|
5737
|
+
if (existing !== void 0) return existing;
|
|
5738
|
+
if (groupSources.length >= MAX_TRIGGER_GROUPS) {
|
|
5739
|
+
fail(
|
|
5740
|
+
"too-many-groups",
|
|
5741
|
+
key,
|
|
5742
|
+
`A turn may track at most ${MAX_TRIGGER_GROUPS} distinct trigger source sets.`
|
|
5743
|
+
);
|
|
5744
|
+
}
|
|
5745
|
+
const index = groupSources.length;
|
|
5746
|
+
groupIndexByKey.set(key, index);
|
|
5747
|
+
groupSources.push([...sourceIds]);
|
|
5748
|
+
return index;
|
|
5749
|
+
};
|
|
5750
|
+
const readsByRider = /* @__PURE__ */ new Map();
|
|
5751
|
+
const perHitGroups = /* @__PURE__ */ new Map();
|
|
5752
|
+
for (const index of order) {
|
|
5753
|
+
const rider = riders[index];
|
|
5754
|
+
if (!IS_HIT_TRIGGER[rider.on]) continue;
|
|
5755
|
+
const group = groupOf(sourceIdsByRider[index]);
|
|
5756
|
+
readsByRider.set(index, group);
|
|
5757
|
+
if (rider.on === "every-hit") perHitGroups.set(riderIds[index], group);
|
|
5758
|
+
}
|
|
5759
|
+
const perHitBySource = /* @__PURE__ */ new Map();
|
|
5760
|
+
for (const index of order) {
|
|
5761
|
+
const rider = riders[index];
|
|
5762
|
+
if (rider.on !== "every-hit") continue;
|
|
5763
|
+
const hit = toPMF(rider.damage, eps, riderIds[index]);
|
|
5764
|
+
const payload = { hit, crit: critPMF(rider, hit, eps) };
|
|
5765
|
+
for (const sourceId of sourceIdsByRider[index]) {
|
|
5766
|
+
const existing = perHitBySource.get(sourceId);
|
|
5767
|
+
if (existing) existing.push(payload);
|
|
5768
|
+
else perHitBySource.set(sourceId, [payload]);
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
const updatesById = /* @__PURE__ */ new Map();
|
|
5772
|
+
groupSources.forEach((sourceIds, groupIndex) => {
|
|
5773
|
+
for (const sourceId of sourceIds) {
|
|
5774
|
+
const existing = updatesById.get(sourceId);
|
|
5775
|
+
if (existing) existing.push(groupIndex);
|
|
5776
|
+
else updatesById.set(sourceId, [groupIndex]);
|
|
5777
|
+
}
|
|
5778
|
+
});
|
|
5779
|
+
const withPerHit = (slices, id) => {
|
|
5780
|
+
const payloads = perHitBySource.get(id);
|
|
5781
|
+
if (!payloads) return slices;
|
|
5782
|
+
let hit = slices.hit;
|
|
5783
|
+
let crit = slices.crit;
|
|
5784
|
+
for (const payload of payloads) {
|
|
5785
|
+
hit = hit.convolve(payload.hit, eps, true);
|
|
5786
|
+
crit = crit.convolve(payload.crit, eps, true);
|
|
5787
|
+
}
|
|
5788
|
+
return { hit, crit, miss: slices.miss };
|
|
5789
|
+
};
|
|
5790
|
+
const steps = attackIds.map((id, index) => ({
|
|
5791
|
+
id,
|
|
5792
|
+
trigger: null,
|
|
5793
|
+
slices: withPerHit(
|
|
5794
|
+
attackSlices[index] ?? {
|
|
5795
|
+
hit: attackPMFs[index],
|
|
5796
|
+
crit: PMF.emptyMass(),
|
|
5797
|
+
miss: PMF.emptyMass()
|
|
5798
|
+
},
|
|
5799
|
+
id
|
|
5800
|
+
),
|
|
5801
|
+
damage: null,
|
|
5802
|
+
updates: updatesById.get(id) ?? [],
|
|
5803
|
+
reads: -1,
|
|
5804
|
+
negates: -1
|
|
5805
|
+
}));
|
|
5806
|
+
const stepIndexByRider = /* @__PURE__ */ new Map();
|
|
5807
|
+
const riderSteps = /* @__PURE__ */ new Map();
|
|
5808
|
+
for (const index of order) {
|
|
5809
|
+
const rider = riders[index];
|
|
5810
|
+
if (rider.on === "every-hit") continue;
|
|
5811
|
+
const id = riderIds[index];
|
|
5812
|
+
const hit = toPMF(rider.damage, eps, id);
|
|
5813
|
+
const slices = sliceSource(hit);
|
|
5814
|
+
if (slices && rider.critDamage !== void 0) {
|
|
5815
|
+
fail(
|
|
5816
|
+
"unused-crit-damage",
|
|
5817
|
+
id,
|
|
5818
|
+
`Rider "${id}" rolls its own attack, so its critDamage would never be used. Remove it, or pass plain damage dice instead.`
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
const negatedRider = rider.on === "not-fired" ? riderIndexById.get(rider.of) : void 0;
|
|
5822
|
+
const step = {
|
|
5823
|
+
id,
|
|
5824
|
+
trigger: rider,
|
|
5825
|
+
slices: slices ? withPerHit(slices, id) : null,
|
|
5826
|
+
damage: slices ? null : { hit, crit: critPMF(rider, hit, eps) },
|
|
5827
|
+
updates: updatesById.get(id) ?? [],
|
|
5828
|
+
reads: readsByRider.get(index) ?? -1,
|
|
5829
|
+
negates: negatedRider === void 0 ? -1 : stepIndexByRider.get(negatedRider)
|
|
5830
|
+
};
|
|
5831
|
+
steps.push(step);
|
|
5832
|
+
stepIndexByRider.set(index, steps.length - 1);
|
|
5833
|
+
riderSteps.set(id, steps.length - 1);
|
|
5834
|
+
}
|
|
5835
|
+
return {
|
|
5836
|
+
steps,
|
|
5837
|
+
groupCount: groupSources.length,
|
|
5838
|
+
attackPMFs,
|
|
5839
|
+
attackIds,
|
|
5840
|
+
riderIds,
|
|
5841
|
+
riderSteps,
|
|
5842
|
+
perHitGroups
|
|
5843
|
+
};
|
|
5844
|
+
}
|
|
5845
|
+
|
|
5846
|
+
// src/turn/state.ts
|
|
5847
|
+
var FIRST_NONE = 0;
|
|
5848
|
+
var FIRST_HIT = 1;
|
|
5849
|
+
var FIRST_CRIT = 2;
|
|
5850
|
+
var CRIT_BIT = 2;
|
|
5851
|
+
var MISS_BIT = 1;
|
|
5852
|
+
var START_CODE = FIRST_NONE << 2;
|
|
5853
|
+
function advance(code, outcome) {
|
|
5854
|
+
if (outcome === "miss") return code | MISS_BIT;
|
|
5855
|
+
const first = code >> 2;
|
|
5856
|
+
const withCrit = outcome === "crit" ? code | CRIT_BIT : code;
|
|
5857
|
+
if (first !== FIRST_NONE) return withCrit;
|
|
5858
|
+
const nextFirst = outcome === "crit" ? FIRST_CRIT : FIRST_HIT;
|
|
5859
|
+
return nextFirst << 2 | withCrit & (CRIT_BIT | MISS_BIT);
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
// src/turn/turn.ts
|
|
5863
|
+
var OUTCOMES = ["hit", "crit", "miss"];
|
|
5864
|
+
function fireMode(step, codes, firedByStep) {
|
|
5865
|
+
const trigger = step.trigger;
|
|
5866
|
+
if (!trigger) return "hit";
|
|
5867
|
+
if (trigger.on === "not-fired") {
|
|
5868
|
+
return firedByStep[step.negates] === null ? "hit" : null;
|
|
5869
|
+
}
|
|
5870
|
+
const code = codes[step.reads];
|
|
5871
|
+
const first = code >> 2;
|
|
5872
|
+
switch (trigger.on) {
|
|
5873
|
+
case "first-hit":
|
|
5874
|
+
if (first === FIRST_NONE) return null;
|
|
5875
|
+
return first === FIRST_CRIT ? "crit" : "hit";
|
|
5876
|
+
case "any-crit":
|
|
5877
|
+
return (code & CRIT_BIT) !== 0 ? "crit" : null;
|
|
5878
|
+
case "any-miss":
|
|
5879
|
+
return (code & MISS_BIT) !== 0 ? "hit" : null;
|
|
5880
|
+
default:
|
|
5881
|
+
return null;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
var Turn = class _Turn {
|
|
5885
|
+
constructor(attacks, riders, eps) {
|
|
5886
|
+
this.declaredAttacks = [...attacks];
|
|
5887
|
+
this.riders = [...riders];
|
|
5888
|
+
this.eps = eps;
|
|
5889
|
+
this.plan = buildPlan({ attacks: this.declaredAttacks, riders: this.riders }, eps);
|
|
5890
|
+
}
|
|
5891
|
+
/**
|
|
5892
|
+
* Builds a turn from plain data, throwing {@link TurnSpecError} if it is
|
|
5893
|
+
* malformed. Use this from a UI, where `error.code` maps to the field state to
|
|
5894
|
+
* show.
|
|
5895
|
+
*/
|
|
5896
|
+
static from(spec, eps = EPS) {
|
|
5897
|
+
return new _Turn(spec.attacks, spec.riders ?? [], eps);
|
|
5898
|
+
}
|
|
5899
|
+
/**
|
|
5900
|
+
* Appends an attack, throwing {@link TurnSpecError} if that makes the turn
|
|
5901
|
+
* invalid.
|
|
5902
|
+
*
|
|
5903
|
+
* A rider with no explicit `of` watches every declared attack *including ones
|
|
5904
|
+
* appended after it*, because `of` is resolved when the plan is built rather
|
|
5905
|
+
* than when the rider is added. Pass an explicit `of` to pin a rider to the
|
|
5906
|
+
* attacks it already saw. One attack must exist before a rider with a default
|
|
5907
|
+
* `of` is added, or the build fails `unknown-id`.
|
|
5908
|
+
*/
|
|
5909
|
+
attack(source, id) {
|
|
5910
|
+
const entry = id === void 0 ? source : { id, source };
|
|
5911
|
+
return new _Turn([...this.declaredAttacks, entry], this.riders, this.eps);
|
|
5912
|
+
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Appends `count` copies of the same attack — the Extra Attack case, which is
|
|
5915
|
+
* most of 5e. Argument order mirrors `roll(count, die)`.
|
|
5916
|
+
*
|
|
5917
|
+
* ```ts
|
|
5918
|
+
* turn().attacks(4, greatsword).onEveryHit(d6); // fighter 20 + hunter's mark
|
|
5919
|
+
* ```
|
|
5920
|
+
*
|
|
5921
|
+
* @throws {RangeError} if `count` is not a positive integer.
|
|
5922
|
+
*/
|
|
5923
|
+
attacks(count, source) {
|
|
5924
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
5925
|
+
throw new RangeError(
|
|
5926
|
+
`attacks(count) needs a positive integer, got ${count}.`
|
|
5927
|
+
);
|
|
5928
|
+
}
|
|
5929
|
+
const added = new Array(count).fill(source);
|
|
5930
|
+
return new _Turn([...this.declaredAttacks, ...added], this.riders, this.eps);
|
|
5931
|
+
}
|
|
5932
|
+
/**
|
|
5933
|
+
* Appends a rider, throwing {@link TurnSpecError} if that makes the turn
|
|
5934
|
+
* invalid. The `onX` methods below are the readable way to call this.
|
|
5935
|
+
*/
|
|
5936
|
+
rider(rider) {
|
|
5937
|
+
return new _Turn(this.declaredAttacks, [...this.riders, rider], this.eps);
|
|
5938
|
+
}
|
|
5939
|
+
/**
|
|
5940
|
+
* Fires once, on the first source that lands, in that source's mode — so a
|
|
5941
|
+
* crit on the first landing attack doubles the rider's dice. Sneak Attack.
|
|
5942
|
+
*/
|
|
5943
|
+
onFirstHit(damage, options = {}) {
|
|
5944
|
+
return this.rider({ ...options, damage, on: "first-hit" });
|
|
5945
|
+
}
|
|
5946
|
+
/**
|
|
5947
|
+
* Fires once if any source crit, always in crit mode. Divine Smite: nothing is
|
|
5948
|
+
* lost by holding it for a crit, so this is "any", not "first".
|
|
5949
|
+
*/
|
|
5950
|
+
onAnyCrit(damage, options = {}) {
|
|
5951
|
+
return this.rider({ ...options, damage, on: "any-crit" });
|
|
5952
|
+
}
|
|
5953
|
+
/**
|
|
5954
|
+
* Fires once if any source missed. The reroll gate: a reroll is a fresh attack,
|
|
5955
|
+
* so pass one as the damage. Kensei's Unerring Accuracy, Lucky.
|
|
5956
|
+
*/
|
|
5957
|
+
onAnyMiss(damage, options = {}) {
|
|
5958
|
+
return this.rider({ ...options, damage, on: "any-miss" });
|
|
5959
|
+
}
|
|
5960
|
+
/**
|
|
5961
|
+
* Fires once per source that lands, in that hit's mode — so it can fire several
|
|
5962
|
+
* times in a turn. Hunter's Mark, Hex, Rage.
|
|
5963
|
+
*/
|
|
5964
|
+
onEveryHit(damage, options = {}) {
|
|
5965
|
+
return this.rider({ ...options, damage, on: "every-hit" });
|
|
5966
|
+
}
|
|
5967
|
+
/**
|
|
5968
|
+
* Damage for the turns where the rider added just before this one did *not*
|
|
5969
|
+
* fire: "flurry of blows if I didn't smite".
|
|
5970
|
+
*
|
|
5971
|
+
* ```ts
|
|
5972
|
+
* turn([dagger, dagger])
|
|
5973
|
+
* .onAnyCrit(roll(2, d8)) // smite
|
|
5974
|
+
* .otherwise([flurry, flurry]) // ... or two more attacks
|
|
5975
|
+
* ```
|
|
5976
|
+
*
|
|
5977
|
+
* Always binds to the *immediately* preceding rider, so the two are branches of
|
|
5978
|
+
* one decision and can never both land. Note that chaining it therefore
|
|
5979
|
+
* alternates rather than laddering: `a.otherwise(b).otherwise(c)` makes `c`
|
|
5980
|
+
* fire whenever `b` did not, which is exactly when `a` did. For a genuine
|
|
5981
|
+
* three-way priority chain, name the riders and use explicit `not-fired`
|
|
5982
|
+
* triggers against the right one.
|
|
5983
|
+
*/
|
|
5984
|
+
otherwise(damage, options = {}) {
|
|
5985
|
+
const index = this.riders.length - 1;
|
|
5986
|
+
if (index < 0) {
|
|
5987
|
+
throw new TurnSpecError(
|
|
5988
|
+
"unknown-id",
|
|
5989
|
+
"",
|
|
5990
|
+
"otherwise() needs a preceding rider to negate."
|
|
5991
|
+
);
|
|
5992
|
+
}
|
|
5993
|
+
const previous = this.riders[index];
|
|
5994
|
+
if (previous.on === "every-hit") {
|
|
5995
|
+
throw new TurnSpecError(
|
|
5996
|
+
"not-an-attack",
|
|
5997
|
+
previous.id ?? `rider ${index + 1}`,
|
|
5998
|
+
"otherwise() cannot negate an every-hit rider: it can fire more than once."
|
|
5999
|
+
);
|
|
6000
|
+
}
|
|
6001
|
+
const target = previous.id ?? `rider ${index + 1}`;
|
|
6002
|
+
const riders = [...this.riders];
|
|
6003
|
+
riders[index] = { ...previous, id: target };
|
|
6004
|
+
riders.push({ ...options, damage, on: "not-fired", of: target });
|
|
6005
|
+
return new _Turn(this.declaredAttacks, riders, this.eps);
|
|
6006
|
+
}
|
|
6007
|
+
/**
|
|
6008
|
+
* The exact joint distribution: mass 1, outcome-labelled. Resolved once and
|
|
6009
|
+
* cached.
|
|
6010
|
+
*
|
|
6011
|
+
* There is no `toPMF(eps)` to match the builders: a turn's epsilon is fixed
|
|
6012
|
+
* when it is constructed, because the plan is validated and its sources are
|
|
6013
|
+
* resolved at that point.
|
|
6014
|
+
*/
|
|
6015
|
+
get pmf() {
|
|
6016
|
+
return this.resolve().pmf;
|
|
6017
|
+
}
|
|
6018
|
+
/** Mean damage for the turn. */
|
|
6019
|
+
mean() {
|
|
6020
|
+
return this.pmf.mean();
|
|
6021
|
+
}
|
|
6022
|
+
/**
|
|
6023
|
+
* A query whose `singles` are the **declared attacks** and whose combined
|
|
6024
|
+
* distribution is the exact turn PMF.
|
|
6025
|
+
*
|
|
6026
|
+
* Riders are inside the combined PMF, not in `singles`, so singles-based
|
|
6027
|
+
* helpers (`probAtLeastOne`, `countSinglesWith`, `outcomeStats`) describe the
|
|
6028
|
+
* attacks only. Read rider-inclusive statistics off the combined PMF —
|
|
6029
|
+
* `outcomeTotals`, `outcomeDamageRanges`, `damageAttributionChartModel`.
|
|
6030
|
+
*/
|
|
6031
|
+
toQuery() {
|
|
6032
|
+
return new DiceQuery([...this.plan.attackPMFs], this.pmf, this.eps);
|
|
6033
|
+
}
|
|
6034
|
+
/**
|
|
6035
|
+
* Attack ids in declaration order, including the `attack 1`, `attack 2`, …
|
|
6036
|
+
* defaults given to bare sources. These are the names `of` accepts.
|
|
6037
|
+
*/
|
|
6038
|
+
get attackIds() {
|
|
6039
|
+
return this.plan.attackIds;
|
|
6040
|
+
}
|
|
6041
|
+
/**
|
|
6042
|
+
* Rider ids in declaration order, including the `rider 1`, `rider 2`, …
|
|
6043
|
+
* defaults. These are the names {@link Turn.fireProbability} accepts.
|
|
6044
|
+
*/
|
|
6045
|
+
get riderIds() {
|
|
6046
|
+
return this.plan.riderIds;
|
|
6047
|
+
}
|
|
6048
|
+
/**
|
|
6049
|
+
* P(this rider fired). For an `every-hit` rider it is P(at least one source
|
|
6050
|
+
* hit), since that rider can fire more than once in a turn.
|
|
6051
|
+
*
|
|
6052
|
+
* @throws {TurnSpecError} `unknown-id` if `id` is not a rider — attack ids
|
|
6053
|
+
* included, since attacks always happen and have no firing probability.
|
|
6054
|
+
*/
|
|
6055
|
+
fireProbability(id) {
|
|
6056
|
+
const mass = this.resolve().fireMass.get(id);
|
|
6057
|
+
if (mass === void 0) {
|
|
6058
|
+
throw new TurnSpecError(
|
|
6059
|
+
"unknown-id",
|
|
6060
|
+
id,
|
|
6061
|
+
`"${id}" is not a rider in this turn. Riders: ${this.plan.riderIds.map((each) => `"${each}"`).join(", ")}.`
|
|
6062
|
+
);
|
|
6063
|
+
}
|
|
6064
|
+
return mass;
|
|
6065
|
+
}
|
|
6066
|
+
resolve() {
|
|
6067
|
+
if (this.resolved) return this.resolved;
|
|
6068
|
+
const plan = this.plan;
|
|
6069
|
+
const eps = this.eps;
|
|
6070
|
+
const width = plan.groupCount;
|
|
6071
|
+
const start = {
|
|
6072
|
+
codes: new Array(width).fill(START_CODE),
|
|
6073
|
+
pmf: PMF.delta(0, eps),
|
|
6074
|
+
fired: new Array(plan.steps.length).fill(null)
|
|
6075
|
+
};
|
|
6076
|
+
let states = /* @__PURE__ */ new Map([[String.fromCharCode(), start]]);
|
|
6077
|
+
plan.steps.forEach((step, stepIndex) => {
|
|
6078
|
+
const next = /* @__PURE__ */ new Map();
|
|
6079
|
+
const merge = (state) => {
|
|
6080
|
+
const key = String.fromCharCode(...state.codes) + "" + state.fired.map((mode) => mode === null ? "-" : "+").join("");
|
|
6081
|
+
const existing = next.get(key);
|
|
6082
|
+
if (existing) existing.pmf = existing.pmf.add(state.pmf);
|
|
6083
|
+
else next.set(key, state);
|
|
6084
|
+
};
|
|
6085
|
+
for (const state of states.values()) {
|
|
6086
|
+
const mode = fireMode(step, state.codes, state.fired);
|
|
6087
|
+
const fired = [...state.fired];
|
|
6088
|
+
fired[stepIndex] = mode;
|
|
6089
|
+
if (mode === null) {
|
|
6090
|
+
merge({ codes: state.codes, pmf: state.pmf, fired });
|
|
6091
|
+
continue;
|
|
6092
|
+
}
|
|
6093
|
+
if (!step.slices) {
|
|
6094
|
+
const payload = step.damage;
|
|
6095
|
+
merge({
|
|
6096
|
+
codes: state.codes,
|
|
6097
|
+
pmf: state.pmf.convolve(
|
|
6098
|
+
mode === "crit" ? payload.crit : payload.hit,
|
|
6099
|
+
eps,
|
|
6100
|
+
true
|
|
6101
|
+
),
|
|
6102
|
+
fired
|
|
6103
|
+
});
|
|
6104
|
+
continue;
|
|
6105
|
+
}
|
|
6106
|
+
for (const outcome of OUTCOMES) {
|
|
6107
|
+
const slice = step.slices[outcome];
|
|
6108
|
+
const sliceMass = slice.mass();
|
|
6109
|
+
if (sliceMass <= eps) continue;
|
|
6110
|
+
const codes = [...state.codes];
|
|
6111
|
+
for (const group of step.updates) {
|
|
6112
|
+
codes[group] = advance(codes[group], outcome);
|
|
6113
|
+
}
|
|
6114
|
+
merge({
|
|
6115
|
+
codes,
|
|
6116
|
+
pmf: state.pmf.convolve(slice, eps, true),
|
|
6117
|
+
fired
|
|
6118
|
+
});
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
states = next;
|
|
6122
|
+
});
|
|
6123
|
+
const fireMass = /* @__PURE__ */ new Map();
|
|
6124
|
+
for (const id of plan.riderSteps.keys()) fireMass.set(id, 0);
|
|
6125
|
+
for (const id of plan.perHitGroups.keys()) fireMass.set(id, 0);
|
|
6126
|
+
let total;
|
|
6127
|
+
for (const state of states.values()) {
|
|
6128
|
+
total = total ? total.add(state.pmf) : state.pmf;
|
|
6129
|
+
const mass = state.pmf.mass();
|
|
6130
|
+
for (const [id, stepIndex] of plan.riderSteps) {
|
|
6131
|
+
if (state.fired[stepIndex] !== null) {
|
|
6132
|
+
fireMass.set(id, fireMass.get(id) + mass);
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
for (const [id, group] of plan.perHitGroups) {
|
|
6136
|
+
if (state.codes[group] >> 2 !== FIRST_NONE) {
|
|
6137
|
+
fireMass.set(id, fireMass.get(id) + mass);
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
const pmf = total ?? PMF.delta(0, eps);
|
|
6142
|
+
const totalMass = pmf.mass();
|
|
6143
|
+
const needsNormalizing = Math.abs(totalMass - 1) > eps && totalMass > 0;
|
|
6144
|
+
if (needsNormalizing) {
|
|
6145
|
+
for (const [id, mass] of fireMass) fireMass.set(id, mass / totalMass);
|
|
6146
|
+
}
|
|
6147
|
+
this.resolved = {
|
|
6148
|
+
pmf: needsNormalizing ? pmf.normalize() : pmf,
|
|
6149
|
+
fireMass
|
|
6150
|
+
};
|
|
6151
|
+
return this.resolved;
|
|
6152
|
+
}
|
|
6153
|
+
};
|
|
6154
|
+
function turn(attacks = [], eps = EPS) {
|
|
6155
|
+
return Turn.from(
|
|
6156
|
+
{ attacks: Array.isArray(attacks) ? attacks : [attacks] },
|
|
6157
|
+
eps
|
|
6158
|
+
);
|
|
6159
|
+
}
|
|
6160
|
+
|
|
5443
6161
|
exports.ACBuilder = ACBuilder;
|
|
5444
6162
|
exports.AlwaysCritBuilder = AlwaysCritBuilder;
|
|
5445
6163
|
exports.AlwaysHitBuilder = AlwaysHitBuilder;
|
|
5446
6164
|
exports.AttackBuilder = AttackBuilder;
|
|
5447
6165
|
exports.DCBuilder = DCBuilder;
|
|
5448
6166
|
exports.HalfRollBuilder = HalfRollBuilder;
|
|
6167
|
+
exports.MAX_TRIGGER_GROUPS = MAX_TRIGGER_GROUPS;
|
|
5449
6168
|
exports.MaxOfRollBuilder = MaxOfRollBuilder;
|
|
5450
6169
|
exports.ParsedRollBuilder = ParsedRollBuilder;
|
|
5451
6170
|
exports.PooledRollBuilder = PooledRollBuilder;
|
|
5452
6171
|
exports.RollBuilder = RollBuilder;
|
|
5453
6172
|
exports.SaveBuilder = SaveBuilder;
|
|
5454
6173
|
exports.ScaleRollBuilder = ScaleRollBuilder;
|
|
6174
|
+
exports.Turn = Turn;
|
|
6175
|
+
exports.TurnSpecError = TurnSpecError;
|
|
5455
6176
|
exports.builderPMFCache = builderPMFCache;
|
|
5456
6177
|
exports.clearAttackCache = clearAttackCache;
|
|
6178
|
+
exports.clearDCCache = clearDCCache;
|
|
6179
|
+
exports.clearRollCache = clearRollCache;
|
|
6180
|
+
exports.clearSaveCache = clearSaveCache;
|
|
5457
6181
|
exports.d = d;
|
|
5458
6182
|
exports.d10 = d10;
|
|
5459
6183
|
exports.d100 = d100;
|
|
@@ -5467,5 +6191,6 @@ exports.flat = flat;
|
|
|
5467
6191
|
exports.hd20 = hd20;
|
|
5468
6192
|
exports.roll = roll;
|
|
5469
6193
|
exports.sumRolls = sumRolls;
|
|
6194
|
+
exports.turn = turn;
|
|
5470
6195
|
//# sourceMappingURL=index.cjs.map
|
|
5471
6196
|
//# sourceMappingURL=index.cjs.map
|