@yipe/dice 0.12.0 → 0.12.1
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/README.md +15 -11
- package/dist/builder/index.cjs +104 -35
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.js +104 -35
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +104 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +104 -35
- package/dist/index.js.map +1 -1
- package/dist/parser/dice.d.ts +9 -2
- package/dist/parser/dice.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,13 +189,17 @@ when used as a payload that doubles (`onHit`, `doubleDice()`); as a rider it is
|
|
|
189
189
|
crit, like the attack or save builder it stands for.
|
|
190
190
|
An attack string with no crit clause crits too: `"(d20 + 8 AC 16) * (2d6)"` rolls its natural 20 as
|
|
191
191
|
`4d6`, like `d20.plus(8).ac(16).onHit(roll(2, d6))`; a `crit (…)` clause still wins. A term after
|
|
192
|
-
the payload joined by `+`, `*`, `**`, `/` or `//`
|
|
193
|
-
grammar reads left to right
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
the payload joined by `+`, `*`, `**`, `/` or `//` is part of the payload and never applies to a
|
|
193
|
+
miss (the grammar reads left to right). `+` adds to every landed hit and crit, and to every
|
|
194
|
+
other outcome that carries a payload (miss damage, a potent-cantrip half, a save's failure or half),
|
|
195
|
+
one whose payload rolled 0 included, like the builder's `plus`: `(d20 + 5 AC 12) * (1d4 - 1) + 1d6` is
|
|
196
|
+
`onHit(roll(1, d4).minus(1).plus(d6))` (mean 3.8). `*`, `**`, `/` and `//` act on the payload's
|
|
197
|
+
value, so a hit that deals 0 still deals 0. `(d20 + 5 AC 15) * (1d8) + 1d6` crits as `2d8 + 2d6`,
|
|
198
|
+
like `onHit(roll(1, d8).plus(roll(1, d6)))`, and keeps its hit, crit and miss labels. After a
|
|
199
|
+
`crit (…)` clause the term is added to the crit as written, and after a `miss (…)` clause to the
|
|
200
|
+
miss damage too. The crit rate reads the check's natural die, its one d20 wherever it sits in the
|
|
201
|
+
sum (with no d20, its largest die), through bonus to-hit dice, advantage (`d20 > d20`, `d20!`,
|
|
202
|
+
`2kh1d20`), disadvantage (`2kl1(1d20)`), elven accuracy (`3kh1(1d20)`) and halfling luck (`hd20`,
|
|
199
203
|
`d20 reroll 1`), for `crit` and `xcrit N` alike: `(d20 > d20 + 5 + 1d4 AC 15) * (2d6)` crits at
|
|
200
204
|
39/400, like `d20.withAdvantage().plus(5).plus(d4)`, `(1d4 + d20 + 5 AC 15)` crits on the d20, not
|
|
201
205
|
the d4, and `(d20 + d100 AC 60)` on the d20, not the d100 (61/2000). A max or min against another
|
|
@@ -438,7 +442,7 @@ associates left to right**: `1d6 + 2 * 3` is `(1d6 + 2) * 3`. Parenthesise to gr
|
|
|
438
442
|
| `7`, `d6`, `3d6`, `hd20` | A number, a die, a sum of dice, a die whose 1 is rerolled once (halfling luck, `d20 reroll 1`). |
|
|
439
443
|
| `N(X)`, `(X)d6` | N independent copies of X, summed. The count may be rolled: `(1d4)d6`. A count of 0 is 0 (`0d6`, `(1d4 - 1)d6` has P(0) = 1/4); a count that can be negative throws. |
|
|
440
444
|
| `NkhK(X)`, `NklK(X)` | The sum of the K highest (lowest) of N independent copies of X, exact for any X: `4kh3d6`, `2kl1(1d20)`, `4kh1(2d20)`. `2kl1(2d6)` keeps the lower of two 2d6 *sums*. |
|
|
441
|
-
| `X + Y` | Adds Y where the total so far is not 0, so a miss (0) stays 0: `(d20 + 5 AC 15) * (
|
|
445
|
+
| `X + Y` | Adds Y where the total so far is not 0, so a miss (0) stays 0. After an attack's or a save's payload it adds to every outcome that carries a payload (a landed hit or crit, a miss clause's damage, a potent-cantrip half, a save's failure or half), one that deals 0 included: `(d20 + 5 AC 15) * (1d4 - 1) + 1d6`. Inside a check total (left of `AC`/`DC`) `+` always adds, so `d20 - 5 + 1d4 AC 1` adds the d4 on a natural 5 too. |
|
|
442
446
|
| `X ~+ Y` | Always adds. |
|
|
443
447
|
| `X - Y`, `-X` | Subtracts. A leading `-` negates the argument after it, repeat included: `-2d6` is `-(2d6)`, `1d6 + -3` is `1d6 - 3`, `-1d8 + 1d6` has mean -1. There is no unary `+`. |
|
|
444
448
|
| `X * Y` | Y where X is not 0, else 0: the hit gate of `(check) * (damage)`. |
|
|
@@ -449,7 +453,7 @@ associates left to right**: `1d6 + 2 * 3` is `(1d6 + 2) * 3`. Parenthesise to gr
|
|
|
449
453
|
| `X = Y` | 1 where X equals Y, else 0. |
|
|
450
454
|
| `X & Y` | A mix weighted by each side's count of outcomes (see the refused shapes above). |
|
|
451
455
|
| `X reroll R` | Rolls X, and on a result in the face set R rolls X again and keeps the second roll. |
|
|
452
|
-
| `X AC T`, `X DC T` | An attack check (X where X ≥ T, else 0) and a saving throw (0 on a save, 1 on a failure). |
|
|
456
|
+
| `X AC T`, `X DC T` | An attack check (X where X ≥ T, else 0) and a saving throw (0 on a save, 1 on a failure). A total of exactly 0 that meets a T of 0 or less lands, though it reads 0 like a miss: `(d20 - 5 AC 0) * (1d6)` hits on a natural 5 and crits on a 20, like `d20.minus(5).ac(0).onHit(roll(1, d6))`. |
|
|
453
457
|
| `… crit (Y)`, `… xcritN (Y)`, `… miss (Y)`, `… save half`, `… pc` | Outcome clauses after an attack's or a save's payload. `xcrit0 (Y)` never crits: every landing is a hit. |
|
|
454
458
|
|
|
455
459
|
**Rerolls.** `reroll N` rerolls the face N only; `reroll dN` rerolls every face from 1 to N; the
|
|
@@ -465,8 +469,8 @@ The reroll decides on the raw face and a minimum applies after: `3>(d6 reroll 1)
|
|
|
465
469
|
**Labels.** An attack labels its outcomes `hit`, `crit`, `missNone` and `missDamage`. A save labels a
|
|
466
470
|
failed save `saveFail` (whatever the payload rolls, 0 included), a halved success `saveHalf`, and a
|
|
467
471
|
success with no `save half` `missNone`, like the builder's `onSaveFailure()`. A landed hit whose
|
|
468
|
-
payload deals 0 is still `hit` (a 0-damage crit is `crit`), like the builder
|
|
469
|
-
`missNone`.
|
|
472
|
+
payload deals 0 is still `hit` (a 0-damage crit is `crit`), like the builder, and so is an AC
|
|
473
|
+
check's landed total of exactly 0; only a miss is `missNone`.
|
|
470
474
|
|
|
471
475
|
A `d0` has no faces: it is only a face set (`reroll d0`); rolled on its own it throws, as does a
|
|
472
476
|
string the grammar cannot read, always as a `DiceParseError`.
|
package/dist/builder/index.cjs
CHANGED
|
@@ -3377,9 +3377,23 @@ var Dice = class _Dice {
|
|
|
3377
3377
|
result.privateData.isDCCheck = true;
|
|
3378
3378
|
return result;
|
|
3379
3379
|
}
|
|
3380
|
+
/**
|
|
3381
|
+
* An attack check: a total that meets the target lands at its own value, and a miss is 0. A total
|
|
3382
|
+
* of exactly 0 that meets the target (a target of 0 or less) lands at 0 too, where the misses sit:
|
|
3383
|
+
* its count is recorded under `hit` at 0, like a payload's 0-damage hit, so the ops after the
|
|
3384
|
+
* check tell it from a miss.
|
|
3385
|
+
*/
|
|
3380
3386
|
ac(other) {
|
|
3381
3387
|
const acCheck = (a, b) => a >= b ? a : 0;
|
|
3382
|
-
|
|
3388
|
+
const result = this.checkTarget(other, acCheck);
|
|
3389
|
+
const zero = this.get(0);
|
|
3390
|
+
if (zero > 0) {
|
|
3391
|
+
let met = 0;
|
|
3392
|
+
if (typeof other === "number") met = other <= 0 ? 1 : 0;
|
|
3393
|
+
else for (const [target, count] of other.getFaceEntries()) if (target <= 0) met += count;
|
|
3394
|
+
if (met > 0) result.setOutcomeDistribution("hit", { 0: zero * met });
|
|
3395
|
+
}
|
|
3396
|
+
return result;
|
|
3383
3397
|
}
|
|
3384
3398
|
deleteFace(face) {
|
|
3385
3399
|
const result = new _Dice();
|
|
@@ -3657,6 +3671,8 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3657
3671
|
const hitText = pending?.slice(0, pending.length - arr.length);
|
|
3658
3672
|
const termText = opText?.slice(0, opText.length - arr.length);
|
|
3659
3673
|
const before = finalResult;
|
|
3674
|
+
const attack = isAttack(finalResult);
|
|
3675
|
+
const acCheck = finalResult.privateData.isACCheck === true;
|
|
3660
3676
|
if (op === Dice.prototype.combine) assertMixable(before, arg, arr);
|
|
3661
3677
|
let crit;
|
|
3662
3678
|
let critNorm = 1;
|
|
@@ -3678,7 +3694,8 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3678
3694
|
} else {
|
|
3679
3695
|
({ crit, rest: finalResult } = splitCrit(finalResult, count));
|
|
3680
3696
|
critNorm = crit.total();
|
|
3681
|
-
|
|
3697
|
+
const critArg = critClause ? parseBinaryArgument(arg, arr, n) : critPayload(hitText, n);
|
|
3698
|
+
crit = HIT_ONLY_OPS.has(op) ? applyToLanded(crit, op, critArg, crit.get(0), acCheck) : op.call(crit, critArg);
|
|
3682
3699
|
critNorm = crit && critNorm ? crit.total() / critNorm : 1;
|
|
3683
3700
|
}
|
|
3684
3701
|
}
|
|
@@ -3689,12 +3706,10 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3689
3706
|
assertToken(arr, "a");
|
|
3690
3707
|
assertToken(arr, "v");
|
|
3691
3708
|
assertToken(arr, "e");
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
finalResult = finalResult.deleteFace(min);
|
|
3697
|
-
save = op.call(save, parseBinaryArgument(arg, arr, n));
|
|
3709
|
+
const { miss: missed, rest } = splitMiss(finalResult, attack);
|
|
3710
|
+
finalResult = rest;
|
|
3711
|
+
saveNorm = missed.total();
|
|
3712
|
+
save = op.call(missed, parseBinaryArgument(arg, arr, n));
|
|
3698
3713
|
saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
|
|
3699
3714
|
}
|
|
3700
3715
|
let pc;
|
|
@@ -3702,12 +3717,10 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3702
3717
|
if (arr.length >= 2 && arr[0] === "p" && arr[1] === "c") {
|
|
3703
3718
|
assertToken(arr, "p");
|
|
3704
3719
|
assertToken(arr, "c");
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
finalResult = finalResult.deleteFace(min);
|
|
3710
|
-
pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
|
|
3720
|
+
const { miss: missed, rest } = splitMiss(finalResult, attack);
|
|
3721
|
+
finalResult = rest;
|
|
3722
|
+
const missBefore = missed.total();
|
|
3723
|
+
pc = op.call(missed, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
|
|
3711
3724
|
const missAfter = pc ? pc.total() : 0;
|
|
3712
3725
|
pcNorm = missBefore ? missAfter / missBefore : 1;
|
|
3713
3726
|
}
|
|
@@ -3718,12 +3731,10 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3718
3731
|
assertToken(arr, "i");
|
|
3719
3732
|
assertToken(arr, "s");
|
|
3720
3733
|
assertToken(arr, "s");
|
|
3721
|
-
miss =
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
finalResult = finalResult.deleteFace(min);
|
|
3726
|
-
miss = op.call(miss, parseBinaryArgument(arg, arr, n));
|
|
3734
|
+
const { miss: missed, rest } = splitMiss(finalResult, attack);
|
|
3735
|
+
finalResult = rest;
|
|
3736
|
+
missNorm = missed.total();
|
|
3737
|
+
miss = op.call(missed, parseBinaryArgument(arg, arr, n));
|
|
3727
3738
|
missNorm = miss && missNorm ? miss.total() / missNorm : 1;
|
|
3728
3739
|
}
|
|
3729
3740
|
let norm = finalResult.total();
|
|
@@ -3732,19 +3743,21 @@ function parseExpression(arr, n, inCheck = false) {
|
|
|
3732
3743
|
const operand = finalResult;
|
|
3733
3744
|
if (!clause && labelled && HIT_ONLY_OPS.has(op)) {
|
|
3734
3745
|
finalResult = applyByOutcome(finalResult, op, arg, termText, n);
|
|
3746
|
+
} else if (HIT_ONLY_OPS.has(op)) {
|
|
3747
|
+
finalResult = applyToLanded(finalResult, op, arg, landedAtZero(finalResult), acCheck);
|
|
3735
3748
|
} else {
|
|
3736
3749
|
finalResult = op.call(finalResult, arg);
|
|
3737
3750
|
}
|
|
3738
3751
|
if (operand.privateData.isDCCheck && HIT_ONLY_OPS.has(op)) {
|
|
3739
3752
|
finalResult.setOutcomeDistribution("saveFail", op.call(operand.deleteFace(0), arg).getFaceMap());
|
|
3740
3753
|
}
|
|
3741
|
-
if (
|
|
3754
|
+
if (attack && HIT_ONLY_OPS.has(op)) {
|
|
3742
3755
|
finalResult.privateData.attackPayload = true;
|
|
3743
|
-
const landed = landedHitsAtZero(operand, op, arg);
|
|
3756
|
+
const landed = landedHitsAtZero(operand, op, arg, acCheck);
|
|
3744
3757
|
if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
|
|
3745
|
-
} else if (op === Dice.prototype.combine && typeof arg !== "number"
|
|
3746
|
-
finalResult.privateData.attackPayload = true;
|
|
3747
|
-
const landed = operand
|
|
3758
|
+
} else if (op === Dice.prototype.combine && typeof arg !== "number") {
|
|
3759
|
+
if (arg.privateData.attackPayload) finalResult.privateData.attackPayload = true;
|
|
3760
|
+
const landed = landedAtZero(operand) + landedAtZero(arg);
|
|
3748
3761
|
if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
|
|
3749
3762
|
}
|
|
3750
3763
|
const gated = op === Dice.prototype.combine && typeof arg !== "number" && arg.privateData.isACCheck;
|
|
@@ -3834,6 +3847,34 @@ function branchesOf(value) {
|
|
|
3834
3847
|
}
|
|
3835
3848
|
var hasOutcomeLabels = (value) => typeof value !== "number" && Object.keys(value.getFullOutcomeDistribution()).some((label) => label !== "hit");
|
|
3836
3849
|
var isSave = (value) => typeof value !== "number" && value.privateData.isDCCheck === true;
|
|
3850
|
+
var isAttack = (value) => value.privateData.isDCCheck !== true && (value.privateData.isACCheck === true || value.privateData.attackPayload === true);
|
|
3851
|
+
var landedAtZero = (value) => typeof value === "number" ? 0 : value.getOutcomeCount("hit", 0);
|
|
3852
|
+
function applyToLanded(value, op, arg, landed, gate) {
|
|
3853
|
+
const lands = op === Dice.prototype.addNonZero || gate && op === Dice.prototype.conditionalApply;
|
|
3854
|
+
if (!(landed > 0) || !lands) return op.call(value, arg);
|
|
3855
|
+
const misses = value.deleteFace(0);
|
|
3856
|
+
const missed = value.get(0) - landed;
|
|
3857
|
+
if (missed > 0) misses.setFace(0, missed);
|
|
3858
|
+
const result = op.call(misses, arg);
|
|
3859
|
+
result.combineInPlace(asValue(arg).normalize(landed));
|
|
3860
|
+
return result;
|
|
3861
|
+
}
|
|
3862
|
+
function applyToAttack(value, op, arg, gate) {
|
|
3863
|
+
const result = applyToLanded(value, op, arg, landedAtZero(value), gate);
|
|
3864
|
+
result.privateData.attackPayload = true;
|
|
3865
|
+
const landed = landedHitsAtZero(value, op, arg, gate);
|
|
3866
|
+
if (landed > 0) result.setOutcomeDistribution("hit", { 0: landed });
|
|
3867
|
+
return result;
|
|
3868
|
+
}
|
|
3869
|
+
function splitMiss(check, attack) {
|
|
3870
|
+
const face = attack ? 0 : check.minFace();
|
|
3871
|
+
const landed = face === 0 ? landedAtZero(check) : 0;
|
|
3872
|
+
const miss = new Dice();
|
|
3873
|
+
miss.increment(face > 0 ? face : 1, check.get(face) - landed);
|
|
3874
|
+
const rest = check.deleteFace(face);
|
|
3875
|
+
if (landed > 0) rest.setFace(0, landed);
|
|
3876
|
+
return { miss, rest };
|
|
3877
|
+
}
|
|
3837
3878
|
function assertMixable(left, right, rest) {
|
|
3838
3879
|
if (hasOutcomeLabels(left) || hasOutcomeLabels(right)) {
|
|
3839
3880
|
throw new Error(
|
|
@@ -3868,7 +3909,7 @@ function followNaturalRoll(before, op, arg, after, clause) {
|
|
|
3868
3909
|
const rights = advantage2 ? lefts : gate ? [asValue(arg)] : branchesOf(arg);
|
|
3869
3910
|
data.branches = lefts.flatMap(
|
|
3870
3911
|
(left) => rights.map((right) => {
|
|
3871
|
-
const part = pairOp.call(left, right);
|
|
3912
|
+
const part = HIT_ONLY_OPS.has(pairOp) && isAttack(left) ? applyToAttack(left, pairOp, right, left.privateData.isACCheck === true) : pairOp.call(left, right);
|
|
3872
3913
|
followNaturalRoll(left, pairOp, right, part, false);
|
|
3873
3914
|
return part;
|
|
3874
3915
|
})
|
|
@@ -3890,7 +3931,10 @@ function followNaturalRoll(before, op, arg, after, clause) {
|
|
|
3890
3931
|
} else if (extremum && track?.bare && argTrack?.bare && sides === argSides) {
|
|
3891
3932
|
data.critTrack = bareTrack(after);
|
|
3892
3933
|
} else if (followed) {
|
|
3893
|
-
const
|
|
3934
|
+
const attack = HIT_ONLY_OPS.has(op) && isAttack(before);
|
|
3935
|
+
const gated = before.privateData.isACCheck === true;
|
|
3936
|
+
const call = (value, other) => attack ? applyToAttack(value, op, other, gated) : op.call(value, other);
|
|
3937
|
+
const apply = own ? (value) => call(value, arg) : (value) => call(before, value);
|
|
3894
3938
|
const step = extremum ? (slice) => keptPart(slice, apply) : apply;
|
|
3895
3939
|
data.critTrack = { sides: followed.sides, bare: false, slice: (face) => step(followed.slice(face)) };
|
|
3896
3940
|
}
|
|
@@ -3917,7 +3961,13 @@ function mixNaturalRolls(after, branches) {
|
|
|
3917
3961
|
bare: tracks.length === branches.length && tracks.every((track) => track.bare),
|
|
3918
3962
|
slice: (face) => {
|
|
3919
3963
|
const slice = new Dice();
|
|
3920
|
-
|
|
3964
|
+
let landed = 0;
|
|
3965
|
+
for (const track of tracks) {
|
|
3966
|
+
const part = track.slice(face);
|
|
3967
|
+
slice.combineInPlace(part);
|
|
3968
|
+
landed += landedAtZero(part);
|
|
3969
|
+
}
|
|
3970
|
+
if (landed > 0) slice.setOutcomeDistribution("hit", { 0: landed });
|
|
3921
3971
|
return slice;
|
|
3922
3972
|
}
|
|
3923
3973
|
};
|
|
@@ -3935,7 +3985,9 @@ function keptPart(slice, apply) {
|
|
|
3935
3985
|
function splitCrit(check, count) {
|
|
3936
3986
|
if (count === 0) {
|
|
3937
3987
|
const none = new Dice();
|
|
3938
|
-
|
|
3988
|
+
const rest2 = subtractCounts(check, none);
|
|
3989
|
+
if (landedAtZero(check) > 0) rest2.setOutcomeDistribution("hit", { 0: landedAtZero(check) });
|
|
3990
|
+
return { crit: none, rest: rest2 };
|
|
3939
3991
|
}
|
|
3940
3992
|
const track = check.privateData.critTrack;
|
|
3941
3993
|
if (!track) {
|
|
@@ -3948,13 +4000,21 @@ function splitCrit(check, count) {
|
|
|
3948
4000
|
throw new Error(`xcrit${count} is wider than the d${sides} it reads its natural roll from`);
|
|
3949
4001
|
}
|
|
3950
4002
|
let crit = new Dice();
|
|
3951
|
-
|
|
4003
|
+
let landed = 0;
|
|
4004
|
+
for (let face = sides; face > sides - count; face--) {
|
|
4005
|
+
const slice = track.slice(face);
|
|
4006
|
+
crit.combineInPlace(slice);
|
|
4007
|
+
landed += landedAtZero(slice);
|
|
4008
|
+
}
|
|
3952
4009
|
const rest = subtractCounts(check, crit);
|
|
3953
|
-
const missed = crit.get(0);
|
|
4010
|
+
const missed = crit.get(0) - landed;
|
|
3954
4011
|
if (missed) {
|
|
3955
4012
|
crit = crit.deleteFace(0);
|
|
4013
|
+
if (landed > 0) crit.setFace(0, landed);
|
|
3956
4014
|
rest.increment(0, missed);
|
|
3957
4015
|
}
|
|
4016
|
+
const restLanded = landedAtZero(check) - landed;
|
|
4017
|
+
if (restLanded > 0) rest.setOutcomeDistribution("hit", { 0: restLanded });
|
|
3958
4018
|
return { crit, rest };
|
|
3959
4019
|
}
|
|
3960
4020
|
function critPayload(text, n) {
|
|
@@ -3971,6 +4031,13 @@ function critPayload(text, n) {
|
|
|
3971
4031
|
}
|
|
3972
4032
|
return payload;
|
|
3973
4033
|
}
|
|
4034
|
+
var PAYLOAD_OUTCOMES = {
|
|
4035
|
+
crit: true,
|
|
4036
|
+
missDamage: true,
|
|
4037
|
+
pc: true,
|
|
4038
|
+
saveFail: true,
|
|
4039
|
+
saveHalf: true
|
|
4040
|
+
};
|
|
3974
4041
|
function applyByOutcome(labelled, op, arg, termText, n) {
|
|
3975
4042
|
const implicit = labelled.privateData.implicitCrit;
|
|
3976
4043
|
const argTotal = typeof arg === "number" ? 1 : arg.total();
|
|
@@ -3984,27 +4051,29 @@ function applyByOutcome(labelled, op, arg, termText, n) {
|
|
|
3984
4051
|
rest = subtractCounts(rest, part);
|
|
3985
4052
|
let applied;
|
|
3986
4053
|
if (label === "crit" && implicit && termText !== void 0) {
|
|
3987
|
-
payload = implicit.payload + termText;
|
|
4054
|
+
payload = implicit.payload + (op === Dice.prototype.addNonZero ? "~" : "") + termText;
|
|
3988
4055
|
const doubled = critPayload(payload, n);
|
|
3989
4056
|
applied = doubled.normalize(part.total() * argTotal / doubled.total());
|
|
4057
|
+
} else if (PAYLOAD_OUTCOMES[label] === true) {
|
|
4058
|
+
applied = applyToLanded(part, op, arg, part.get(0), false);
|
|
3990
4059
|
} else {
|
|
3991
4060
|
applied = op.call(part, arg);
|
|
3992
4061
|
}
|
|
3993
4062
|
result.combineInPlace(applied);
|
|
3994
4063
|
result.setOutcomeDistribution(label, applied.getFaceMap());
|
|
3995
4064
|
}
|
|
3996
|
-
result.combineInPlace(
|
|
4065
|
+
result.combineInPlace(applyToLanded(rest, op, arg, landedAtZero(labelled), false));
|
|
3997
4066
|
if (labelled.privateData.isDCCheck) result.privateData.isDCCheck = true;
|
|
3998
4067
|
if (payload !== void 0) result.privateData.implicitCrit = { payload };
|
|
3999
4068
|
return result;
|
|
4000
4069
|
}
|
|
4001
|
-
function landedHitsAtZero(operand, op, arg) {
|
|
4070
|
+
function landedHitsAtZero(operand, op, arg, gate) {
|
|
4002
4071
|
let landed = 0;
|
|
4003
4072
|
for (const [face, count] of Object.entries(operand.calculateHitDistribution())) {
|
|
4004
4073
|
if (!(count > 0)) continue;
|
|
4005
4074
|
const hit = new Dice();
|
|
4006
4075
|
hit.setFace(Number(face), count);
|
|
4007
|
-
landed +=
|
|
4076
|
+
landed += applyToLanded(hit, op, arg, Number(face) === 0 ? count : 0, gate).get(0);
|
|
4008
4077
|
}
|
|
4009
4078
|
return landed;
|
|
4010
4079
|
}
|