@yipe/dice 0.7.0 → 0.8.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.
@@ -16,6 +16,7 @@ export declare class ACBuilder extends RollBuilder {
16
16
  onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
17
17
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
18
18
  get critThreshold(): number;
19
+ cacheKey(): string | null;
19
20
  critOn(threshold: number): ACBuilder;
20
21
  alwaysCrits(): AlwaysCritBuilder;
21
22
  toExpression(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,qBAAa,SAAU,SAAQ,WAAW;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,EAQzE;IAMD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAMrE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAGD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAMnC;IAED,WAAW,IAAI,iBAAiB,CAS/B;IAGQ,YAAY,IAAI,MAAM,CAM9B;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CA0BnC;IAEQ,IAAI,IAAI,SAAS,CAOzB;CACF"}
1
+ {"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,qBAAa,SAAU,SAAQ,WAAW;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,EAQzE;IAMD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAMrE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAEQ,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGjC;IAGD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAMnC;IAED,WAAW,IAAI,iBAAiB,CAS/B;IAGQ,YAAY,IAAI,MAAM,CAM9B;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CA0BnC;IAEQ,IAAI,IAAI,SAAS,CAOzB;CACF"}
@@ -4,6 +4,8 @@ import type { ACBuilder } from "./ac.js";
4
4
  import { AlwaysCritBuilder, AlwaysHitBuilder, RollBuilder } from "./roll.js";
5
5
  import type { AttackResolution, CheckBuilder } from "./types.js";
6
6
  type ActionEffect = RollBuilder;
7
+ /** Clears the resolved-attack PMF cache (test/bench seam; mirrors {@link clearParserCache}). */
8
+ export declare function clearAttackCache(): void;
7
9
  export declare class AttackBuilder implements CheckBuilder {
8
10
  readonly check: ACBuilder | AlwaysHitBuilder | AlwaysCritBuilder;
9
11
  private readonly hitEffect?;
@@ -33,6 +35,13 @@ export declare class AttackBuilder implements CheckBuilder {
33
35
  pMiss: number;
34
36
  };
35
37
  resolve(eps?: number): AttackResolution;
38
+ /**
39
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
40
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
41
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
42
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
43
+ */
44
+ private cacheKey;
36
45
  toPMF(eps?: number): PMF;
37
46
  get pmf(): PMF;
38
47
  toQuery(eps?: number): DiceQuery;
@@ -1 +1 @@
1
- {"version":3,"file":"attack.d.ts","sourceRoot":"","sources":["../../src/builder/attack.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAGtC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAEhB,WAAW,EACZ,MAAM,QAAQ,CAAC;AAChB,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE9D,KAAK,YAAY,GAAG,WAAW,CAAC;AAEhC,qBAAa,aAAc,YAAW,YAAY;IAE9C,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAJ9B,YACW,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB,EAC/C,SAAS,CAAC,EAAE,YAAY,YAAA,EACxB,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,YAAA,EAChC,UAAU,CAAC,EAAE,YAAY,YAAA,EACxC;IAEJ,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACzE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAWtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACzE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAWtE,MAAM,IAAI,aAAa,CAEtB;IAGD,YAAY,IAAI,MAAM,CA+CrB;IAED,oBAAoB,CAClB,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB,EACvD,GAAG,GAAE,MAAU,GACd;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAmGlE;IAED,OAAO,CAAC,GAAG,GAAE,MAAY,GAAG,gBAAgB,CA+D3C;IAGD,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CAE1B;IAED,IAAI,GAAG,QAEN;IAGD,OAAO,CAAC,GAAG,GAAE,MAAU,GAAG,SAAS,CAElC;CACF"}
1
+ {"version":3,"file":"attack.d.ts","sourceRoot":"","sources":["../../src/builder/attack.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ import { SaveBuilder } from "./save.js";
4
4
  interface SaveConfig {
5
5
  dc: number;
6
6
  }
7
+ /** Clears the DC-check PMF cache (test/bench seam). */
8
+ export declare function clearDCCache(): void;
7
9
  export declare class DCBuilder extends RollBuilder {
8
10
  private readonly saveConfig;
9
11
  constructor(baseRoll: RollBuilder, saveConfig?: SaveConfig);
@@ -20,6 +22,12 @@ export declare class DCBuilder extends RollBuilder {
20
22
  onSaveFailure(count: number, sides: number, modifier: number): SaveBuilder;
21
23
  withElvenAccuracy(): never;
22
24
  toExpression(): string;
25
+ /**
26
+ * The DC check's PMF is fully determined by the save DC plus everything `toPMF` below reads off the roll
27
+ * configs (`rollType`, `baseReroll`, `modifier`, bonus dice) — all of which `super.cacheKey()` already
28
+ * serializes. So extend the base key with the DC, mirroring {@link AlwaysHitBuilder.cacheKey}.
29
+ */
30
+ cacheKey(): string | null;
23
31
  toPMF(eps?: number): PMF;
24
32
  }
25
33
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"dc.d.ts","sourceRoot":"","sources":["../../src/builder/dc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAGjC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,qBAAa,SAAU,SAAQ,WAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC,YAAY,QAAQ,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,UAAU,EAGzD;IAEQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAOrC;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAEQ,GAAG,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,CAGhD;IAEQ,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAG1C;IAED,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACxC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACxC,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC;IAC7C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC;IAC5D,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC;IACzD,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9E,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC;IAMlE,iBAAiB,IAAI,KAAK,CAIlC;IAGQ,YAAY,IAAI,MAAM,CAK9B;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CA2BnC;CACF"}
1
+ {"version":3,"file":"dc.d.ts","sourceRoot":"","sources":["../../src/builder/dc.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAGjC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ;AAQD,uDAAuD;AACvD,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,qBAAa,SAAU,SAAQ,WAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC,YAAY,QAAQ,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,UAAU,EAGzD;IAEQ,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAOrC;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAEQ,GAAG,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,CAGhD;IAEQ,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAG1C;IAED,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACxC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACxC,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC;IAC7C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC;IAC5D,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC;IACzD,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9E,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC;IAMlE,iBAAiB,IAAI,KAAK,CAIlC;IAGQ,YAAY,IAAI,MAAM,CAK9B;IAED;;;;OAIG;IACM,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGjC;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CAoCnC;CACF"}
@@ -3454,6 +3454,10 @@ function d20PMF(rerollOne) {
3454
3454
  }
3455
3455
 
3456
3456
  // src/builder/roll.ts
3457
+ var rollPMFCache = new LRUCache(4e3);
3458
+ function clearRollCache() {
3459
+ rollPMFCache.clear();
3460
+ }
3457
3461
  var defaultConfig = {
3458
3462
  count: 1,
3459
3463
  sides: 0,
@@ -3503,6 +3507,17 @@ var RollBuilder = class _RollBuilder {
3503
3507
  getSubRollConfigs() {
3504
3508
  return this.subRollConfigs.map((c) => ({ ...c }));
3505
3509
  }
3510
+ /**
3511
+ * A cheap, stable string that FULLY identifies this builder's PMF — used by {@link AttackBuilder.toPMF}
3512
+ * to cache resolved attack PMFs across rebuilds without walking the AST via {@link toExpression}. A plain
3513
+ * roll is fully determined by its {@link RollConfig} array (count/sides/modifier/reroll/explode/minimum/
3514
+ * bestOf/keep/rollType/isSubtraction), so serializing that is sound. Subclasses whose PMF depends on
3515
+ * hidden state NOT captured by `subRollConfigs` (half/scale/max/parsed/pooled/composite transforms) return
3516
+ * `null` to opt OUT of caching — a conservative miss is always safe; a wrong key would corrupt DPR.
3517
+ */
3518
+ cacheKey() {
3519
+ return JSON.stringify(this.subRollConfigs);
3520
+ }
3506
3521
  // for testing
3507
3522
  static fromConfig(config) {
3508
3523
  return new _RollBuilder([{ ...defaultConfig, ...config }]);
@@ -3855,8 +3870,16 @@ var RollBuilder = class _RollBuilder {
3855
3870
  }
3856
3871
  return result.replace(/\+ -/g, "-");
3857
3872
  }
3873
+ // Main AST entry point. Cached by the cheap config key across identical rebuilds; see `rollPMFCache`.
3858
3874
  toPMF(eps = 0) {
3859
- return pmfFromRollBuilder(this, eps);
3875
+ const key = this.cacheKey();
3876
+ if (key === null) return pmfFromRollBuilder(this, eps);
3877
+ const fullKey = `${key}*e${eps}`;
3878
+ const cached = rollPMFCache.get(fullKey);
3879
+ if (cached) return cached;
3880
+ const pmf = pmfFromRollBuilder(this, eps);
3881
+ rollPMFCache.set(fullKey, pmf);
3882
+ return pmf;
3860
3883
  }
3861
3884
  get pmf() {
3862
3885
  return this.toPMF();
@@ -4011,6 +4034,9 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
4011
4034
  hasHiddenState() {
4012
4035
  return this.innerRoll.hasHiddenState();
4013
4036
  }
4037
+ cacheKey() {
4038
+ return null;
4039
+ }
4014
4040
  // No need to override create if we don't expose RollBuilder methods that use it,
4015
4041
  // but HalfRollBuilder extends RollBuilder so it does.
4016
4042
  // However, HalfRollBuilder seems to just wrap another roll.
@@ -4055,6 +4081,9 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4055
4081
  hasHiddenState() {
4056
4082
  return this.innerRoll.hasHiddenState();
4057
4083
  }
4084
+ cacheKey() {
4085
+ return null;
4086
+ }
4058
4087
  get lastConfig() {
4059
4088
  return this.innerRoll.lastConfig;
4060
4089
  }
@@ -4099,6 +4128,9 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
4099
4128
  hasHiddenState() {
4100
4129
  return this.innerRoll.hasHiddenState();
4101
4130
  }
4131
+ cacheKey() {
4132
+ return null;
4133
+ }
4102
4134
  get lastConfig() {
4103
4135
  return this.innerRoll.lastConfig;
4104
4136
  }
@@ -4176,6 +4208,10 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
4176
4208
  get critThreshold() {
4177
4209
  return this.attackConfig.critThreshold;
4178
4210
  }
4211
+ cacheKey() {
4212
+ const base = super.cacheKey();
4213
+ return base === null ? null : `H|${this.attackConfig.critThreshold}|${base}`;
4214
+ }
4179
4215
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
4180
4216
  critOn(critThreshold) {
4181
4217
  const newConfig = { critThreshold };
@@ -4226,6 +4262,10 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4226
4262
  get critThreshold() {
4227
4263
  return this.attackConfig.critThreshold;
4228
4264
  }
4265
+ cacheKey() {
4266
+ const base = super.cacheKey();
4267
+ return base === null ? null : `C|${this.fromAlwaysHit ? 1 : 0}|${this.attackConfig.critThreshold}|${this.attackConfig.ac ?? ""}|${base}`;
4268
+ }
4229
4269
  critOn(critThreshold) {
4230
4270
  const newConfig = { critThreshold, ac: this.attackConfig.ac };
4231
4271
  return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
@@ -4256,6 +4296,9 @@ var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
4256
4296
  hasHiddenState() {
4257
4297
  return true;
4258
4298
  }
4299
+ cacheKey() {
4300
+ return null;
4301
+ }
4259
4302
  create(configs) {
4260
4303
  return new RollBuilder(configs);
4261
4304
  }
@@ -4291,6 +4334,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
4291
4334
  hasHiddenState() {
4292
4335
  return true;
4293
4336
  }
4337
+ cacheKey() {
4338
+ return null;
4339
+ }
4294
4340
  d(_sides) {
4295
4341
  throw new Error("Cannot add dice to a pooled roll. The pool is finalized.");
4296
4342
  }
@@ -4394,6 +4440,9 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
4394
4440
  hasHiddenState() {
4395
4441
  return true;
4396
4442
  }
4443
+ cacheKey() {
4444
+ return null;
4445
+ }
4397
4446
  getSubRollConfigs() {
4398
4447
  return [];
4399
4448
  }
@@ -4946,6 +4995,10 @@ function getASTSignature(node) {
4946
4995
  }
4947
4996
 
4948
4997
  // src/builder/attack.ts
4998
+ var attackPMFCache = new LRUCache(4e3);
4999
+ function clearAttackCache() {
5000
+ attackPMFCache.clear();
5001
+ }
4949
5002
  var AttackBuilder = class _AttackBuilder {
4950
5003
  constructor(check, hitEffect, critEffect, missEffect) {
4951
5004
  this.check = check;
@@ -5124,9 +5177,46 @@ var AttackBuilder = class _AttackBuilder {
5124
5177
  weights: { hit: phit, crit: pcrit, miss: pmiss }
5125
5178
  };
5126
5179
  }
5127
- // By default, create PMF with no pruning
5180
+ /**
5181
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
5182
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
5183
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
5184
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
5185
+ */
5186
+ cacheKey(eps) {
5187
+ const checkKey = this.check.cacheKey();
5188
+ if (checkKey === null) return null;
5189
+ let hitKey = "";
5190
+ if (this.hitEffect) {
5191
+ const k = this.hitEffect.cacheKey();
5192
+ if (k === null) return null;
5193
+ hitKey = k;
5194
+ }
5195
+ let critKey;
5196
+ if (this.critEffect === null) critKey = "n";
5197
+ else if (this.critEffect === void 0) critKey = "a";
5198
+ else {
5199
+ const k = this.critEffect.cacheKey();
5200
+ if (k === null) return null;
5201
+ critKey = k;
5202
+ }
5203
+ let missKey = "";
5204
+ if (this.missEffect) {
5205
+ const k = this.missEffect.cacheKey();
5206
+ if (k === null) return null;
5207
+ missKey = k;
5208
+ }
5209
+ return `${checkKey}*H${hitKey}*C${critKey}*M${missKey}*e${eps}`;
5210
+ }
5211
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5128
5212
  toPMF(eps = 0) {
5129
- return this.resolve(eps).pmf;
5213
+ const key = this.cacheKey(eps);
5214
+ if (key === null) return this.resolve(eps).pmf;
5215
+ const cached = attackPMFCache.get(key);
5216
+ if (cached) return cached;
5217
+ const pmf = this.resolve(eps).pmf;
5218
+ attackPMFCache.set(key, pmf);
5219
+ return pmf;
5130
5220
  }
5131
5221
  get pmf() {
5132
5222
  return this.toPMF();
@@ -5154,6 +5244,10 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
5154
5244
  get critThreshold() {
5155
5245
  return this.attackConfig.critThreshold;
5156
5246
  }
5247
+ cacheKey() {
5248
+ const base = super.cacheKey();
5249
+ return base === null ? null : `A|${this.attackConfig.ac}|${this.attackConfig.critThreshold}|${base}`;
5250
+ }
5157
5251
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
5158
5252
  critOn(threshold) {
5159
5253
  const newConfig = {
@@ -5214,6 +5308,10 @@ RollBuilder.prototype.ac = function(targetAC) {
5214
5308
  };
5215
5309
 
5216
5310
  // src/builder/save.ts
5311
+ var savePMFCache = new LRUCache(4e3);
5312
+ function clearSaveCache() {
5313
+ savePMFCache.clear();
5314
+ }
5217
5315
  var SaveBuilder = class _SaveBuilder {
5218
5316
  constructor(check, failureEffect, saveOutcome = "normal") {
5219
5317
  this.check = check;
@@ -5250,9 +5348,32 @@ var SaveBuilder = class _SaveBuilder {
5250
5348
  weights: { success: psuccess, fail: pfail }
5251
5349
  };
5252
5350
  }
5253
- // By default, create PMF with no pruning
5351
+ /**
5352
+ * A cheap, complete key for this save's resolved PMF, or `null` when it can't be cached soundly.
5353
+ * {@link resolve} reads exactly three things: the DC check (via `resolveProbabilities`), the failure
5354
+ * effect's PMF, and the save outcome — so composing their keys pins it. A `ParsedRollBuilder` failure
5355
+ * effect returns `null`, which correctly forces this uncached.
5356
+ */
5357
+ cacheKey(eps) {
5358
+ const checkKey = this.check.cacheKey();
5359
+ if (checkKey === null) return null;
5360
+ let failKey = "";
5361
+ if (this.failureEffect) {
5362
+ const k = this.failureEffect.cacheKey();
5363
+ if (k === null) return null;
5364
+ failKey = k;
5365
+ }
5366
+ return `${checkKey}*F${failKey}*O${this.saveOutcome}*e${eps}`;
5367
+ }
5368
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5254
5369
  toPMF(eps = 0) {
5255
- return this.resolve(eps).pmf;
5370
+ const key = this.cacheKey(eps);
5371
+ if (key === null) return this.resolve(eps).pmf;
5372
+ const cached = savePMFCache.get(key);
5373
+ if (cached) return cached;
5374
+ const pmf = this.resolve(eps).pmf;
5375
+ savePMFCache.set(key, pmf);
5376
+ return pmf;
5256
5377
  }
5257
5378
  get pmf() {
5258
5379
  return this.toPMF();
@@ -5288,6 +5409,10 @@ function resolveProbabilities(check) {
5288
5409
  }
5289
5410
 
5290
5411
  // src/builder/dc.ts
5412
+ var dcPMFCache = new LRUCache(4e3);
5413
+ function clearDCCache() {
5414
+ dcPMFCache.clear();
5415
+ }
5291
5416
  var DCBuilder = class _DCBuilder extends RollBuilder {
5292
5417
  constructor(baseRoll, saveConfig) {
5293
5418
  super(baseRoll.getSubRollConfigs());
@@ -5328,7 +5453,22 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5328
5453
  const expression = new RollBuilder(allConfigs).toExpression();
5329
5454
  return `(${expression} DC ${this.saveConfig.dc})`;
5330
5455
  }
5456
+ /**
5457
+ * The DC check's PMF is fully determined by the save DC plus everything `toPMF` below reads off the roll
5458
+ * configs (`rollType`, `baseReroll`, `modifier`, bonus dice) — all of which `super.cacheKey()` already
5459
+ * serializes. So extend the base key with the DC, mirroring {@link AlwaysHitBuilder.cacheKey}.
5460
+ */
5461
+ cacheKey() {
5462
+ const base = super.cacheKey();
5463
+ return base === null ? null : `DC|${this.saveConfig.dc}|${base}`;
5464
+ }
5331
5465
  toPMF(eps = 0) {
5466
+ const key = this.cacheKey();
5467
+ const fullKey = key === null ? null : `${key}*e${eps}`;
5468
+ if (fullKey !== null) {
5469
+ const cached = dcPMFCache.get(fullKey);
5470
+ if (cached) return cached;
5471
+ }
5332
5472
  const saveDC = this.saveDC;
5333
5473
  const rollType = this.rollType;
5334
5474
  const rerollOne = this.baseReroll > 0;
@@ -5350,7 +5490,9 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5350
5490
  [0, psuccess > 0 ? psuccess : 0],
5351
5491
  [1, pfail > 0 ? pfail : 0]
5352
5492
  ]);
5353
- return PMF.fromMap(m, eps);
5493
+ const pmf = PMF.fromMap(m, eps);
5494
+ if (fullKey !== null) dcPMFCache.set(fullKey, pmf);
5495
+ return pmf;
5354
5496
  }
5355
5497
  };
5356
5498
  RollBuilder.prototype.dc = function(saveDC) {
@@ -5371,6 +5513,10 @@ exports.RollBuilder = RollBuilder;
5371
5513
  exports.SaveBuilder = SaveBuilder;
5372
5514
  exports.ScaleRollBuilder = ScaleRollBuilder;
5373
5515
  exports.builderPMFCache = builderPMFCache;
5516
+ exports.clearAttackCache = clearAttackCache;
5517
+ exports.clearDCCache = clearDCCache;
5518
+ exports.clearRollCache = clearRollCache;
5519
+ exports.clearSaveCache = clearSaveCache;
5374
5520
  exports.d = d;
5375
5521
  exports.d10 = d10;
5376
5522
  exports.d100 = d100;