@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.
@@ -3375,9 +3375,23 @@ var Dice = class _Dice {
3375
3375
  result.privateData.isDCCheck = true;
3376
3376
  return result;
3377
3377
  }
3378
+ /**
3379
+ * An attack check: a total that meets the target lands at its own value, and a miss is 0. A total
3380
+ * of exactly 0 that meets the target (a target of 0 or less) lands at 0 too, where the misses sit:
3381
+ * its count is recorded under `hit` at 0, like a payload's 0-damage hit, so the ops after the
3382
+ * check tell it from a miss.
3383
+ */
3378
3384
  ac(other) {
3379
3385
  const acCheck = (a, b) => a >= b ? a : 0;
3380
- return this.checkTarget(other, acCheck);
3386
+ const result = this.checkTarget(other, acCheck);
3387
+ const zero = this.get(0);
3388
+ if (zero > 0) {
3389
+ let met = 0;
3390
+ if (typeof other === "number") met = other <= 0 ? 1 : 0;
3391
+ else for (const [target, count] of other.getFaceEntries()) if (target <= 0) met += count;
3392
+ if (met > 0) result.setOutcomeDistribution("hit", { 0: zero * met });
3393
+ }
3394
+ return result;
3381
3395
  }
3382
3396
  deleteFace(face) {
3383
3397
  const result = new _Dice();
@@ -3655,6 +3669,8 @@ function parseExpression(arr, n, inCheck = false) {
3655
3669
  const hitText = pending?.slice(0, pending.length - arr.length);
3656
3670
  const termText = opText?.slice(0, opText.length - arr.length);
3657
3671
  const before = finalResult;
3672
+ const attack = isAttack(finalResult);
3673
+ const acCheck = finalResult.privateData.isACCheck === true;
3658
3674
  if (op === Dice.prototype.combine) assertMixable(before, arg, arr);
3659
3675
  let crit;
3660
3676
  let critNorm = 1;
@@ -3676,7 +3692,8 @@ function parseExpression(arr, n, inCheck = false) {
3676
3692
  } else {
3677
3693
  ({ crit, rest: finalResult } = splitCrit(finalResult, count));
3678
3694
  critNorm = crit.total();
3679
- crit = op.call(crit, critClause ? parseBinaryArgument(arg, arr, n) : critPayload(hitText, n));
3695
+ const critArg = critClause ? parseBinaryArgument(arg, arr, n) : critPayload(hitText, n);
3696
+ crit = HIT_ONLY_OPS.has(op) ? applyToLanded(crit, op, critArg, crit.get(0), acCheck) : op.call(crit, critArg);
3680
3697
  critNorm = crit && critNorm ? crit.total() / critNorm : 1;
3681
3698
  }
3682
3699
  }
@@ -3687,12 +3704,10 @@ function parseExpression(arr, n, inCheck = false) {
3687
3704
  assertToken(arr, "a");
3688
3705
  assertToken(arr, "v");
3689
3706
  assertToken(arr, "e");
3690
- save = new Dice();
3691
- const min = finalResult.minFace();
3692
- save.increment(min > 0 ? min : 1, finalResult.get(min));
3693
- saveNorm = save.total();
3694
- finalResult = finalResult.deleteFace(min);
3695
- save = op.call(save, parseBinaryArgument(arg, arr, n));
3707
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3708
+ finalResult = rest;
3709
+ saveNorm = missed.total();
3710
+ save = op.call(missed, parseBinaryArgument(arg, arr, n));
3696
3711
  saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
3697
3712
  }
3698
3713
  let pc;
@@ -3700,12 +3715,10 @@ function parseExpression(arr, n, inCheck = false) {
3700
3715
  if (arr.length >= 2 && arr[0] === "p" && arr[1] === "c") {
3701
3716
  assertToken(arr, "p");
3702
3717
  assertToken(arr, "c");
3703
- pc = new Dice();
3704
- const min = finalResult.minFace();
3705
- pc.increment(min > 0 ? min : 1, finalResult.get(min));
3706
- const missBefore = pc.total();
3707
- finalResult = finalResult.deleteFace(min);
3708
- pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
3718
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3719
+ finalResult = rest;
3720
+ const missBefore = missed.total();
3721
+ pc = op.call(missed, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
3709
3722
  const missAfter = pc ? pc.total() : 0;
3710
3723
  pcNorm = missBefore ? missAfter / missBefore : 1;
3711
3724
  }
@@ -3716,12 +3729,10 @@ function parseExpression(arr, n, inCheck = false) {
3716
3729
  assertToken(arr, "i");
3717
3730
  assertToken(arr, "s");
3718
3731
  assertToken(arr, "s");
3719
- miss = new Dice();
3720
- const min = finalResult.minFace();
3721
- miss.increment(min > 0 ? min : 1, finalResult.get(min));
3722
- missNorm = miss.total();
3723
- finalResult = finalResult.deleteFace(min);
3724
- miss = op.call(miss, parseBinaryArgument(arg, arr, n));
3732
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3733
+ finalResult = rest;
3734
+ missNorm = missed.total();
3735
+ miss = op.call(missed, parseBinaryArgument(arg, arr, n));
3725
3736
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3726
3737
  }
3727
3738
  let norm = finalResult.total();
@@ -3730,19 +3741,21 @@ function parseExpression(arr, n, inCheck = false) {
3730
3741
  const operand = finalResult;
3731
3742
  if (!clause && labelled && HIT_ONLY_OPS.has(op)) {
3732
3743
  finalResult = applyByOutcome(finalResult, op, arg, termText, n);
3744
+ } else if (HIT_ONLY_OPS.has(op)) {
3745
+ finalResult = applyToLanded(finalResult, op, arg, landedAtZero(finalResult), acCheck);
3733
3746
  } else {
3734
3747
  finalResult = op.call(finalResult, arg);
3735
3748
  }
3736
3749
  if (operand.privateData.isDCCheck && HIT_ONLY_OPS.has(op)) {
3737
3750
  finalResult.setOutcomeDistribution("saveFail", op.call(operand.deleteFace(0), arg).getFaceMap());
3738
3751
  }
3739
- if (!operand.privateData.isDCCheck && HIT_ONLY_OPS.has(op) && (hitText !== void 0 || operand.privateData.attackPayload)) {
3752
+ if (attack && HIT_ONLY_OPS.has(op)) {
3740
3753
  finalResult.privateData.attackPayload = true;
3741
- const landed = landedHitsAtZero(operand, op, arg);
3754
+ const landed = landedHitsAtZero(operand, op, arg, acCheck);
3742
3755
  if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
3743
- } else if (op === Dice.prototype.combine && typeof arg !== "number" && arg.privateData.attackPayload) {
3744
- finalResult.privateData.attackPayload = true;
3745
- const landed = operand.getOutcomeCount("hit", 0) + arg.getOutcomeCount("hit", 0);
3756
+ } else if (op === Dice.prototype.combine && typeof arg !== "number") {
3757
+ if (arg.privateData.attackPayload) finalResult.privateData.attackPayload = true;
3758
+ const landed = landedAtZero(operand) + landedAtZero(arg);
3746
3759
  if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
3747
3760
  }
3748
3761
  const gated = op === Dice.prototype.combine && typeof arg !== "number" && arg.privateData.isACCheck;
@@ -3832,6 +3845,34 @@ function branchesOf(value) {
3832
3845
  }
3833
3846
  var hasOutcomeLabels = (value) => typeof value !== "number" && Object.keys(value.getFullOutcomeDistribution()).some((label) => label !== "hit");
3834
3847
  var isSave = (value) => typeof value !== "number" && value.privateData.isDCCheck === true;
3848
+ var isAttack = (value) => value.privateData.isDCCheck !== true && (value.privateData.isACCheck === true || value.privateData.attackPayload === true);
3849
+ var landedAtZero = (value) => typeof value === "number" ? 0 : value.getOutcomeCount("hit", 0);
3850
+ function applyToLanded(value, op, arg, landed, gate) {
3851
+ const lands = op === Dice.prototype.addNonZero || gate && op === Dice.prototype.conditionalApply;
3852
+ if (!(landed > 0) || !lands) return op.call(value, arg);
3853
+ const misses = value.deleteFace(0);
3854
+ const missed = value.get(0) - landed;
3855
+ if (missed > 0) misses.setFace(0, missed);
3856
+ const result = op.call(misses, arg);
3857
+ result.combineInPlace(asValue(arg).normalize(landed));
3858
+ return result;
3859
+ }
3860
+ function applyToAttack(value, op, arg, gate) {
3861
+ const result = applyToLanded(value, op, arg, landedAtZero(value), gate);
3862
+ result.privateData.attackPayload = true;
3863
+ const landed = landedHitsAtZero(value, op, arg, gate);
3864
+ if (landed > 0) result.setOutcomeDistribution("hit", { 0: landed });
3865
+ return result;
3866
+ }
3867
+ function splitMiss(check, attack) {
3868
+ const face = attack ? 0 : check.minFace();
3869
+ const landed = face === 0 ? landedAtZero(check) : 0;
3870
+ const miss = new Dice();
3871
+ miss.increment(face > 0 ? face : 1, check.get(face) - landed);
3872
+ const rest = check.deleteFace(face);
3873
+ if (landed > 0) rest.setFace(0, landed);
3874
+ return { miss, rest };
3875
+ }
3835
3876
  function assertMixable(left, right, rest) {
3836
3877
  if (hasOutcomeLabels(left) || hasOutcomeLabels(right)) {
3837
3878
  throw new Error(
@@ -3866,7 +3907,7 @@ function followNaturalRoll(before, op, arg, after, clause) {
3866
3907
  const rights = advantage2 ? lefts : gate ? [asValue(arg)] : branchesOf(arg);
3867
3908
  data.branches = lefts.flatMap(
3868
3909
  (left) => rights.map((right) => {
3869
- const part = pairOp.call(left, right);
3910
+ const part = HIT_ONLY_OPS.has(pairOp) && isAttack(left) ? applyToAttack(left, pairOp, right, left.privateData.isACCheck === true) : pairOp.call(left, right);
3870
3911
  followNaturalRoll(left, pairOp, right, part, false);
3871
3912
  return part;
3872
3913
  })
@@ -3888,7 +3929,10 @@ function followNaturalRoll(before, op, arg, after, clause) {
3888
3929
  } else if (extremum && track?.bare && argTrack?.bare && sides === argSides) {
3889
3930
  data.critTrack = bareTrack(after);
3890
3931
  } else if (followed) {
3891
- const apply = own ? (value) => op.call(value, arg) : (value) => op.call(before, value);
3932
+ const attack = HIT_ONLY_OPS.has(op) && isAttack(before);
3933
+ const gated = before.privateData.isACCheck === true;
3934
+ const call = (value, other) => attack ? applyToAttack(value, op, other, gated) : op.call(value, other);
3935
+ const apply = own ? (value) => call(value, arg) : (value) => call(before, value);
3892
3936
  const step = extremum ? (slice) => keptPart(slice, apply) : apply;
3893
3937
  data.critTrack = { sides: followed.sides, bare: false, slice: (face) => step(followed.slice(face)) };
3894
3938
  }
@@ -3915,7 +3959,13 @@ function mixNaturalRolls(after, branches) {
3915
3959
  bare: tracks.length === branches.length && tracks.every((track) => track.bare),
3916
3960
  slice: (face) => {
3917
3961
  const slice = new Dice();
3918
- for (const track of tracks) slice.combineInPlace(track.slice(face));
3962
+ let landed = 0;
3963
+ for (const track of tracks) {
3964
+ const part = track.slice(face);
3965
+ slice.combineInPlace(part);
3966
+ landed += landedAtZero(part);
3967
+ }
3968
+ if (landed > 0) slice.setOutcomeDistribution("hit", { 0: landed });
3919
3969
  return slice;
3920
3970
  }
3921
3971
  };
@@ -3933,7 +3983,9 @@ function keptPart(slice, apply) {
3933
3983
  function splitCrit(check, count) {
3934
3984
  if (count === 0) {
3935
3985
  const none = new Dice();
3936
- return { crit: none, rest: subtractCounts(check, none) };
3986
+ const rest2 = subtractCounts(check, none);
3987
+ if (landedAtZero(check) > 0) rest2.setOutcomeDistribution("hit", { 0: landedAtZero(check) });
3988
+ return { crit: none, rest: rest2 };
3937
3989
  }
3938
3990
  const track = check.privateData.critTrack;
3939
3991
  if (!track) {
@@ -3946,13 +3998,21 @@ function splitCrit(check, count) {
3946
3998
  throw new Error(`xcrit${count} is wider than the d${sides} it reads its natural roll from`);
3947
3999
  }
3948
4000
  let crit = new Dice();
3949
- for (let face = sides; face > sides - count; face--) crit.combineInPlace(track.slice(face));
4001
+ let landed = 0;
4002
+ for (let face = sides; face > sides - count; face--) {
4003
+ const slice = track.slice(face);
4004
+ crit.combineInPlace(slice);
4005
+ landed += landedAtZero(slice);
4006
+ }
3950
4007
  const rest = subtractCounts(check, crit);
3951
- const missed = crit.get(0);
4008
+ const missed = crit.get(0) - landed;
3952
4009
  if (missed) {
3953
4010
  crit = crit.deleteFace(0);
4011
+ if (landed > 0) crit.setFace(0, landed);
3954
4012
  rest.increment(0, missed);
3955
4013
  }
4014
+ const restLanded = landedAtZero(check) - landed;
4015
+ if (restLanded > 0) rest.setOutcomeDistribution("hit", { 0: restLanded });
3956
4016
  return { crit, rest };
3957
4017
  }
3958
4018
  function critPayload(text, n) {
@@ -3969,6 +4029,13 @@ function critPayload(text, n) {
3969
4029
  }
3970
4030
  return payload;
3971
4031
  }
4032
+ var PAYLOAD_OUTCOMES = {
4033
+ crit: true,
4034
+ missDamage: true,
4035
+ pc: true,
4036
+ saveFail: true,
4037
+ saveHalf: true
4038
+ };
3972
4039
  function applyByOutcome(labelled, op, arg, termText, n) {
3973
4040
  const implicit = labelled.privateData.implicitCrit;
3974
4041
  const argTotal = typeof arg === "number" ? 1 : arg.total();
@@ -3982,27 +4049,29 @@ function applyByOutcome(labelled, op, arg, termText, n) {
3982
4049
  rest = subtractCounts(rest, part);
3983
4050
  let applied;
3984
4051
  if (label === "crit" && implicit && termText !== void 0) {
3985
- payload = implicit.payload + termText;
4052
+ payload = implicit.payload + (op === Dice.prototype.addNonZero ? "~" : "") + termText;
3986
4053
  const doubled = critPayload(payload, n);
3987
4054
  applied = doubled.normalize(part.total() * argTotal / doubled.total());
4055
+ } else if (PAYLOAD_OUTCOMES[label] === true) {
4056
+ applied = applyToLanded(part, op, arg, part.get(0), false);
3988
4057
  } else {
3989
4058
  applied = op.call(part, arg);
3990
4059
  }
3991
4060
  result.combineInPlace(applied);
3992
4061
  result.setOutcomeDistribution(label, applied.getFaceMap());
3993
4062
  }
3994
- result.combineInPlace(op.call(rest, arg));
4063
+ result.combineInPlace(applyToLanded(rest, op, arg, landedAtZero(labelled), false));
3995
4064
  if (labelled.privateData.isDCCheck) result.privateData.isDCCheck = true;
3996
4065
  if (payload !== void 0) result.privateData.implicitCrit = { payload };
3997
4066
  return result;
3998
4067
  }
3999
- function landedHitsAtZero(operand, op, arg) {
4068
+ function landedHitsAtZero(operand, op, arg, gate) {
4000
4069
  let landed = 0;
4001
4070
  for (const [face, count] of Object.entries(operand.calculateHitDistribution())) {
4002
4071
  if (!(count > 0)) continue;
4003
4072
  const hit = new Dice();
4004
4073
  hit.setFace(Number(face), count);
4005
- landed += op.call(hit, arg).get(0);
4074
+ landed += applyToLanded(hit, op, arg, Number(face) === 0 ? count : 0, gate).get(0);
4006
4075
  }
4007
4076
  return landed;
4008
4077
  }