@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/dist/index.cjs CHANGED
@@ -3189,9 +3189,23 @@ var Dice = class _Dice {
3189
3189
  result.privateData.isDCCheck = true;
3190
3190
  return result;
3191
3191
  }
3192
+ /**
3193
+ * An attack check: a total that meets the target lands at its own value, and a miss is 0. A total
3194
+ * of exactly 0 that meets the target (a target of 0 or less) lands at 0 too, where the misses sit:
3195
+ * its count is recorded under `hit` at 0, like a payload's 0-damage hit, so the ops after the
3196
+ * check tell it from a miss.
3197
+ */
3192
3198
  ac(other) {
3193
3199
  const acCheck = (a, b) => a >= b ? a : 0;
3194
- return this.checkTarget(other, acCheck);
3200
+ const result = this.checkTarget(other, acCheck);
3201
+ const zero = this.get(0);
3202
+ if (zero > 0) {
3203
+ let met = 0;
3204
+ if (typeof other === "number") met = other <= 0 ? 1 : 0;
3205
+ else for (const [target, count] of other.getFaceEntries()) if (target <= 0) met += count;
3206
+ if (met > 0) result.setOutcomeDistribution("hit", { 0: zero * met });
3207
+ }
3208
+ return result;
3195
3209
  }
3196
3210
  deleteFace(face) {
3197
3211
  const result = new _Dice();
@@ -3692,6 +3706,8 @@ function parseExpression(arr, n, inCheck = false) {
3692
3706
  const hitText = pending?.slice(0, pending.length - arr.length);
3693
3707
  const termText = opText?.slice(0, opText.length - arr.length);
3694
3708
  const before = finalResult;
3709
+ const attack = isAttack(finalResult);
3710
+ const acCheck = finalResult.privateData.isACCheck === true;
3695
3711
  if (op === Dice.prototype.combine) assertMixable(before, arg, arr);
3696
3712
  let crit;
3697
3713
  let critNorm = 1;
@@ -3713,7 +3729,8 @@ function parseExpression(arr, n, inCheck = false) {
3713
3729
  } else {
3714
3730
  ({ crit, rest: finalResult } = splitCrit(finalResult, count));
3715
3731
  critNorm = crit.total();
3716
- crit = op.call(crit, critClause ? parseBinaryArgument(arg, arr, n) : critPayload(hitText, n));
3732
+ const critArg = critClause ? parseBinaryArgument(arg, arr, n) : critPayload(hitText, n);
3733
+ crit = HIT_ONLY_OPS.has(op) ? applyToLanded(crit, op, critArg, crit.get(0), acCheck) : op.call(crit, critArg);
3717
3734
  critNorm = crit && critNorm ? crit.total() / critNorm : 1;
3718
3735
  }
3719
3736
  }
@@ -3724,12 +3741,10 @@ function parseExpression(arr, n, inCheck = false) {
3724
3741
  assertToken(arr, "a");
3725
3742
  assertToken(arr, "v");
3726
3743
  assertToken(arr, "e");
3727
- save = new Dice();
3728
- const min = finalResult.minFace();
3729
- save.increment(min > 0 ? min : 1, finalResult.get(min));
3730
- saveNorm = save.total();
3731
- finalResult = finalResult.deleteFace(min);
3732
- save = op.call(save, parseBinaryArgument(arg, arr, n));
3744
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3745
+ finalResult = rest;
3746
+ saveNorm = missed.total();
3747
+ save = op.call(missed, parseBinaryArgument(arg, arr, n));
3733
3748
  saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
3734
3749
  }
3735
3750
  let pc;
@@ -3737,12 +3752,10 @@ function parseExpression(arr, n, inCheck = false) {
3737
3752
  if (arr.length >= 2 && arr[0] === "p" && arr[1] === "c") {
3738
3753
  assertToken(arr, "p");
3739
3754
  assertToken(arr, "c");
3740
- pc = new Dice();
3741
- const min = finalResult.minFace();
3742
- pc.increment(min > 0 ? min : 1, finalResult.get(min));
3743
- const missBefore = pc.total();
3744
- finalResult = finalResult.deleteFace(min);
3745
- pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
3755
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3756
+ finalResult = rest;
3757
+ const missBefore = missed.total();
3758
+ pc = op.call(missed, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
3746
3759
  const missAfter = pc ? pc.total() : 0;
3747
3760
  pcNorm = missBefore ? missAfter / missBefore : 1;
3748
3761
  }
@@ -3753,12 +3766,10 @@ function parseExpression(arr, n, inCheck = false) {
3753
3766
  assertToken(arr, "i");
3754
3767
  assertToken(arr, "s");
3755
3768
  assertToken(arr, "s");
3756
- miss = new Dice();
3757
- const min = finalResult.minFace();
3758
- miss.increment(min > 0 ? min : 1, finalResult.get(min));
3759
- missNorm = miss.total();
3760
- finalResult = finalResult.deleteFace(min);
3761
- miss = op.call(miss, parseBinaryArgument(arg, arr, n));
3769
+ const { miss: missed, rest } = splitMiss(finalResult, attack);
3770
+ finalResult = rest;
3771
+ missNorm = missed.total();
3772
+ miss = op.call(missed, parseBinaryArgument(arg, arr, n));
3762
3773
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3763
3774
  }
3764
3775
  let norm = finalResult.total();
@@ -3767,19 +3778,21 @@ function parseExpression(arr, n, inCheck = false) {
3767
3778
  const operand = finalResult;
3768
3779
  if (!clause && labelled && HIT_ONLY_OPS.has(op)) {
3769
3780
  finalResult = applyByOutcome(finalResult, op, arg, termText, n);
3781
+ } else if (HIT_ONLY_OPS.has(op)) {
3782
+ finalResult = applyToLanded(finalResult, op, arg, landedAtZero(finalResult), acCheck);
3770
3783
  } else {
3771
3784
  finalResult = op.call(finalResult, arg);
3772
3785
  }
3773
3786
  if (operand.privateData.isDCCheck && HIT_ONLY_OPS.has(op)) {
3774
3787
  finalResult.setOutcomeDistribution("saveFail", op.call(operand.deleteFace(0), arg).getFaceMap());
3775
3788
  }
3776
- if (!operand.privateData.isDCCheck && HIT_ONLY_OPS.has(op) && (hitText !== void 0 || operand.privateData.attackPayload)) {
3789
+ if (attack && HIT_ONLY_OPS.has(op)) {
3777
3790
  finalResult.privateData.attackPayload = true;
3778
- const landed = landedHitsAtZero(operand, op, arg);
3791
+ const landed = landedHitsAtZero(operand, op, arg, acCheck);
3779
3792
  if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
3780
- } else if (op === Dice.prototype.combine && typeof arg !== "number" && arg.privateData.attackPayload) {
3781
- finalResult.privateData.attackPayload = true;
3782
- const landed = operand.getOutcomeCount("hit", 0) + arg.getOutcomeCount("hit", 0);
3793
+ } else if (op === Dice.prototype.combine && typeof arg !== "number") {
3794
+ if (arg.privateData.attackPayload) finalResult.privateData.attackPayload = true;
3795
+ const landed = landedAtZero(operand) + landedAtZero(arg);
3783
3796
  if (landed > 0) finalResult.setOutcomeDistribution("hit", { 0: landed });
3784
3797
  }
3785
3798
  const gated = op === Dice.prototype.combine && typeof arg !== "number" && arg.privateData.isACCheck;
@@ -3869,6 +3882,34 @@ function branchesOf(value) {
3869
3882
  }
3870
3883
  var hasOutcomeLabels = (value) => typeof value !== "number" && Object.keys(value.getFullOutcomeDistribution()).some((label) => label !== "hit");
3871
3884
  var isSave = (value) => typeof value !== "number" && value.privateData.isDCCheck === true;
3885
+ var isAttack = (value) => value.privateData.isDCCheck !== true && (value.privateData.isACCheck === true || value.privateData.attackPayload === true);
3886
+ var landedAtZero = (value) => typeof value === "number" ? 0 : value.getOutcomeCount("hit", 0);
3887
+ function applyToLanded(value, op, arg, landed, gate) {
3888
+ const lands = op === Dice.prototype.addNonZero || gate && op === Dice.prototype.conditionalApply;
3889
+ if (!(landed > 0) || !lands) return op.call(value, arg);
3890
+ const misses = value.deleteFace(0);
3891
+ const missed = value.get(0) - landed;
3892
+ if (missed > 0) misses.setFace(0, missed);
3893
+ const result = op.call(misses, arg);
3894
+ result.combineInPlace(asValue(arg).normalize(landed));
3895
+ return result;
3896
+ }
3897
+ function applyToAttack(value, op, arg, gate) {
3898
+ const result = applyToLanded(value, op, arg, landedAtZero(value), gate);
3899
+ result.privateData.attackPayload = true;
3900
+ const landed = landedHitsAtZero(value, op, arg, gate);
3901
+ if (landed > 0) result.setOutcomeDistribution("hit", { 0: landed });
3902
+ return result;
3903
+ }
3904
+ function splitMiss(check, attack) {
3905
+ const face = attack ? 0 : check.minFace();
3906
+ const landed = face === 0 ? landedAtZero(check) : 0;
3907
+ const miss = new Dice();
3908
+ miss.increment(face > 0 ? face : 1, check.get(face) - landed);
3909
+ const rest = check.deleteFace(face);
3910
+ if (landed > 0) rest.setFace(0, landed);
3911
+ return { miss, rest };
3912
+ }
3872
3913
  function assertMixable(left, right, rest) {
3873
3914
  if (hasOutcomeLabels(left) || hasOutcomeLabels(right)) {
3874
3915
  throw new Error(
@@ -3903,7 +3944,7 @@ function followNaturalRoll(before, op, arg, after, clause) {
3903
3944
  const rights = advantage ? lefts : gate ? [asValue(arg)] : branchesOf(arg);
3904
3945
  data.branches = lefts.flatMap(
3905
3946
  (left) => rights.map((right) => {
3906
- const part = pairOp.call(left, right);
3947
+ const part = HIT_ONLY_OPS.has(pairOp) && isAttack(left) ? applyToAttack(left, pairOp, right, left.privateData.isACCheck === true) : pairOp.call(left, right);
3907
3948
  followNaturalRoll(left, pairOp, right, part, false);
3908
3949
  return part;
3909
3950
  })
@@ -3925,7 +3966,10 @@ function followNaturalRoll(before, op, arg, after, clause) {
3925
3966
  } else if (extremum && track?.bare && argTrack?.bare && sides === argSides) {
3926
3967
  data.critTrack = bareTrack(after);
3927
3968
  } else if (followed) {
3928
- const apply = own ? (value) => op.call(value, arg) : (value) => op.call(before, value);
3969
+ const attack = HIT_ONLY_OPS.has(op) && isAttack(before);
3970
+ const gated = before.privateData.isACCheck === true;
3971
+ const call = (value, other) => attack ? applyToAttack(value, op, other, gated) : op.call(value, other);
3972
+ const apply = own ? (value) => call(value, arg) : (value) => call(before, value);
3929
3973
  const step = extremum ? (slice) => keptPart(slice, apply) : apply;
3930
3974
  data.critTrack = { sides: followed.sides, bare: false, slice: (face) => step(followed.slice(face)) };
3931
3975
  }
@@ -3952,7 +3996,13 @@ function mixNaturalRolls(after, branches) {
3952
3996
  bare: tracks.length === branches.length && tracks.every((track) => track.bare),
3953
3997
  slice: (face) => {
3954
3998
  const slice = new Dice();
3955
- for (const track of tracks) slice.combineInPlace(track.slice(face));
3999
+ let landed = 0;
4000
+ for (const track of tracks) {
4001
+ const part = track.slice(face);
4002
+ slice.combineInPlace(part);
4003
+ landed += landedAtZero(part);
4004
+ }
4005
+ if (landed > 0) slice.setOutcomeDistribution("hit", { 0: landed });
3956
4006
  return slice;
3957
4007
  }
3958
4008
  };
@@ -3970,7 +4020,9 @@ function keptPart(slice, apply) {
3970
4020
  function splitCrit(check, count) {
3971
4021
  if (count === 0) {
3972
4022
  const none = new Dice();
3973
- return { crit: none, rest: subtractCounts(check, none) };
4023
+ const rest2 = subtractCounts(check, none);
4024
+ if (landedAtZero(check) > 0) rest2.setOutcomeDistribution("hit", { 0: landedAtZero(check) });
4025
+ return { crit: none, rest: rest2 };
3974
4026
  }
3975
4027
  const track = check.privateData.critTrack;
3976
4028
  if (!track) {
@@ -3983,13 +4035,21 @@ function splitCrit(check, count) {
3983
4035
  throw new Error(`xcrit${count} is wider than the d${sides} it reads its natural roll from`);
3984
4036
  }
3985
4037
  let crit = new Dice();
3986
- for (let face = sides; face > sides - count; face--) crit.combineInPlace(track.slice(face));
4038
+ let landed = 0;
4039
+ for (let face = sides; face > sides - count; face--) {
4040
+ const slice = track.slice(face);
4041
+ crit.combineInPlace(slice);
4042
+ landed += landedAtZero(slice);
4043
+ }
3987
4044
  const rest = subtractCounts(check, crit);
3988
- const missed = crit.get(0);
4045
+ const missed = crit.get(0) - landed;
3989
4046
  if (missed) {
3990
4047
  crit = crit.deleteFace(0);
4048
+ if (landed > 0) crit.setFace(0, landed);
3991
4049
  rest.increment(0, missed);
3992
4050
  }
4051
+ const restLanded = landedAtZero(check) - landed;
4052
+ if (restLanded > 0) rest.setOutcomeDistribution("hit", { 0: restLanded });
3993
4053
  return { crit, rest };
3994
4054
  }
3995
4055
  function critPayload(text, n) {
@@ -4006,6 +4066,13 @@ function critPayload(text, n) {
4006
4066
  }
4007
4067
  return payload;
4008
4068
  }
4069
+ var PAYLOAD_OUTCOMES = {
4070
+ crit: true,
4071
+ missDamage: true,
4072
+ pc: true,
4073
+ saveFail: true,
4074
+ saveHalf: true
4075
+ };
4009
4076
  function applyByOutcome(labelled, op, arg, termText, n) {
4010
4077
  const implicit = labelled.privateData.implicitCrit;
4011
4078
  const argTotal = typeof arg === "number" ? 1 : arg.total();
@@ -4019,27 +4086,29 @@ function applyByOutcome(labelled, op, arg, termText, n) {
4019
4086
  rest = subtractCounts(rest, part);
4020
4087
  let applied;
4021
4088
  if (label === "crit" && implicit && termText !== void 0) {
4022
- payload = implicit.payload + termText;
4089
+ payload = implicit.payload + (op === Dice.prototype.addNonZero ? "~" : "") + termText;
4023
4090
  const doubled = critPayload(payload, n);
4024
4091
  applied = doubled.normalize(part.total() * argTotal / doubled.total());
4092
+ } else if (PAYLOAD_OUTCOMES[label] === true) {
4093
+ applied = applyToLanded(part, op, arg, part.get(0), false);
4025
4094
  } else {
4026
4095
  applied = op.call(part, arg);
4027
4096
  }
4028
4097
  result.combineInPlace(applied);
4029
4098
  result.setOutcomeDistribution(label, applied.getFaceMap());
4030
4099
  }
4031
- result.combineInPlace(op.call(rest, arg));
4100
+ result.combineInPlace(applyToLanded(rest, op, arg, landedAtZero(labelled), false));
4032
4101
  if (labelled.privateData.isDCCheck) result.privateData.isDCCheck = true;
4033
4102
  if (payload !== void 0) result.privateData.implicitCrit = { payload };
4034
4103
  return result;
4035
4104
  }
4036
- function landedHitsAtZero(operand, op, arg) {
4105
+ function landedHitsAtZero(operand, op, arg, gate) {
4037
4106
  let landed = 0;
4038
4107
  for (const [face, count] of Object.entries(operand.calculateHitDistribution())) {
4039
4108
  if (!(count > 0)) continue;
4040
4109
  const hit = new Dice();
4041
4110
  hit.setFace(Number(face), count);
4042
- landed += op.call(hit, arg).get(0);
4111
+ landed += applyToLanded(hit, op, arg, Number(face) === 0 ? count : 0, gate).get(0);
4043
4112
  }
4044
4113
  return landed;
4045
4114
  }