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