@yipe/dice 0.5.0 → 0.7.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.
- package/dist/builder/ac.d.ts +25 -0
- package/dist/builder/ac.d.ts.map +1 -0
- package/dist/builder/ast.d.ts +9 -0
- package/dist/builder/ast.d.ts.map +1 -0
- package/dist/builder/attack.d.ts +41 -0
- package/dist/builder/attack.d.ts.map +1 -0
- package/dist/builder/d20.d.ts +6 -0
- package/dist/builder/d20.d.ts.map +1 -0
- package/dist/builder/dc.d.ts +26 -0
- package/dist/builder/dc.d.ts.map +1 -0
- package/dist/builder/example.d.ts +356 -0
- package/dist/builder/example.d.ts.map +1 -0
- package/dist/builder/factory.d.ts +17 -0
- package/dist/builder/factory.d.ts.map +1 -0
- package/dist/builder/index.cjs +172 -285
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.ts +8 -429
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +172 -285
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/nodes.d.ts +61 -0
- package/dist/builder/nodes.d.ts.map +1 -0
- package/dist/builder/prob.d.ts +3 -0
- package/dist/builder/prob.d.ts.map +1 -0
- package/dist/builder/roll.d.ts +205 -0
- package/dist/builder/roll.d.ts.map +1 -0
- package/dist/builder/save.d.ts +19 -0
- package/dist/builder/save.d.ts.map +1 -0
- package/dist/builder/types.d.ts +66 -0
- package/dist/builder/types.d.ts.map +1 -0
- package/dist/common/bounce.d.ts +32 -0
- package/dist/common/bounce.d.ts.map +1 -0
- package/dist/common/errors.d.ts +26 -0
- package/dist/common/errors.d.ts.map +1 -0
- package/dist/common/lru-cache.d.ts +17 -0
- package/dist/common/lru-cache.d.ts.map +1 -0
- package/dist/common/types.d.ts +65 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/index.cjs +152 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -111
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +152 -285
- package/dist/index.js.map +1 -1
- package/dist/parser/dice.d.ts +70 -0
- package/dist/parser/dice.d.ts.map +1 -0
- package/dist/parser/parser.d.ts +14 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/pmf/mixture.d.ts +37 -0
- package/dist/pmf/mixture.d.ts.map +1 -0
- package/dist/pmf/pmf.d.ts +449 -0
- package/dist/pmf/pmf.d.ts.map +1 -0
- package/dist/{pmf-D5VRghZI.d.cts → pmf/query.d.ts} +18 -548
- package/dist/pmf/query.d.ts.map +1 -0
- package/package.json +16 -15
- package/.claude/worktrees/amazing-matsumoto-27220c/LICENSE +0 -21
- package/.claude/worktrees/amazing-matsumoto-27220c/README.md +0 -518
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/LICENSE +0 -21
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/README.md +0 -518
- package/.claude/worktrees/wizardly-mclean-e375de/LICENSE +0 -21
- package/.claude/worktrees/wizardly-mclean-e375de/README.md +0 -518
- package/CHANGELOG.md +0 -239
- package/dist/builder/index.d.cts +0 -429
- package/dist/index.d.cts +0 -111
- package/dist/pmf-D5VRghZI.d.ts +0 -1129
package/dist/index.d.ts
CHANGED
|
@@ -1,111 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
* - **Elemental Adept** (`minimumDieRoll >= 2`): rolls below the minimum are
|
|
11
|
-
* bumped up to it, collapsing the low faces onto a single heavier value.
|
|
12
|
-
* - **Empowered Spell** (`rerollDamageDice > 0`): a number of dice may be
|
|
13
|
-
* rerolled once, giving a second chance at a match.
|
|
14
|
-
*
|
|
15
|
-
* The base and Elemental-Adept cases are computed exactly (see
|
|
16
|
-
* {@link pAllDistinct}); the Empowered-Spell reroll is an explicit model layered
|
|
17
|
-
* on the exact base match probability.
|
|
18
|
-
*/
|
|
19
|
-
/** Options that modify bounce odds via metamagic / feats. */
|
|
20
|
-
interface BounceOddsOptions {
|
|
21
|
-
/** Minimum die roll — e.g. 2 for Elemental Adept, 3 for Great Weapon Fighting 2024. */
|
|
22
|
-
minimumDieRoll?: number;
|
|
23
|
-
/** Number of dice that may be rerolled once — e.g. CHA modifier for Empowered Spell. */
|
|
24
|
-
rerollDamageDice?: number;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* P(at least two of `diceCount` dice with `dieFaces` faces match), honoring
|
|
28
|
-
* Elemental Adept and Empowered Spell. Returns a probability in [0, 1].
|
|
29
|
-
*
|
|
30
|
-
* @param diceCount Number of dice rolled.
|
|
31
|
-
* @param dieFaces Faces per die (e.g. 8 for d8).
|
|
32
|
-
* @param options Optional metamagic / feat modifiers.
|
|
33
|
-
*/
|
|
34
|
-
declare function calculateBounceOdds(diceCount: number, dieFaces: number, options?: BounceOddsOptions): number;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Error thrown when a dice expression cannot be parsed.
|
|
38
|
-
*
|
|
39
|
-
* Extends the built-in {@link Error}, so existing `catch (e)` / message checks
|
|
40
|
-
* continue to work, while callers can now narrow with `instanceof DiceParseError`.
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* try {
|
|
44
|
-
* parse("d6@3");
|
|
45
|
-
* } catch (e) {
|
|
46
|
-
* if (e instanceof DiceParseError) {
|
|
47
|
-
* // e.expression === "d6@3"
|
|
48
|
-
* }
|
|
49
|
-
* }
|
|
50
|
-
*/
|
|
51
|
-
declare class DiceParseError extends Error {
|
|
52
|
-
/** The original expression that failed to parse, when available. */
|
|
53
|
-
readonly expression?: string;
|
|
54
|
-
/** The underlying error that triggered this one, when available. */
|
|
55
|
-
readonly cause?: unknown;
|
|
56
|
-
constructor(message: string, options?: {
|
|
57
|
-
expression?: string;
|
|
58
|
-
cause?: unknown;
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Enable or disable the internal parse cache. */
|
|
63
|
-
declare function setCachingEnabled(enabled: boolean): void;
|
|
64
|
-
/** Returns whether the internal parse cache is currently enabled. */
|
|
65
|
-
declare function getCachingEnabled(): boolean;
|
|
66
|
-
/** Clears the internal parse cache. */
|
|
67
|
-
declare function clearParserCache(): void;
|
|
68
|
-
/**
|
|
69
|
-
* Parse a dice expression into a PMF.
|
|
70
|
-
*
|
|
71
|
-
* - Expression is case-insensitive and ignores spaces.
|
|
72
|
-
*/
|
|
73
|
-
declare function parse(expression: string, n?: number): PMF;
|
|
74
|
-
|
|
75
|
-
/** A labeled mixture builder that preserves provenance in Bin.count. */
|
|
76
|
-
declare class Mixture<L extends string = string> {
|
|
77
|
-
private readonly totals;
|
|
78
|
-
private readonly labelMass;
|
|
79
|
-
private readonly eps;
|
|
80
|
-
constructor(eps?: number);
|
|
81
|
-
/** Remove all accumulated state. */
|
|
82
|
-
clear(): this;
|
|
83
|
-
/** Number of distinct outcome values currently accumulated. */
|
|
84
|
-
size(): number;
|
|
85
|
-
/** Whether a label was ever added. */
|
|
86
|
-
hasLabel(label: L): boolean;
|
|
87
|
-
/**
|
|
88
|
-
* Add a labeled component with a mixture weight.
|
|
89
|
-
* Weight can be any positive finite number. Very small contributions are pruned by eps.
|
|
90
|
-
*/
|
|
91
|
-
add(label: L, pmf: PMF, weight?: number): this;
|
|
92
|
-
buildPMF(eps?: number): PMF;
|
|
93
|
-
/**
|
|
94
|
-
* Produce normalized *per-label* PMFs (labels independent).
|
|
95
|
-
* These are unlabeled PMFs built from the raw mass of that label alone.
|
|
96
|
-
*/
|
|
97
|
-
byOutcome(): Record<L, PMF>;
|
|
98
|
-
/**
|
|
99
|
-
* Mixture weights per label, normalized to sum to 1 over labels that appeared.
|
|
100
|
-
* Uses raw mass before per-outcome normalization.
|
|
101
|
-
*/
|
|
102
|
-
weights(): Record<L, number>;
|
|
103
|
-
toJSON(): {
|
|
104
|
-
totals: Array<[number, number]>;
|
|
105
|
-
labels: Array<[number, Record<L, number>]>;
|
|
106
|
-
eps: number;
|
|
107
|
-
};
|
|
108
|
-
static mix<L extends string = string>(items: Array<[label: L, pmf: PMF, weight: number]>, eps?: number): PMF;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export { type BounceOddsOptions, DiceParseError, Mixture, PMF, calculateBounceOdds, clearParserCache, getCachingEnabled, parse, setCachingEnabled };
|
|
1
|
+
export * from "./common/bounce.js";
|
|
2
|
+
export * from "./common/errors.js";
|
|
3
|
+
export * from "./common/lru-cache.js";
|
|
4
|
+
export * from "./common/types.js";
|
|
5
|
+
export * from "./parser/parser.js";
|
|
6
|
+
export * from "./pmf/mixture.js";
|
|
7
|
+
export * from "./pmf/pmf.js";
|
|
8
|
+
export * from "./pmf/query.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -201,7 +201,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
201
201
|
* const attack = d20.plus(5).ac(15).onHit(d(2,6).plus(3)).onCrit(d(2,6))
|
|
202
202
|
* const query = attack.toQuery()
|
|
203
203
|
* const pmf = query.combinedWithAttribution()
|
|
204
|
-
* // Now pmf can be used with
|
|
204
|
+
* // Now pmf can be used with attributionByValue() / damageAttributionChartModel()
|
|
205
205
|
*/
|
|
206
206
|
combinedWithAttribution() {
|
|
207
207
|
if (this._combinedWithAttr) {
|
|
@@ -227,6 +227,14 @@ var _DiceQuery = class _DiceQuery {
|
|
|
227
227
|
attributionByValue() {
|
|
228
228
|
return this.combinedWithAttribution().attributionByValue();
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Full numeric model for the stacked damage-attribution chart. Convenience for
|
|
232
|
+
* `combinedWithAttribution().damageAttributionChartModel(options)`; see
|
|
233
|
+
* {@link PMF.damageAttributionChartModel}.
|
|
234
|
+
*/
|
|
235
|
+
damageAttributionChartModel(options) {
|
|
236
|
+
return this.combinedWithAttribution().damageAttributionChartModel(options);
|
|
237
|
+
}
|
|
230
238
|
/**
|
|
231
239
|
* How many of the independent single PMFs can produce the given outcome
|
|
232
240
|
* label. Useful for "all of them succeeded" style probabilities where the
|
|
@@ -831,290 +839,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
831
839
|
}));
|
|
832
840
|
return { labels: damageValues, datasets };
|
|
833
841
|
}
|
|
834
|
-
/**
|
|
835
|
-
* Returns pure mathematical data for attribution charts showing outcome contributions.
|
|
836
|
-
*
|
|
837
|
-
* Automatically discovers all outcome types present in the PMF, applies filtering rules,
|
|
838
|
-
* and returns proportional data suitable for stacked visualization.
|
|
839
|
-
*
|
|
840
|
-
* @param options Configuration options
|
|
841
|
-
* @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
|
|
842
|
-
* @param options.filterRules Function to determine if outcome should be included for a given damage value
|
|
843
|
-
* @param options.asPercentages Whether to return percentages (0-100) or probabilities (0-1)
|
|
844
|
-
* @returns Pure data structure with support, outcomes, and proportional data
|
|
845
|
-
*
|
|
846
|
-
* @example
|
|
847
|
-
* query.toAttributionChartSeries()
|
|
848
|
-
* // → {support: [0, 6, 12], outcomes: ['hit', 'crit'], data: {hit: [5.2, 8.1, ...], crit: [0, 2.3, ...]}}
|
|
849
|
-
*/
|
|
850
|
-
toAttributionChartSeries(options = {}) {
|
|
851
|
-
const {
|
|
852
|
-
stackOrder = [
|
|
853
|
-
"missNone",
|
|
854
|
-
"missDamage",
|
|
855
|
-
"saveFail",
|
|
856
|
-
"saveHalf",
|
|
857
|
-
"pc",
|
|
858
|
-
"hit",
|
|
859
|
-
"crit"
|
|
860
|
-
],
|
|
861
|
-
filterRules = (outcome, damage) => !(outcome === "missNone" && damage !== 0),
|
|
862
|
-
asPercentages = true
|
|
863
|
-
} = options;
|
|
864
|
-
const originalSupport = this.combined.support();
|
|
865
|
-
if (originalSupport.length === 0) {
|
|
866
|
-
return { support: [], outcomes: [], data: {} };
|
|
867
|
-
}
|
|
868
|
-
const minDamage = Math.min(...originalSupport);
|
|
869
|
-
const maxDamage = Math.max(...originalSupport);
|
|
870
|
-
const support = Array.from(
|
|
871
|
-
{ length: maxDamage - minDamage + 1 },
|
|
872
|
-
(_, i) => minDamage + i
|
|
873
|
-
);
|
|
874
|
-
const allOutcomeTypes = /* @__PURE__ */ new Set();
|
|
875
|
-
for (const [, bin] of this.combined.map) {
|
|
876
|
-
for (const outcomeType in bin.count) {
|
|
877
|
-
if (bin.count[outcomeType] && bin.count[outcomeType] > 0) {
|
|
878
|
-
allOutcomeTypes.add(outcomeType);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
const existingOutcomes = Array.from(allOutcomeTypes).sort((a, b) => {
|
|
883
|
-
const indexA = stackOrder.indexOf(a);
|
|
884
|
-
const indexB = stackOrder.indexOf(b);
|
|
885
|
-
if (indexA >= 0 && indexB >= 0) return indexA - indexB;
|
|
886
|
-
if (indexA >= 0) return -1;
|
|
887
|
-
if (indexB >= 0) return 1;
|
|
888
|
-
return a.localeCompare(b);
|
|
889
|
-
});
|
|
890
|
-
if (existingOutcomes.length === 0) {
|
|
891
|
-
return { support, outcomes: [], data: {} };
|
|
892
|
-
}
|
|
893
|
-
const data = {};
|
|
894
|
-
for (const outcome of existingOutcomes) {
|
|
895
|
-
data[outcome] = support.map((damage) => {
|
|
896
|
-
const bin = this.combined.map.get(damage);
|
|
897
|
-
if (!bin) return 0;
|
|
898
|
-
if (!filterRules(outcome, damage)) {
|
|
899
|
-
return 0;
|
|
900
|
-
}
|
|
901
|
-
const outcomeCount = bin.count[outcome] || 0;
|
|
902
|
-
let totalChartableCount = 0;
|
|
903
|
-
for (const [outcomeName, count] of Object.entries(bin.count)) {
|
|
904
|
-
if (filterRules(outcomeName, damage)) {
|
|
905
|
-
totalChartableCount += count || 0;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
if (totalChartableCount === 0) return 0;
|
|
909
|
-
const outcomeFraction = outcomeCount / totalChartableCount;
|
|
910
|
-
const outcomeProbability = bin.p * outcomeFraction;
|
|
911
|
-
return asPercentages ? outcomeProbability * 100 : outcomeProbability;
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
support,
|
|
916
|
-
outcomes: existingOutcomes,
|
|
917
|
-
data
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Returns pure mathematical data for damage attribution charts showing damage contribution
|
|
922
|
-
* from each outcome type at each damage value.
|
|
923
|
-
*
|
|
924
|
-
* Similar to toAttributionChartSeries() but uses bin.attr (damage attribution) instead of
|
|
925
|
-
* bin.count (probability attribution).
|
|
926
|
-
*
|
|
927
|
-
* @param options Configuration options
|
|
928
|
-
* @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
|
|
929
|
-
* @param options.filterRules Function to determine if outcome should be included for a given damage value
|
|
930
|
-
* @param options.asPercentages Whether to return percentages (0-100) or raw damage values (0+)
|
|
931
|
-
* @returns Pure data structure with support, outcomes, and damage attribution data
|
|
932
|
-
*
|
|
933
|
-
* @example
|
|
934
|
-
* query.toDamageAttributionChartSeries()
|
|
935
|
-
* // → {support: [0, 6, 12], outcomes: ['hit', 'crit'], data: {hit: [3.2, 5.1, ...], crit: [0, 1.8, ...]}}
|
|
936
|
-
*/
|
|
937
|
-
toDamageAttributionChartSeries(options = {}) {
|
|
938
|
-
const {
|
|
939
|
-
stackOrder = [
|
|
940
|
-
"missNone",
|
|
941
|
-
"missDamage",
|
|
942
|
-
"saveFail",
|
|
943
|
-
"saveHalf",
|
|
944
|
-
"pc",
|
|
945
|
-
"hit",
|
|
946
|
-
"crit"
|
|
947
|
-
],
|
|
948
|
-
filterRules = (outcome, damage) => !(outcome === "missNone" && damage !== 0),
|
|
949
|
-
asPercentages = true
|
|
950
|
-
} = options;
|
|
951
|
-
const originalSupport = this.combined.support();
|
|
952
|
-
if (originalSupport.length === 0) {
|
|
953
|
-
return { support: [], outcomes: [], data: {} };
|
|
954
|
-
}
|
|
955
|
-
const minDamage = Math.min(...originalSupport);
|
|
956
|
-
const maxDamage = Math.max(...originalSupport);
|
|
957
|
-
const support = Array.from(
|
|
958
|
-
{ length: maxDamage - minDamage + 1 },
|
|
959
|
-
(_, i) => minDamage + i
|
|
960
|
-
);
|
|
961
|
-
const allOutcomeTypes = /* @__PURE__ */ new Set();
|
|
962
|
-
for (const [, bin] of this.combined.map) {
|
|
963
|
-
if (bin.attr) {
|
|
964
|
-
for (const outcomeType in bin.attr) {
|
|
965
|
-
if (bin.attr[outcomeType] && bin.attr[outcomeType] > 0) {
|
|
966
|
-
allOutcomeTypes.add(outcomeType);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
const existingOutcomes = Array.from(allOutcomeTypes).sort((a, b) => {
|
|
972
|
-
const indexA = stackOrder.indexOf(a);
|
|
973
|
-
const indexB = stackOrder.indexOf(b);
|
|
974
|
-
if (indexA >= 0 && indexB >= 0) return indexA - indexB;
|
|
975
|
-
if (indexA >= 0) return -1;
|
|
976
|
-
if (indexB >= 0) return 1;
|
|
977
|
-
return a.localeCompare(b);
|
|
978
|
-
});
|
|
979
|
-
if (existingOutcomes.length === 0) {
|
|
980
|
-
return { support, outcomes: [], data: {} };
|
|
981
|
-
}
|
|
982
|
-
const data = {};
|
|
983
|
-
for (const outcome of existingOutcomes) {
|
|
984
|
-
data[outcome] = support.map((damage) => {
|
|
985
|
-
const bin = this.combined.map.get(damage);
|
|
986
|
-
if (!bin || !bin.attr) return 0;
|
|
987
|
-
if (!filterRules(outcome, damage)) {
|
|
988
|
-
return 0;
|
|
989
|
-
}
|
|
990
|
-
const outcomeDamageAttribution = bin.attr[outcome] || 0;
|
|
991
|
-
if (asPercentages) {
|
|
992
|
-
let totalDamageAttribution = 0;
|
|
993
|
-
for (const [outcomeName, damageAttr] of Object.entries(bin.attr)) {
|
|
994
|
-
if (filterRules(outcomeName, damage)) {
|
|
995
|
-
totalDamageAttribution += damageAttr || 0;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
if (totalDamageAttribution === 0) return 0;
|
|
999
|
-
const damagePercentage = outcomeDamageAttribution / totalDamageAttribution * 100;
|
|
1000
|
-
return damagePercentage * bin.p * 100;
|
|
1001
|
-
} else {
|
|
1002
|
-
return outcomeDamageAttribution;
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
return {
|
|
1007
|
-
support,
|
|
1008
|
-
outcomes: existingOutcomes,
|
|
1009
|
-
data
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Returns pure mathematical data for outcome attribution charts showing which
|
|
1014
|
-
* attack outcome combinations can produce each damage value.
|
|
1015
|
-
*
|
|
1016
|
-
* Unlike toDamageAttributionChartSeries() which tracks damage sources, this tracks
|
|
1017
|
-
* outcome combinations - answering "what attack outcomes produced this damage?"
|
|
1018
|
-
*
|
|
1019
|
-
* @param options Configuration options
|
|
1020
|
-
* @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
|
|
1021
|
-
* @param options.filterRules Function to determine if outcome should be included for a given damage value
|
|
1022
|
-
* @param options.asPercentages Whether to return percentages (0-100) or probabilities (0-1)
|
|
1023
|
-
* @returns Pure data structure with support, outcomes, and outcome combination probabilities
|
|
1024
|
-
*
|
|
1025
|
-
* @example
|
|
1026
|
-
* query.toOutcomeAttributionChartSeries()
|
|
1027
|
-
* // → {support: [0, 6, 12], outcomes: ['all_miss', 'mixed', 'all_hit'], data: {all_miss: [15, 0, 0], mixed: [60, 80, 20], all_hit: [25, 20, 80]}}
|
|
1028
|
-
*/
|
|
1029
|
-
toOutcomeAttributionChartSeries(options = {}) {
|
|
1030
|
-
const {
|
|
1031
|
-
stackOrder = [
|
|
1032
|
-
"missNone",
|
|
1033
|
-
"missDamage",
|
|
1034
|
-
"saveFail",
|
|
1035
|
-
"saveHalf",
|
|
1036
|
-
"pc",
|
|
1037
|
-
"hit",
|
|
1038
|
-
"crit"
|
|
1039
|
-
],
|
|
1040
|
-
filterRules = (outcome, damage) => !(outcome === "missNone" && damage !== 0),
|
|
1041
|
-
asPercentages = true
|
|
1042
|
-
} = options;
|
|
1043
|
-
const originalSupport = this.combined.support();
|
|
1044
|
-
if (originalSupport.length === 0) {
|
|
1045
|
-
return { support: [], outcomes: [], data: {} };
|
|
1046
|
-
}
|
|
1047
|
-
const minDamage = Math.min(...originalSupport);
|
|
1048
|
-
const maxDamage = Math.max(...originalSupport);
|
|
1049
|
-
const support = Array.from(
|
|
1050
|
-
{ length: maxDamage - minDamage + 1 },
|
|
1051
|
-
(_, i) => minDamage + i
|
|
1052
|
-
);
|
|
1053
|
-
const allOutcomeTypes = /* @__PURE__ */ new Set();
|
|
1054
|
-
for (const [, bin] of this.combined.map) {
|
|
1055
|
-
for (const outcomeType in bin.count) {
|
|
1056
|
-
if (bin.count[outcomeType] && bin.count[outcomeType] > 0) {
|
|
1057
|
-
allOutcomeTypes.add(outcomeType);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
const existingOutcomes = Array.from(allOutcomeTypes).sort((a, b) => {
|
|
1062
|
-
const indexA = stackOrder.indexOf(a);
|
|
1063
|
-
const indexB = stackOrder.indexOf(b);
|
|
1064
|
-
if (indexA >= 0 && indexB >= 0) return indexA - indexB;
|
|
1065
|
-
if (indexA >= 0) return -1;
|
|
1066
|
-
if (indexB >= 0) return 1;
|
|
1067
|
-
return a.localeCompare(b);
|
|
1068
|
-
});
|
|
1069
|
-
if (existingOutcomes.length === 0) {
|
|
1070
|
-
return { support, outcomes: [], data: {} };
|
|
1071
|
-
}
|
|
1072
|
-
const data = {};
|
|
1073
|
-
for (const outcome of existingOutcomes) {
|
|
1074
|
-
data[outcome] = support.map((damage) => {
|
|
1075
|
-
const bin = this.combined.map.get(damage);
|
|
1076
|
-
if (!bin) return 0;
|
|
1077
|
-
if (!filterRules(outcome, damage)) {
|
|
1078
|
-
return 0;
|
|
1079
|
-
}
|
|
1080
|
-
if (outcome === "missNone") {
|
|
1081
|
-
const outcomeCount = bin.count[outcome] || 0;
|
|
1082
|
-
if (outcomeCount === 0) return 0;
|
|
1083
|
-
if (asPercentages) {
|
|
1084
|
-
let totalChartableCount = 0;
|
|
1085
|
-
for (const [outcomeName, count] of Object.entries(bin.count)) {
|
|
1086
|
-
if (filterRules(outcomeName, damage)) {
|
|
1087
|
-
totalChartableCount += count || 0;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
if (totalChartableCount === 0) return 0;
|
|
1091
|
-
const outcomeFraction = outcomeCount / totalChartableCount;
|
|
1092
|
-
return outcomeFraction * bin.p * 100;
|
|
1093
|
-
} else {
|
|
1094
|
-
return outcomeCount;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
if (!bin.attr) return 0;
|
|
1098
|
-
const outcomeDamageContribution = bin.attr[outcome] || 0;
|
|
1099
|
-
if (asPercentages) {
|
|
1100
|
-
let totalDamageAttribution = 0;
|
|
1101
|
-
for (const [, damageAttr] of Object.entries(bin.attr)) {
|
|
1102
|
-
totalDamageAttribution += damageAttr || 0;
|
|
1103
|
-
}
|
|
1104
|
-
if (totalDamageAttribution === 0) return 0;
|
|
1105
|
-
const outcomeFraction = outcomeDamageContribution / totalDamageAttribution;
|
|
1106
|
-
return outcomeFraction * bin.p * 100;
|
|
1107
|
-
} else {
|
|
1108
|
-
return outcomeDamageContribution;
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
support,
|
|
1114
|
-
outcomes: existingOutcomes,
|
|
1115
|
-
data
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
842
|
/**
|
|
1119
843
|
* Returns pure mathematical data for cumulative distribution function (CDF).
|
|
1120
844
|
* Shows P(X ≤ x) - the probability of getting at most x damage.
|
|
@@ -2486,6 +2210,149 @@ var _PMF = class _PMF {
|
|
|
2486
2210
|
}
|
|
2487
2211
|
return result;
|
|
2488
2212
|
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Reversed-convention CCDF percentile markers used by the attribution chart:
|
|
2215
|
+
* for each target probability t, the largest damage x still reached with
|
|
2216
|
+
* P(X ≥ x) > t%, falling back to the smallest/largest support value at the
|
|
2217
|
+
* edges. Ported verbatim from the app so `p80/p50/p20` keep their intentional
|
|
2218
|
+
* reversed meaning (p80 is the low-damage end). Computed on the full, un-binned
|
|
2219
|
+
* support. Assumes a non-empty map.
|
|
2220
|
+
*/
|
|
2221
|
+
attributionPercentiles() {
|
|
2222
|
+
const sortedKeys = [...this.map.keys()].sort((a, b) => a - b);
|
|
2223
|
+
const sparseCCDF = [];
|
|
2224
|
+
let cumulativeP = 0;
|
|
2225
|
+
for (let i = sortedKeys.length - 1; i >= 0; i--) {
|
|
2226
|
+
const key = sortedKeys[i];
|
|
2227
|
+
const bin = this.map.get(key);
|
|
2228
|
+
if (!bin) continue;
|
|
2229
|
+
cumulativeP += bin.p;
|
|
2230
|
+
sparseCCDF.unshift({ x: key, y: cumulativeP * 100 });
|
|
2231
|
+
}
|
|
2232
|
+
const findDamageAtProbability = (targetProb) => {
|
|
2233
|
+
for (let i = 0; i < sparseCCDF.length; i++) {
|
|
2234
|
+
if (sparseCCDF[i].y <= targetProb) {
|
|
2235
|
+
return i > 0 ? sparseCCDF[i - 1].x : sparseCCDF[i].x;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return sparseCCDF[sparseCCDF.length - 1].x;
|
|
2239
|
+
};
|
|
2240
|
+
return {
|
|
2241
|
+
p80: findDamageAtProbability(80),
|
|
2242
|
+
p50: findDamageAtProbability(50),
|
|
2243
|
+
p20: findDamageAtProbability(20)
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Full numeric model for the stacked damage-attribution chart — bar-height
|
|
2248
|
+
* masses, tooltip shares, bucket labels/ranges, percentile markers, and the
|
|
2249
|
+
* mean. The caller only maps these into a rendering format (colors, labels,
|
|
2250
|
+
* axis units); all of the dice-and-probability logic lives here.
|
|
2251
|
+
*
|
|
2252
|
+
* Built split-first-then-bin: the attribution split ({@link attributionByValue})
|
|
2253
|
+
* runs on the un-binned distribution, then the resulting series are coarsened.
|
|
2254
|
+
* {@link rebin} is deliberately *not* used — rebinning first would fold any
|
|
2255
|
+
* sub-`binSize` damage into the damage-0 bucket, which the split then mistakes
|
|
2256
|
+
* for a clean miss and drops.
|
|
2257
|
+
*
|
|
2258
|
+
* @param options.maxBuckets Coarsen to at most this many equal-width buckets
|
|
2259
|
+
* when the integer support is wider (`range > maxBuckets`); omit for a dense,
|
|
2260
|
+
* per-integer model.
|
|
2261
|
+
* @param options.stackOrder Preferred outcome order (defaults to
|
|
2262
|
+
* {@link ALL_OUTCOME_TYPES}); labels outside it sort alphabetically after.
|
|
2263
|
+
* @param options.epsilon Bucket-total floor below which a `shares` entry is 0
|
|
2264
|
+
* (divide-by-~0 guard). Defaults to 1e-9.
|
|
2265
|
+
*/
|
|
2266
|
+
damageAttributionChartModel(options = {}) {
|
|
2267
|
+
const { maxBuckets, stackOrder = ALL_OUTCOME_TYPES, epsilon = 1e-9 } = options;
|
|
2268
|
+
const empty = {
|
|
2269
|
+
labels: [],
|
|
2270
|
+
outcomes: [],
|
|
2271
|
+
series: /* @__PURE__ */ new Map(),
|
|
2272
|
+
shares: /* @__PURE__ */ new Map(),
|
|
2273
|
+
totals: [],
|
|
2274
|
+
percentiles: { p80: 0, p50: 0, p20: 0 },
|
|
2275
|
+
mean: 0
|
|
2276
|
+
};
|
|
2277
|
+
if (this.map.size === 0) return empty;
|
|
2278
|
+
const split = this.attributionByValue();
|
|
2279
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
2280
|
+
for (const [, bin] of this.map) {
|
|
2281
|
+
for (const k in bin.count) discovered.add(k);
|
|
2282
|
+
if (bin.attr) for (const k in bin.attr) discovered.add(k);
|
|
2283
|
+
}
|
|
2284
|
+
const outcomes = sortOutcomes([...discovered], stackOrder);
|
|
2285
|
+
const hasAttribution = outcomes.length > 0;
|
|
2286
|
+
let min = Infinity;
|
|
2287
|
+
let max = -Infinity;
|
|
2288
|
+
const widen = (d) => {
|
|
2289
|
+
if (d < min) min = d;
|
|
2290
|
+
if (d > max) max = d;
|
|
2291
|
+
};
|
|
2292
|
+
if (hasAttribution) {
|
|
2293
|
+
for (const s of split.values())
|
|
2294
|
+
for (const [d, m] of s) if (m > 0) widen(d);
|
|
2295
|
+
} else {
|
|
2296
|
+
for (const [d, bin] of this.map) if ((bin.p || 0) > 0) widen(d);
|
|
2297
|
+
}
|
|
2298
|
+
if (max < min) return empty;
|
|
2299
|
+
const range = max - min;
|
|
2300
|
+
const binned = maxBuckets !== void 0 && maxBuckets > 0 && range > maxBuckets;
|
|
2301
|
+
const binSize = binned ? Math.ceil((range + 1) / maxBuckets) : 1;
|
|
2302
|
+
const numBins = binned ? Math.ceil((range + 1) / binSize) : range + 1;
|
|
2303
|
+
const bucketOf = (d) => Math.floor((d - min) / binSize);
|
|
2304
|
+
const labels = [];
|
|
2305
|
+
const binRanges = binned ? [] : void 0;
|
|
2306
|
+
for (let i = 0; i < numBins; i++) {
|
|
2307
|
+
const start = min + i * binSize;
|
|
2308
|
+
labels.push(start);
|
|
2309
|
+
if (binRanges)
|
|
2310
|
+
binRanges.push({ start, end: Math.min(start + binSize - 1, max) });
|
|
2311
|
+
}
|
|
2312
|
+
const series = /* @__PURE__ */ new Map();
|
|
2313
|
+
for (const outcome of outcomes) {
|
|
2314
|
+
const arr = new Array(numBins).fill(0);
|
|
2315
|
+
const s = split.get(outcome);
|
|
2316
|
+
if (s) {
|
|
2317
|
+
for (const [d, m] of s) {
|
|
2318
|
+
const b = bucketOf(d);
|
|
2319
|
+
if (b >= 0 && b < numBins) arr[b] += m;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
series.set(outcome, arr);
|
|
2323
|
+
}
|
|
2324
|
+
const totals = new Array(numBins).fill(0);
|
|
2325
|
+
if (hasAttribution) {
|
|
2326
|
+
for (const arr of series.values())
|
|
2327
|
+
for (let i = 0; i < numBins; i++) totals[i] += arr[i];
|
|
2328
|
+
} else {
|
|
2329
|
+
for (const [d, bin] of this.map) {
|
|
2330
|
+
const p = bin.p || 0;
|
|
2331
|
+
if (p <= 0) continue;
|
|
2332
|
+
const b = bucketOf(d);
|
|
2333
|
+
if (b >= 0 && b < numBins) totals[b] += p;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
const shares = /* @__PURE__ */ new Map();
|
|
2337
|
+
for (const outcome of outcomes) {
|
|
2338
|
+
const arr = series.get(outcome);
|
|
2339
|
+
const sh = new Array(numBins).fill(0);
|
|
2340
|
+
for (let i = 0; i < numBins; i++) {
|
|
2341
|
+
sh[i] = totals[i] > epsilon ? arr[i] / totals[i] : 0;
|
|
2342
|
+
}
|
|
2343
|
+
shares.set(outcome, sh);
|
|
2344
|
+
}
|
|
2345
|
+
return {
|
|
2346
|
+
labels,
|
|
2347
|
+
binRanges,
|
|
2348
|
+
outcomes,
|
|
2349
|
+
series,
|
|
2350
|
+
shares,
|
|
2351
|
+
totals,
|
|
2352
|
+
percentiles: this.attributionPercentiles(),
|
|
2353
|
+
mean: this.mean()
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2489
2356
|
tailProbGE(t) {
|
|
2490
2357
|
let s = 0;
|
|
2491
2358
|
for (const [x, bin] of this) {
|