@yipe/dice 0.7.0 → 0.8.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.
@@ -3501,6 +3501,17 @@ var RollBuilder = class _RollBuilder {
3501
3501
  getSubRollConfigs() {
3502
3502
  return this.subRollConfigs.map((c) => ({ ...c }));
3503
3503
  }
3504
+ /**
3505
+ * A cheap, stable string that FULLY identifies this builder's PMF — used by {@link AttackBuilder.toPMF}
3506
+ * to cache resolved attack PMFs across rebuilds without walking the AST via {@link toExpression}. A plain
3507
+ * roll is fully determined by its {@link RollConfig} array (count/sides/modifier/reroll/explode/minimum/
3508
+ * bestOf/keep/rollType/isSubtraction), so serializing that is sound. Subclasses whose PMF depends on
3509
+ * hidden state NOT captured by `subRollConfigs` (half/scale/max/parsed/pooled/composite transforms) return
3510
+ * `null` to opt OUT of caching — a conservative miss is always safe; a wrong key would corrupt DPR.
3511
+ */
3512
+ cacheKey() {
3513
+ return JSON.stringify(this.subRollConfigs);
3514
+ }
3504
3515
  // for testing
3505
3516
  static fromConfig(config) {
3506
3517
  return new _RollBuilder([{ ...defaultConfig, ...config }]);
@@ -4009,6 +4020,9 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
4009
4020
  hasHiddenState() {
4010
4021
  return this.innerRoll.hasHiddenState();
4011
4022
  }
4023
+ cacheKey() {
4024
+ return null;
4025
+ }
4012
4026
  // No need to override create if we don't expose RollBuilder methods that use it,
4013
4027
  // but HalfRollBuilder extends RollBuilder so it does.
4014
4028
  // However, HalfRollBuilder seems to just wrap another roll.
@@ -4053,6 +4067,9 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4053
4067
  hasHiddenState() {
4054
4068
  return this.innerRoll.hasHiddenState();
4055
4069
  }
4070
+ cacheKey() {
4071
+ return null;
4072
+ }
4056
4073
  get lastConfig() {
4057
4074
  return this.innerRoll.lastConfig;
4058
4075
  }
@@ -4097,6 +4114,9 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
4097
4114
  hasHiddenState() {
4098
4115
  return this.innerRoll.hasHiddenState();
4099
4116
  }
4117
+ cacheKey() {
4118
+ return null;
4119
+ }
4100
4120
  get lastConfig() {
4101
4121
  return this.innerRoll.lastConfig;
4102
4122
  }
@@ -4174,6 +4194,10 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
4174
4194
  get critThreshold() {
4175
4195
  return this.attackConfig.critThreshold;
4176
4196
  }
4197
+ cacheKey() {
4198
+ const base = super.cacheKey();
4199
+ return base === null ? null : `H|${this.attackConfig.critThreshold}|${base}`;
4200
+ }
4177
4201
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
4178
4202
  critOn(critThreshold) {
4179
4203
  const newConfig = { critThreshold };
@@ -4224,6 +4248,10 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4224
4248
  get critThreshold() {
4225
4249
  return this.attackConfig.critThreshold;
4226
4250
  }
4251
+ cacheKey() {
4252
+ const base = super.cacheKey();
4253
+ return base === null ? null : `C|${this.fromAlwaysHit ? 1 : 0}|${this.attackConfig.critThreshold}|${this.attackConfig.ac ?? ""}|${base}`;
4254
+ }
4227
4255
  critOn(critThreshold) {
4228
4256
  const newConfig = { critThreshold, ac: this.attackConfig.ac };
4229
4257
  return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
@@ -4254,6 +4282,9 @@ var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
4254
4282
  hasHiddenState() {
4255
4283
  return true;
4256
4284
  }
4285
+ cacheKey() {
4286
+ return null;
4287
+ }
4257
4288
  create(configs) {
4258
4289
  return new RollBuilder(configs);
4259
4290
  }
@@ -4289,6 +4320,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
4289
4320
  hasHiddenState() {
4290
4321
  return true;
4291
4322
  }
4323
+ cacheKey() {
4324
+ return null;
4325
+ }
4292
4326
  d(_sides) {
4293
4327
  throw new Error("Cannot add dice to a pooled roll. The pool is finalized.");
4294
4328
  }
@@ -4392,6 +4426,9 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
4392
4426
  hasHiddenState() {
4393
4427
  return true;
4394
4428
  }
4429
+ cacheKey() {
4430
+ return null;
4431
+ }
4395
4432
  getSubRollConfigs() {
4396
4433
  return [];
4397
4434
  }
@@ -4944,6 +4981,10 @@ function getASTSignature(node) {
4944
4981
  }
4945
4982
 
4946
4983
  // src/builder/attack.ts
4984
+ var attackPMFCache = new LRUCache(4e3);
4985
+ function clearAttackCache() {
4986
+ attackPMFCache.clear();
4987
+ }
4947
4988
  var AttackBuilder = class _AttackBuilder {
4948
4989
  constructor(check, hitEffect, critEffect, missEffect) {
4949
4990
  this.check = check;
@@ -5122,9 +5163,46 @@ var AttackBuilder = class _AttackBuilder {
5122
5163
  weights: { hit: phit, crit: pcrit, miss: pmiss }
5123
5164
  };
5124
5165
  }
5125
- // By default, create PMF with no pruning
5166
+ /**
5167
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
5168
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
5169
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
5170
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
5171
+ */
5172
+ cacheKey(eps) {
5173
+ const checkKey = this.check.cacheKey();
5174
+ if (checkKey === null) return null;
5175
+ let hitKey = "";
5176
+ if (this.hitEffect) {
5177
+ const k = this.hitEffect.cacheKey();
5178
+ if (k === null) return null;
5179
+ hitKey = k;
5180
+ }
5181
+ let critKey;
5182
+ if (this.critEffect === null) critKey = "n";
5183
+ else if (this.critEffect === void 0) critKey = "a";
5184
+ else {
5185
+ const k = this.critEffect.cacheKey();
5186
+ if (k === null) return null;
5187
+ critKey = k;
5188
+ }
5189
+ let missKey = "";
5190
+ if (this.missEffect) {
5191
+ const k = this.missEffect.cacheKey();
5192
+ if (k === null) return null;
5193
+ missKey = k;
5194
+ }
5195
+ return `${checkKey}*H${hitKey}*C${critKey}*M${missKey}*e${eps}`;
5196
+ }
5197
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5126
5198
  toPMF(eps = 0) {
5127
- return this.resolve(eps).pmf;
5199
+ const key = this.cacheKey(eps);
5200
+ if (key === null) return this.resolve(eps).pmf;
5201
+ const cached = attackPMFCache.get(key);
5202
+ if (cached) return cached;
5203
+ const pmf = this.resolve(eps).pmf;
5204
+ attackPMFCache.set(key, pmf);
5205
+ return pmf;
5128
5206
  }
5129
5207
  get pmf() {
5130
5208
  return this.toPMF();
@@ -5152,6 +5230,10 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
5152
5230
  get critThreshold() {
5153
5231
  return this.attackConfig.critThreshold;
5154
5232
  }
5233
+ cacheKey() {
5234
+ const base = super.cacheKey();
5235
+ return base === null ? null : `A|${this.attackConfig.ac}|${this.attackConfig.critThreshold}|${base}`;
5236
+ }
5155
5237
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
5156
5238
  critOn(threshold) {
5157
5239
  const newConfig = {
@@ -5356,6 +5438,6 @@ RollBuilder.prototype.dc = function(saveDC) {
5356
5438
  return new DCBuilder(this).dc(saveDC);
5357
5439
  };
5358
5440
 
5359
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
5441
+ export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, builderPMFCache, clearAttackCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
5360
5442
  //# sourceMappingURL=index.js.map
5361
5443
  //# sourceMappingURL=index.js.map