@yipe/dice 0.3.0 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { P as PMF, D as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-DqUCnYN9.cjs';
1
+ import { b as RollType, P as PMF, l as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-D5VRghZI.cjs';
2
2
 
3
3
  type RollFactory = {
4
4
  (count: number, sides?: number, modifier?: number): RollBuilder;
@@ -14,7 +14,6 @@ type RollFactory = {
14
14
  d100(): RollBuilder;
15
15
  flat(n: number): RollBuilder;
16
16
  };
17
- type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
18
17
  type KeepMode = "highest" | "lowest";
19
18
  type RollConfig = {
20
19
  count: number;
@@ -377,4 +376,4 @@ declare const flat: (n: number) => RollBuilder;
377
376
  declare const roll: RollFactory;
378
377
  declare const builderPMFCache: LRUCache<string, PMF>;
379
378
 
380
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, type RollType, SaveBuilder, type SaveOutcome, type SaveResolution, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
379
+ export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, RollType, SaveBuilder, type SaveOutcome, type SaveResolution, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
@@ -1,4 +1,4 @@
1
- import { P as PMF, D as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-DqUCnYN9.js';
1
+ import { b as RollType, P as PMF, l as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-D5VRghZI.js';
2
2
 
3
3
  type RollFactory = {
4
4
  (count: number, sides?: number, modifier?: number): RollBuilder;
@@ -14,7 +14,6 @@ type RollFactory = {
14
14
  d100(): RollBuilder;
15
15
  flat(n: number): RollBuilder;
16
16
  };
17
- type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
18
17
  type KeepMode = "highest" | "lowest";
19
18
  type RollConfig = {
20
19
  count: number;
@@ -377,4 +376,4 @@ declare const flat: (n: number) => RollBuilder;
377
376
  declare const roll: RollFactory;
378
377
  declare const builderPMFCache: LRUCache<string, PMF>;
379
378
 
380
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, type RollType, SaveBuilder, type SaveOutcome, type SaveResolution, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
379
+ export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, RollType, SaveBuilder, type SaveOutcome, type SaveResolution, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
@@ -42,6 +42,7 @@ var LRUCache = class {
42
42
 
43
43
  // src/common/types.ts
44
44
  var EPS = 1e-12;
45
+ var MISS_NONE_OUTCOME = "missNone";
45
46
 
46
47
  // src/pmf/query.ts
47
48
  var _DiceQuery = class _DiceQuery {
@@ -104,6 +105,29 @@ var _DiceQuery = class _DiceQuery {
104
105
  this._combinedWithAttr = normalized;
105
106
  return normalized;
106
107
  }
108
+ /**
109
+ * Per-label `damage value → probability mass` series for the combined,
110
+ * attribution-carrying distribution — the provenance core of the stacked
111
+ * damage-attribution chart. Convenience for
112
+ * `combinedWithAttribution().attributionByValue()`; see
113
+ * {@link PMF.attributionByValue}.
114
+ */
115
+ attributionByValue() {
116
+ return this.combinedWithAttribution().attributionByValue();
117
+ }
118
+ /**
119
+ * How many of the independent single PMFs can produce the given outcome
120
+ * label. Useful for "all of them succeeded" style probabilities where the
121
+ * exponent is the number of contributing attacks (see
122
+ * {@link DiceQuery.probExactlyK}).
123
+ */
124
+ countSinglesWith(label) {
125
+ let count = 0;
126
+ for (const single of this.singles) {
127
+ if (single.hasOutcome(label)) count++;
128
+ }
129
+ return count;
130
+ }
107
131
  /**
108
132
  * Returns the expected damage across all possible outcomes.
109
133
  *
@@ -1387,6 +1411,20 @@ var _PMF = class _PMF {
1387
1411
  static delta(value, epsilon = EPS) {
1388
1412
  return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
1389
1413
  }
1414
+ /**
1415
+ * Point mass at damage 0 tagged with the canonical `missNone` outcome.
1416
+ *
1417
+ * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
1418
+ * builder's attack-resolution vocabulary. This uses the `missNone`
1419
+ * {@link OutcomeType} that the attribution charts and outcome stats key on,
1420
+ * so it is the correct "clean miss / no damage" delta for provenance-aware
1421
+ * mixtures feeding those consumers.
1422
+ */
1423
+ static missNone(epsilon = EPS) {
1424
+ const m = /* @__PURE__ */ new Map();
1425
+ m.set(0, { p: 1, count: { [MISS_NONE_OUTCOME]: 1 }, attr: {} });
1426
+ return new _PMF(m, epsilon, false, "missNone");
1427
+ }
1390
1428
  // This creates a single bin at value 0, but with weight 0.
1391
1429
  static emptyMass() {
1392
1430
  return _PMF.zero().scaleMass(0);
@@ -1908,6 +1946,49 @@ var _PMF = class _PMF {
1908
1946
  `${this.identifier}+scaled(${branch.identifier},${probability})`
1909
1947
  );
1910
1948
  }
1949
+ /**
1950
+ * Redistributes probability mass to model an effect that only occurs with
1951
+ * probability `frequency` — a conditional attack, an on-hit rider, or a
1952
+ * sub-one AoE target fraction.
1953
+ *
1954
+ * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
1955
+ * per-label `count`, AND per-label `attr` — and the freed mass is moved into
1956
+ * the miss bin at damage 0, tagged with the canonical `missNone` outcome.
1957
+ * Total probability mass is preserved.
1958
+ *
1959
+ * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
1960
+ * attribution (`attr`) intact, so a frequency-scaled PMF still renders
1961
+ * correctly in the damage-attribution charts.
1962
+ *
1963
+ * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
1964
+ * collapses all mass into the miss bin. The miss outcome is assumed to be
1965
+ * encoded at damage value 0.
1966
+ *
1967
+ * @param frequency Probability in [0, 1] that the effect occurs.
1968
+ */
1969
+ applyHitFrequency(frequency) {
1970
+ if (!Number.isFinite(frequency) || frequency >= 1) return this;
1971
+ const freq = Math.max(0, frequency);
1972
+ const pMiss = this.pAt(0);
1973
+ const pHit = 1 - pMiss;
1974
+ const newMissMass = pMiss + (1 - freq) * pHit;
1975
+ const newMap = /* @__PURE__ */ new Map();
1976
+ newMap.set(0, {
1977
+ p: newMissMass,
1978
+ count: { [MISS_NONE_OUTCOME]: newMissMass },
1979
+ attr: {}
1980
+ });
1981
+ for (const [damage, bin] of this.map) {
1982
+ if (damage <= 0) continue;
1983
+ newMap.set(damage, _PMF.scaleBin(bin, freq));
1984
+ }
1985
+ return new _PMF(
1986
+ newMap,
1987
+ this.epsilon,
1988
+ false,
1989
+ `freq(${this.identifier},${freq})`
1990
+ );
1991
+ }
1911
1992
  scaleMass(factor) {
1912
1993
  if (factor === 1) return this;
1913
1994
  const scaledMap = /* @__PURE__ */ new Map();
@@ -2135,6 +2216,39 @@ var _PMF = class _PMF {
2135
2216
  pAt(x) {
2136
2217
  return this.map.get(x)?.p ?? 0;
2137
2218
  }
2219
+ /**
2220
+ * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
2221
+ * Assumes a miss is encoded as the damage-0 bin (the convention used across
2222
+ * attack/save PMFs). The dual of {@link missProbability}.
2223
+ */
2224
+ hitProbability() {
2225
+ return 1 - this.pAt(0);
2226
+ }
2227
+ /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
2228
+ missProbability() {
2229
+ return this.pAt(0);
2230
+ }
2231
+ /**
2232
+ * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
2233
+ * damage buckets, aggregating probability mass (and `count`/`attr`
2234
+ * provenance) into each bucket's start value. Returns this PMF unchanged when
2235
+ * its integer support already fits within `maxBuckets`.
2236
+ *
2237
+ * This is a lossy display/downsampling transform (bucket start replaces the
2238
+ * exact damage value) — use it for charting wide distributions, not for DPR
2239
+ * math.
2240
+ */
2241
+ rebin(maxBuckets) {
2242
+ if (!(maxBuckets > 0)) return this;
2243
+ const support = this.support();
2244
+ if (support.length === 0) return this;
2245
+ const min = support[0];
2246
+ const max = support[support.length - 1];
2247
+ const range = max - min;
2248
+ if (range + 1 <= maxBuckets) return this;
2249
+ const binSize = Math.ceil((range + 1) / maxBuckets);
2250
+ return this.mapDamage((d2) => min + Math.floor((d2 - min) / binSize) * binSize);
2251
+ }
2138
2252
  /** Dense integer support from min..max (inclusive).
2139
2253
  * Useful for showing empty bars in charts.
2140
2254
  */
@@ -2210,6 +2324,56 @@ var _PMF = class _PMF {
2210
2324
  }
2211
2325
  return false;
2212
2326
  }
2327
+ /**
2328
+ * Split each damage value's probability mass across outcome labels, returning
2329
+ * per-label maps of `damage value → probability mass attributable to that
2330
+ * label`. Summing over labels at a given value recovers that value's `p`.
2331
+ *
2332
+ * Damage-bearing bins are split by `attr` weight (the share of damage each
2333
+ * outcome contributed); the clean-miss bin at 0 is split by `count` weight
2334
+ * (there is no damage to attribute). Attribution is computed on demand via
2335
+ * {@link withAttribution} when absent, so builder-generated PMFs work too.
2336
+ *
2337
+ * This is the provenance core of the stacked damage-attribution chart — the
2338
+ * caller only maps these series into its rendering format (colors, binning,
2339
+ * axis labels).
2340
+ */
2341
+ attributionByValue() {
2342
+ const src = this.hasAttribution() ? this : this.withAttribution();
2343
+ const result = /* @__PURE__ */ new Map();
2344
+ const add = (label, damage, mass) => {
2345
+ if (!(mass > 0)) return;
2346
+ let series = result.get(label);
2347
+ if (!series) {
2348
+ series = /* @__PURE__ */ new Map();
2349
+ result.set(label, series);
2350
+ }
2351
+ series.set(damage, (series.get(damage) ?? 0) + mass);
2352
+ };
2353
+ for (const [damage, bin] of src.map) {
2354
+ const p = bin.p || 0;
2355
+ if (p <= 0) continue;
2356
+ const isMissBin = damage === 0;
2357
+ if (isMissBin) {
2358
+ let totalCount = 0;
2359
+ for (const k in bin.count) totalCount += bin.count[k] || 0;
2360
+ if (totalCount > 0) {
2361
+ const c = bin.count[MISS_NONE_OUTCOME] || 0;
2362
+ add(MISS_NONE_OUTCOME, damage, c / totalCount * p);
2363
+ }
2364
+ continue;
2365
+ }
2366
+ let totalAttr = 0;
2367
+ if (bin.attr) for (const k in bin.attr) totalAttr += bin.attr[k] || 0;
2368
+ if (bin.attr && totalAttr > 0) {
2369
+ for (const k in bin.attr) {
2370
+ if (k === MISS_NONE_OUTCOME) continue;
2371
+ add(k, damage, (bin.attr[k] || 0) / totalAttr * p);
2372
+ }
2373
+ }
2374
+ }
2375
+ return result;
2376
+ }
2213
2377
  tailProbGE(t) {
2214
2378
  let s = 0;
2215
2379
  for (const [x, bin] of this) {