@yipe/dice 0.3.0 → 0.5.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.
@@ -36,6 +36,41 @@ type DamageDistribution = Record<number, number>;
36
36
  /** Canonical outcome labels supported by the query helpers. */
37
37
  type OutcomeType = "crit" | "hit" | "missNone" | "missDamage" | "saveHalf" | "saveFail" | "pc";
38
38
  type Rounding = "none" | "floor" | "round" | "ceil";
39
+ /** How a d20 attack roll resolves: single die, keep-highest of 2/3, or keep-lowest of 2. */
40
+ type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
41
+ /**
42
+ * P(critical hit) for the given crit window and d20 {@link RollType}.
43
+ *
44
+ * `critRange` is the number of top faces that crit (1 for a natural 20, 2 for
45
+ * 19–20, …), so a single die crits with probability `critRange / 20`. Advantage
46
+ * rolls two d20s / elven accuracy three, keeping the best; disadvantage keeps
47
+ * the worst of two.
48
+ */
49
+ declare function critProbability(critRange: number, rollType?: RollType): number;
50
+ /**
51
+ * The canonical "clean miss" outcome — a point of zero damage with no rider.
52
+ * This is the {@link OutcomeType} that attribution charts and outcome stats key
53
+ * on, and is distinct from the builder's attack-resolution `miss` weight label.
54
+ */
55
+ declare const MISS_NONE_OUTCOME: OutcomeType;
56
+ /**
57
+ * All outcome types in canonical severity order — clean miss → crit. This is
58
+ * also the natural stacking order for attribution charts (least- to
59
+ * most-impactful, bottom → top). Enumerates every {@link OutcomeType} exactly
60
+ * once; use it instead of hand-maintained per-consumer outcome tables.
61
+ */
62
+ declare const ALL_OUTCOME_TYPES: OutcomeType[];
63
+ /**
64
+ * Outcome types in display order for stats / breakdown rows — most prominent
65
+ * first (crit, hit, …) down to the clean miss.
66
+ */
67
+ declare const OUTCOME_DISPLAY_ORDER: OutcomeType[];
68
+ /**
69
+ * Sort outcome labels by a canonical order (defaults to {@link ALL_OUTCOME_TYPES}).
70
+ * Labels not present in `order` sort after known ones, alphabetically — so
71
+ * ad-hoc/test labels outside the {@link OutcomeType} union stay stable.
72
+ */
73
+ declare function sortOutcomes<T extends string>(outcomes: Iterable<T>, order?: readonly string[]): T[];
39
74
  declare const onAnyHit: OutcomeType[];
40
75
  declare const onCritOnly: OutcomeType[];
41
76
  declare const onHitOnly: OutcomeType[];
@@ -92,6 +127,21 @@ declare class DiceQuery {
92
127
  * // Now pmf can be used with toDamageAttributionChartSeries()
93
128
  */
94
129
  combinedWithAttribution(): PMF;
130
+ /**
131
+ * Per-label `damage value → probability mass` series for the combined,
132
+ * attribution-carrying distribution — the provenance core of the stacked
133
+ * damage-attribution chart. Convenience for
134
+ * `combinedWithAttribution().attributionByValue()`; see
135
+ * {@link PMF.attributionByValue}.
136
+ */
137
+ attributionByValue(): Map<string, Map<number, number>>;
138
+ /**
139
+ * How many of the independent single PMFs can produce the given outcome
140
+ * label. Useful for "all of them succeeded" style probabilities where the
141
+ * exponent is the number of contributing attacks (see
142
+ * {@link DiceQuery.probExactlyK}).
143
+ */
144
+ countSinglesWith(label: string): number;
95
145
  /**
96
146
  * Returns the expected damage across all possible outcomes.
97
147
  *
@@ -722,6 +772,16 @@ declare class PMF {
722
772
  static empty(epsilon?: number, identifier?: string): PMF;
723
773
  static zero(epsilon?: number): PMF;
724
774
  static delta(value: number, epsilon?: number): PMF;
775
+ /**
776
+ * Point mass at damage 0 tagged with the canonical `missNone` outcome.
777
+ *
778
+ * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
779
+ * builder's attack-resolution vocabulary. This uses the `missNone`
780
+ * {@link OutcomeType} that the attribution charts and outcome stats key on,
781
+ * so it is the correct "clean miss / no damage" delta for provenance-aware
782
+ * mixtures feeding those consumers.
783
+ */
784
+ static missNone(epsilon?: number): PMF;
725
785
  static emptyMass(): PMF;
726
786
  [Symbol.iterator](): IterableIterator<[number, Bin]>;
727
787
  static clearCache(): void;
@@ -897,6 +957,27 @@ declare class PMF {
897
957
  * Example: `pmf.addScaled(critBranch, 0.05)` → PMF including 5% crit outcomes
898
958
  */
899
959
  addScaled(branch: PMF, probability: number): PMF;
960
+ /**
961
+ * Redistributes probability mass to model an effect that only occurs with
962
+ * probability `frequency` — a conditional attack, an on-hit rider, or a
963
+ * sub-one AoE target fraction.
964
+ *
965
+ * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
966
+ * per-label `count`, AND per-label `attr` — and the freed mass is moved into
967
+ * the miss bin at damage 0, tagged with the canonical `missNone` outcome.
968
+ * Total probability mass is preserved.
969
+ *
970
+ * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
971
+ * attribution (`attr`) intact, so a frequency-scaled PMF still renders
972
+ * correctly in the damage-attribution charts.
973
+ *
974
+ * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
975
+ * collapses all mass into the miss bin. The miss outcome is assumed to be
976
+ * encoded at damage value 0.
977
+ *
978
+ * @param frequency Probability in [0, 1] that the effect occurs.
979
+ */
980
+ applyHitFrequency(frequency: number): PMF;
900
981
  scaleMass(factor: number): PMF;
901
982
  mapDamage(damageTransformFunction: (damageValue: number) => number): PMF;
902
983
  scaleDamage(factor: number, rounding?: "floor" | "round" | "ceil"): PMF;
@@ -950,6 +1031,25 @@ declare class PMF {
950
1031
  prune(epsRel: number, minBins?: number): PMF;
951
1032
  /** Probability mass at exactly x. */
952
1033
  pAt(x: number): number;
1034
+ /**
1035
+ * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
1036
+ * Assumes a miss is encoded as the damage-0 bin (the convention used across
1037
+ * attack/save PMFs). The dual of {@link missProbability}.
1038
+ */
1039
+ hitProbability(): number;
1040
+ /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
1041
+ missProbability(): number;
1042
+ /**
1043
+ * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
1044
+ * damage buckets, aggregating probability mass (and `count`/`attr`
1045
+ * provenance) into each bucket's start value. Returns this PMF unchanged when
1046
+ * its integer support already fits within `maxBuckets`.
1047
+ *
1048
+ * This is a lossy display/downsampling transform (bucket start replaces the
1049
+ * exact damage value) — use it for charting wide distributions, not for DPR
1050
+ * math.
1051
+ */
1052
+ rebin(maxBuckets: number): PMF;
953
1053
  /** Dense integer support from min..max (inclusive).
954
1054
  * Useful for showing empty bars in charts.
955
1055
  */
@@ -974,6 +1074,21 @@ declare class PMF {
974
1074
  } | null;
975
1075
  /** Check if outcome exists in this PMF. */
976
1076
  hasOutcome(outcome: string): boolean;
1077
+ /**
1078
+ * Split each damage value's probability mass across outcome labels, returning
1079
+ * per-label maps of `damage value → probability mass attributable to that
1080
+ * label`. Summing over labels at a given value recovers that value's `p`.
1081
+ *
1082
+ * Damage-bearing bins are split by `attr` weight (the share of damage each
1083
+ * outcome contributed); the clean-miss bin at 0 is split by `count` weight
1084
+ * (there is no damage to attribute). Attribution is computed on demand via
1085
+ * {@link withAttribution} when absent, so builder-generated PMFs work too.
1086
+ *
1087
+ * This is the provenance core of the stacked damage-attribution chart — the
1088
+ * caller only maps these series into its rendering format (colors, binning,
1089
+ * axis labels).
1090
+ */
1091
+ attributionByValue(): Map<string, Map<number, number>>;
977
1092
  tailProbGE(t: number): number;
978
1093
  tailProbGT(t: number): number;
979
1094
  /**
@@ -1011,4 +1126,4 @@ declare class PMF {
1011
1126
  query(): DiceQuery;
1012
1127
  }
1013
1128
 
1014
- export { type Bin as B, type CritConfig as C, DiceQuery as D, EPS as E, LRUCache as L, type OutcomeLabelMap as O, PMF as P, type Rounding as R, type Snapshot as S, type DamageDistribution as a, type OutcomeSnapshot as b, type OutcomeType as c, onCritOnly as d, onHitOnly as e, onMissDamageOnly as f, onMissOnly as g, onPotentCantripOnly as h, onSaveFailOnly as i, onSaveHalfOnly as j, onAnyHit as o, pmfCache as p };
1129
+ export { ALL_OUTCOME_TYPES as A, type Bin as B, type CritConfig as C, type DamageDistribution as D, EPS as E, LRUCache as L, MISS_NONE_OUTCOME as M, type OutcomeLabelMap as O, PMF as P, type Rounding as R, type Snapshot as S, type OutcomeType as a, type RollType as b, critProbability as c, OUTCOME_DISPLAY_ORDER as d, onCritOnly as e, onHitOnly as f, onMissOnly as g, onMissDamageOnly as h, onSaveHalfOnly as i, onSaveFailOnly as j, onPotentCantripOnly as k, DiceQuery as l, type OutcomeSnapshot as m, onAnyHit as o, pmfCache as p, sortOutcomes as s };
@@ -36,6 +36,41 @@ type DamageDistribution = Record<number, number>;
36
36
  /** Canonical outcome labels supported by the query helpers. */
37
37
  type OutcomeType = "crit" | "hit" | "missNone" | "missDamage" | "saveHalf" | "saveFail" | "pc";
38
38
  type Rounding = "none" | "floor" | "round" | "ceil";
39
+ /** How a d20 attack roll resolves: single die, keep-highest of 2/3, or keep-lowest of 2. */
40
+ type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
41
+ /**
42
+ * P(critical hit) for the given crit window and d20 {@link RollType}.
43
+ *
44
+ * `critRange` is the number of top faces that crit (1 for a natural 20, 2 for
45
+ * 19–20, …), so a single die crits with probability `critRange / 20`. Advantage
46
+ * rolls two d20s / elven accuracy three, keeping the best; disadvantage keeps
47
+ * the worst of two.
48
+ */
49
+ declare function critProbability(critRange: number, rollType?: RollType): number;
50
+ /**
51
+ * The canonical "clean miss" outcome — a point of zero damage with no rider.
52
+ * This is the {@link OutcomeType} that attribution charts and outcome stats key
53
+ * on, and is distinct from the builder's attack-resolution `miss` weight label.
54
+ */
55
+ declare const MISS_NONE_OUTCOME: OutcomeType;
56
+ /**
57
+ * All outcome types in canonical severity order — clean miss → crit. This is
58
+ * also the natural stacking order for attribution charts (least- to
59
+ * most-impactful, bottom → top). Enumerates every {@link OutcomeType} exactly
60
+ * once; use it instead of hand-maintained per-consumer outcome tables.
61
+ */
62
+ declare const ALL_OUTCOME_TYPES: OutcomeType[];
63
+ /**
64
+ * Outcome types in display order for stats / breakdown rows — most prominent
65
+ * first (crit, hit, …) down to the clean miss.
66
+ */
67
+ declare const OUTCOME_DISPLAY_ORDER: OutcomeType[];
68
+ /**
69
+ * Sort outcome labels by a canonical order (defaults to {@link ALL_OUTCOME_TYPES}).
70
+ * Labels not present in `order` sort after known ones, alphabetically — so
71
+ * ad-hoc/test labels outside the {@link OutcomeType} union stay stable.
72
+ */
73
+ declare function sortOutcomes<T extends string>(outcomes: Iterable<T>, order?: readonly string[]): T[];
39
74
  declare const onAnyHit: OutcomeType[];
40
75
  declare const onCritOnly: OutcomeType[];
41
76
  declare const onHitOnly: OutcomeType[];
@@ -92,6 +127,21 @@ declare class DiceQuery {
92
127
  * // Now pmf can be used with toDamageAttributionChartSeries()
93
128
  */
94
129
  combinedWithAttribution(): PMF;
130
+ /**
131
+ * Per-label `damage value → probability mass` series for the combined,
132
+ * attribution-carrying distribution — the provenance core of the stacked
133
+ * damage-attribution chart. Convenience for
134
+ * `combinedWithAttribution().attributionByValue()`; see
135
+ * {@link PMF.attributionByValue}.
136
+ */
137
+ attributionByValue(): Map<string, Map<number, number>>;
138
+ /**
139
+ * How many of the independent single PMFs can produce the given outcome
140
+ * label. Useful for "all of them succeeded" style probabilities where the
141
+ * exponent is the number of contributing attacks (see
142
+ * {@link DiceQuery.probExactlyK}).
143
+ */
144
+ countSinglesWith(label: string): number;
95
145
  /**
96
146
  * Returns the expected damage across all possible outcomes.
97
147
  *
@@ -722,6 +772,16 @@ declare class PMF {
722
772
  static empty(epsilon?: number, identifier?: string): PMF;
723
773
  static zero(epsilon?: number): PMF;
724
774
  static delta(value: number, epsilon?: number): PMF;
775
+ /**
776
+ * Point mass at damage 0 tagged with the canonical `missNone` outcome.
777
+ *
778
+ * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
779
+ * builder's attack-resolution vocabulary. This uses the `missNone`
780
+ * {@link OutcomeType} that the attribution charts and outcome stats key on,
781
+ * so it is the correct "clean miss / no damage" delta for provenance-aware
782
+ * mixtures feeding those consumers.
783
+ */
784
+ static missNone(epsilon?: number): PMF;
725
785
  static emptyMass(): PMF;
726
786
  [Symbol.iterator](): IterableIterator<[number, Bin]>;
727
787
  static clearCache(): void;
@@ -897,6 +957,27 @@ declare class PMF {
897
957
  * Example: `pmf.addScaled(critBranch, 0.05)` → PMF including 5% crit outcomes
898
958
  */
899
959
  addScaled(branch: PMF, probability: number): PMF;
960
+ /**
961
+ * Redistributes probability mass to model an effect that only occurs with
962
+ * probability `frequency` — a conditional attack, an on-hit rider, or a
963
+ * sub-one AoE target fraction.
964
+ *
965
+ * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
966
+ * per-label `count`, AND per-label `attr` — and the freed mass is moved into
967
+ * the miss bin at damage 0, tagged with the canonical `missNone` outcome.
968
+ * Total probability mass is preserved.
969
+ *
970
+ * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
971
+ * attribution (`attr`) intact, so a frequency-scaled PMF still renders
972
+ * correctly in the damage-attribution charts.
973
+ *
974
+ * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
975
+ * collapses all mass into the miss bin. The miss outcome is assumed to be
976
+ * encoded at damage value 0.
977
+ *
978
+ * @param frequency Probability in [0, 1] that the effect occurs.
979
+ */
980
+ applyHitFrequency(frequency: number): PMF;
900
981
  scaleMass(factor: number): PMF;
901
982
  mapDamage(damageTransformFunction: (damageValue: number) => number): PMF;
902
983
  scaleDamage(factor: number, rounding?: "floor" | "round" | "ceil"): PMF;
@@ -950,6 +1031,25 @@ declare class PMF {
950
1031
  prune(epsRel: number, minBins?: number): PMF;
951
1032
  /** Probability mass at exactly x. */
952
1033
  pAt(x: number): number;
1034
+ /**
1035
+ * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
1036
+ * Assumes a miss is encoded as the damage-0 bin (the convention used across
1037
+ * attack/save PMFs). The dual of {@link missProbability}.
1038
+ */
1039
+ hitProbability(): number;
1040
+ /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
1041
+ missProbability(): number;
1042
+ /**
1043
+ * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
1044
+ * damage buckets, aggregating probability mass (and `count`/`attr`
1045
+ * provenance) into each bucket's start value. Returns this PMF unchanged when
1046
+ * its integer support already fits within `maxBuckets`.
1047
+ *
1048
+ * This is a lossy display/downsampling transform (bucket start replaces the
1049
+ * exact damage value) — use it for charting wide distributions, not for DPR
1050
+ * math.
1051
+ */
1052
+ rebin(maxBuckets: number): PMF;
953
1053
  /** Dense integer support from min..max (inclusive).
954
1054
  * Useful for showing empty bars in charts.
955
1055
  */
@@ -974,6 +1074,21 @@ declare class PMF {
974
1074
  } | null;
975
1075
  /** Check if outcome exists in this PMF. */
976
1076
  hasOutcome(outcome: string): boolean;
1077
+ /**
1078
+ * Split each damage value's probability mass across outcome labels, returning
1079
+ * per-label maps of `damage value → probability mass attributable to that
1080
+ * label`. Summing over labels at a given value recovers that value's `p`.
1081
+ *
1082
+ * Damage-bearing bins are split by `attr` weight (the share of damage each
1083
+ * outcome contributed); the clean-miss bin at 0 is split by `count` weight
1084
+ * (there is no damage to attribute). Attribution is computed on demand via
1085
+ * {@link withAttribution} when absent, so builder-generated PMFs work too.
1086
+ *
1087
+ * This is the provenance core of the stacked damage-attribution chart — the
1088
+ * caller only maps these series into its rendering format (colors, binning,
1089
+ * axis labels).
1090
+ */
1091
+ attributionByValue(): Map<string, Map<number, number>>;
977
1092
  tailProbGE(t: number): number;
978
1093
  tailProbGT(t: number): number;
979
1094
  /**
@@ -1011,4 +1126,4 @@ declare class PMF {
1011
1126
  query(): DiceQuery;
1012
1127
  }
1013
1128
 
1014
- export { type Bin as B, type CritConfig as C, DiceQuery as D, EPS as E, LRUCache as L, type OutcomeLabelMap as O, PMF as P, type Rounding as R, type Snapshot as S, type DamageDistribution as a, type OutcomeSnapshot as b, type OutcomeType as c, onCritOnly as d, onHitOnly as e, onMissDamageOnly as f, onMissOnly as g, onPotentCantripOnly as h, onSaveFailOnly as i, onSaveHalfOnly as j, onAnyHit as o, pmfCache as p };
1129
+ export { ALL_OUTCOME_TYPES as A, type Bin as B, type CritConfig as C, type DamageDistribution as D, EPS as E, LRUCache as L, MISS_NONE_OUTCOME as M, type OutcomeLabelMap as O, PMF as P, type Rounding as R, type Snapshot as S, type OutcomeType as a, type RollType as b, critProbability as c, OUTCOME_DISPLAY_ORDER as d, onCritOnly as e, onHitOnly as f, onMissOnly as g, onMissDamageOnly as h, onSaveHalfOnly as i, onSaveFailOnly as j, onPotentCantripOnly as k, DiceQuery as l, type OutcomeSnapshot as m, onAnyHit as o, pmfCache as p, sortOutcomes as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yipe/dice",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A high-performance dice probability engine for D&D 5e DPR calculations. Powers dprcalc.com.",
5
5
  "keywords": [
6
6
  "dnd",
@@ -90,4 +90,4 @@
90
90
  "typescript": "^6.0.3",
91
91
  "vitest": "^4.1.9"
92
92
  }
93
- }
93
+ }