@axiapps/bridge-metrics 0.1.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/aggregationTypes.cjs +18 -0
- package/dist/aggregationTypes.d.cts +21 -0
- package/dist/aggregationTypes.d.ts +21 -0
- package/dist/aggregationTypes.js +1 -0
- package/dist/boonGeneration.cjs +308 -0
- package/dist/boonGeneration.d.cts +65 -0
- package/dist/boonGeneration.d.ts +65 -0
- package/dist/boonGeneration.js +16 -0
- package/dist/chunk-42DVJJLC.js +106 -0
- package/dist/chunk-4F55Q6K2.js +0 -0
- package/dist/chunk-ELKDB763.js +349 -0
- package/dist/chunk-FU74LJEM.js +77 -0
- package/dist/chunk-J3YCFY3C.js +37 -0
- package/dist/chunk-JP2ZL44R.js +18 -0
- package/dist/chunk-K2SRAMGC.js +0 -0
- package/dist/chunk-KRHODGVU.js +48 -0
- package/dist/chunk-LIGIXSSA.js +1383 -0
- package/dist/chunk-M2WR3JBQ.js +0 -0
- package/dist/chunk-PMVLNDZZ.js +279 -0
- package/dist/chunk-R5EJF5AW.js +147 -0
- package/dist/chunk-RGXSI3AI.js +298 -0
- package/dist/chunk-UFJJ6WLD.js +197 -0
- package/dist/chunk-WW5XFXGC.js +234 -0
- package/dist/chunk-ZFZS7JFU.js +10 -0
- package/dist/combatMetrics.cjs +251 -0
- package/dist/combatMetrics.d.cts +26 -0
- package/dist/combatMetrics.d.ts +26 -0
- package/dist/combatMetrics.js +21 -0
- package/dist/computePlayerAggregation.cjs +2042 -0
- package/dist/computePlayerAggregation.d.cts +249 -0
- package/dist/computePlayerAggregation.d.ts +249 -0
- package/dist/computePlayerAggregation.js +30 -0
- package/dist/conditionsMetrics.cjs +328 -0
- package/dist/conditionsMetrics.d.cts +67 -0
- package/dist/conditionsMetrics.d.ts +67 -0
- package/dist/conditionsMetrics.js +18 -0
- package/dist/constants.cjs +36 -0
- package/dist/constants.d.cts +18 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.js +10 -0
- package/dist/dashboardMetrics.cjs +226 -0
- package/dist/dashboardMetrics.d.cts +29 -0
- package/dist/dashboardMetrics.d.ts +29 -0
- package/dist/dashboardMetrics.js +42 -0
- package/dist/dpsReportTypes.cjs +18 -0
- package/dist/dpsReportTypes.d.cts +294 -0
- package/dist/dpsReportTypes.d.ts +294 -0
- package/dist/dpsReportTypes.js +1 -0
- package/dist/index.cjs +2902 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +152 -0
- package/dist/metrics-methods.json +38 -0
- package/dist/metricsSettings.cjs +75 -0
- package/dist/metricsSettings.d.cts +23 -0
- package/dist/metricsSettings.d.ts +23 -0
- package/dist/metricsSettings.js +8 -0
- package/dist/professionUtils.cjs +265 -0
- package/dist/professionUtils.d.cts +10 -0
- package/dist/professionUtils.d.ts +10 -0
- package/dist/professionUtils.js +20 -0
- package/dist/reportMetrics.cjs +224 -0
- package/dist/reportMetrics.d.cts +85 -0
- package/dist/reportMetrics.d.ts +85 -0
- package/dist/reportMetrics.js +12 -0
- package/dist/resUtility.cjs +52 -0
- package/dist/resUtility.d.cts +5 -0
- package/dist/resUtility.d.ts +5 -0
- package/dist/resUtility.js +8 -0
- package/dist/roles.cjs +18 -0
- package/dist/roles.d.cts +17 -0
- package/dist/roles.d.ts +17 -0
- package/dist/roles.js +1 -0
- package/dist/rollup.cjs +378 -0
- package/dist/rollup.d.cts +103 -0
- package/dist/rollup.d.ts +103 -0
- package/dist/rollup.js +16 -0
- package/dist/statsMetrics.cjs +153 -0
- package/dist/statsMetrics.d.cts +39 -0
- package/dist/statsMetrics.d.ts +39 -0
- package/dist/statsMetrics.js +22 -0
- package/dist/timestampUtils.cjs +63 -0
- package/dist/timestampUtils.d.cts +12 -0
- package/dist/timestampUtils.d.ts +12 -0
- package/dist/timestampUtils.js +9 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2902 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
DEFAULT_DISRUPTION_METHOD: () => DEFAULT_DISRUPTION_METHOD,
|
|
24
|
+
METRICS_SPEC: () => METRICS_SPEC,
|
|
25
|
+
NON_DAMAGING_CONDITIONS: () => NON_DAMAGING_CONDITIONS,
|
|
26
|
+
PROFESSION_COLORS: () => PROFESSION_COLORS,
|
|
27
|
+
ROLLUP_SOURCES_VERSION: () => ROLLUP_SOURCES_VERSION,
|
|
28
|
+
ReportSchemaError: () => ReportSchemaError,
|
|
29
|
+
aggregatePlayers: () => aggregatePlayers,
|
|
30
|
+
applySquadStabilityGeneration: () => applySquadStabilityGeneration,
|
|
31
|
+
buildConditionIconMap: () => buildConditionIconMap,
|
|
32
|
+
buildRollupData: () => buildRollupData,
|
|
33
|
+
compareRunSets: () => compareRunSets,
|
|
34
|
+
computeDownContribution: () => computeDownContribution,
|
|
35
|
+
computeIncomingDisruptions: () => computeIncomingDisruptions,
|
|
36
|
+
computeOutgoingConditions: () => computeOutgoingConditions,
|
|
37
|
+
computeOutgoingCrowdControl: () => computeOutgoingCrowdControl,
|
|
38
|
+
computePlayerAggregation: () => computePlayerAggregation,
|
|
39
|
+
computeSquadBarrier: () => computeSquadBarrier,
|
|
40
|
+
computeSquadHealing: () => computeSquadHealing,
|
|
41
|
+
createPlayerAggregationAccumulators: () => createPlayerAggregationAccumulators,
|
|
42
|
+
extractRollupSource: () => extractRollupSource,
|
|
43
|
+
extractRunSummary: () => extractRunSummary,
|
|
44
|
+
finalizePlayerAggregation: () => finalizePlayerAggregation,
|
|
45
|
+
getDefaultConditionIcon: () => getDefaultConditionIcon,
|
|
46
|
+
getFightDownsDeaths: () => getFightDownsDeaths,
|
|
47
|
+
getFightOutcome: () => getFightOutcome,
|
|
48
|
+
getPlayerBlocked: () => getPlayerBlocked,
|
|
49
|
+
getPlayerBreakbarDamage: () => getPlayerBreakbarDamage,
|
|
50
|
+
getPlayerCleanses: () => getPlayerCleanses,
|
|
51
|
+
getPlayerDamage: () => getPlayerDamage,
|
|
52
|
+
getPlayerDamageTaken: () => getPlayerDamageTaken,
|
|
53
|
+
getPlayerDashboardTotals: () => getPlayerDashboardTotals,
|
|
54
|
+
getPlayerDeaths: () => getPlayerDeaths,
|
|
55
|
+
getPlayerDistanceToTag: () => getPlayerDistanceToTag,
|
|
56
|
+
getPlayerDodges: () => getPlayerDodges,
|
|
57
|
+
getPlayerDownsTaken: () => getPlayerDownsTaken,
|
|
58
|
+
getPlayerDps: () => getPlayerDps,
|
|
59
|
+
getPlayerEvaded: () => getPlayerEvaded,
|
|
60
|
+
getPlayerMissed: () => getPlayerMissed,
|
|
61
|
+
getPlayerOutgoingInterrupts: () => getPlayerOutgoingInterrupts,
|
|
62
|
+
getPlayerResurrects: () => getPlayerResurrects,
|
|
63
|
+
getPlayerStrips: () => getPlayerStrips,
|
|
64
|
+
getProfessionAbbrev: () => getProfessionAbbrev,
|
|
65
|
+
getProfessionAbbrevSuperscript: () => getProfessionAbbrevSuperscript,
|
|
66
|
+
getProfessionBase: () => getProfessionBase,
|
|
67
|
+
getProfessionColor: () => getProfessionColor,
|
|
68
|
+
getProfessionEmoji: () => getProfessionEmoji,
|
|
69
|
+
getTargetStatTotal: () => getTargetStatTotal,
|
|
70
|
+
hexToRgba: () => hexToRgba,
|
|
71
|
+
ingestLogPlayerData: () => ingestLogPlayerData,
|
|
72
|
+
isResUtilitySkill: () => isResUtilitySkill,
|
|
73
|
+
normalizeConditionLabel: () => normalizeConditionLabel,
|
|
74
|
+
parseFightTimestamp: () => parseTimestamp,
|
|
75
|
+
parseRollupSourcesFile: () => parseRollupSourcesFile,
|
|
76
|
+
precomputeGlobalEnemySkillStats: () => precomputeGlobalEnemySkillStats,
|
|
77
|
+
removeRollupSources: () => removeRollupSources,
|
|
78
|
+
resolveBuffMetaById: () => resolveBuffMetaById,
|
|
79
|
+
resolveConditionNameFromEntry: () => resolveConditionNameFromEntry,
|
|
80
|
+
resolveDisruptionValue: () => resolveDisruptionValue,
|
|
81
|
+
resolveFightTimestamp: () => resolveFightTimestamp,
|
|
82
|
+
resolveProfessionLabel: () => resolveProfessionLabel,
|
|
83
|
+
toSuperscript: () => toSuperscript,
|
|
84
|
+
updateRollupSourcesForPublish: () => updateRollupSourcesForPublish
|
|
85
|
+
});
|
|
86
|
+
module.exports = __toCommonJS(src_exports);
|
|
87
|
+
|
|
88
|
+
// src/metrics-methods.json
|
|
89
|
+
var metrics_methods_default = {
|
|
90
|
+
specVersion: "v5",
|
|
91
|
+
methods: {
|
|
92
|
+
count: {
|
|
93
|
+
label: "Count Events",
|
|
94
|
+
summary: "Counts each CC/strip event once.",
|
|
95
|
+
implications: [
|
|
96
|
+
"Simple and stable across EI versions.",
|
|
97
|
+
"Multi-hit skills can inflate totals."
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
duration: {
|
|
101
|
+
label: "Duration (Seconds)",
|
|
102
|
+
summary: "Uses total CC/strip duration in seconds.",
|
|
103
|
+
implications: [
|
|
104
|
+
"Reflects control impact more directly.",
|
|
105
|
+
"Totals represent time, not event count."
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
tiered: {
|
|
109
|
+
label: "Tiered Impact",
|
|
110
|
+
summary: "Weights events based on average duration tiers.",
|
|
111
|
+
implications: [
|
|
112
|
+
"Balances short vs long CC/strip effects.",
|
|
113
|
+
"Uses averaged durations per player."
|
|
114
|
+
],
|
|
115
|
+
tiers: {
|
|
116
|
+
shortMs: 500,
|
|
117
|
+
mediumMs: 1500,
|
|
118
|
+
weights: {
|
|
119
|
+
short: 0.5,
|
|
120
|
+
medium: 1,
|
|
121
|
+
long: 2
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/metricsSettings.ts
|
|
129
|
+
var METRICS_SPEC = metrics_methods_default;
|
|
130
|
+
var DEFAULT_DISRUPTION_METHOD = "count";
|
|
131
|
+
|
|
132
|
+
// src/boonGeneration.ts
|
|
133
|
+
var CATEGORY_COUNT = {
|
|
134
|
+
selfBuffs: () => 1,
|
|
135
|
+
groupBuffs: (groupCount) => Math.max(groupCount - 1, 0),
|
|
136
|
+
squadBuffs: (_groupCount, squadCount) => Math.max(squadCount - 1, 0)
|
|
137
|
+
};
|
|
138
|
+
var toBoonId = (id) => `b${id}`;
|
|
139
|
+
var computeGenerationMs = (category, stacking, generation, wasted, durationMs, groupCount, squadCount) => {
|
|
140
|
+
const count = CATEGORY_COUNT[category](groupCount, squadCount);
|
|
141
|
+
if (!count || !durationMs) {
|
|
142
|
+
return { generationMs: 0, wastedMs: 0 };
|
|
143
|
+
}
|
|
144
|
+
if (stacking) {
|
|
145
|
+
return {
|
|
146
|
+
generationMs: generation * durationMs * count,
|
|
147
|
+
wastedMs: wasted * durationMs * count
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
generationMs: generation / 100 * durationMs * count,
|
|
152
|
+
wastedMs: wasted / 100 * durationMs * count
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
var getPlayerBoonGenerationMs = (player, category, boonId, durationMs, groupCount, squadCount, buffMap = {}) => {
|
|
156
|
+
const buffs = player?.[category] || [];
|
|
157
|
+
const target = buffs.find((buff) => buff.id === boonId);
|
|
158
|
+
if (!target) {
|
|
159
|
+
return { generationMs: 0, wastedMs: 0 };
|
|
160
|
+
}
|
|
161
|
+
const meta = buffMap[toBoonId(boonId)];
|
|
162
|
+
const stacking = meta?.stacking ?? false;
|
|
163
|
+
const generation = target.buffData?.[0]?.generation ?? 0;
|
|
164
|
+
const wasted = target.buffData?.[0]?.wasted ?? 0;
|
|
165
|
+
return computeGenerationMs(category, stacking, generation, wasted, durationMs, groupCount, squadCount);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/constants.ts
|
|
169
|
+
var TIMESTAMP_MS_THRESHOLD = 1e12;
|
|
170
|
+
var STABILITY_BOON_ID = 1122;
|
|
171
|
+
|
|
172
|
+
// src/combatMetrics.ts
|
|
173
|
+
var toSeconds = (ms) => (ms || 0) / 1e3;
|
|
174
|
+
var applyTierWeight = (count, durationMs) => {
|
|
175
|
+
if (!count) return 0;
|
|
176
|
+
const tiers = METRICS_SPEC.methods.tiered?.tiers;
|
|
177
|
+
if (!tiers) return count;
|
|
178
|
+
const avg = durationMs / Math.max(count, 1);
|
|
179
|
+
if (avg <= tiers.shortMs) return count * tiers.weights.short;
|
|
180
|
+
if (avg <= tiers.mediumMs) return count * tiers.weights.medium;
|
|
181
|
+
return count * tiers.weights.long;
|
|
182
|
+
};
|
|
183
|
+
var resolveDisruptionValue = (count, durationMs, method) => {
|
|
184
|
+
if (method === "duration") return toSeconds(durationMs);
|
|
185
|
+
if (method === "tiered") return applyTierWeight(count, durationMs);
|
|
186
|
+
return count;
|
|
187
|
+
};
|
|
188
|
+
function computeOutgoingCrowdControl(player, method = DEFAULT_DISRUPTION_METHOD) {
|
|
189
|
+
const stats = player.statsAll?.[0];
|
|
190
|
+
const count = Number(stats?.appliedCrowdControl ?? 0);
|
|
191
|
+
const durationMs = Number(stats?.appliedCrowdControlDuration ?? 0);
|
|
192
|
+
return resolveDisruptionValue(count, durationMs, method);
|
|
193
|
+
}
|
|
194
|
+
function computeIncomingDisruptions(player, method = DEFAULT_DISRUPTION_METHOD) {
|
|
195
|
+
const defenses = player.defenses?.[0];
|
|
196
|
+
const incomingCcCount = Number(defenses?.receivedCrowdControl ?? 0);
|
|
197
|
+
const incomingCcDurationMs = Number(defenses?.receivedCrowdControlDuration ?? 0);
|
|
198
|
+
const incomingStripCount = Number(defenses?.boonStrips ?? 0);
|
|
199
|
+
const incomingStripDurationMs = Number(defenses?.boonStripsTime ?? 0);
|
|
200
|
+
const resolveValue = (count, durationMs) => resolveDisruptionValue(count, durationMs, method);
|
|
201
|
+
return {
|
|
202
|
+
strips: {
|
|
203
|
+
total: resolveValue(incomingStripCount, incomingStripDurationMs),
|
|
204
|
+
missed: 0,
|
|
205
|
+
blocked: 0
|
|
206
|
+
},
|
|
207
|
+
cc: {
|
|
208
|
+
total: resolveValue(incomingCcCount, incomingCcDurationMs),
|
|
209
|
+
missed: 0,
|
|
210
|
+
blocked: 0
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function applySquadStabilityGeneration(players, context) {
|
|
215
|
+
const durationMs = context?.durationMS || 0;
|
|
216
|
+
const buffMap = context?.buffMap || {};
|
|
217
|
+
const squadPlayers = players.filter((p) => !p.notInSquad);
|
|
218
|
+
const squadCount = squadPlayers.length;
|
|
219
|
+
const groupCounts = /* @__PURE__ */ new Map();
|
|
220
|
+
squadPlayers.forEach((player) => {
|
|
221
|
+
const group = player.group ?? 0;
|
|
222
|
+
groupCounts.set(group, (groupCounts.get(group) || 0) + 1);
|
|
223
|
+
});
|
|
224
|
+
squadPlayers.forEach((player) => {
|
|
225
|
+
const groupCount = groupCounts.get(player.group ?? 0) || 1;
|
|
226
|
+
const self = getPlayerBoonGenerationMs(
|
|
227
|
+
player,
|
|
228
|
+
"selfBuffs",
|
|
229
|
+
STABILITY_BOON_ID,
|
|
230
|
+
durationMs,
|
|
231
|
+
groupCount,
|
|
232
|
+
squadCount,
|
|
233
|
+
buffMap
|
|
234
|
+
);
|
|
235
|
+
const squad = getPlayerBoonGenerationMs(
|
|
236
|
+
player,
|
|
237
|
+
"squadBuffs",
|
|
238
|
+
STABILITY_BOON_ID,
|
|
239
|
+
durationMs,
|
|
240
|
+
groupCount,
|
|
241
|
+
squadCount,
|
|
242
|
+
buffMap
|
|
243
|
+
);
|
|
244
|
+
player.stabGeneration = (self.generationMs + squad.generationMs) / 1e3;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function computeDownContribution(player) {
|
|
248
|
+
if (player.statsAll && player.statsAll.length > 0) {
|
|
249
|
+
const val = player.statsAll[0].downContribution;
|
|
250
|
+
if (typeof val === "number" && val > 0) return val;
|
|
251
|
+
}
|
|
252
|
+
let total = 0;
|
|
253
|
+
if (player.totalDamageDist) {
|
|
254
|
+
for (const targetList of player.totalDamageDist) {
|
|
255
|
+
if (targetList) {
|
|
256
|
+
for (const entry of targetList) {
|
|
257
|
+
total += entry.downContribution || 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (total > 0) return total;
|
|
263
|
+
if (player.statsTargets) {
|
|
264
|
+
for (const targetStats of player.statsTargets) {
|
|
265
|
+
if (targetStats && targetStats.length > 0) {
|
|
266
|
+
total += targetStats[0].downContribution || 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return total;
|
|
271
|
+
}
|
|
272
|
+
function computeSquadBarrier(player) {
|
|
273
|
+
if (!player.extBarrierStats || !player.extBarrierStats.outgoingBarrierAllies) return 0;
|
|
274
|
+
let total = 0;
|
|
275
|
+
for (const squadMember of player.extBarrierStats.outgoingBarrierAllies) {
|
|
276
|
+
if (!squadMember) continue;
|
|
277
|
+
for (const phaseData of squadMember) {
|
|
278
|
+
if (phaseData) {
|
|
279
|
+
total += phaseData.barrier || 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return total;
|
|
284
|
+
}
|
|
285
|
+
function computeSquadHealing(player) {
|
|
286
|
+
if (!player.extHealingStats || !player.extHealingStats.outgoingHealingAllies) return 0;
|
|
287
|
+
let total = 0;
|
|
288
|
+
for (const squadMember of player.extHealingStats.outgoingHealingAllies) {
|
|
289
|
+
if (!squadMember) continue;
|
|
290
|
+
for (const phaseData of squadMember) {
|
|
291
|
+
if (phaseData) {
|
|
292
|
+
total += phaseData.healing || 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return total;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/dashboardMetrics.ts
|
|
300
|
+
var getPlayerDamage = (player) => player.dpsAll?.[0]?.damage || 0;
|
|
301
|
+
var getPlayerDps = (player) => player.dpsAll?.[0]?.dps || 0;
|
|
302
|
+
var getPlayerCleanses = (player) => (player.support?.[0]?.condiCleanse || 0) + (player.support?.[0]?.condiCleanseSelf || 0);
|
|
303
|
+
var getPlayerStrips = (player, method = DEFAULT_DISRUPTION_METHOD) => {
|
|
304
|
+
const support = player.support?.[0];
|
|
305
|
+
const count = Number(support?.boonStrips ?? 0);
|
|
306
|
+
const durationMs = Number(support?.boonStripsTime ?? 0);
|
|
307
|
+
return resolveDisruptionValue(count, durationMs, method);
|
|
308
|
+
};
|
|
309
|
+
var getPlayerResurrects = (player) => player.support?.[0]?.resurrects || 0;
|
|
310
|
+
var getPlayerDistanceToTag = (player) => {
|
|
311
|
+
const stats = player.statsAll?.[0];
|
|
312
|
+
const distToCom = stats?.distToCom;
|
|
313
|
+
if (distToCom !== void 0 && distToCom !== null) {
|
|
314
|
+
return distToCom;
|
|
315
|
+
}
|
|
316
|
+
return stats?.stackDist || 0;
|
|
317
|
+
};
|
|
318
|
+
var getPlayerBreakbarDamage = (player) => player.dpsAll?.[0]?.breakbarDamage || 0;
|
|
319
|
+
var getPlayerDamageTaken = (player) => player.defenses?.[0]?.damageTaken || 0;
|
|
320
|
+
var getPlayerDeaths = (player) => player.defenses?.[0]?.deadCount || 0;
|
|
321
|
+
var getPlayerDodges = (player) => player.defenses?.[0]?.dodgeCount || 0;
|
|
322
|
+
var getPlayerMissed = (player) => player.defenses?.[0]?.missedCount || 0;
|
|
323
|
+
var getPlayerBlocked = (player) => player.defenses?.[0]?.blockedCount || 0;
|
|
324
|
+
var getPlayerEvaded = (player) => player.defenses?.[0]?.evadedCount || 0;
|
|
325
|
+
var getPlayerDownsTaken = (player) => player.defenses?.[0]?.downCount || 0;
|
|
326
|
+
var getTargetStatTotal = (player, field) => {
|
|
327
|
+
let total = 0;
|
|
328
|
+
const statsTargets = player.statsTargets || [];
|
|
329
|
+
for (const targetStats of statsTargets) {
|
|
330
|
+
if (targetStats && targetStats.length > 0) {
|
|
331
|
+
total += Number(targetStats[0][field] || 0);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return total;
|
|
335
|
+
};
|
|
336
|
+
var getPlayerOutgoingInterrupts = (player) => getTargetStatTotal(player, "interrupts");
|
|
337
|
+
var getPlayerDashboardTotals = (player, method = DEFAULT_DISRUPTION_METHOD) => ({
|
|
338
|
+
downContrib: computeDownContribution(player),
|
|
339
|
+
cleanses: getPlayerCleanses(player),
|
|
340
|
+
strips: getPlayerStrips(player, method),
|
|
341
|
+
healing: computeSquadHealing(player),
|
|
342
|
+
barrier: computeSquadBarrier(player),
|
|
343
|
+
cc: computeOutgoingCrowdControl(player, method)
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/conditionsMetrics.ts
|
|
347
|
+
var NON_DAMAGING_CONDITIONS = /* @__PURE__ */ new Set([
|
|
348
|
+
"Vulnerability",
|
|
349
|
+
"Weakness",
|
|
350
|
+
"Blind",
|
|
351
|
+
"Cripple",
|
|
352
|
+
"Chill",
|
|
353
|
+
"Immobilize",
|
|
354
|
+
"Slow",
|
|
355
|
+
"Fear",
|
|
356
|
+
"Taunt"
|
|
357
|
+
]);
|
|
358
|
+
var CONDITION_NAME_MAP = /* @__PURE__ */ new Map([
|
|
359
|
+
["bleeding", "Bleeding"],
|
|
360
|
+
["burning", "Burning"],
|
|
361
|
+
["confusion", "Confusion"],
|
|
362
|
+
["poison", "Poison"],
|
|
363
|
+
["torment", "Torment"],
|
|
364
|
+
["vulnerability", "Vulnerability"],
|
|
365
|
+
["weakness", "Weakness"],
|
|
366
|
+
["weakened", "Weakness"],
|
|
367
|
+
["blind", "Blind"],
|
|
368
|
+
["blinded", "Blind"],
|
|
369
|
+
["blinding", "Blind"],
|
|
370
|
+
["cripple", "Cripple"],
|
|
371
|
+
["crippled", "Cripple"],
|
|
372
|
+
["chill", "Chill"],
|
|
373
|
+
["chilled", "Chill"],
|
|
374
|
+
["immob", "Immobilize"],
|
|
375
|
+
["immobile", "Immobilize"],
|
|
376
|
+
["immobilized", "Immobilize"],
|
|
377
|
+
["slow", "Slow"],
|
|
378
|
+
["slowed", "Slow"],
|
|
379
|
+
["fear", "Fear"],
|
|
380
|
+
["feared", "Fear"],
|
|
381
|
+
["taunt", "Taunt"],
|
|
382
|
+
["taunted", "Taunt"]
|
|
383
|
+
]);
|
|
384
|
+
var DEFAULT_CONDITION_ICONS = {
|
|
385
|
+
Blind: "https://render.guildwars2.com/file/09770136BB76FD0DBE1CC4267DEED54774CB20F6/102837.png",
|
|
386
|
+
Chill: "https://render.guildwars2.com/file/28C4EC547A3516AF0242E826772DA43A5EAC3DF3/102839.png",
|
|
387
|
+
Cripple: "https://render.guildwars2.com/file/070325E519C178D502A8160523766070D30C0C19/102838.png",
|
|
388
|
+
Fear: "https://render.guildwars2.com/file/30307A6E766D74B6EB09EDA12A4A2DE50E4D76F4/102869.png",
|
|
389
|
+
Immobilize: "https://render.guildwars2.com/file/397A613651BFCA2832B6469CE34735580A2C120E/102844.png",
|
|
390
|
+
Slow: "https://render.guildwars2.com/file/F60D1EF5271D7B9319610855676D320CD25F01C6/961397.png",
|
|
391
|
+
Taunt: "https://render.guildwars2.com/file/02EED459AD65FAF7DF32A260E479C625070841B9/1228472.png",
|
|
392
|
+
Vulnerability: "https://render.guildwars2.com/file/3A394C1A0A3257EB27A44842DDEEF0DF000E1241/102850.png",
|
|
393
|
+
Weakness: "https://render.guildwars2.com/file/6CB0E64AF9AA292E332A38C1770CE577E2CDE0E8/102853.png"
|
|
394
|
+
};
|
|
395
|
+
var getDefaultConditionIcon = (name) => {
|
|
396
|
+
if (!name) return void 0;
|
|
397
|
+
return DEFAULT_CONDITION_ICONS[name];
|
|
398
|
+
};
|
|
399
|
+
var resolveBuffMetaById = (buffMap, id) => {
|
|
400
|
+
if (!buffMap || id === void 0 || id === null) return void 0;
|
|
401
|
+
const numericId = Number(id);
|
|
402
|
+
if (Number.isFinite(numericId)) {
|
|
403
|
+
return buffMap[`b${numericId}`] || buffMap[String(numericId)];
|
|
404
|
+
}
|
|
405
|
+
return buffMap[String(id)];
|
|
406
|
+
};
|
|
407
|
+
var getConditionName = (name) => {
|
|
408
|
+
if (!name) return null;
|
|
409
|
+
const cleaned = name.trim().toLowerCase();
|
|
410
|
+
const directMatch = CONDITION_NAME_MAP.get(cleaned);
|
|
411
|
+
if (directMatch) return directMatch;
|
|
412
|
+
const tokens = cleaned.split(/[^a-z]+/).filter(Boolean);
|
|
413
|
+
for (const token of tokens) {
|
|
414
|
+
const match = CONDITION_NAME_MAP.get(token);
|
|
415
|
+
if (match) return match;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
};
|
|
419
|
+
var normalizeConditionLabel = (name) => getConditionName(name);
|
|
420
|
+
var buildConditionIconMap = (buffMap) => {
|
|
421
|
+
const map = /* @__PURE__ */ new Map();
|
|
422
|
+
if (!buffMap) return map;
|
|
423
|
+
Object.values(buffMap).forEach((meta) => {
|
|
424
|
+
if (!meta?.icon || !meta?.name) return;
|
|
425
|
+
const normalized = getConditionName(meta.name);
|
|
426
|
+
if (!normalized) return;
|
|
427
|
+
if (!map.has(normalized)) map.set(normalized, meta.icon);
|
|
428
|
+
});
|
|
429
|
+
Object.entries(DEFAULT_CONDITION_ICONS).forEach(([name, icon]) => {
|
|
430
|
+
if (!map.has(name)) map.set(name, icon);
|
|
431
|
+
});
|
|
432
|
+
return map;
|
|
433
|
+
};
|
|
434
|
+
var resolveConditionNameFromEntry = (skillName, id, buffMap) => {
|
|
435
|
+
if (id && buffMap) {
|
|
436
|
+
const buffName = resolveBuffMetaById(buffMap, id)?.name;
|
|
437
|
+
const resolved = getConditionName(buffName);
|
|
438
|
+
if (resolved) return resolved;
|
|
439
|
+
}
|
|
440
|
+
if (!skillName) return null;
|
|
441
|
+
return getConditionName(skillName);
|
|
442
|
+
};
|
|
443
|
+
var defaultGetPlayerKey = (player) => {
|
|
444
|
+
const account = player?.account || "Unknown";
|
|
445
|
+
if (account && account !== "Unknown") return account;
|
|
446
|
+
const name = player?.name || "Unknown";
|
|
447
|
+
return name || null;
|
|
448
|
+
};
|
|
449
|
+
var countAppliedFromStates = (states) => {
|
|
450
|
+
if (!states || states.length === 0) return 0;
|
|
451
|
+
let applied = 0;
|
|
452
|
+
let prev = null;
|
|
453
|
+
states.forEach((entry) => {
|
|
454
|
+
const value = Number(entry[1] ?? 0);
|
|
455
|
+
if (!Number.isFinite(value)) return;
|
|
456
|
+
if (prev === null) {
|
|
457
|
+
prev = value;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (value > prev) {
|
|
461
|
+
applied += value - prev;
|
|
462
|
+
}
|
|
463
|
+
prev = value;
|
|
464
|
+
});
|
|
465
|
+
return applied;
|
|
466
|
+
};
|
|
467
|
+
var computeUptimeFromStates = (states) => {
|
|
468
|
+
if (!states || states.length === 0) return 0;
|
|
469
|
+
let uptimeMs = 0;
|
|
470
|
+
let buffOn = 0;
|
|
471
|
+
let firstTime = 0;
|
|
472
|
+
for (const [time, value] of states) {
|
|
473
|
+
if (time === 0) continue;
|
|
474
|
+
if (value >= 1 && buffOn === 0) {
|
|
475
|
+
buffOn = value;
|
|
476
|
+
firstTime = time;
|
|
477
|
+
} else if (value === 0 && buffOn > 0) {
|
|
478
|
+
uptimeMs += time - firstTime;
|
|
479
|
+
buffOn = 0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return uptimeMs;
|
|
483
|
+
};
|
|
484
|
+
var countActiveStateEntries = (states) => {
|
|
485
|
+
if (!states || states.length === 0) return 0;
|
|
486
|
+
let count = 0;
|
|
487
|
+
states.forEach((entry) => {
|
|
488
|
+
const time = Number(entry[0] ?? 0);
|
|
489
|
+
const value = Number(entry[1] ?? 0);
|
|
490
|
+
if (!Number.isFinite(value)) return;
|
|
491
|
+
if (time === 0) return;
|
|
492
|
+
if (value > 0) count += 1;
|
|
493
|
+
});
|
|
494
|
+
return count;
|
|
495
|
+
};
|
|
496
|
+
var computeOutgoingConditions = (payload) => {
|
|
497
|
+
const { players, targets, skillMap, buffMap } = payload;
|
|
498
|
+
const getPlayerKey = payload.getPlayerKey || defaultGetPlayerKey;
|
|
499
|
+
const conditionIconMap = buildConditionIconMap(buffMap);
|
|
500
|
+
const playerConditions = {};
|
|
501
|
+
const summary = {};
|
|
502
|
+
players.forEach((player) => {
|
|
503
|
+
if (player?.notInSquad) return;
|
|
504
|
+
const key = getPlayerKey(player);
|
|
505
|
+
if (!key) return;
|
|
506
|
+
if (!playerConditions[key]) {
|
|
507
|
+
playerConditions[key] = {};
|
|
508
|
+
}
|
|
509
|
+
if (!player?.totalDamageDist) return;
|
|
510
|
+
player.totalDamageDist.forEach((distList) => {
|
|
511
|
+
if (!distList) return;
|
|
512
|
+
distList.forEach((entry) => {
|
|
513
|
+
if (!entry.id) return;
|
|
514
|
+
let skillName = `Skill ${entry.id}`;
|
|
515
|
+
let skillIcon;
|
|
516
|
+
if (skillMap) {
|
|
517
|
+
if (skillMap[`s${entry.id}`]) {
|
|
518
|
+
skillName = skillMap[`s${entry.id}`].name || skillName;
|
|
519
|
+
skillIcon = skillMap[`s${entry.id}`].icon || skillIcon;
|
|
520
|
+
} else if (skillMap[`${entry.id}`]) {
|
|
521
|
+
skillName = skillMap[`${entry.id}`].name || skillName;
|
|
522
|
+
skillIcon = skillMap[`${entry.id}`].icon || skillIcon;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const buffMeta = resolveBuffMetaById(buffMap, entry.id);
|
|
526
|
+
if (skillName.startsWith("Skill ") && buffMeta?.name) {
|
|
527
|
+
skillName = buffMeta.name;
|
|
528
|
+
skillIcon = buffMeta.icon || skillIcon;
|
|
529
|
+
}
|
|
530
|
+
const conditionName = resolveConditionNameFromEntry(skillName, entry.id, buffMap);
|
|
531
|
+
if (!conditionName) return;
|
|
532
|
+
const buffName = buffMeta?.name;
|
|
533
|
+
const conditionIcon = conditionIconMap.get(conditionName) || buffMeta?.icon;
|
|
534
|
+
const skillLabel = skillName.startsWith("Skill ") ? buffName || conditionName : skillName;
|
|
535
|
+
const skillLabelIcon = skillIcon || buffMeta?.icon;
|
|
536
|
+
const connectedHits = Number(entry.connectedHits ?? 0);
|
|
537
|
+
const rawHits = Number(entry.hits ?? 0);
|
|
538
|
+
const hits = connectedHits > 0 ? connectedHits : rawHits;
|
|
539
|
+
const damage = Number(entry.totalDamage ?? 0);
|
|
540
|
+
if (!Number.isFinite(hits) && !Number.isFinite(damage)) return;
|
|
541
|
+
const existing = summary[conditionName] || {
|
|
542
|
+
name: conditionName,
|
|
543
|
+
icon: conditionIcon,
|
|
544
|
+
applications: 0,
|
|
545
|
+
damage: 0
|
|
546
|
+
};
|
|
547
|
+
existing.applications += Number.isFinite(hits) ? hits : 0;
|
|
548
|
+
existing.damage += Number.isFinite(damage) ? damage : 0;
|
|
549
|
+
if (!existing.icon && conditionIcon) existing.icon = conditionIcon;
|
|
550
|
+
summary[conditionName] = existing;
|
|
551
|
+
const playerConditionTotals = playerConditions[key][conditionName] || {
|
|
552
|
+
icon: conditionIcon,
|
|
553
|
+
applications: 0,
|
|
554
|
+
damage: 0,
|
|
555
|
+
skills: {}
|
|
556
|
+
};
|
|
557
|
+
playerConditionTotals.applications += Number.isFinite(hits) ? hits : 0;
|
|
558
|
+
playerConditionTotals.damage += Number.isFinite(damage) ? damage : 0;
|
|
559
|
+
const skillEntry = playerConditionTotals.skills[skillLabel] || { name: skillLabel, hits: 0, damage: 0, icon: skillLabelIcon };
|
|
560
|
+
skillEntry.hits += Number.isFinite(hits) ? hits : 0;
|
|
561
|
+
skillEntry.damage += Number.isFinite(damage) ? damage : 0;
|
|
562
|
+
if (!skillEntry.icon && skillLabelIcon) skillEntry.icon = skillLabelIcon;
|
|
563
|
+
playerConditionTotals.skills[skillLabel] = skillEntry;
|
|
564
|
+
if (!playerConditionTotals.icon && conditionIcon) playerConditionTotals.icon = conditionIcon;
|
|
565
|
+
playerConditions[key][conditionName] = playerConditionTotals;
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
const nameToKey = /* @__PURE__ */ new Map();
|
|
570
|
+
players.forEach((player) => {
|
|
571
|
+
if (player?.notInSquad) return;
|
|
572
|
+
const key = getPlayerKey(player);
|
|
573
|
+
if (!key) return;
|
|
574
|
+
if (player?.name) {
|
|
575
|
+
nameToKey.set(player.name, key);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
let buffStateApplicationsTotal = 0;
|
|
579
|
+
let buffStateSourcesSeen = 0;
|
|
580
|
+
let targetBuffEntriesSeen = 0;
|
|
581
|
+
targets.forEach((target) => {
|
|
582
|
+
if (!target?.buffs) return;
|
|
583
|
+
target.buffs.forEach((buff) => {
|
|
584
|
+
const buffId = Number(buff?.id);
|
|
585
|
+
if (!Number.isFinite(buffId)) return;
|
|
586
|
+
const buffMeta = resolveBuffMetaById(buffMap, buffId);
|
|
587
|
+
const normalizedName = getConditionName(buffMeta?.name);
|
|
588
|
+
if (!normalizedName) return;
|
|
589
|
+
if (buffMeta?.classification && buffMeta.classification !== "Condition") return;
|
|
590
|
+
const conditionName = normalizedName;
|
|
591
|
+
if (!conditionName) return;
|
|
592
|
+
const statesPerSource = buff.statesPerSource || {};
|
|
593
|
+
targetBuffEntriesSeen += 1;
|
|
594
|
+
Object.entries(statesPerSource).forEach(([sourceName, states]) => {
|
|
595
|
+
const key = nameToKey.get(sourceName);
|
|
596
|
+
if (!key) return;
|
|
597
|
+
buffStateSourcesSeen += 1;
|
|
598
|
+
const appliedCounts = countAppliedFromStates(states);
|
|
599
|
+
const activeCounts = countActiveStateEntries(states);
|
|
600
|
+
const uptimeMs = computeUptimeFromStates(states);
|
|
601
|
+
buffStateApplicationsTotal += appliedCounts;
|
|
602
|
+
const playerConditionTotals = playerConditions[key]?.[conditionName] || {
|
|
603
|
+
applications: 0,
|
|
604
|
+
damage: 0,
|
|
605
|
+
skills: {}
|
|
606
|
+
};
|
|
607
|
+
playerConditionTotals.applicationsFromBuffs = (playerConditionTotals.applicationsFromBuffs || 0) + appliedCounts;
|
|
608
|
+
playerConditionTotals.applicationsFromBuffsActive = (playerConditionTotals.applicationsFromBuffsActive || 0) + activeCounts;
|
|
609
|
+
playerConditionTotals.uptimeMs = (playerConditionTotals.uptimeMs || 0) + uptimeMs;
|
|
610
|
+
playerConditions[key] = playerConditions[key] || {};
|
|
611
|
+
playerConditions[key][conditionName] = playerConditionTotals;
|
|
612
|
+
const overallTotals = summary[conditionName] || {
|
|
613
|
+
name: conditionName,
|
|
614
|
+
applications: 0,
|
|
615
|
+
damage: 0
|
|
616
|
+
};
|
|
617
|
+
overallTotals.applicationsFromBuffs = (overallTotals.applicationsFromBuffs || 0) + appliedCounts;
|
|
618
|
+
overallTotals.applicationsFromBuffsActive = (overallTotals.applicationsFromBuffsActive || 0) + activeCounts;
|
|
619
|
+
overallTotals.uptimeMs = (overallTotals.uptimeMs || 0) + uptimeMs;
|
|
620
|
+
summary[conditionName] = overallTotals;
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
playerConditions,
|
|
626
|
+
summary,
|
|
627
|
+
meta: {
|
|
628
|
+
buffStateApplicationsTotal,
|
|
629
|
+
targetBuffEntriesSeen,
|
|
630
|
+
buffStateSourcesSeen
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// src/professionUtils.ts
|
|
636
|
+
var PROFESSION_COLORS = {
|
|
637
|
+
"Guardian": "#72C1D9",
|
|
638
|
+
"Dragonhunter": "#72C1D9",
|
|
639
|
+
"Firebrand": "#72C1D9",
|
|
640
|
+
"Willbender": "#72C1D9",
|
|
641
|
+
"Revenant": "#D16E5A",
|
|
642
|
+
"Herald": "#D16E5A",
|
|
643
|
+
"Renegade": "#D16E5A",
|
|
644
|
+
"Vindicator": "#D16E5A",
|
|
645
|
+
"Warrior": "#FFD166",
|
|
646
|
+
"Berserker": "#FFD166",
|
|
647
|
+
"Spellbreaker": "#FFD166",
|
|
648
|
+
"Bladesworn": "#FFD166",
|
|
649
|
+
"Engineer": "#D09C59",
|
|
650
|
+
"Scrapper": "#D09C59",
|
|
651
|
+
"Holosmith": "#D09C59",
|
|
652
|
+
"Mechanist": "#D09C59",
|
|
653
|
+
"Ranger": "#8CDC82",
|
|
654
|
+
"Druid": "#8CDC82",
|
|
655
|
+
"Soulbeast": "#8CDC82",
|
|
656
|
+
"Untamed": "#8CDC82",
|
|
657
|
+
"Thief": "#C08F95",
|
|
658
|
+
"Daredevil": "#C08F95",
|
|
659
|
+
"Deadeye": "#C08F95",
|
|
660
|
+
"Specter": "#C08F95",
|
|
661
|
+
"Elementalist": "#F68A87",
|
|
662
|
+
"Tempest": "#F68A87",
|
|
663
|
+
"Weaver": "#F68A87",
|
|
664
|
+
"Catalyst": "#F68A87",
|
|
665
|
+
"Mesmer": "#B679D5",
|
|
666
|
+
"Chronomancer": "#B679D5",
|
|
667
|
+
"Mirage": "#B679D5",
|
|
668
|
+
"Virtuoso": "#B679D5",
|
|
669
|
+
"Necromancer": "#52A76F",
|
|
670
|
+
"Reaper": "#52A76F",
|
|
671
|
+
"Scourge": "#52A76F",
|
|
672
|
+
"Harbinger": "#52A76F",
|
|
673
|
+
"Ritualist": "#52A76F",
|
|
674
|
+
"Luminary": "#72C1D9",
|
|
675
|
+
"Conduit": "#D16E5A",
|
|
676
|
+
"Paragon": "#FFD166",
|
|
677
|
+
"Amalgam": "#D09C59",
|
|
678
|
+
"Galeshot": "#8CDC82",
|
|
679
|
+
"Antiquary": "#C08F95",
|
|
680
|
+
"Evoker": "#F68A87",
|
|
681
|
+
"Troubadour": "#B679D5",
|
|
682
|
+
"Unknown": "#64748B"
|
|
683
|
+
};
|
|
684
|
+
var PROFESSION_BASE = {
|
|
685
|
+
Guardian: "Guardian",
|
|
686
|
+
Dragonhunter: "Guardian",
|
|
687
|
+
Firebrand: "Guardian",
|
|
688
|
+
Willbender: "Guardian",
|
|
689
|
+
Revenant: "Revenant",
|
|
690
|
+
Herald: "Revenant",
|
|
691
|
+
Renegade: "Revenant",
|
|
692
|
+
Vindicator: "Revenant",
|
|
693
|
+
Warrior: "Warrior",
|
|
694
|
+
Berserker: "Warrior",
|
|
695
|
+
Spellbreaker: "Warrior",
|
|
696
|
+
Bladesworn: "Warrior",
|
|
697
|
+
Engineer: "Engineer",
|
|
698
|
+
Scrapper: "Engineer",
|
|
699
|
+
Holosmith: "Engineer",
|
|
700
|
+
Mechanist: "Engineer",
|
|
701
|
+
Ranger: "Ranger",
|
|
702
|
+
Druid: "Ranger",
|
|
703
|
+
Soulbeast: "Ranger",
|
|
704
|
+
Untamed: "Ranger",
|
|
705
|
+
Thief: "Thief",
|
|
706
|
+
Daredevil: "Thief",
|
|
707
|
+
Deadeye: "Thief",
|
|
708
|
+
Specter: "Thief",
|
|
709
|
+
Elementalist: "Elementalist",
|
|
710
|
+
Tempest: "Elementalist",
|
|
711
|
+
Weaver: "Elementalist",
|
|
712
|
+
Catalyst: "Elementalist",
|
|
713
|
+
Mesmer: "Mesmer",
|
|
714
|
+
Chronomancer: "Mesmer",
|
|
715
|
+
Mirage: "Mesmer",
|
|
716
|
+
Virtuoso: "Mesmer",
|
|
717
|
+
Necromancer: "Necromancer",
|
|
718
|
+
Reaper: "Necromancer",
|
|
719
|
+
Scourge: "Necromancer",
|
|
720
|
+
Harbinger: "Necromancer",
|
|
721
|
+
Ritualist: "Necromancer",
|
|
722
|
+
Luminary: "Guardian",
|
|
723
|
+
Conduit: "Revenant",
|
|
724
|
+
Paragon: "Warrior",
|
|
725
|
+
Amalgam: "Engineer",
|
|
726
|
+
Galeshot: "Ranger",
|
|
727
|
+
Antiquary: "Thief",
|
|
728
|
+
Evoker: "Elementalist",
|
|
729
|
+
Troubadour: "Mesmer",
|
|
730
|
+
Unknown: "Unknown"
|
|
731
|
+
};
|
|
732
|
+
var PROFESSION_ABBREVIATIONS = {
|
|
733
|
+
Guardian: "gdn",
|
|
734
|
+
Dragonhunter: "dh",
|
|
735
|
+
Firebrand: "fb",
|
|
736
|
+
Willbender: "wb",
|
|
737
|
+
Revenant: "rev",
|
|
738
|
+
Herald: "her",
|
|
739
|
+
Renegade: "ren",
|
|
740
|
+
Vindicator: "vin",
|
|
741
|
+
Warrior: "war",
|
|
742
|
+
Berserker: "ber",
|
|
743
|
+
Spellbreaker: "spb",
|
|
744
|
+
Bladesworn: "bsw",
|
|
745
|
+
Engineer: "eng",
|
|
746
|
+
Scrapper: "scr",
|
|
747
|
+
Holosmith: "hol",
|
|
748
|
+
Mechanist: "mec",
|
|
749
|
+
Ranger: "rng",
|
|
750
|
+
Druid: "dru",
|
|
751
|
+
Soulbeast: "slb",
|
|
752
|
+
Untamed: "unt",
|
|
753
|
+
Thief: "thi",
|
|
754
|
+
Daredevil: "dar",
|
|
755
|
+
Deadeye: "dea",
|
|
756
|
+
Specter: "spe",
|
|
757
|
+
Elementalist: "ele",
|
|
758
|
+
Tempest: "tem",
|
|
759
|
+
Weaver: "wev",
|
|
760
|
+
Catalyst: "cat",
|
|
761
|
+
Mesmer: "mes",
|
|
762
|
+
Chronomancer: "chr",
|
|
763
|
+
Mirage: "mir",
|
|
764
|
+
Virtuoso: "vir",
|
|
765
|
+
Necromancer: "nec",
|
|
766
|
+
Reaper: "rea",
|
|
767
|
+
Scourge: "sco",
|
|
768
|
+
Harbinger: "har",
|
|
769
|
+
Ritualist: "rit",
|
|
770
|
+
Luminary: "lum",
|
|
771
|
+
Conduit: "con",
|
|
772
|
+
Paragon: "par",
|
|
773
|
+
Amalgam: "ama",
|
|
774
|
+
Galeshot: "gal",
|
|
775
|
+
Antiquary: "ant",
|
|
776
|
+
Evoker: "evo",
|
|
777
|
+
Troubadour: "tro",
|
|
778
|
+
Unknown: "unk"
|
|
779
|
+
};
|
|
780
|
+
var PROFESSION_EMOJI = {
|
|
781
|
+
Guardian: "\u{1F535}",
|
|
782
|
+
Revenant: "\u{1F534}",
|
|
783
|
+
Warrior: "\u{1F7E1}",
|
|
784
|
+
Engineer: "\u{1F7E0}",
|
|
785
|
+
Ranger: "\u{1F7E2}",
|
|
786
|
+
Thief: "\u26AB\uFE0F",
|
|
787
|
+
Elementalist: "\u{1F534}",
|
|
788
|
+
Mesmer: "\u{1F7E3}",
|
|
789
|
+
Necromancer: "\u{1F7E2}",
|
|
790
|
+
Unknown: "\u26AA\uFE0F"
|
|
791
|
+
};
|
|
792
|
+
function getProfessionColor(profession) {
|
|
793
|
+
return PROFESSION_COLORS[profession] || PROFESSION_COLORS["Unknown"];
|
|
794
|
+
}
|
|
795
|
+
function hexToRgba(hex, alpha) {
|
|
796
|
+
const h = hex.replace("#", "");
|
|
797
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
798
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
799
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
800
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
801
|
+
}
|
|
802
|
+
function getProfessionBase(profession) {
|
|
803
|
+
if (!profession) return "Unknown";
|
|
804
|
+
return PROFESSION_BASE[profession] || profession;
|
|
805
|
+
}
|
|
806
|
+
function getProfessionAbbrev(profession) {
|
|
807
|
+
if (!profession) return PROFESSION_ABBREVIATIONS.Unknown;
|
|
808
|
+
return PROFESSION_ABBREVIATIONS[profession] || profession.slice(0, 3).toLowerCase();
|
|
809
|
+
}
|
|
810
|
+
function getProfessionEmoji(profession) {
|
|
811
|
+
const base = getProfessionBase(profession);
|
|
812
|
+
return PROFESSION_EMOJI[base] || PROFESSION_EMOJI.Unknown;
|
|
813
|
+
}
|
|
814
|
+
var SUPERSCRIPT_MAP = {
|
|
815
|
+
a: "\u1D43",
|
|
816
|
+
b: "\u1D47",
|
|
817
|
+
c: "\u1D9C",
|
|
818
|
+
d: "\u1D48",
|
|
819
|
+
e: "\u1D49",
|
|
820
|
+
f: "\u1DA0",
|
|
821
|
+
g: "\u1D4D",
|
|
822
|
+
h: "\u02B0",
|
|
823
|
+
i: "\u2071",
|
|
824
|
+
j: "\u02B2",
|
|
825
|
+
k: "\u1D4F",
|
|
826
|
+
l: "\u02E1",
|
|
827
|
+
m: "\u1D50",
|
|
828
|
+
n: "\u207F",
|
|
829
|
+
o: "\u1D52",
|
|
830
|
+
p: "\u1D56",
|
|
831
|
+
q: "\u146B",
|
|
832
|
+
r: "\u02B3",
|
|
833
|
+
s: "\u02E2",
|
|
834
|
+
t: "\u1D57",
|
|
835
|
+
u: "\u1D58",
|
|
836
|
+
v: "\u1D5B",
|
|
837
|
+
w: "\u02B7",
|
|
838
|
+
x: "\u02E3",
|
|
839
|
+
y: "\u02B8",
|
|
840
|
+
z: "\u1DBB",
|
|
841
|
+
"0": "\u2070",
|
|
842
|
+
"1": "\xB9",
|
|
843
|
+
"2": "\xB2",
|
|
844
|
+
"3": "\xB3",
|
|
845
|
+
"4": "\u2074",
|
|
846
|
+
"5": "\u2075",
|
|
847
|
+
"6": "\u2076",
|
|
848
|
+
"7": "\u2077",
|
|
849
|
+
"8": "\u2078",
|
|
850
|
+
"9": "\u2079"
|
|
851
|
+
};
|
|
852
|
+
function toSuperscript(value) {
|
|
853
|
+
return value.toLowerCase().split("").map((char) => SUPERSCRIPT_MAP[char] || char).join("");
|
|
854
|
+
}
|
|
855
|
+
function getProfessionAbbrevSuperscript(profession) {
|
|
856
|
+
return toSuperscript(getProfessionAbbrev(profession));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/statsMetrics.ts
|
|
860
|
+
var OFFENSE_METRICS = [
|
|
861
|
+
{ id: "damage", label: "Damage", field: "damage", source: "dpsAll" },
|
|
862
|
+
{ id: "directDmg", label: "Direct Damage", field: "directDmg", source: "statsTargets" },
|
|
863
|
+
{ id: "connectedDamageCount", label: "Connected Damage Count", field: "connectedDamageCount", source: "statsTargets" },
|
|
864
|
+
{ id: "connectedDirectDamageCount", label: "Connected Direct Damage Count", field: "connectedDirectDamageCount", source: "statsTargets" },
|
|
865
|
+
{ id: "battleStandardHits", label: "Battle Standard Tracking" },
|
|
866
|
+
{ id: "criticalRate", label: "Critical Rate", field: "criticalRate", isRate: true, isPercent: true, denomField: "critableDirectDamageCount", source: "statsTargets" },
|
|
867
|
+
{ id: "criticalDmg", label: "Critical Damage", field: "criticalDmg", source: "statsTargets" },
|
|
868
|
+
{ id: "flankingRate", label: "Flanking Rate", field: "flankingRate", isRate: true, isPercent: true, denomField: "connectedDirectDamageCount", source: "statsTargets" },
|
|
869
|
+
{ id: "glanceRate", label: "Glance Rate", field: "glanceRate", isRate: true, isPercent: true, denomField: "connectedDirectDamageCount", source: "statsTargets" },
|
|
870
|
+
{ id: "missed", label: "Missed", field: "missed", source: "statsTargets" },
|
|
871
|
+
{ id: "evaded", label: "Evaded (enemy)", field: "evaded", source: "statsTargets" },
|
|
872
|
+
{ id: "blocked", label: "Blocked (enemy)", field: "blocked", source: "statsTargets" },
|
|
873
|
+
{ id: "interrupts", label: "Interrupts", field: "interrupts", source: "statsTargets" },
|
|
874
|
+
{ id: "invulned", label: "Invulned", field: "invulned", source: "statsTargets" },
|
|
875
|
+
{ id: "killed", label: "Killed", field: "killed", source: "statsTargets" },
|
|
876
|
+
{ id: "downed", label: "Downed", field: "downed", source: "statsTargets" },
|
|
877
|
+
{ id: "downContribution", label: "Down Contribution", field: "downContribution", source: "statsTargets" },
|
|
878
|
+
{ id: "downContributionPercent", label: "Down Contribution %", isRate: true, isPercent: true },
|
|
879
|
+
{ id: "againstDownedDamage", label: "Against Downed Damage", field: "againstDownedDamage", source: "statsTargets" },
|
|
880
|
+
{ id: "appliedCrowdControl", label: "Applied CC", field: "appliedCrowdControl", source: "statsTargets" },
|
|
881
|
+
{ id: "appliedCrowdControlDuration", label: "Applied CC Duration", field: "appliedCrowdControlDuration", source: "statsTargets" },
|
|
882
|
+
{ id: "appliedCrowdControlDownContribution", label: "Applied CC Down Contribution", field: "appliedCrowdControlDownContribution", source: "statsTargets" },
|
|
883
|
+
{ id: "appliedCrowdControlDurationDownContribution", label: "Applied CC Duration Down Contribution", field: "appliedCrowdControlDurationDownContribution", source: "statsTargets" },
|
|
884
|
+
{ id: "boonStrips", label: "Boon Strips", field: "boonStrips", source: "support" }
|
|
885
|
+
];
|
|
886
|
+
var DEFENSE_METRICS = [
|
|
887
|
+
{ id: "damageTaken", label: "Damage Taken", field: "damageTaken" },
|
|
888
|
+
{ id: "minionDamageTaken", label: "Minion Damage Taken" },
|
|
889
|
+
{ id: "damageTakenCount", label: "Damage Taken Count", field: "damageTakenCount" },
|
|
890
|
+
{ id: "conditionDamageTaken", label: "Condition Damage Taken", field: "conditionDamageTaken" },
|
|
891
|
+
{ id: "conditionDamageTakenCount", label: "Condition Damage Taken Count", field: "conditionDamageTakenCount" },
|
|
892
|
+
{ id: "powerDamageTaken", label: "Power Damage Taken", field: "powerDamageTaken" },
|
|
893
|
+
{ id: "powerDamageTakenCount", label: "Power Damage Taken Count", field: "powerDamageTakenCount" },
|
|
894
|
+
{ id: "downedDamageTaken", label: "Downed Damage Taken", field: "downedDamageTaken" },
|
|
895
|
+
{ id: "downedDamageTakenCount", label: "Downed Damage Taken Count", field: "downedDamageTakenCount" },
|
|
896
|
+
{ id: "damageBarrier", label: "Damage Barrier", field: "damageBarrier" },
|
|
897
|
+
{ id: "damageBarrierCount", label: "Damage Barrier Count", field: "damageBarrierCount" },
|
|
898
|
+
{ id: "blockedCount", label: "Blocked Count", field: "blockedCount" },
|
|
899
|
+
{ id: "evadedCount", label: "Evaded Count", field: "evadedCount" },
|
|
900
|
+
{ id: "missedCount", label: "Missed Count", field: "missedCount" },
|
|
901
|
+
{ id: "dodgeCount", label: "Dodge Count", field: "dodgeCount" },
|
|
902
|
+
{ id: "invulnedCount", label: "Invulnerable Count", field: "invulnedCount" },
|
|
903
|
+
{ id: "interruptedCount", label: "Interrupted Count", field: "interruptedCount" },
|
|
904
|
+
{ id: "downCount", label: "Down Count", field: "downCount" },
|
|
905
|
+
{ id: "deadCount", label: "Death Count", field: "deadCount" },
|
|
906
|
+
{ id: "boonStrips", label: "Boon Strips (Incoming)", field: "boonStrips" },
|
|
907
|
+
{ id: "conditionCleanses", label: "Cleanses (Incoming)", field: "conditionCleanses" },
|
|
908
|
+
{ id: "receivedCrowdControl", label: "Crowd Control (Incoming)", field: "receivedCrowdControl" }
|
|
909
|
+
];
|
|
910
|
+
var SUPPORT_METRICS = [
|
|
911
|
+
{ id: "condiCleanse", label: "Condition Cleanses", field: "condiCleanse" },
|
|
912
|
+
{ id: "condiCleanseTime", label: "Condition Cleanse Time", field: "condiCleanseTime", isTime: true },
|
|
913
|
+
{ id: "condiCleanseSelf", label: "Condition Cleanse Self", field: "condiCleanseSelf" },
|
|
914
|
+
{ id: "condiCleanseTimeSelf", label: "Condition Cleanse Time Self", field: "condiCleanseTimeSelf", isTime: true },
|
|
915
|
+
{ id: "boonStrips", label: "Boon Strips", field: "boonStrips" },
|
|
916
|
+
{ id: "boonStripsTime", label: "Boon Strips Time", field: "boonStripsTime", isTime: true },
|
|
917
|
+
{ id: "boonStripDownContribution", label: "Boon Strip Down Contribution", field: "boonStripDownContribution" },
|
|
918
|
+
{ id: "boonStripDownContributionTime", label: "Boon Strip Down Contribution Time", field: "boonStripDownContributionTime", isTime: true },
|
|
919
|
+
{ id: "stunBreak", label: "Stun Breaks", field: "stunBreak" },
|
|
920
|
+
{ id: "removedStunDuration", label: "Removed Stun Duration", field: "removedStunDuration", isTime: true },
|
|
921
|
+
{ id: "resurrects", label: "Resurrects", field: "resurrects" },
|
|
922
|
+
{ id: "resurrectTime", label: "Resurrect Time", field: "resurrectTime", isTime: true }
|
|
923
|
+
];
|
|
924
|
+
var RES_UTILITY_NAME_MATCHES = [
|
|
925
|
+
"battle standard",
|
|
926
|
+
"glyph of renewal",
|
|
927
|
+
"glyph of the stars",
|
|
928
|
+
"illusion of life",
|
|
929
|
+
"spirit of nature",
|
|
930
|
+
"nature spirit",
|
|
931
|
+
"search and rescue",
|
|
932
|
+
"signet of mercy"
|
|
933
|
+
];
|
|
934
|
+
var RES_UTILITY_IDS = /* @__PURE__ */ new Set([10244]);
|
|
935
|
+
|
|
936
|
+
// src/resUtility.ts
|
|
937
|
+
var isResUtilitySkill = (id, skillMap) => {
|
|
938
|
+
if (RES_UTILITY_IDS.has(id)) {
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
const entry = skillMap?.[`s${id}`] || skillMap?.[`${id}`];
|
|
942
|
+
const name = entry?.name?.toLowerCase() || "";
|
|
943
|
+
return RES_UTILITY_NAME_MATCHES.some((match) => name.includes(match));
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
// src/timestampUtils.ts
|
|
947
|
+
var parseTimestamp = (value) => {
|
|
948
|
+
if (value === void 0 || value === null || value === "") return 0;
|
|
949
|
+
if (typeof value === "number") {
|
|
950
|
+
if (!Number.isFinite(value) || value <= 0) return 0;
|
|
951
|
+
return value > TIMESTAMP_MS_THRESHOLD ? value : value * 1e3;
|
|
952
|
+
}
|
|
953
|
+
if (value instanceof Date) {
|
|
954
|
+
const ms = value.getTime();
|
|
955
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
956
|
+
}
|
|
957
|
+
const str = String(value).trim();
|
|
958
|
+
if (!str) return 0;
|
|
959
|
+
const numeric = Number(str);
|
|
960
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
961
|
+
return numeric > TIMESTAMP_MS_THRESHOLD ? numeric : numeric * 1e3;
|
|
962
|
+
}
|
|
963
|
+
const parsed = Date.parse(str);
|
|
964
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
965
|
+
const normalized = str.replace(/([+-]\d{2})$/, "$1:00");
|
|
966
|
+
const reparsed = Date.parse(normalized);
|
|
967
|
+
return Number.isFinite(reparsed) && reparsed > 0 ? reparsed : 0;
|
|
968
|
+
};
|
|
969
|
+
var resolveFightTimestamp = (details, log) => {
|
|
970
|
+
return parseTimestamp(
|
|
971
|
+
details?.timeStartStd ?? details?.timeStart ?? details?.timeEndStd ?? details?.timeEnd ?? details?.timeStartText ?? details?.timeEndText ?? details?.uploadTime ?? log?.uploadTime
|
|
972
|
+
);
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/computePlayerAggregation.ts
|
|
976
|
+
var getFightDownsDeaths = (details) => {
|
|
977
|
+
const players = details?.players || [];
|
|
978
|
+
const squadPlayers = players.filter((p) => !p.notInSquad);
|
|
979
|
+
let squadDownsDeaths = 0;
|
|
980
|
+
let enemyDownsDeaths = 0;
|
|
981
|
+
let squadDeaths = 0;
|
|
982
|
+
let enemyDeaths = 0;
|
|
983
|
+
squadPlayers.forEach((p) => {
|
|
984
|
+
const defenses = p.defenses?.[0];
|
|
985
|
+
if (!defenses) return;
|
|
986
|
+
const downCount = Number(defenses.downCount || 0);
|
|
987
|
+
const deadCount = Number(defenses.deadCount || 0);
|
|
988
|
+
squadDownsDeaths += downCount + deadCount;
|
|
989
|
+
squadDeaths += deadCount;
|
|
990
|
+
});
|
|
991
|
+
squadPlayers.forEach((p) => {
|
|
992
|
+
if (!p.statsTargets || p.statsTargets.length === 0) return;
|
|
993
|
+
p.statsTargets.forEach((targetStats) => {
|
|
994
|
+
if (!Array.isArray(targetStats)) return;
|
|
995
|
+
targetStats.forEach((phaseStats) => {
|
|
996
|
+
if (!phaseStats) return;
|
|
997
|
+
const downed = Number(phaseStats.downed || 0);
|
|
998
|
+
const killed = Number(phaseStats.killed || 0);
|
|
999
|
+
enemyDownsDeaths += downed + killed;
|
|
1000
|
+
enemyDeaths += killed;
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
return { squadDownsDeaths, enemyDownsDeaths, squadDeaths, enemyDeaths };
|
|
1005
|
+
};
|
|
1006
|
+
var getFightOutcome = (details) => {
|
|
1007
|
+
const { squadDownsDeaths, enemyDownsDeaths } = getFightDownsDeaths(details);
|
|
1008
|
+
if (squadDownsDeaths > 0 || enemyDownsDeaths > 0) {
|
|
1009
|
+
return enemyDownsDeaths > squadDownsDeaths;
|
|
1010
|
+
}
|
|
1011
|
+
if (typeof details?.success === "boolean") return details.success;
|
|
1012
|
+
return false;
|
|
1013
|
+
};
|
|
1014
|
+
var knownProfessionNames = new Set(Object.keys(PROFESSION_COLORS));
|
|
1015
|
+
var knownProfessionList = Object.keys(PROFESSION_COLORS).filter((name) => name && name !== "Unknown").sort((a, b) => b.length - a.length);
|
|
1016
|
+
var baseProfessionNames = [
|
|
1017
|
+
"Guardian",
|
|
1018
|
+
"Revenant",
|
|
1019
|
+
"Warrior",
|
|
1020
|
+
"Engineer",
|
|
1021
|
+
"Ranger",
|
|
1022
|
+
"Thief",
|
|
1023
|
+
"Elementalist",
|
|
1024
|
+
"Mesmer",
|
|
1025
|
+
"Necromancer"
|
|
1026
|
+
];
|
|
1027
|
+
var resolveProfessionLabel = (name) => {
|
|
1028
|
+
if (!name) return "Unknown";
|
|
1029
|
+
const cleaned = String(name).replace(/\s*\([^)]*\)\s*$/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/\s\d+$/, "").trim();
|
|
1030
|
+
if (knownProfessionNames.has(cleaned)) return cleaned;
|
|
1031
|
+
const lower = cleaned.toLowerCase();
|
|
1032
|
+
for (const prof of knownProfessionList) {
|
|
1033
|
+
if (lower.includes(prof.toLowerCase())) return prof;
|
|
1034
|
+
}
|
|
1035
|
+
const baseMatch = baseProfessionNames.find((prof) => lower.includes(prof.toLowerCase()));
|
|
1036
|
+
return baseMatch || cleaned || "Unknown";
|
|
1037
|
+
};
|
|
1038
|
+
var supportTimeSanityFields = /* @__PURE__ */ new Set(["boonStripsTime", "condiCleanseTime", "condiCleanseTimeSelf"]);
|
|
1039
|
+
var isBoon = (meta) => {
|
|
1040
|
+
if (!meta?.classification) return true;
|
|
1041
|
+
return meta.classification === "Boon";
|
|
1042
|
+
};
|
|
1043
|
+
var createMitigationTotals = () => ({
|
|
1044
|
+
totalHits: 0,
|
|
1045
|
+
blocked: 0,
|
|
1046
|
+
evaded: 0,
|
|
1047
|
+
glanced: 0,
|
|
1048
|
+
missed: 0,
|
|
1049
|
+
invulned: 0,
|
|
1050
|
+
interrupted: 0,
|
|
1051
|
+
totalMitigation: 0,
|
|
1052
|
+
minMitigation: 0
|
|
1053
|
+
});
|
|
1054
|
+
var readNumber = (value) => {
|
|
1055
|
+
const numeric = Number(value);
|
|
1056
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
1057
|
+
};
|
|
1058
|
+
var parseMitigationKey = (rawKey) => {
|
|
1059
|
+
const parts = String(rawKey).split("|");
|
|
1060
|
+
const name = parts[0] || "Unknown";
|
|
1061
|
+
const profession = resolveProfessionLabel(parts[1] || "Unknown");
|
|
1062
|
+
const account = parts[2] || name || "Unknown";
|
|
1063
|
+
return { name, profession, account };
|
|
1064
|
+
};
|
|
1065
|
+
var ensureMitigationRow = (map, key, base) => {
|
|
1066
|
+
let row = map.get(key);
|
|
1067
|
+
if (!row) {
|
|
1068
|
+
row = {
|
|
1069
|
+
account: base.account,
|
|
1070
|
+
name: base.name,
|
|
1071
|
+
profession: base.profession,
|
|
1072
|
+
professionList: base.profession ? [base.profession] : [],
|
|
1073
|
+
activeMs: 0,
|
|
1074
|
+
mitigationTotals: createMitigationTotals()
|
|
1075
|
+
};
|
|
1076
|
+
map.set(key, row);
|
|
1077
|
+
} else if (base.profession && !row.professionList.includes(base.profession)) {
|
|
1078
|
+
row.professionList.push(base.profession);
|
|
1079
|
+
}
|
|
1080
|
+
return row;
|
|
1081
|
+
};
|
|
1082
|
+
var ensureMitigationMinionRow = (map, key, base) => {
|
|
1083
|
+
let row = map.get(key);
|
|
1084
|
+
if (!row) {
|
|
1085
|
+
row = {
|
|
1086
|
+
account: base.account,
|
|
1087
|
+
name: base.name,
|
|
1088
|
+
profession: base.profession,
|
|
1089
|
+
professionList: base.profession ? [base.profession] : [],
|
|
1090
|
+
activeMs: 0,
|
|
1091
|
+
mitigationTotals: createMitigationTotals(),
|
|
1092
|
+
minion: base.minion
|
|
1093
|
+
};
|
|
1094
|
+
map.set(key, row);
|
|
1095
|
+
} else if (base.profession && !row.professionList.includes(base.profession)) {
|
|
1096
|
+
row.professionList.push(base.profession);
|
|
1097
|
+
}
|
|
1098
|
+
return row;
|
|
1099
|
+
};
|
|
1100
|
+
var addMitigationTotals = (totals, entry) => {
|
|
1101
|
+
totals.totalHits += readNumber(entry?.skill_hits ?? entry?.skillHits);
|
|
1102
|
+
totals.blocked += readNumber(entry?.blocked);
|
|
1103
|
+
totals.evaded += readNumber(entry?.evaded);
|
|
1104
|
+
totals.glanced += readNumber(entry?.glanced);
|
|
1105
|
+
totals.missed += readNumber(entry?.missed);
|
|
1106
|
+
totals.invulned += readNumber(entry?.invulned);
|
|
1107
|
+
totals.interrupted += readNumber(entry?.interrupted);
|
|
1108
|
+
totals.totalMitigation += readNumber(entry?.avoided_damage ?? entry?.avoidedDamage);
|
|
1109
|
+
totals.minMitigation += readNumber(entry?.min_avoided_damage ?? entry?.minAvoidedDamage);
|
|
1110
|
+
};
|
|
1111
|
+
var resolveGlobalEnemyStats = (globalEnemySkillStats, skillId) => {
|
|
1112
|
+
const bucket = globalEnemySkillStats.get(skillId);
|
|
1113
|
+
if (!bucket) {
|
|
1114
|
+
return { hasSkill: false, avg: 1, min: 1, hits: 0 };
|
|
1115
|
+
}
|
|
1116
|
+
const hits = bucket.connectedHits || 0;
|
|
1117
|
+
const avg = hits > 0 ? bucket.totalDamage / hits : 0;
|
|
1118
|
+
const min = bucket.minCount > 0 ? bucket.minTotal / bucket.minCount : 0;
|
|
1119
|
+
return { hasSkill: true, avg, min, hits };
|
|
1120
|
+
};
|
|
1121
|
+
var updateCumulativeCounts = (map, key, delta) => {
|
|
1122
|
+
const existing = map.get(key) || createMitigationTotals();
|
|
1123
|
+
existing.totalHits += delta.totalHits;
|
|
1124
|
+
existing.blocked += delta.blocked;
|
|
1125
|
+
existing.evaded += delta.evaded;
|
|
1126
|
+
existing.glanced += delta.glanced;
|
|
1127
|
+
existing.missed += delta.missed;
|
|
1128
|
+
existing.invulned += delta.invulned;
|
|
1129
|
+
existing.interrupted += delta.interrupted;
|
|
1130
|
+
map.set(key, existing);
|
|
1131
|
+
return existing;
|
|
1132
|
+
};
|
|
1133
|
+
var getPlayerIdentity = (player, splitPlayersByClass) => {
|
|
1134
|
+
const baseAccount = player?.account && player.account !== "Unknown" ? player.account : player?.name || "Unknown";
|
|
1135
|
+
const profession = resolveProfessionLabel(player?.profession || "Unknown");
|
|
1136
|
+
const isSplit = splitPlayersByClass && profession !== "Unknown";
|
|
1137
|
+
const key = isSplit ? `${baseAccount}::${profession}` : baseAccount;
|
|
1138
|
+
const accountLabel = baseAccount;
|
|
1139
|
+
return { key, baseAccount, profession, accountLabel, isSplit };
|
|
1140
|
+
};
|
|
1141
|
+
var getMitigationIdentity = (account, profession, splitPlayersByClass) => {
|
|
1142
|
+
const resolvedProfession = resolveProfessionLabel(profession || "Unknown");
|
|
1143
|
+
const isSplit = splitPlayersByClass && resolvedProfession !== "Unknown";
|
|
1144
|
+
const key = isSplit ? `${account}::${resolvedProfession}` : account;
|
|
1145
|
+
const accountLabel = account;
|
|
1146
|
+
return { key, accountLabel, profession: resolvedProfession };
|
|
1147
|
+
};
|
|
1148
|
+
var normalizeStatePairs = (states) => {
|
|
1149
|
+
if (!Array.isArray(states)) return [];
|
|
1150
|
+
return states.map((entry) => {
|
|
1151
|
+
if (Array.isArray(entry)) return [Number(entry[0]), Number(entry[1])];
|
|
1152
|
+
if (entry && typeof entry === "object") return [Number(entry.time), Number(entry.value)];
|
|
1153
|
+
return null;
|
|
1154
|
+
}).filter(
|
|
1155
|
+
(entry) => !!entry && Number.isFinite(entry[0]) && Number.isFinite(entry[1]) && entry[0] >= 0
|
|
1156
|
+
).sort((a, b) => a[0] - b[0]);
|
|
1157
|
+
};
|
|
1158
|
+
var resolveNonStackingFactor = (rawValue) => {
|
|
1159
|
+
const safeValue = Math.max(0, Number(rawValue || 0));
|
|
1160
|
+
if (!Number.isFinite(safeValue) || safeValue <= 0) return 0;
|
|
1161
|
+
if (safeValue <= 1) return safeValue;
|
|
1162
|
+
if (safeValue <= 100) return safeValue / 100;
|
|
1163
|
+
return 1;
|
|
1164
|
+
};
|
|
1165
|
+
var integrateSourceStates = (states, durationMs, stacking) => {
|
|
1166
|
+
const normalized = normalizeStatePairs(states);
|
|
1167
|
+
if (normalized.length === 0 || durationMs <= 0) return { totalMs: 0, uptimeMs: 0 };
|
|
1168
|
+
const resolvedDurationMs = Math.max(1, Number(durationMs || 0));
|
|
1169
|
+
const clampTime = (value) => Math.max(0, Math.min(resolvedDurationMs, Number(value || 0)));
|
|
1170
|
+
let totalMs = 0;
|
|
1171
|
+
let uptimeMs = 0;
|
|
1172
|
+
const addSegment = (startMs, endMs, value) => {
|
|
1173
|
+
const start = clampTime(startMs);
|
|
1174
|
+
const end = clampTime(endMs);
|
|
1175
|
+
if (end <= start) return;
|
|
1176
|
+
const segmentMs = Math.max(0, end - start);
|
|
1177
|
+
if (segmentMs <= 0) return;
|
|
1178
|
+
const safeValue = Math.max(0, Number(value || 0));
|
|
1179
|
+
if (safeValue <= 0) return;
|
|
1180
|
+
uptimeMs += segmentMs;
|
|
1181
|
+
if (stacking) {
|
|
1182
|
+
totalMs += segmentMs * safeValue;
|
|
1183
|
+
} else {
|
|
1184
|
+
totalMs += segmentMs * resolveNonStackingFactor(safeValue);
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
let prevTime = clampTime(normalized[0][0]);
|
|
1188
|
+
let prevValue = Math.max(0, Number(normalized[0][1] || 0));
|
|
1189
|
+
for (let idx = 1; idx < normalized.length; idx += 1) {
|
|
1190
|
+
const [timeMs, rawValue] = normalized[idx];
|
|
1191
|
+
const currentTime = clampTime(timeMs);
|
|
1192
|
+
addSegment(prevTime, currentTime, prevValue);
|
|
1193
|
+
prevTime = currentTime;
|
|
1194
|
+
prevValue = Math.max(0, Number(rawValue || 0));
|
|
1195
|
+
}
|
|
1196
|
+
addSegment(prevTime, resolvedDurationMs, prevValue);
|
|
1197
|
+
return { totalMs, uptimeMs };
|
|
1198
|
+
};
|
|
1199
|
+
var ensureSpecialBuffEntry = (map, buffId, key, account, profession) => {
|
|
1200
|
+
if (!map.has(buffId)) {
|
|
1201
|
+
map.set(buffId, /* @__PURE__ */ new Map());
|
|
1202
|
+
}
|
|
1203
|
+
const bucket = map.get(buffId);
|
|
1204
|
+
let agg = bucket.get(key);
|
|
1205
|
+
if (!agg) {
|
|
1206
|
+
agg = {
|
|
1207
|
+
key,
|
|
1208
|
+
account,
|
|
1209
|
+
profession: profession || "Unknown",
|
|
1210
|
+
professions: /* @__PURE__ */ new Set(),
|
|
1211
|
+
professionTimeMs: {},
|
|
1212
|
+
totalMs: 0,
|
|
1213
|
+
uptimeMs: 0,
|
|
1214
|
+
durationMs: 0
|
|
1215
|
+
};
|
|
1216
|
+
bucket.set(key, agg);
|
|
1217
|
+
}
|
|
1218
|
+
return agg;
|
|
1219
|
+
};
|
|
1220
|
+
var createPlayerAggregationAccumulators = () => ({
|
|
1221
|
+
playerStats: /* @__PURE__ */ new Map(),
|
|
1222
|
+
skillDamageMap: {},
|
|
1223
|
+
incomingSkillDamageMap: {},
|
|
1224
|
+
playerSkillBreakdownMap: /* @__PURE__ */ new Map(),
|
|
1225
|
+
healingBreakdownMap: /* @__PURE__ */ new Map(),
|
|
1226
|
+
outgoingCondiTotals: {},
|
|
1227
|
+
incomingCondiTotals: {},
|
|
1228
|
+
enemyProfessionCounts: {},
|
|
1229
|
+
specialBuffMeta: /* @__PURE__ */ new Map(),
|
|
1230
|
+
specialBuffAgg: /* @__PURE__ */ new Map(),
|
|
1231
|
+
specialBuffOutputAgg: /* @__PURE__ */ new Map(),
|
|
1232
|
+
damageMitigationPlayersMap: /* @__PURE__ */ new Map(),
|
|
1233
|
+
damageMitigationMinionsMap: /* @__PURE__ */ new Map(),
|
|
1234
|
+
globalEnemySkillStats: /* @__PURE__ */ new Map(),
|
|
1235
|
+
mitigationCumulativeCounts: /* @__PURE__ */ new Map(),
|
|
1236
|
+
mitigationMinionCumulativeCounts: /* @__PURE__ */ new Map(),
|
|
1237
|
+
wins: 0,
|
|
1238
|
+
losses: 0,
|
|
1239
|
+
totalSquadSizeAccum: 0,
|
|
1240
|
+
totalEnemiesAccum: 0,
|
|
1241
|
+
totalSquadDeaths: 0,
|
|
1242
|
+
totalSquadKills: 0,
|
|
1243
|
+
totalEnemyDeaths: 0,
|
|
1244
|
+
totalEnemyKills: 0,
|
|
1245
|
+
totalSquadDowns: 0,
|
|
1246
|
+
totalEnemyDowns: 0
|
|
1247
|
+
});
|
|
1248
|
+
var precomputeGlobalEnemySkillStats = (log, acc) => {
|
|
1249
|
+
const details = log.details;
|
|
1250
|
+
if (!details) return;
|
|
1251
|
+
const targets = details.targets || [];
|
|
1252
|
+
targets.forEach((target) => {
|
|
1253
|
+
const dist = target?.totalDamageDist || [];
|
|
1254
|
+
const list = Array.isArray(dist) ? dist[0] : null;
|
|
1255
|
+
list?.forEach((entry) => {
|
|
1256
|
+
if (!entry?.id) return;
|
|
1257
|
+
const skillId = Number(entry.id);
|
|
1258
|
+
const globalBucket = acc.globalEnemySkillStats.get(skillId) || { totalDamage: 0, connectedHits: 0, minTotal: 0, minCount: 0 };
|
|
1259
|
+
globalBucket.totalDamage += Number(entry.totalDamage || 0);
|
|
1260
|
+
globalBucket.connectedHits += Number(entry.connectedHits || 0);
|
|
1261
|
+
const minValueGlobal = Number.isFinite(Number(entry.min)) ? Number(entry.min) : 0;
|
|
1262
|
+
globalBucket.minTotal += minValueGlobal;
|
|
1263
|
+
globalBucket.minCount += 1;
|
|
1264
|
+
acc.globalEnemySkillStats.set(skillId, globalBucket);
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
};
|
|
1268
|
+
var ingestLogPlayerData = (log, acc, options) => {
|
|
1269
|
+
const { method, skillDamageSource, splitPlayersByClass } = options;
|
|
1270
|
+
const details = log.details;
|
|
1271
|
+
if (!details) return;
|
|
1272
|
+
const players = details.players;
|
|
1273
|
+
const squadPlayers = players.filter((player) => !player?.notInSquad);
|
|
1274
|
+
const allPlayers = Array.isArray(players) ? players : [];
|
|
1275
|
+
const specialBuffSourceIdentityByName = /* @__PURE__ */ new Map();
|
|
1276
|
+
const specialBuffSourceIdentityByInstanceId = /* @__PURE__ */ new Map();
|
|
1277
|
+
const specialBuffSourceActiveMsByKey = /* @__PURE__ */ new Map();
|
|
1278
|
+
const specialBuffOutputDurationSeen = /* @__PURE__ */ new Set();
|
|
1279
|
+
const registerSpecialSourceIdentity = (value, identity) => {
|
|
1280
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
1281
|
+
if (!normalized) return;
|
|
1282
|
+
if (!specialBuffSourceIdentityByName.has(normalized)) {
|
|
1283
|
+
specialBuffSourceIdentityByName.set(normalized, identity);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
allPlayers.forEach((player) => {
|
|
1287
|
+
const identity = getPlayerIdentity(player, splitPlayersByClass);
|
|
1288
|
+
const sourceActiveMs = Array.isArray(player?.activeTimes) && typeof player.activeTimes[0] === "number" ? Number(player.activeTimes[0] || 0) : Number(details?.durationMS || 0);
|
|
1289
|
+
specialBuffSourceActiveMsByKey.set(identity.key, Math.max(0, sourceActiveMs));
|
|
1290
|
+
const instanceId = Number(player?.instanceID);
|
|
1291
|
+
if (Number.isFinite(instanceId) && instanceId > 0 && !specialBuffSourceIdentityByInstanceId.has(instanceId)) {
|
|
1292
|
+
specialBuffSourceIdentityByInstanceId.set(instanceId, identity);
|
|
1293
|
+
}
|
|
1294
|
+
[player?.name, player?.display_name, player?.character_name, player?.account].forEach((candidate) => {
|
|
1295
|
+
registerSpecialSourceIdentity(candidate, identity);
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
const targets = details.targets || [];
|
|
1299
|
+
const replayMeta = details.combatReplayMetaData || {};
|
|
1300
|
+
const inchesToPixel = replayMeta?.inchToPixel > 0 ? replayMeta.inchToPixel : 1;
|
|
1301
|
+
const pollingRate = replayMeta?.pollingRate > 0 ? replayMeta.pollingRate : 1;
|
|
1302
|
+
const RUN_BACK_RANGE = 5e3;
|
|
1303
|
+
const toPairs = (value) => {
|
|
1304
|
+
if (!Array.isArray(value)) return [];
|
|
1305
|
+
return value.map((entry) => Array.isArray(entry) ? [Number(entry[0]), Number(entry[1])] : null).filter((entry) => !!entry && Number.isFinite(entry[0]));
|
|
1306
|
+
};
|
|
1307
|
+
let commanderTagPositions = [];
|
|
1308
|
+
let deadTagMark = details.durationMS || 0;
|
|
1309
|
+
let deadTag = false;
|
|
1310
|
+
const commanderPlayer = players.find((player) => player?.hasCommanderTag && !player?.notInSquad);
|
|
1311
|
+
if (commanderPlayer?.combatReplayData?.positions) {
|
|
1312
|
+
commanderTagPositions = commanderPlayer.combatReplayData.positions;
|
|
1313
|
+
const commanderDeaths = toPairs(commanderPlayer.combatReplayData.dead);
|
|
1314
|
+
commanderDeaths.forEach(([deathTime]) => {
|
|
1315
|
+
if (deathTime > 0) {
|
|
1316
|
+
deadTag = true;
|
|
1317
|
+
deadTagMark = Math.min(deadTagMark, deathTime);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const getDistanceToTag = (p) => {
|
|
1322
|
+
const stats = p.statsAll?.[0];
|
|
1323
|
+
if (stats?.distToCom !== void 0 && stats?.distToCom !== "Infinity") return Math.round(Number(stats.distToCom));
|
|
1324
|
+
if (stats?.stackDist !== void 0) return Math.round(Number(stats.stackDist)) || 0;
|
|
1325
|
+
if (p.hasCommanderTag) return 0;
|
|
1326
|
+
const combatData = p.combatReplayData;
|
|
1327
|
+
if (!combatData?.positions || !commanderTagPositions.length) return 0;
|
|
1328
|
+
const playerPositions = combatData.positions;
|
|
1329
|
+
const playerDeaths = toPairs(combatData.dead);
|
|
1330
|
+
const playerDowns = toPairs(combatData.down);
|
|
1331
|
+
const playerOffset = Math.floor((combatData.start || 0) / pollingRate);
|
|
1332
|
+
let playerDistToTag = 0;
|
|
1333
|
+
if (playerDeaths.length && playerDowns.length) {
|
|
1334
|
+
for (const [deathKey] of playerDeaths) {
|
|
1335
|
+
if (deathKey < 0) continue;
|
|
1336
|
+
const positionMark = Math.max(0, Math.floor(deathKey / pollingRate)) - playerOffset;
|
|
1337
|
+
for (const [downKey, downValue] of playerDowns) {
|
|
1338
|
+
if (deathKey !== downValue) continue;
|
|
1339
|
+
const playerDeadPoll = deadTag && downKey > deadTagMark ? Math.max(1, Math.floor(deadTagMark / pollingRate)) : positionMark;
|
|
1340
|
+
const limit = Math.max(0, Math.min(playerDeadPoll, playerPositions.length, commanderTagPositions.length));
|
|
1341
|
+
if (limit <= 0) continue;
|
|
1342
|
+
let sum = 0;
|
|
1343
|
+
for (let i = 0; i < limit; i++) {
|
|
1344
|
+
const [px, py] = playerPositions[i];
|
|
1345
|
+
const [tx, ty] = commanderTagPositions[i];
|
|
1346
|
+
sum += Math.hypot(px - tx, py - ty);
|
|
1347
|
+
}
|
|
1348
|
+
playerDistToTag = Math.round(sum / limit / inchesToPixel);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return playerDistToTag;
|
|
1353
|
+
};
|
|
1354
|
+
acc.totalSquadSizeAccum += squadPlayers.length;
|
|
1355
|
+
acc.totalEnemiesAccum += targets.filter((t) => !t.isFake).length;
|
|
1356
|
+
applySquadStabilityGeneration(players, { durationMS: details.durationMS, buffMap: details.buffMap });
|
|
1357
|
+
const { squadDownsDeaths, enemyDownsDeaths, squadDeaths, enemyDeaths } = getFightDownsDeaths(details);
|
|
1358
|
+
acc.totalSquadDeaths += squadDeaths;
|
|
1359
|
+
acc.totalSquadKills += enemyDeaths;
|
|
1360
|
+
acc.totalEnemyKills += squadDeaths;
|
|
1361
|
+
acc.totalEnemyDeaths += enemyDeaths;
|
|
1362
|
+
acc.totalSquadDowns += Math.max(0, squadDownsDeaths - squadDeaths);
|
|
1363
|
+
acc.totalEnemyDowns += Math.max(0, enemyDownsDeaths - enemyDeaths);
|
|
1364
|
+
const isWin = getFightOutcome(details);
|
|
1365
|
+
if (isWin) acc.wins++;
|
|
1366
|
+
else acc.losses++;
|
|
1367
|
+
const battleStandardSkillId = details.skillMap ? Number(Object.keys(details.skillMap).find((key) => details.skillMap?.[key]?.name === "Battle Standard")?.replace(/^s/, "")) : null;
|
|
1368
|
+
const healAddonCharacters = /* @__PURE__ */ new Set();
|
|
1369
|
+
const usedExtensions = Array.isArray(details.usedExtensions) ? details.usedExtensions : [];
|
|
1370
|
+
usedExtensions.forEach((ext) => {
|
|
1371
|
+
if (ext?.name !== "Healing Stats") return;
|
|
1372
|
+
const running = Array.isArray(ext.runningExtension) ? ext.runningExtension : [];
|
|
1373
|
+
running.forEach((charName) => {
|
|
1374
|
+
if (typeof charName === "string" && charName.length > 0) healAddonCharacters.add(charName);
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
players.forEach((p, playerIndex) => {
|
|
1378
|
+
if (p.notInSquad) return;
|
|
1379
|
+
const identity = getPlayerIdentity(p, splitPlayersByClass);
|
|
1380
|
+
const key = identity.key;
|
|
1381
|
+
const name = p.name || "Unknown";
|
|
1382
|
+
if (!acc.playerStats.has(key)) {
|
|
1383
|
+
acc.playerStats.set(key, {
|
|
1384
|
+
name,
|
|
1385
|
+
account: identity.accountLabel,
|
|
1386
|
+
characterNames: /* @__PURE__ */ new Set(),
|
|
1387
|
+
downContrib: 0,
|
|
1388
|
+
cleanses: 0,
|
|
1389
|
+
strips: 0,
|
|
1390
|
+
stab: 0,
|
|
1391
|
+
healing: 0,
|
|
1392
|
+
barrier: 0,
|
|
1393
|
+
cc: 0,
|
|
1394
|
+
interrupts: 0,
|
|
1395
|
+
logsJoined: 0,
|
|
1396
|
+
totalDist: 0,
|
|
1397
|
+
distCount: 0,
|
|
1398
|
+
stackedLogCount: 0,
|
|
1399
|
+
dodges: 0,
|
|
1400
|
+
downs: 0,
|
|
1401
|
+
deaths: 0,
|
|
1402
|
+
kills: 0,
|
|
1403
|
+
enemyDowns: 0,
|
|
1404
|
+
damageTaken: 0,
|
|
1405
|
+
breakbar: 0,
|
|
1406
|
+
blocks: 0,
|
|
1407
|
+
evades: 0,
|
|
1408
|
+
misses: 0,
|
|
1409
|
+
totalFightMs: 0,
|
|
1410
|
+
offenseTotals: {},
|
|
1411
|
+
offenseRateWeights: {},
|
|
1412
|
+
defenseActiveMs: 0,
|
|
1413
|
+
defenseTotals: {},
|
|
1414
|
+
defenseMinionDamageTaken: {},
|
|
1415
|
+
supportActiveMs: 0,
|
|
1416
|
+
supportTotals: {},
|
|
1417
|
+
healingActiveMs: 0,
|
|
1418
|
+
healingTotals: {},
|
|
1419
|
+
hasHealAddon: false,
|
|
1420
|
+
profession: identity.profession,
|
|
1421
|
+
professions: /* @__PURE__ */ new Set(),
|
|
1422
|
+
professionTimeMs: {},
|
|
1423
|
+
squadActiveMs: 0,
|
|
1424
|
+
firstSeenFightTs: 0,
|
|
1425
|
+
lastSeenFightTs: 0,
|
|
1426
|
+
lastSeenFightDurationMs: 0,
|
|
1427
|
+
isCommander: false,
|
|
1428
|
+
damage: 0,
|
|
1429
|
+
dps: 0,
|
|
1430
|
+
revives: 0,
|
|
1431
|
+
outgoingConditions: {},
|
|
1432
|
+
incomingConditions: {},
|
|
1433
|
+
damageModTotals: {},
|
|
1434
|
+
incomingDamageModTotals: {},
|
|
1435
|
+
roleClassification: { role: "damage", supportScore: 0, confidenceScore: 0, threshold: 0, factors: [] }
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
const s = acc.playerStats.get(key);
|
|
1439
|
+
if (p.hasCommanderTag) s.isCommander = true;
|
|
1440
|
+
if (name && name !== "Unknown") s.characterNames.add(String(name));
|
|
1441
|
+
if (p.profession && p.profession !== "Unknown") {
|
|
1442
|
+
s.profession = p.profession;
|
|
1443
|
+
s.professions.add(p.profession);
|
|
1444
|
+
const activeMs2 = Array.isArray(p.activeTimes) && typeof p.activeTimes[0] === "number" ? p.activeTimes[0] : details.durationMS || 0;
|
|
1445
|
+
s.professionTimeMs[p.profession] = (s.professionTimeMs[p.profession] || 0) + activeMs2;
|
|
1446
|
+
}
|
|
1447
|
+
s.logsJoined++;
|
|
1448
|
+
s.downContrib += computeDownContribution(p);
|
|
1449
|
+
s.cleanses += getPlayerCleanses(p);
|
|
1450
|
+
s.strips += getPlayerStrips(p, method);
|
|
1451
|
+
s.healing += computeSquadHealing(p);
|
|
1452
|
+
s.barrier += computeSquadBarrier(p);
|
|
1453
|
+
s.cc += computeOutgoingCrowdControl(p, method);
|
|
1454
|
+
s.interrupts += getPlayerOutgoingInterrupts(p);
|
|
1455
|
+
s.stab += p.stabGeneration || 0;
|
|
1456
|
+
const dist = getDistanceToTag(p);
|
|
1457
|
+
if (dist <= RUN_BACK_RANGE) {
|
|
1458
|
+
s.totalDist += dist;
|
|
1459
|
+
s.distCount++;
|
|
1460
|
+
}
|
|
1461
|
+
if (dist <= 600) {
|
|
1462
|
+
s.stackedLogCount++;
|
|
1463
|
+
}
|
|
1464
|
+
if (p.defenses?.[0]) {
|
|
1465
|
+
s.dodges += p.defenses[0].dodgeCount || 0;
|
|
1466
|
+
s.downs += p.defenses[0].downCount || 0;
|
|
1467
|
+
s.deaths += p.defenses[0].deadCount || 0;
|
|
1468
|
+
}
|
|
1469
|
+
s.damageTaken += getPlayerDamageTaken(p);
|
|
1470
|
+
s.breakbar += getPlayerBreakbarDamage(p);
|
|
1471
|
+
s.blocks += getPlayerBlocked(p);
|
|
1472
|
+
s.evades += getPlayerEvaded(p);
|
|
1473
|
+
s.misses += getPlayerMissed(p);
|
|
1474
|
+
s.kills += getTargetStatTotal(p, "killed");
|
|
1475
|
+
s.enemyDowns += getTargetStatTotal(p, "downed");
|
|
1476
|
+
if (details.durationMS) s.totalFightMs += details.durationMS;
|
|
1477
|
+
const activeMs = Array.isArray(p.activeTimes) && typeof p.activeTimes[0] === "number" ? p.activeTimes[0] : details.durationMS || 0;
|
|
1478
|
+
s.squadActiveMs += activeMs;
|
|
1479
|
+
const fightTs = resolveFightTimestamp(details, log);
|
|
1480
|
+
const fightDurationMs = Number(details?.durationMS || 0);
|
|
1481
|
+
if (fightTs > 0) {
|
|
1482
|
+
if (s.firstSeenFightTs <= 0 || fightTs < s.firstSeenFightTs) {
|
|
1483
|
+
s.firstSeenFightTs = fightTs;
|
|
1484
|
+
}
|
|
1485
|
+
if (s.lastSeenFightTs <= 0 || fightTs > s.lastSeenFightTs) {
|
|
1486
|
+
s.lastSeenFightTs = fightTs;
|
|
1487
|
+
s.lastSeenFightDurationMs = fightDurationMs;
|
|
1488
|
+
} else if (fightTs === s.lastSeenFightTs && fightDurationMs > s.lastSeenFightDurationMs) {
|
|
1489
|
+
s.lastSeenFightDurationMs = fightDurationMs;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
s.defenseActiveMs += activeMs;
|
|
1493
|
+
s.supportActiveMs += activeMs;
|
|
1494
|
+
s.healingActiveMs += activeMs;
|
|
1495
|
+
if (Array.isArray(p.buffUptimes) && p.buffUptimes.length > 0) {
|
|
1496
|
+
const nonDamagingBuffs = p.buffUptimes.filter((buff) => {
|
|
1497
|
+
if (typeof buff?.id !== "number") return false;
|
|
1498
|
+
const buffId = `b${buff.id}`;
|
|
1499
|
+
const meta = details.buffMap?.[buffId] || details.buffMap?.[String(buff.id)];
|
|
1500
|
+
if (!meta || isBoon(meta)) return false;
|
|
1501
|
+
return true;
|
|
1502
|
+
});
|
|
1503
|
+
nonDamagingBuffs.forEach((buff) => {
|
|
1504
|
+
if (typeof buff?.id !== "number") return;
|
|
1505
|
+
const buffId = `b${buff.id}`;
|
|
1506
|
+
const meta = details.buffMap?.[buffId] || details.buffMap?.[String(buff.id)];
|
|
1507
|
+
const uptime = Number(buff.buffData?.[0]?.uptime ?? 0);
|
|
1508
|
+
const presence = Number(buff.buffData?.[0]?.presence ?? 0);
|
|
1509
|
+
if ((!Number.isFinite(uptime) || uptime <= 0) && (!Number.isFinite(presence) || presence <= 0)) return;
|
|
1510
|
+
const stacking = meta?.stacking ?? false;
|
|
1511
|
+
const uptimeFactor = stacking ? Number.isFinite(uptime) ? uptime : 0 : Number.isFinite(uptime) ? uptime / 100 : 0;
|
|
1512
|
+
const totalMs = uptimeFactor * activeMs;
|
|
1513
|
+
const uptimePercentRaw = stacking ? presence : uptime;
|
|
1514
|
+
const uptimePercentFactor = Math.min(Math.max(Number.isFinite(uptimePercentRaw) ? uptimePercentRaw : 0, 0), 100) / 100;
|
|
1515
|
+
const uptimeMs = uptimePercentFactor * activeMs;
|
|
1516
|
+
const hasTotalMs = Number.isFinite(totalMs) && totalMs > 0;
|
|
1517
|
+
const hasUptimeMs = Number.isFinite(uptimeMs) && uptimeMs > 0;
|
|
1518
|
+
if (!hasTotalMs && !hasUptimeMs) return;
|
|
1519
|
+
const conditionName = normalizeConditionLabel(meta?.name);
|
|
1520
|
+
if (conditionName && NON_DAMAGING_CONDITIONS.has(conditionName) && (!meta?.classification || meta.classification === "Condition")) {
|
|
1521
|
+
const seconds = totalMs / 1e3;
|
|
1522
|
+
const conditionIcon = meta?.icon;
|
|
1523
|
+
const incomingSummary = acc.incomingCondiTotals[conditionName] || {
|
|
1524
|
+
name: conditionName,
|
|
1525
|
+
icon: conditionIcon,
|
|
1526
|
+
applications: 0,
|
|
1527
|
+
damage: 0
|
|
1528
|
+
};
|
|
1529
|
+
incomingSummary.applicationsFromUptime = (incomingSummary.applicationsFromUptime || 0) + seconds;
|
|
1530
|
+
if (!incomingSummary.icon && conditionIcon) incomingSummary.icon = conditionIcon;
|
|
1531
|
+
acc.incomingCondiTotals[conditionName] = incomingSummary;
|
|
1532
|
+
const incomingEntry = s.incomingConditions[conditionName] || {
|
|
1533
|
+
applications: 0,
|
|
1534
|
+
damage: 0,
|
|
1535
|
+
skills: {},
|
|
1536
|
+
icon: conditionIcon
|
|
1537
|
+
};
|
|
1538
|
+
incomingEntry.applicationsFromUptime = (incomingEntry.applicationsFromUptime || 0) + seconds;
|
|
1539
|
+
if (!incomingEntry.icon && conditionIcon) incomingEntry.icon = conditionIcon;
|
|
1540
|
+
s.incomingConditions[conditionName] = incomingEntry;
|
|
1541
|
+
}
|
|
1542
|
+
if (!acc.specialBuffMeta.has(buffId)) {
|
|
1543
|
+
acc.specialBuffMeta.set(buffId, { name: meta?.name, stacking, icon: meta?.icon });
|
|
1544
|
+
}
|
|
1545
|
+
const specialBucketKey = identity.key;
|
|
1546
|
+
const agg = ensureSpecialBuffEntry(
|
|
1547
|
+
acc.specialBuffAgg,
|
|
1548
|
+
buffId,
|
|
1549
|
+
specialBucketKey,
|
|
1550
|
+
identity.accountLabel,
|
|
1551
|
+
s.profession || p.profession || "Unknown"
|
|
1552
|
+
);
|
|
1553
|
+
const prof = p.profession || s.profession;
|
|
1554
|
+
if (prof && prof !== "Unknown") {
|
|
1555
|
+
agg.professions.add(prof);
|
|
1556
|
+
agg.professionTimeMs[prof] = (agg.professionTimeMs[prof] || 0) + activeMs;
|
|
1557
|
+
agg.profession = prof;
|
|
1558
|
+
}
|
|
1559
|
+
agg.totalMs += hasTotalMs ? totalMs : 0;
|
|
1560
|
+
agg.uptimeMs += hasUptimeMs ? uptimeMs : 0;
|
|
1561
|
+
agg.durationMs += activeMs;
|
|
1562
|
+
const statesPerSource = buff?.statesPerSource && typeof buff.statesPerSource === "object" ? buff.statesPerSource : null;
|
|
1563
|
+
if (!statesPerSource || Object.keys(statesPerSource).length === 0) {
|
|
1564
|
+
} else {
|
|
1565
|
+
Object.entries(statesPerSource).forEach(([sourceName, states]) => {
|
|
1566
|
+
const sourceNameText = String(sourceName || "").trim();
|
|
1567
|
+
const sourceNameLower = sourceNameText.toLowerCase();
|
|
1568
|
+
const instanceMatch = sourceNameText.match(/\bpl-(\d+)\b/i);
|
|
1569
|
+
const sourceIdentity = specialBuffSourceIdentityByName.get(sourceNameLower) || (instanceMatch ? specialBuffSourceIdentityByInstanceId.get(Number(instanceMatch[1])) : void 0) || {
|
|
1570
|
+
key: `source:${sourceNameLower || "unknown"}`,
|
|
1571
|
+
accountLabel: sourceNameText || "Unknown Source",
|
|
1572
|
+
profession: resolveProfessionLabel(sourceNameText.replace(/\s+pl-\d+\s*$/i, "").trim() || "Unknown")
|
|
1573
|
+
};
|
|
1574
|
+
const sourceDurationMs = Number(specialBuffSourceActiveMsByKey.get(sourceIdentity.key) || details?.durationMS || 0);
|
|
1575
|
+
if (!Number.isFinite(sourceDurationMs) || sourceDurationMs <= 0) return;
|
|
1576
|
+
const sourceContribution = integrateSourceStates(states, sourceDurationMs, stacking);
|
|
1577
|
+
const hasSourceTotal = Number.isFinite(sourceContribution.totalMs) && sourceContribution.totalMs > 0;
|
|
1578
|
+
const hasSourceUptime = Number.isFinite(sourceContribution.uptimeMs) && sourceContribution.uptimeMs > 0;
|
|
1579
|
+
if (!hasSourceTotal && !hasSourceUptime) return;
|
|
1580
|
+
const sourceAgg = ensureSpecialBuffEntry(
|
|
1581
|
+
acc.specialBuffOutputAgg,
|
|
1582
|
+
buffId,
|
|
1583
|
+
sourceIdentity.key,
|
|
1584
|
+
sourceIdentity.accountLabel,
|
|
1585
|
+
sourceIdentity.profession
|
|
1586
|
+
);
|
|
1587
|
+
const sourceProf = sourceIdentity.profession || "Unknown";
|
|
1588
|
+
if (sourceProf && sourceProf !== "Unknown") {
|
|
1589
|
+
sourceAgg.professions.add(sourceProf);
|
|
1590
|
+
sourceAgg.profession = sourceProf;
|
|
1591
|
+
}
|
|
1592
|
+
sourceAgg.totalMs += hasSourceTotal ? sourceContribution.totalMs : 0;
|
|
1593
|
+
sourceAgg.uptimeMs += hasSourceUptime ? sourceContribution.uptimeMs : 0;
|
|
1594
|
+
const durationSeenKey = `${buffId}::${sourceIdentity.key}`;
|
|
1595
|
+
if (!specialBuffOutputDurationSeen.has(durationSeenKey)) {
|
|
1596
|
+
specialBuffOutputDurationSeen.add(durationSeenKey);
|
|
1597
|
+
if (sourceProf && sourceProf !== "Unknown") {
|
|
1598
|
+
sourceAgg.professionTimeMs[sourceProf] = (sourceAgg.professionTimeMs[sourceProf] || 0) + sourceDurationMs;
|
|
1599
|
+
}
|
|
1600
|
+
sourceAgg.durationMs += sourceDurationMs;
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
if (p.defenses?.[0]) {
|
|
1607
|
+
const normalizeMinionName = (rawName) => {
|
|
1608
|
+
let minionName = String(rawName || "Unknown").replace(/^Juvenile\s+/i, "") || "Unknown";
|
|
1609
|
+
if (minionName.toUpperCase().includes("UNKNOWN")) minionName = "Unknown";
|
|
1610
|
+
return minionName;
|
|
1611
|
+
};
|
|
1612
|
+
const getMinionDamageTaken = () => {
|
|
1613
|
+
const minions = Array.isArray(p?.minions) ? p.minions : [];
|
|
1614
|
+
if (minions.length === 0) return { total: 0, byMinion: {} };
|
|
1615
|
+
let total = 0;
|
|
1616
|
+
const byMinion = {};
|
|
1617
|
+
minions.forEach((minion) => {
|
|
1618
|
+
const dist2 = Array.isArray(minion?.totalDamageTakenDist) ? minion.totalDamageTakenDist : [];
|
|
1619
|
+
const entries = Array.isArray(dist2[0]) ? dist2[0] : [];
|
|
1620
|
+
const minionName = normalizeMinionName(minion?.name);
|
|
1621
|
+
entries.forEach((entry) => {
|
|
1622
|
+
const damage = Number(entry?.totalDamage ?? entry?.damageTaken ?? 0);
|
|
1623
|
+
if (Number.isFinite(damage)) {
|
|
1624
|
+
total += damage;
|
|
1625
|
+
byMinion[minionName] = (byMinion[minionName] || 0) + damage;
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
return { total, byMinion };
|
|
1630
|
+
};
|
|
1631
|
+
const minionDamage = getMinionDamageTaken();
|
|
1632
|
+
if (minionDamage.byMinion && typeof minionDamage.byMinion === "object") {
|
|
1633
|
+
Object.entries(minionDamage.byMinion).forEach(([minionName, damage]) => {
|
|
1634
|
+
const numeric = Number(damage || 0);
|
|
1635
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return;
|
|
1636
|
+
s.defenseMinionDamageTaken[minionName] = (s.defenseMinionDamageTaken[minionName] || 0) + numeric;
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
DEFENSE_METRICS.forEach((m) => {
|
|
1640
|
+
const val = m.id === "minionDamageTaken" ? minionDamage.total : Number(p.defenses[0][m.field] ?? 0);
|
|
1641
|
+
if (Number.isFinite(val)) s.defenseTotals[m.id] = (s.defenseTotals[m.id] || 0) + val;
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (p.support?.[0]) {
|
|
1645
|
+
SUPPORT_METRICS.forEach((m) => {
|
|
1646
|
+
if (m.id === "boonStrips") return;
|
|
1647
|
+
let val = Number(p.support[0][m.field] ?? 0);
|
|
1648
|
+
if (Number.isFinite(val)) {
|
|
1649
|
+
if (supportTimeSanityFields.has(m.field) && val > 999999) val = 0;
|
|
1650
|
+
s.supportTotals[m.id] = (s.supportTotals[m.id] || 0) + val;
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
s.supportTotals.boonStrips = (s.supportTotals.boonStrips || 0) + getPlayerStrips(p, method);
|
|
1654
|
+
}
|
|
1655
|
+
const addHealing = (key2, val) => {
|
|
1656
|
+
if (Number.isFinite(val)) s.healingTotals[key2] = (s.healingTotals[key2] || 0) + val;
|
|
1657
|
+
};
|
|
1658
|
+
const sumPhaseValue = (phases, field) => {
|
|
1659
|
+
if (!Array.isArray(phases)) return 0;
|
|
1660
|
+
return phases.reduce((sum, phase) => sum + Number(phase?.[field] ?? 0), 0);
|
|
1661
|
+
};
|
|
1662
|
+
const addHealingByCategory = (fieldBase, allyIdx, value) => {
|
|
1663
|
+
if (!Number.isFinite(value) || value <= 0) return;
|
|
1664
|
+
const ally = players[allyIdx];
|
|
1665
|
+
const isSelf = allyIdx === playerIndex;
|
|
1666
|
+
const isOffSquad = ally?.notInSquad;
|
|
1667
|
+
const isSquad = !isOffSquad;
|
|
1668
|
+
const isGroup = isSquad && ally?.group != null && ally.group === p.group;
|
|
1669
|
+
addHealing(fieldBase, value);
|
|
1670
|
+
if (isSquad) addHealing(`squad${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
|
|
1671
|
+
if (isGroup) addHealing(`group${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
|
|
1672
|
+
if (isSelf) addHealing(`self${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
|
|
1673
|
+
if (isOffSquad) addHealing(`offSquad${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
|
|
1674
|
+
};
|
|
1675
|
+
if (Array.isArray(p.rotation)) {
|
|
1676
|
+
let resCasts = 0;
|
|
1677
|
+
p.rotation.forEach((rot) => {
|
|
1678
|
+
if (!rot?.id || !isResUtilitySkill(rot.id, details.skillMap)) return;
|
|
1679
|
+
const count = rot.skills?.length || 0;
|
|
1680
|
+
if (count > 0) {
|
|
1681
|
+
resCasts += count;
|
|
1682
|
+
addHealing(`resUtility_s${rot.id}`, count);
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
if (resCasts > 0) addHealing("resUtility", resCasts);
|
|
1686
|
+
}
|
|
1687
|
+
const outgoingHealingAllies = p.extHealingStats?.outgoingHealingAllies;
|
|
1688
|
+
if (Array.isArray(outgoingHealingAllies)) {
|
|
1689
|
+
outgoingHealingAllies.forEach((allyPhases, allyIdx) => {
|
|
1690
|
+
const healing = sumPhaseValue(allyPhases, "healing");
|
|
1691
|
+
const downedHealing = sumPhaseValue(allyPhases, "downedHealing");
|
|
1692
|
+
addHealingByCategory("healing", allyIdx, healing);
|
|
1693
|
+
addHealingByCategory("downedHealing", allyIdx, downedHealing);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
const outgoingBarrierAllies = p.extBarrierStats?.outgoingBarrierAllies;
|
|
1697
|
+
if (Array.isArray(outgoingBarrierAllies)) {
|
|
1698
|
+
outgoingBarrierAllies.forEach((allyPhases, allyIdx) => {
|
|
1699
|
+
const barrier = sumPhaseValue(allyPhases, "barrier");
|
|
1700
|
+
addHealingByCategory("barrier", allyIdx, barrier);
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
const statsAll = p.statsAll?.[0];
|
|
1704
|
+
const dpsAll = p.dpsAll?.[0];
|
|
1705
|
+
const support = p.support?.[0];
|
|
1706
|
+
OFFENSE_METRICS.forEach((m) => {
|
|
1707
|
+
if (m.id === "downContributionPercent" || m.id === "downContribution" || m.id === "boonStrips") return;
|
|
1708
|
+
if (!m.field) return;
|
|
1709
|
+
let val = 0;
|
|
1710
|
+
let denom = 0;
|
|
1711
|
+
if (m.source === "dpsAll" && dpsAll) val = Number(dpsAll[m.field] ?? 0);
|
|
1712
|
+
else if (m.source === "statsAll" && statsAll) {
|
|
1713
|
+
val = Number(statsAll[m.field] ?? 0);
|
|
1714
|
+
denom = Number(statsAll[m.denomField || m.weightField || "connectedDamageCount"] ?? 0);
|
|
1715
|
+
} else if (m.source === "support" && support) {
|
|
1716
|
+
val = Number(support[m.field] ?? 0);
|
|
1717
|
+
} else if (p.statsTargets) {
|
|
1718
|
+
p.statsTargets.forEach((t) => {
|
|
1719
|
+
if (!t?.[0]) return;
|
|
1720
|
+
val += Number(t[0][m.field] ?? 0);
|
|
1721
|
+
denom += Number(t[0][m.denomField || m.weightField || "connectedDamageCount"] ?? 0);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
if (Number.isFinite(val)) {
|
|
1725
|
+
s.offenseTotals[m.id] = (s.offenseTotals[m.id] || 0) + val;
|
|
1726
|
+
if (m.isRate && denom > 0) s.offenseRateWeights[m.id] = (s.offenseRateWeights[m.id] || 0) + denom;
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
s.offenseTotals.downContribution = (s.offenseTotals.downContribution || 0) + computeDownContribution(p);
|
|
1730
|
+
s.offenseTotals.boonStrips = (s.offenseTotals.boonStrips || 0) + getPlayerStrips(p, method);
|
|
1731
|
+
if (p.targetDamageDist && Number.isFinite(battleStandardSkillId)) {
|
|
1732
|
+
const connectedHits = p.targetDamageDist.flatMap((group) => group || []).flatMap((list) => list || []).reduce((sum, entry) => {
|
|
1733
|
+
if (!entry?.id) return sum;
|
|
1734
|
+
return entry.id === battleStandardSkillId ? sum + (entry?.connectedHits || 0) : sum;
|
|
1735
|
+
}, 0);
|
|
1736
|
+
s.offenseTotals.battleStandardHits = (s.offenseTotals.battleStandardHits || 0) + connectedHits;
|
|
1737
|
+
}
|
|
1738
|
+
s.revives += p.support?.[0]?.resurrects || 0;
|
|
1739
|
+
if (dpsAll) {
|
|
1740
|
+
s.damage += dpsAll.damage || 0;
|
|
1741
|
+
s.dps += dpsAll.dps || 0;
|
|
1742
|
+
}
|
|
1743
|
+
const resolveSkillMeta = (entry) => {
|
|
1744
|
+
let name2 = `Skill ${entry.id}`;
|
|
1745
|
+
const mapped = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
|
|
1746
|
+
let icon = mapped?.icon;
|
|
1747
|
+
if (mapped?.name) name2 = mapped.name;
|
|
1748
|
+
const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
|
|
1749
|
+
if (name2.startsWith("Skill ") && buffMeta?.name) {
|
|
1750
|
+
name2 = buffMeta.name;
|
|
1751
|
+
icon = buffMeta.icon || icon;
|
|
1752
|
+
}
|
|
1753
|
+
if (name2.startsWith("Skill ")) {
|
|
1754
|
+
const conditionName = resolveConditionNameFromEntry(name2, entry.id, details.buffMap);
|
|
1755
|
+
if (conditionName) {
|
|
1756
|
+
name2 = conditionName;
|
|
1757
|
+
icon = buffMeta?.icon || icon;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return { name: name2, icon };
|
|
1761
|
+
};
|
|
1762
|
+
const playerProfession = p.profession || "Unknown";
|
|
1763
|
+
const extractPhase0 = (dist2) => {
|
|
1764
|
+
if (!Array.isArray(dist2)) return [];
|
|
1765
|
+
const phase0 = dist2[0];
|
|
1766
|
+
return Array.isArray(phase0) ? phase0 : [];
|
|
1767
|
+
};
|
|
1768
|
+
const healingPlayerKey = identity.key;
|
|
1769
|
+
let healingBd = acc.healingBreakdownMap.get(healingPlayerKey);
|
|
1770
|
+
if (!healingBd) {
|
|
1771
|
+
healingBd = {
|
|
1772
|
+
key: healingPlayerKey,
|
|
1773
|
+
account: identity.accountLabel,
|
|
1774
|
+
displayName: identity.accountLabel,
|
|
1775
|
+
profession: playerProfession,
|
|
1776
|
+
professionList: [playerProfession],
|
|
1777
|
+
healingSkills: /* @__PURE__ */ new Map(),
|
|
1778
|
+
barrierSkills: /* @__PURE__ */ new Map(),
|
|
1779
|
+
hasHealAddon: false
|
|
1780
|
+
};
|
|
1781
|
+
acc.healingBreakdownMap.set(healingPlayerKey, healingBd);
|
|
1782
|
+
}
|
|
1783
|
+
if (playerProfession && !healingBd.professionList.includes(playerProfession)) {
|
|
1784
|
+
healingBd.professionList.push(playerProfession);
|
|
1785
|
+
}
|
|
1786
|
+
if (typeof p.name === "string" && healAddonCharacters.has(p.name)) {
|
|
1787
|
+
healingBd.hasHealAddon = true;
|
|
1788
|
+
const ps = acc.playerStats.get(healingPlayerKey);
|
|
1789
|
+
if (ps) ps.hasHealAddon = true;
|
|
1790
|
+
}
|
|
1791
|
+
const pushHealingSkillEntry = (entry, skillMap, totalField) => {
|
|
1792
|
+
if (!entry?.id) return;
|
|
1793
|
+
const amount = Number(entry[totalField] || 0);
|
|
1794
|
+
if (!Number.isFinite(amount) || amount <= 0) return;
|
|
1795
|
+
const { name: name2, icon } = resolveSkillMeta(entry);
|
|
1796
|
+
const skillId = `s${entry.id}`;
|
|
1797
|
+
let existing = skillMap.get(skillId);
|
|
1798
|
+
if (!existing) {
|
|
1799
|
+
existing = { id: skillId, name: name2, icon, total: 0, hits: 0, max: 0 };
|
|
1800
|
+
skillMap.set(skillId, existing);
|
|
1801
|
+
}
|
|
1802
|
+
if (existing.name.startsWith("Skill ") && !name2.startsWith("Skill ")) existing.name = name2;
|
|
1803
|
+
if (!existing.icon && icon) existing.icon = icon;
|
|
1804
|
+
existing.total += amount;
|
|
1805
|
+
existing.hits += Number(entry.hits || 0);
|
|
1806
|
+
existing.max = Math.max(existing.max, Number(entry.max || 0));
|
|
1807
|
+
};
|
|
1808
|
+
extractPhase0(p.extHealingStats?.totalHealingDist).forEach((entry) => {
|
|
1809
|
+
pushHealingSkillEntry(entry, healingBd.healingSkills, "totalHealing");
|
|
1810
|
+
});
|
|
1811
|
+
extractPhase0(p.extBarrierStats?.totalBarrierDist).forEach((entry) => {
|
|
1812
|
+
pushHealingSkillEntry(entry, healingBd.barrierSkills, "totalBarrier");
|
|
1813
|
+
});
|
|
1814
|
+
const pushSkillDamageEntry = (entry) => {
|
|
1815
|
+
if (!entry?.id) return;
|
|
1816
|
+
const { name: name2, icon } = resolveSkillMeta(entry);
|
|
1817
|
+
if (!acc.skillDamageMap[entry.id]) acc.skillDamageMap[entry.id] = { name: name2, icon, damage: 0, hits: 0, downContribution: 0 };
|
|
1818
|
+
if (acc.skillDamageMap[entry.id].name.startsWith("Skill ") && !name2.startsWith("Skill ")) acc.skillDamageMap[entry.id].name = name2;
|
|
1819
|
+
if (!acc.skillDamageMap[entry.id].icon && icon) acc.skillDamageMap[entry.id].icon = icon;
|
|
1820
|
+
acc.skillDamageMap[entry.id].damage += entry.totalDamage;
|
|
1821
|
+
acc.skillDamageMap[entry.id].hits += entry.connectedHits;
|
|
1822
|
+
acc.skillDamageMap[entry.id].downContribution += Number(entry.downContribution || 0);
|
|
1823
|
+
};
|
|
1824
|
+
const playerKey = identity.key;
|
|
1825
|
+
let playerBreakdown = acc.playerSkillBreakdownMap.get(playerKey);
|
|
1826
|
+
if (!playerBreakdown) {
|
|
1827
|
+
playerBreakdown = {
|
|
1828
|
+
key: playerKey,
|
|
1829
|
+
account: identity.accountLabel,
|
|
1830
|
+
displayName: identity.accountLabel,
|
|
1831
|
+
profession: playerProfession,
|
|
1832
|
+
professionList: [playerProfession],
|
|
1833
|
+
totalFightMs: 0,
|
|
1834
|
+
skills: /* @__PURE__ */ new Map()
|
|
1835
|
+
};
|
|
1836
|
+
acc.playerSkillBreakdownMap.set(playerKey, playerBreakdown);
|
|
1837
|
+
}
|
|
1838
|
+
if (playerProfession && !playerBreakdown.professionList.includes(playerProfession)) {
|
|
1839
|
+
playerBreakdown.professionList.push(playerProfession);
|
|
1840
|
+
}
|
|
1841
|
+
playerBreakdown.totalFightMs += details.durationMS || 0;
|
|
1842
|
+
const pushPlayerSkillEntry = (entry) => {
|
|
1843
|
+
if (!entry?.id) return;
|
|
1844
|
+
const { name: name2, icon } = resolveSkillMeta(entry);
|
|
1845
|
+
const skillId = `s${entry.id}`;
|
|
1846
|
+
let skillEntry = playerBreakdown.skills.get(skillId);
|
|
1847
|
+
if (!skillEntry) {
|
|
1848
|
+
skillEntry = { id: skillId, name: name2, icon, damage: 0, downContribution: 0, hits: 0, casts: 0, min: Infinity, max: 0 };
|
|
1849
|
+
playerBreakdown.skills.set(skillId, skillEntry);
|
|
1850
|
+
}
|
|
1851
|
+
if (skillEntry.name.startsWith("Skill ") && !name2.startsWith("Skill ")) skillEntry.name = name2;
|
|
1852
|
+
if (!skillEntry.icon && icon) skillEntry.icon = icon;
|
|
1853
|
+
skillEntry.damage += Number(entry.totalDamage || 0);
|
|
1854
|
+
skillEntry.downContribution += Number(entry.downContribution || 0);
|
|
1855
|
+
skillEntry.hits += Number(entry.hits || 0);
|
|
1856
|
+
const entryMin = Number(entry.min);
|
|
1857
|
+
if (Number.isFinite(entryMin) && entryMin > 0) {
|
|
1858
|
+
skillEntry.min = Math.min(skillEntry.min, entryMin);
|
|
1859
|
+
}
|
|
1860
|
+
skillEntry.max = Math.max(skillEntry.max, Number(entry.max || 0));
|
|
1861
|
+
};
|
|
1862
|
+
if (skillDamageSource === "total") {
|
|
1863
|
+
p.totalDamageDist?.forEach((list) => {
|
|
1864
|
+
list?.forEach((entry) => {
|
|
1865
|
+
pushSkillDamageEntry(entry);
|
|
1866
|
+
pushPlayerSkillEntry(entry);
|
|
1867
|
+
});
|
|
1868
|
+
});
|
|
1869
|
+
} else {
|
|
1870
|
+
const targetSkillTotals = /* @__PURE__ */ new Map();
|
|
1871
|
+
p.targetDamageDist?.forEach((targetGroup) => {
|
|
1872
|
+
targetGroup?.forEach((list) => {
|
|
1873
|
+
list?.forEach((entry) => {
|
|
1874
|
+
const skillId = Number(entry?.id);
|
|
1875
|
+
if (Number.isFinite(skillId)) {
|
|
1876
|
+
const existing = targetSkillTotals.get(skillId) || { damage: 0, hits: 0, downContribution: 0 };
|
|
1877
|
+
existing.damage += Number(entry?.totalDamage || 0);
|
|
1878
|
+
existing.hits += Number(entry?.connectedHits || 0);
|
|
1879
|
+
existing.downContribution += Number(entry?.downContribution || 0);
|
|
1880
|
+
targetSkillTotals.set(skillId, existing);
|
|
1881
|
+
}
|
|
1882
|
+
pushSkillDamageEntry(entry);
|
|
1883
|
+
pushPlayerSkillEntry(entry);
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
});
|
|
1887
|
+
const allowTotalSupplement = !details?.detailedWvW;
|
|
1888
|
+
if (allowTotalSupplement) {
|
|
1889
|
+
p.totalDamageDist?.forEach((list) => {
|
|
1890
|
+
list?.forEach((entry) => {
|
|
1891
|
+
const skillId = Number(entry?.id);
|
|
1892
|
+
if (!Number.isFinite(skillId)) return;
|
|
1893
|
+
const target = targetSkillTotals.get(skillId);
|
|
1894
|
+
if (!target) {
|
|
1895
|
+
pushSkillDamageEntry(entry);
|
|
1896
|
+
pushPlayerSkillEntry(entry);
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const totalDamage = Number(entry?.totalDamage || 0);
|
|
1900
|
+
const totalHits = Number(entry?.connectedHits || 0);
|
|
1901
|
+
const totalDownContribution = Number(entry?.downContribution || 0);
|
|
1902
|
+
const deltaDamage = totalDamage - Number(target.damage || 0);
|
|
1903
|
+
const deltaHits = totalHits - Number(target.hits || 0);
|
|
1904
|
+
const deltaDownContribution = totalDownContribution - Number(target.downContribution || 0);
|
|
1905
|
+
if (deltaDamage <= 0 && deltaHits <= 0 && deltaDownContribution <= 0) return;
|
|
1906
|
+
const reconciledEntry = {
|
|
1907
|
+
...entry,
|
|
1908
|
+
totalDamage: Math.max(0, deltaDamage),
|
|
1909
|
+
connectedHits: Math.max(0, deltaHits),
|
|
1910
|
+
downContribution: Math.max(0, deltaDownContribution),
|
|
1911
|
+
hits: 0,
|
|
1912
|
+
min: 0,
|
|
1913
|
+
max: 0
|
|
1914
|
+
};
|
|
1915
|
+
pushSkillDamageEntry(reconciledEntry);
|
|
1916
|
+
pushPlayerSkillEntry(reconciledEntry);
|
|
1917
|
+
});
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (Array.isArray(p.rotation)) {
|
|
1922
|
+
p.rotation.forEach((rot) => {
|
|
1923
|
+
if (!rot?.id) return;
|
|
1924
|
+
const count = rot.skills?.length || 0;
|
|
1925
|
+
if (count <= 0) return;
|
|
1926
|
+
const skillId = `s${rot.id}`;
|
|
1927
|
+
const skillEntry = playerBreakdown.skills.get(skillId);
|
|
1928
|
+
if (skillEntry) {
|
|
1929
|
+
skillEntry.casts += count;
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
if (p.totalDamageTaken) {
|
|
1934
|
+
p.totalDamageTaken.forEach((list) => {
|
|
1935
|
+
list?.forEach((entry) => {
|
|
1936
|
+
if (!entry?.id) return;
|
|
1937
|
+
let name2 = `Skill ${entry.id}`;
|
|
1938
|
+
const mapped = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
|
|
1939
|
+
let icon = mapped?.icon;
|
|
1940
|
+
if (mapped?.name) name2 = mapped.name;
|
|
1941
|
+
const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
|
|
1942
|
+
if (name2.startsWith("Skill ") && buffMeta?.name) {
|
|
1943
|
+
name2 = buffMeta.name;
|
|
1944
|
+
icon = buffMeta.icon || icon;
|
|
1945
|
+
}
|
|
1946
|
+
if (name2.startsWith("Skill ")) {
|
|
1947
|
+
const conditionName = resolveConditionNameFromEntry(name2, entry.id, details.buffMap);
|
|
1948
|
+
if (conditionName) {
|
|
1949
|
+
name2 = conditionName;
|
|
1950
|
+
icon = buffMeta?.icon || icon;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (!acc.incomingSkillDamageMap[entry.id]) acc.incomingSkillDamageMap[entry.id] = { name: name2, icon, damage: 0, hits: 0 };
|
|
1954
|
+
if (!acc.incomingSkillDamageMap[entry.id].name.startsWith("Skill ") || name2.startsWith("Skill ")) acc.incomingSkillDamageMap[entry.id].name = name2;
|
|
1955
|
+
if (!acc.incomingSkillDamageMap[entry.id].icon && icon) acc.incomingSkillDamageMap[entry.id].icon = icon;
|
|
1956
|
+
acc.incomingSkillDamageMap[entry.id].damage += entry.totalDamage;
|
|
1957
|
+
acc.incomingSkillDamageMap[entry.id].hits += entry.hits;
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
if (p.damageModifiers) {
|
|
1962
|
+
for (const entry of p.damageModifiers) {
|
|
1963
|
+
const phase0 = entry.damageModifiers?.[0];
|
|
1964
|
+
if (!phase0) continue;
|
|
1965
|
+
const key2 = `d${entry.id}`;
|
|
1966
|
+
const existing = s.damageModTotals[key2];
|
|
1967
|
+
if (existing) {
|
|
1968
|
+
existing.damageGain += phase0.damageGain;
|
|
1969
|
+
existing.hitCount += phase0.hitCount;
|
|
1970
|
+
existing.totalHitCount += phase0.totalHitCount;
|
|
1971
|
+
existing.totalDamage += phase0.totalDamage;
|
|
1972
|
+
} else {
|
|
1973
|
+
s.damageModTotals[key2] = {
|
|
1974
|
+
damageGain: phase0.damageGain,
|
|
1975
|
+
hitCount: phase0.hitCount,
|
|
1976
|
+
totalHitCount: phase0.totalHitCount,
|
|
1977
|
+
totalDamage: phase0.totalDamage
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (p.incomingDamageModifiers) {
|
|
1983
|
+
for (const entry of p.incomingDamageModifiers) {
|
|
1984
|
+
const phase0 = entry.damageModifiers?.[0];
|
|
1985
|
+
if (!phase0) continue;
|
|
1986
|
+
const key2 = `d${entry.id}`;
|
|
1987
|
+
const existing = s.incomingDamageModTotals[key2];
|
|
1988
|
+
if (existing) {
|
|
1989
|
+
existing.damageGain += phase0.damageGain;
|
|
1990
|
+
existing.hitCount += phase0.hitCount;
|
|
1991
|
+
existing.totalHitCount += phase0.totalHitCount;
|
|
1992
|
+
existing.totalDamage += phase0.totalDamage;
|
|
1993
|
+
} else {
|
|
1994
|
+
s.incomingDamageModTotals[key2] = {
|
|
1995
|
+
damageGain: phase0.damageGain,
|
|
1996
|
+
hitCount: phase0.hitCount,
|
|
1997
|
+
totalHitCount: phase0.totalHitCount,
|
|
1998
|
+
totalDamage: phase0.totalDamage
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
targets.forEach((t) => {
|
|
2005
|
+
if (t?.isFake) return;
|
|
2006
|
+
const name = resolveProfessionLabel(t?.profession || t?.name || t?.id);
|
|
2007
|
+
if (!name) return;
|
|
2008
|
+
acc.enemyProfessionCounts[name] = (acc.enemyProfessionCounts[name] || 0) + 1;
|
|
2009
|
+
});
|
|
2010
|
+
const conditionResult = computeOutgoingConditions({
|
|
2011
|
+
players,
|
|
2012
|
+
targets,
|
|
2013
|
+
skillMap: details.skillMap,
|
|
2014
|
+
buffMap: details.buffMap,
|
|
2015
|
+
getPlayerKey: (pl) => getPlayerIdentity(pl, splitPlayersByClass).key
|
|
2016
|
+
});
|
|
2017
|
+
const conditionIconMap = buildConditionIconMap(details.buffMap);
|
|
2018
|
+
Object.entries(conditionResult.playerConditions).forEach(([k, totals]) => {
|
|
2019
|
+
const ps = acc.playerStats.get(k);
|
|
2020
|
+
if (!ps) return;
|
|
2021
|
+
Object.entries(totals).forEach(([cName, v]) => {
|
|
2022
|
+
const ex = ps.outgoingConditions[cName] || { applications: 0, damage: 0, skills: {}, icon: v.icon };
|
|
2023
|
+
ex.applications += Number(v.applications || 0);
|
|
2024
|
+
ex.damage += Number(v.damage || 0);
|
|
2025
|
+
if (v.applicationsFromBuffs) ex.applicationsFromBuffs = (ex.applicationsFromBuffs || 0) + v.applicationsFromBuffs;
|
|
2026
|
+
if (v.applicationsFromBuffsActive) ex.applicationsFromBuffsActive = (ex.applicationsFromBuffsActive || 0) + v.applicationsFromBuffsActive;
|
|
2027
|
+
if (v.uptimeMs) ex.uptimeMs = (ex.uptimeMs || 0) + v.uptimeMs;
|
|
2028
|
+
Object.entries(v.skills || {}).forEach(([sn, sv]) => {
|
|
2029
|
+
const sk = ex.skills[sn] || { name: sv.name, hits: 0, damage: 0, icon: sv.icon };
|
|
2030
|
+
sk.hits += Number(sv.hits || 0);
|
|
2031
|
+
sk.damage += Number(sv.damage || 0);
|
|
2032
|
+
if (!sk.icon && sv.icon) sk.icon = sv.icon;
|
|
2033
|
+
ex.skills[sn] = sk;
|
|
2034
|
+
});
|
|
2035
|
+
if (!ex.icon && v.icon) ex.icon = v.icon;
|
|
2036
|
+
ps.outgoingConditions[cName] = ex;
|
|
2037
|
+
});
|
|
2038
|
+
});
|
|
2039
|
+
Object.entries(conditionResult.summary).forEach(([cName, v]) => {
|
|
2040
|
+
const ex = acc.outgoingCondiTotals[cName] || { name: v.name || cName, icon: v.icon, applications: 0, damage: 0 };
|
|
2041
|
+
ex.applications += Number(v.applications || 0);
|
|
2042
|
+
ex.damage += Number(v.damage || 0);
|
|
2043
|
+
if (v.applicationsFromBuffs) ex.applicationsFromBuffs = (ex.applicationsFromBuffs || 0) + v.applicationsFromBuffs;
|
|
2044
|
+
if (v.applicationsFromBuffsActive) ex.applicationsFromBuffsActive = (ex.applicationsFromBuffsActive || 0) + v.applicationsFromBuffsActive;
|
|
2045
|
+
if (v.uptimeMs) ex.uptimeMs = (ex.uptimeMs || 0) + v.uptimeMs;
|
|
2046
|
+
if (!ex.icon && v.icon) ex.icon = v.icon;
|
|
2047
|
+
acc.outgoingCondiTotals[cName] = ex;
|
|
2048
|
+
});
|
|
2049
|
+
squadPlayers.forEach((p) => {
|
|
2050
|
+
const key = getPlayerIdentity(p, splitPlayersByClass).key;
|
|
2051
|
+
const ps = acc.playerStats.get(key);
|
|
2052
|
+
if (!ps || !p.totalDamageTaken) return;
|
|
2053
|
+
p.totalDamageTaken.forEach((list) => list?.forEach((entry) => {
|
|
2054
|
+
if (!entry?.id) return;
|
|
2055
|
+
let sName = `Skill ${entry.id}`;
|
|
2056
|
+
const sm = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
|
|
2057
|
+
if (sm?.name) sName = sm.name;
|
|
2058
|
+
const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
|
|
2059
|
+
if (sName.startsWith("Skill ") && buffMeta?.name) sName = buffMeta.name;
|
|
2060
|
+
const finalName = resolveConditionNameFromEntry(sName, entry.id, details.buffMap);
|
|
2061
|
+
if (!finalName) return;
|
|
2062
|
+
const buffName = buffMeta?.name;
|
|
2063
|
+
const conditionIcon = conditionIconMap.get(finalName) || buffMeta?.icon;
|
|
2064
|
+
const skillIcon = sm?.icon || buffMeta?.icon || conditionIcon;
|
|
2065
|
+
if (sName.startsWith("Skill ") && (buffName || finalName)) {
|
|
2066
|
+
sName = buffName || finalName;
|
|
2067
|
+
}
|
|
2068
|
+
const hits = Number(entry.hits ?? 0);
|
|
2069
|
+
const dmg = Number(entry.totalDamage ?? 0);
|
|
2070
|
+
const summ = acc.incomingCondiTotals[finalName] || { name: finalName, icon: conditionIcon, applications: 0, damage: 0 };
|
|
2071
|
+
summ.applications += Number.isFinite(hits) ? hits : 0;
|
|
2072
|
+
summ.damage += Number.isFinite(dmg) ? dmg : 0;
|
|
2073
|
+
if (!summ.icon && conditionIcon) summ.icon = conditionIcon;
|
|
2074
|
+
acc.incomingCondiTotals[finalName] = summ;
|
|
2075
|
+
const pEntry = ps.incomingConditions[finalName] || { applications: 0, damage: 0, skills: {}, icon: conditionIcon };
|
|
2076
|
+
pEntry.applications += Number.isFinite(hits) ? hits : 0;
|
|
2077
|
+
pEntry.damage += Number.isFinite(dmg) ? dmg : 0;
|
|
2078
|
+
const skEntry = pEntry.skills[sName] || { name: sName, hits: 0, damage: 0, icon: skillIcon };
|
|
2079
|
+
skEntry.hits += Number.isFinite(hits) ? hits : 0;
|
|
2080
|
+
skEntry.damage += Number.isFinite(dmg) ? dmg : 0;
|
|
2081
|
+
if (!skEntry.icon && skillIcon) skEntry.icon = skillIcon;
|
|
2082
|
+
pEntry.skills[sName] = skEntry;
|
|
2083
|
+
if (!pEntry.icon && conditionIcon) pEntry.icon = conditionIcon;
|
|
2084
|
+
ps.incomingConditions[finalName] = pEntry;
|
|
2085
|
+
}));
|
|
2086
|
+
});
|
|
2087
|
+
const mitigationSource = details.player_damage_mitigation || details.playerDamageMitigation;
|
|
2088
|
+
const hasMitigationSource = mitigationSource && typeof mitigationSource === "object" && Object.keys(mitigationSource).length > 0;
|
|
2089
|
+
if (hasMitigationSource) {
|
|
2090
|
+
Object.entries(mitigationSource).forEach(([rawKey, skillMap]) => {
|
|
2091
|
+
if (!skillMap || typeof skillMap !== "object") return;
|
|
2092
|
+
const base = parseMitigationKey(rawKey);
|
|
2093
|
+
const identity = getMitigationIdentity(base.account, base.profession, splitPlayersByClass);
|
|
2094
|
+
const row = ensureMitigationRow(acc.damageMitigationPlayersMap, identity.key, { ...base, account: identity.accountLabel, profession: identity.profession });
|
|
2095
|
+
Object.values(skillMap).forEach((entry) => {
|
|
2096
|
+
const avoided = readNumber(entry?.avoided_damage ?? entry?.avoidedDamage);
|
|
2097
|
+
if (avoided <= 0) return;
|
|
2098
|
+
addMitigationTotals(row.mitigationTotals, entry);
|
|
2099
|
+
});
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
const mitigationMinionSource = details.player_minion_damage_mitigation || details.playerMinionDamageMitigation;
|
|
2103
|
+
const hasMitigationMinionSource = mitigationMinionSource && typeof mitigationMinionSource === "object" && Object.keys(mitigationMinionSource).length > 0;
|
|
2104
|
+
if (hasMitigationMinionSource) {
|
|
2105
|
+
Object.entries(mitigationMinionSource).forEach(([rawKey, minionMap]) => {
|
|
2106
|
+
if (!minionMap || typeof minionMap !== "object") return;
|
|
2107
|
+
const base = parseMitigationKey(rawKey);
|
|
2108
|
+
const identity = getMitigationIdentity(base.account, base.profession, splitPlayersByClass);
|
|
2109
|
+
Object.entries(minionMap).forEach(([minionName, skillMap]) => {
|
|
2110
|
+
if (!skillMap || typeof skillMap !== "object") return;
|
|
2111
|
+
const rowKey = `${identity.key}::${minionName}`;
|
|
2112
|
+
const row = ensureMitigationMinionRow(acc.damageMitigationMinionsMap, rowKey, {
|
|
2113
|
+
...base,
|
|
2114
|
+
account: identity.accountLabel,
|
|
2115
|
+
profession: identity.profession,
|
|
2116
|
+
minion: String(minionName || "Unknown")
|
|
2117
|
+
});
|
|
2118
|
+
Object.values(skillMap).forEach((entry) => {
|
|
2119
|
+
addMitigationTotals(row.mitigationTotals, entry);
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
if (!hasMitigationSource || !hasMitigationMinionSource) {
|
|
2125
|
+
if (!hasMitigationSource) {
|
|
2126
|
+
squadPlayers.forEach((player) => {
|
|
2127
|
+
const entries = player?.totalDamageTaken || [];
|
|
2128
|
+
if (!Array.isArray(entries) || entries.length === 0) return;
|
|
2129
|
+
const identity = getPlayerIdentity(player, splitPlayersByClass);
|
|
2130
|
+
const base = {
|
|
2131
|
+
account: identity.accountLabel,
|
|
2132
|
+
name: player.name || identity.accountLabel,
|
|
2133
|
+
profession: identity.profession
|
|
2134
|
+
};
|
|
2135
|
+
ensureMitigationRow(acc.damageMitigationPlayersMap, identity.key, base);
|
|
2136
|
+
const list = Array.isArray(entries) ? entries[0] : null;
|
|
2137
|
+
list?.forEach((entry) => {
|
|
2138
|
+
if (!entry?.id) return;
|
|
2139
|
+
const blocked = readNumber(entry.blocked);
|
|
2140
|
+
const evaded = readNumber(entry.evaded);
|
|
2141
|
+
const glanced = readNumber(entry.glance ?? entry.glanced);
|
|
2142
|
+
const missed = readNumber(entry.missed);
|
|
2143
|
+
const invulned = readNumber(entry.invulned);
|
|
2144
|
+
const interrupted = readNumber(entry.interrupted);
|
|
2145
|
+
const skillHits = readNumber(entry.hits ?? entry.connectedHits);
|
|
2146
|
+
const skillId = Number(entry.id);
|
|
2147
|
+
updateCumulativeCounts(
|
|
2148
|
+
acc.mitigationCumulativeCounts,
|
|
2149
|
+
`${identity.key}::${skillId}`,
|
|
2150
|
+
{
|
|
2151
|
+
totalHits: skillHits,
|
|
2152
|
+
blocked,
|
|
2153
|
+
evaded,
|
|
2154
|
+
glanced,
|
|
2155
|
+
missed,
|
|
2156
|
+
invulned,
|
|
2157
|
+
interrupted,
|
|
2158
|
+
totalMitigation: 0,
|
|
2159
|
+
minMitigation: 0
|
|
2160
|
+
}
|
|
2161
|
+
);
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
if (!hasMitigationMinionSource) {
|
|
2166
|
+
squadPlayers.forEach((player) => {
|
|
2167
|
+
const minions = player?.minions || [];
|
|
2168
|
+
if (!Array.isArray(minions) || minions.length === 0) return;
|
|
2169
|
+
const identity = getPlayerIdentity(player, splitPlayersByClass);
|
|
2170
|
+
const base = {
|
|
2171
|
+
account: identity.accountLabel,
|
|
2172
|
+
name: player.name || identity.accountLabel,
|
|
2173
|
+
profession: identity.profession
|
|
2174
|
+
};
|
|
2175
|
+
minions.forEach((minion) => {
|
|
2176
|
+
const minionEntries = minion?.totalDamageTakenDist || [];
|
|
2177
|
+
if (!Array.isArray(minionEntries) || minionEntries.length === 0) return;
|
|
2178
|
+
let minionName = String(minion?.name || "Unknown").replace(/^Juvenile\s+/i, "") || "Unknown";
|
|
2179
|
+
if (minionName.toUpperCase().includes("UNKNOWN")) minionName = "Unknown";
|
|
2180
|
+
const rowKey = `${identity.key}::${minionName}`;
|
|
2181
|
+
ensureMitigationMinionRow(acc.damageMitigationMinionsMap, rowKey, { ...base, minion: minionName });
|
|
2182
|
+
const list = Array.isArray(minionEntries) ? minionEntries[0] : null;
|
|
2183
|
+
list?.forEach((entry) => {
|
|
2184
|
+
if (!entry?.id) return;
|
|
2185
|
+
const blocked = readNumber(entry.blocked);
|
|
2186
|
+
const evaded = readNumber(entry.evaded);
|
|
2187
|
+
const glanced = readNumber(entry.glance ?? entry.glanced);
|
|
2188
|
+
const missed = readNumber(entry.missed);
|
|
2189
|
+
const invulned = readNumber(entry.invulned);
|
|
2190
|
+
const interrupted = readNumber(entry.interrupted);
|
|
2191
|
+
const skillHits = readNumber(entry.hits ?? entry.connectedHits);
|
|
2192
|
+
const skillId = Number(entry.id);
|
|
2193
|
+
updateCumulativeCounts(
|
|
2194
|
+
acc.mitigationMinionCumulativeCounts,
|
|
2195
|
+
`${rowKey}::${skillId}`,
|
|
2196
|
+
{
|
|
2197
|
+
totalHits: skillHits,
|
|
2198
|
+
blocked,
|
|
2199
|
+
evaded,
|
|
2200
|
+
glanced,
|
|
2201
|
+
missed,
|
|
2202
|
+
invulned,
|
|
2203
|
+
interrupted,
|
|
2204
|
+
totalMitigation: 0,
|
|
2205
|
+
minMitigation: 0
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
});
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
var recomputeMitigationTotals = (rows, cumulative, globalEnemySkillStats) => {
|
|
2215
|
+
const aggregate = /* @__PURE__ */ new Map();
|
|
2216
|
+
const ensureAggregate = (key) => {
|
|
2217
|
+
let bucket = aggregate.get(key);
|
|
2218
|
+
if (!bucket) {
|
|
2219
|
+
bucket = createMitigationTotals();
|
|
2220
|
+
aggregate.set(key, bucket);
|
|
2221
|
+
}
|
|
2222
|
+
return bucket;
|
|
2223
|
+
};
|
|
2224
|
+
cumulative.forEach((counts, key) => {
|
|
2225
|
+
const splitIndex = key.lastIndexOf("::");
|
|
2226
|
+
const rowKey = splitIndex > -1 ? key.slice(0, splitIndex) : key;
|
|
2227
|
+
const skillId = splitIndex > -1 ? Number(key.slice(splitIndex + 2)) : Number.NaN;
|
|
2228
|
+
const enemy = Number.isFinite(skillId) ? resolveGlobalEnemyStats(globalEnemySkillStats, skillId) : { hasSkill: false, avg: 0, min: 0, hits: 0 };
|
|
2229
|
+
if (!enemy.hasSkill || enemy.hits <= 0) return;
|
|
2230
|
+
const avg = enemy.avg;
|
|
2231
|
+
const min = enemy.min;
|
|
2232
|
+
const avoid = counts.glanced * avg / 2 + (counts.blocked + counts.evaded + counts.missed + counts.invulned + counts.interrupted) * avg;
|
|
2233
|
+
const avoidMin = counts.glanced * min / 2 + (counts.blocked + counts.evaded + counts.missed + counts.invulned + counts.interrupted) * min;
|
|
2234
|
+
if (avoid <= 0) return;
|
|
2235
|
+
const bucket = ensureAggregate(rowKey);
|
|
2236
|
+
bucket.totalHits += counts.totalHits;
|
|
2237
|
+
bucket.blocked += counts.blocked;
|
|
2238
|
+
bucket.evaded += counts.evaded;
|
|
2239
|
+
bucket.glanced += counts.glanced;
|
|
2240
|
+
bucket.missed += counts.missed;
|
|
2241
|
+
bucket.invulned += counts.invulned;
|
|
2242
|
+
bucket.interrupted += counts.interrupted;
|
|
2243
|
+
bucket.totalMitigation += avoid;
|
|
2244
|
+
bucket.minMitigation += avoidMin;
|
|
2245
|
+
});
|
|
2246
|
+
rows.forEach((row, rowKey) => {
|
|
2247
|
+
const totals = aggregate.get(rowKey) || createMitigationTotals();
|
|
2248
|
+
row.mitigationTotals.totalHits = totals.totalHits;
|
|
2249
|
+
row.mitigationTotals.blocked = totals.blocked;
|
|
2250
|
+
row.mitigationTotals.evaded = totals.evaded;
|
|
2251
|
+
row.mitigationTotals.glanced = totals.glanced;
|
|
2252
|
+
row.mitigationTotals.missed = totals.missed;
|
|
2253
|
+
row.mitigationTotals.invulned = totals.invulned;
|
|
2254
|
+
row.mitigationTotals.interrupted = totals.interrupted;
|
|
2255
|
+
row.mitigationTotals.totalMitigation = totals.totalMitigation;
|
|
2256
|
+
row.mitigationTotals.minMitigation = totals.minMitigation;
|
|
2257
|
+
});
|
|
2258
|
+
};
|
|
2259
|
+
var finalizePlayerAggregation = (acc) => {
|
|
2260
|
+
recomputeMitigationTotals(acc.damageMitigationPlayersMap, acc.mitigationCumulativeCounts, acc.globalEnemySkillStats);
|
|
2261
|
+
recomputeMitigationTotals(acc.damageMitigationMinionsMap, acc.mitigationMinionCumulativeCounts, acc.globalEnemySkillStats);
|
|
2262
|
+
};
|
|
2263
|
+
var computePlayerAggregation = ({
|
|
2264
|
+
validLogs,
|
|
2265
|
+
method,
|
|
2266
|
+
skillDamageSource,
|
|
2267
|
+
splitPlayersByClass
|
|
2268
|
+
}) => {
|
|
2269
|
+
const acc = createPlayerAggregationAccumulators();
|
|
2270
|
+
const options = { method, skillDamageSource, splitPlayersByClass };
|
|
2271
|
+
for (const log of validLogs) {
|
|
2272
|
+
precomputeGlobalEnemySkillStats(log, acc);
|
|
2273
|
+
}
|
|
2274
|
+
for (const log of validLogs) {
|
|
2275
|
+
ingestLogPlayerData(log, acc, options);
|
|
2276
|
+
}
|
|
2277
|
+
finalizePlayerAggregation(acc);
|
|
2278
|
+
return {
|
|
2279
|
+
playerStats: acc.playerStats,
|
|
2280
|
+
skillDamageMap: acc.skillDamageMap,
|
|
2281
|
+
incomingSkillDamageMap: acc.incomingSkillDamageMap,
|
|
2282
|
+
playerSkillBreakdownMap: acc.playerSkillBreakdownMap,
|
|
2283
|
+
healingBreakdownMap: acc.healingBreakdownMap,
|
|
2284
|
+
outgoingCondiTotals: acc.outgoingCondiTotals,
|
|
2285
|
+
incomingCondiTotals: acc.incomingCondiTotals,
|
|
2286
|
+
enemyProfessionCounts: acc.enemyProfessionCounts,
|
|
2287
|
+
specialBuffMeta: acc.specialBuffMeta,
|
|
2288
|
+
specialBuffAgg: acc.specialBuffAgg,
|
|
2289
|
+
specialBuffOutputAgg: acc.specialBuffOutputAgg,
|
|
2290
|
+
damageMitigationPlayersMap: acc.damageMitigationPlayersMap,
|
|
2291
|
+
damageMitigationMinionsMap: acc.damageMitigationMinionsMap,
|
|
2292
|
+
mitigationCumulativeCounts: acc.mitigationCumulativeCounts,
|
|
2293
|
+
mitigationMinionCumulativeCounts: acc.mitigationMinionCumulativeCounts,
|
|
2294
|
+
wins: acc.wins,
|
|
2295
|
+
losses: acc.losses,
|
|
2296
|
+
totalSquadSizeAccum: acc.totalSquadSizeAccum,
|
|
2297
|
+
totalEnemiesAccum: acc.totalEnemiesAccum,
|
|
2298
|
+
totalSquadDeaths: acc.totalSquadDeaths,
|
|
2299
|
+
totalSquadKills: acc.totalSquadKills,
|
|
2300
|
+
totalEnemyDeaths: acc.totalEnemyDeaths,
|
|
2301
|
+
totalEnemyKills: acc.totalEnemyKills,
|
|
2302
|
+
totalSquadDowns: acc.totalSquadDowns,
|
|
2303
|
+
totalEnemyDowns: acc.totalEnemyDowns
|
|
2304
|
+
};
|
|
2305
|
+
};
|
|
2306
|
+
|
|
2307
|
+
// src/rollup.ts
|
|
2308
|
+
var ROLLUP_SOURCES_VERSION = 1;
|
|
2309
|
+
var extractRollupSource = (payload) => {
|
|
2310
|
+
const meta = payload?.meta || {};
|
|
2311
|
+
const commanderRows = Array.isArray(payload?.stats?.commanderStats?.rows) ? payload.stats.commanderStats.rows : [];
|
|
2312
|
+
const attendanceRows = Array.isArray(payload?.stats?.attendanceData) ? payload.stats.attendanceData : [];
|
|
2313
|
+
return {
|
|
2314
|
+
meta: {
|
|
2315
|
+
id: meta.id,
|
|
2316
|
+
dateStart: meta.dateStart,
|
|
2317
|
+
dateEnd: meta.dateEnd,
|
|
2318
|
+
generatedAt: meta.generatedAt
|
|
2319
|
+
},
|
|
2320
|
+
stats: {
|
|
2321
|
+
commanderStats: {
|
|
2322
|
+
rows: commanderRows.map((row) => ({
|
|
2323
|
+
key: row?.key,
|
|
2324
|
+
account: row?.account,
|
|
2325
|
+
characterNames: Array.isArray(row?.characterNames) ? row.characterNames : [],
|
|
2326
|
+
profession: row?.profession,
|
|
2327
|
+
fights: row?.fights,
|
|
2328
|
+
kills: row?.kills,
|
|
2329
|
+
downs: row?.downs,
|
|
2330
|
+
commanderDeaths: row?.commanderDeaths,
|
|
2331
|
+
alliesDead: row?.alliesDead,
|
|
2332
|
+
wins: row?.wins,
|
|
2333
|
+
losses: row?.losses
|
|
2334
|
+
}))
|
|
2335
|
+
},
|
|
2336
|
+
attendanceData: attendanceRows.map((row) => ({
|
|
2337
|
+
account: row?.account,
|
|
2338
|
+
characterNames: Array.isArray(row?.characterNames) ? row.characterNames : [],
|
|
2339
|
+
combatTimeMs: row?.combatTimeMs,
|
|
2340
|
+
squadTimeMs: row?.squadTimeMs,
|
|
2341
|
+
classTimes: (Array.isArray(row?.classTimes) ? row.classTimes : []).map((entry) => ({
|
|
2342
|
+
profession: entry?.profession,
|
|
2343
|
+
timeMs: entry?.timeMs
|
|
2344
|
+
}))
|
|
2345
|
+
}))
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
};
|
|
2349
|
+
var updateRollupSourcesForPublish = (options) => {
|
|
2350
|
+
const { existingSources, currentReport, validIds, loadLocalReport } = options;
|
|
2351
|
+
const sourcesById = /* @__PURE__ */ new Map();
|
|
2352
|
+
existingSources.forEach((source) => {
|
|
2353
|
+
const id = String(source?.meta?.id || "").trim();
|
|
2354
|
+
if (id) sourcesById.set(id, source);
|
|
2355
|
+
});
|
|
2356
|
+
const currentId = String(currentReport?.meta?.id || "").trim();
|
|
2357
|
+
if (currentId) {
|
|
2358
|
+
sourcesById.set(currentId, extractRollupSource(currentReport));
|
|
2359
|
+
}
|
|
2360
|
+
const validIdSet = new Set(validIds.map((id) => String(id || "").trim()).filter(Boolean));
|
|
2361
|
+
if (loadLocalReport) {
|
|
2362
|
+
for (const id of validIdSet) {
|
|
2363
|
+
if (sourcesById.has(id)) continue;
|
|
2364
|
+
const localReport = loadLocalReport(id);
|
|
2365
|
+
if (localReport) {
|
|
2366
|
+
sourcesById.set(id, extractRollupSource(localReport));
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
const sources = Array.from(sourcesById.entries()).filter(([id]) => validIdSet.has(id)).map(([, source]) => source);
|
|
2371
|
+
return {
|
|
2372
|
+
version: ROLLUP_SOURCES_VERSION,
|
|
2373
|
+
sources,
|
|
2374
|
+
rollup: buildRollupData(sources)
|
|
2375
|
+
};
|
|
2376
|
+
};
|
|
2377
|
+
var removeRollupSources = (file, deletedIds) => {
|
|
2378
|
+
const deleted = new Set(deletedIds.map((id) => String(id || "").trim()).filter(Boolean));
|
|
2379
|
+
const sources = file.sources.filter(
|
|
2380
|
+
(source) => !deleted.has(String(source?.meta?.id || "").trim())
|
|
2381
|
+
);
|
|
2382
|
+
return {
|
|
2383
|
+
version: ROLLUP_SOURCES_VERSION,
|
|
2384
|
+
sources,
|
|
2385
|
+
rollup: buildRollupData(sources)
|
|
2386
|
+
};
|
|
2387
|
+
};
|
|
2388
|
+
var parseRollupSourcesFile = (data) => {
|
|
2389
|
+
const candidate = data;
|
|
2390
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
2391
|
+
if (candidate.version !== ROLLUP_SOURCES_VERSION) return null;
|
|
2392
|
+
if (!Array.isArray(candidate.sources)) return null;
|
|
2393
|
+
if (!candidate.rollup || typeof candidate.rollup !== "object") return null;
|
|
2394
|
+
return candidate;
|
|
2395
|
+
};
|
|
2396
|
+
var toFiniteNumber = (value) => {
|
|
2397
|
+
const numeric = Number(value ?? 0);
|
|
2398
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
2399
|
+
};
|
|
2400
|
+
var parseTimestamp2 = (meta) => {
|
|
2401
|
+
const candidates = [meta?.dateEnd, meta?.dateStart];
|
|
2402
|
+
for (const candidate of candidates) {
|
|
2403
|
+
if (!candidate) continue;
|
|
2404
|
+
const parsed = new Date(candidate).getTime();
|
|
2405
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
2406
|
+
return parsed;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return 0;
|
|
2410
|
+
};
|
|
2411
|
+
var parseGeneratedTimestamp = (meta) => {
|
|
2412
|
+
const generated = meta?.generatedAt ? new Date(meta.generatedAt).getTime() : 0;
|
|
2413
|
+
if (Number.isFinite(generated) && generated > 0) {
|
|
2414
|
+
return generated;
|
|
2415
|
+
}
|
|
2416
|
+
return parseTimestamp2(meta);
|
|
2417
|
+
};
|
|
2418
|
+
var buildRaidKey = (report, fallbackIndex) => {
|
|
2419
|
+
const dateStart = String(report?.meta?.dateStart || "").trim();
|
|
2420
|
+
const dateEnd = String(report?.meta?.dateEnd || "").trim();
|
|
2421
|
+
if (dateStart || dateEnd) {
|
|
2422
|
+
return `${dateStart}|${dateEnd}`;
|
|
2423
|
+
}
|
|
2424
|
+
const id = String(report?.meta?.id || "").trim();
|
|
2425
|
+
if (id) return id;
|
|
2426
|
+
return `report-${fallbackIndex}`;
|
|
2427
|
+
};
|
|
2428
|
+
var choosePrimaryProfession = (professionTimeMs, fallback = "Unknown") => {
|
|
2429
|
+
const entries = Object.entries(professionTimeMs).filter(([profession, timeMs]) => profession && timeMs > 0).sort((a, b) => {
|
|
2430
|
+
const delta = b[1] - a[1];
|
|
2431
|
+
if (delta !== 0) return delta;
|
|
2432
|
+
return a[0].localeCompare(b[0]);
|
|
2433
|
+
});
|
|
2434
|
+
return entries[0]?.[0] || fallback;
|
|
2435
|
+
};
|
|
2436
|
+
var chooseMostCommonProfession = (professionCounts, fallback = "Unknown") => {
|
|
2437
|
+
const entries = Object.entries(professionCounts).filter(([profession, count]) => profession && count > 0).sort((a, b) => {
|
|
2438
|
+
const delta = b[1] - a[1];
|
|
2439
|
+
if (delta !== 0) return delta;
|
|
2440
|
+
return a[0].localeCompare(b[0]);
|
|
2441
|
+
});
|
|
2442
|
+
return entries[0]?.[0] || fallback;
|
|
2443
|
+
};
|
|
2444
|
+
var buildProfessionBreakdown = (professionRuns) => Object.entries(professionRuns).filter(([profession, runs]) => profession && runs > 0).sort((a, b) => {
|
|
2445
|
+
const delta = b[1] - a[1];
|
|
2446
|
+
if (delta !== 0) return delta;
|
|
2447
|
+
return a[0].localeCompare(b[0]);
|
|
2448
|
+
}).map(([profession, runs]) => ({ profession, runs }));
|
|
2449
|
+
var buildRollupData = (reports) => {
|
|
2450
|
+
const uniqueRaidKeys = /* @__PURE__ */ new Set();
|
|
2451
|
+
const reportsByRaid = /* @__PURE__ */ new Map();
|
|
2452
|
+
reports.forEach((report, index) => {
|
|
2453
|
+
const raidKey = buildRaidKey(report, index);
|
|
2454
|
+
uniqueRaidKeys.add(raidKey);
|
|
2455
|
+
const generatedTs = parseGeneratedTimestamp(report?.meta);
|
|
2456
|
+
if (!reportsByRaid.has(raidKey)) {
|
|
2457
|
+
reportsByRaid.set(raidKey, []);
|
|
2458
|
+
}
|
|
2459
|
+
reportsByRaid.get(raidKey).push({ report, generatedTs, index });
|
|
2460
|
+
});
|
|
2461
|
+
const raidGroups = Array.from(reportsByRaid.values()).sort((a, b) => {
|
|
2462
|
+
const aLatest = Math.max(...a.map((entry) => entry.generatedTs || 0));
|
|
2463
|
+
const bLatest = Math.max(...b.map((entry) => entry.generatedTs || 0));
|
|
2464
|
+
if (bLatest !== aLatest) return bLatest - aLatest;
|
|
2465
|
+
const aIndex = Math.max(...a.map((entry) => entry.index));
|
|
2466
|
+
const bIndex = Math.max(...b.map((entry) => entry.index));
|
|
2467
|
+
return bIndex - aIndex;
|
|
2468
|
+
}).map((entries) => [...entries].sort((a, b) => {
|
|
2469
|
+
if (b.generatedTs !== a.generatedTs) return b.generatedTs - a.generatedTs;
|
|
2470
|
+
return b.index - a.index;
|
|
2471
|
+
}));
|
|
2472
|
+
const commanders = /* @__PURE__ */ new Map();
|
|
2473
|
+
const players = /* @__PURE__ */ new Map();
|
|
2474
|
+
let reportsWithCommanderDetails = 0;
|
|
2475
|
+
let reportsMissingCommanderDetails = 0;
|
|
2476
|
+
let reportsWithAttendanceDetails = 0;
|
|
2477
|
+
let reportsMissingAttendanceDetails = 0;
|
|
2478
|
+
let includedRaidGroups = 0;
|
|
2479
|
+
let includedSourceReports = 0;
|
|
2480
|
+
raidGroups.forEach((group) => {
|
|
2481
|
+
const attendanceSource = group.find((entry) => Array.isArray(entry.report?.stats?.attendanceData) && entry.report.stats.attendanceData.length > 0) || null;
|
|
2482
|
+
const attendanceReport = attendanceSource?.report || null;
|
|
2483
|
+
const attendanceTimestamp = attendanceReport ? parseTimestamp2(attendanceReport.meta) : 0;
|
|
2484
|
+
const attendanceRows = Array.isArray(attendanceReport?.stats?.attendanceData) ? attendanceReport.stats.attendanceData : [];
|
|
2485
|
+
if (attendanceRows.length > 0) {
|
|
2486
|
+
reportsWithAttendanceDetails += 1;
|
|
2487
|
+
} else {
|
|
2488
|
+
reportsMissingAttendanceDetails += 1;
|
|
2489
|
+
}
|
|
2490
|
+
const latestCommanderRowsByAccount = /* @__PURE__ */ new Map();
|
|
2491
|
+
group.forEach((entry) => {
|
|
2492
|
+
const report = entry.report;
|
|
2493
|
+
const timestamp = parseTimestamp2(report?.meta);
|
|
2494
|
+
const commanderRows2 = Array.isArray(report?.stats?.commanderStats?.rows) ? report.stats.commanderStats.rows : [];
|
|
2495
|
+
commanderRows2.forEach((row) => {
|
|
2496
|
+
const account = String(row?.account || row?.key || "").trim();
|
|
2497
|
+
if (!account) return;
|
|
2498
|
+
const existing = latestCommanderRowsByAccount.get(account);
|
|
2499
|
+
if (!existing) {
|
|
2500
|
+
latestCommanderRowsByAccount.set(account, { row, timestamp, generatedTs: entry.generatedTs, index: entry.index });
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const shouldReplace = entry.generatedTs > existing.generatedTs || entry.generatedTs === existing.generatedTs && entry.index > existing.index;
|
|
2504
|
+
if (shouldReplace) {
|
|
2505
|
+
latestCommanderRowsByAccount.set(account, { row, timestamp, generatedTs: entry.generatedTs, index: entry.index });
|
|
2506
|
+
}
|
|
2507
|
+
});
|
|
2508
|
+
});
|
|
2509
|
+
if (latestCommanderRowsByAccount.size > 0) {
|
|
2510
|
+
reportsWithCommanderDetails += 1;
|
|
2511
|
+
} else {
|
|
2512
|
+
reportsMissingCommanderDetails += 1;
|
|
2513
|
+
}
|
|
2514
|
+
if (attendanceRows.length === 0 || latestCommanderRowsByAccount.size === 0) {
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
includedRaidGroups += 1;
|
|
2518
|
+
includedSourceReports += group.length;
|
|
2519
|
+
attendanceRows.forEach((row) => {
|
|
2520
|
+
const account = String(row?.account || "").trim();
|
|
2521
|
+
if (!account || account === "Unknown") return;
|
|
2522
|
+
const existing = players.get(account) || {
|
|
2523
|
+
account,
|
|
2524
|
+
characterNames: /* @__PURE__ */ new Set(),
|
|
2525
|
+
professionTimeMs: {},
|
|
2526
|
+
professionRuns: {},
|
|
2527
|
+
runs: 0,
|
|
2528
|
+
combatTimeMs: 0,
|
|
2529
|
+
squadTimeMs: 0,
|
|
2530
|
+
lastSeenTs: 0
|
|
2531
|
+
};
|
|
2532
|
+
existing.runs += 1;
|
|
2533
|
+
existing.combatTimeMs += Math.max(0, toFiniteNumber(row?.combatTimeMs));
|
|
2534
|
+
existing.squadTimeMs += Math.max(0, toFiniteNumber(row?.squadTimeMs));
|
|
2535
|
+
existing.lastSeenTs = Math.max(existing.lastSeenTs, attendanceTimestamp);
|
|
2536
|
+
if (Array.isArray(row?.characterNames)) {
|
|
2537
|
+
row.characterNames.forEach((name) => {
|
|
2538
|
+
const normalized = String(name || "").trim();
|
|
2539
|
+
if (normalized) existing.characterNames.add(normalized);
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
const classTimes = Array.isArray(row?.classTimes) ? row.classTimes : [];
|
|
2543
|
+
const professionsThisRun = /* @__PURE__ */ new Set();
|
|
2544
|
+
classTimes.forEach((classRow) => {
|
|
2545
|
+
const profession = String(classRow?.profession || "").trim();
|
|
2546
|
+
if (!profession || profession === "Unknown") return;
|
|
2547
|
+
const timeMs = Math.max(0, toFiniteNumber(classRow?.timeMs));
|
|
2548
|
+
existing.professionTimeMs[profession] = (existing.professionTimeMs[profession] || 0) + timeMs;
|
|
2549
|
+
if (timeMs > 0) professionsThisRun.add(profession);
|
|
2550
|
+
});
|
|
2551
|
+
professionsThisRun.forEach((profession) => {
|
|
2552
|
+
existing.professionRuns[profession] = (existing.professionRuns[profession] || 0) + 1;
|
|
2553
|
+
});
|
|
2554
|
+
players.set(account, existing);
|
|
2555
|
+
});
|
|
2556
|
+
latestCommanderRowsByAccount.forEach(({ row, timestamp }) => {
|
|
2557
|
+
const account = String(row?.account || row?.key || "").trim();
|
|
2558
|
+
if (!account) return;
|
|
2559
|
+
const existing = commanders.get(account) || {
|
|
2560
|
+
account,
|
|
2561
|
+
characterNames: /* @__PURE__ */ new Set(),
|
|
2562
|
+
professionCounts: {},
|
|
2563
|
+
runs: 0,
|
|
2564
|
+
fightsLed: 0,
|
|
2565
|
+
kills: 0,
|
|
2566
|
+
downs: 0,
|
|
2567
|
+
commanderDeaths: 0,
|
|
2568
|
+
alliesDead: 0,
|
|
2569
|
+
wins: 0,
|
|
2570
|
+
losses: 0,
|
|
2571
|
+
lastSeenTs: 0
|
|
2572
|
+
};
|
|
2573
|
+
existing.runs += 1;
|
|
2574
|
+
existing.fightsLed += Math.max(0, toFiniteNumber(row?.fights));
|
|
2575
|
+
existing.kills += Math.max(0, toFiniteNumber(row?.kills));
|
|
2576
|
+
existing.downs += Math.max(0, toFiniteNumber(row?.downs));
|
|
2577
|
+
existing.commanderDeaths += Math.max(0, toFiniteNumber(row?.commanderDeaths));
|
|
2578
|
+
existing.alliesDead += Math.max(0, toFiniteNumber(row?.alliesDead));
|
|
2579
|
+
existing.wins += Math.max(0, toFiniteNumber(row?.wins));
|
|
2580
|
+
existing.losses += Math.max(0, toFiniteNumber(row?.losses));
|
|
2581
|
+
existing.lastSeenTs = Math.max(existing.lastSeenTs, timestamp);
|
|
2582
|
+
if (Array.isArray(row?.characterNames)) {
|
|
2583
|
+
row.characterNames.forEach((name) => {
|
|
2584
|
+
const normalized = String(name || "").trim();
|
|
2585
|
+
if (normalized) existing.characterNames.add(normalized);
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
const profession = String(row?.profession || "").trim();
|
|
2589
|
+
if (profession) {
|
|
2590
|
+
existing.professionCounts[profession] = (existing.professionCounts[profession] || 0) + 1;
|
|
2591
|
+
}
|
|
2592
|
+
commanders.set(account, existing);
|
|
2593
|
+
});
|
|
2594
|
+
});
|
|
2595
|
+
const commanderRows = Array.from(commanders.values()).map((entry) => {
|
|
2596
|
+
const kdr = entry.alliesDead > 0 ? entry.kills / entry.alliesDead : entry.kills;
|
|
2597
|
+
return {
|
|
2598
|
+
account: entry.account,
|
|
2599
|
+
characterNames: Array.from(entry.characterNames.values()).sort((a, b) => a.localeCompare(b)),
|
|
2600
|
+
profession: chooseMostCommonProfession(entry.professionCounts),
|
|
2601
|
+
professionBreakdown: buildProfessionBreakdown(entry.professionCounts),
|
|
2602
|
+
runs: entry.runs,
|
|
2603
|
+
fightsLed: entry.fightsLed,
|
|
2604
|
+
kills: entry.kills,
|
|
2605
|
+
downs: entry.downs,
|
|
2606
|
+
commanderDeaths: entry.commanderDeaths,
|
|
2607
|
+
alliesDead: entry.alliesDead,
|
|
2608
|
+
wins: entry.wins,
|
|
2609
|
+
losses: entry.losses,
|
|
2610
|
+
kdr,
|
|
2611
|
+
lastSeenTs: entry.lastSeenTs
|
|
2612
|
+
};
|
|
2613
|
+
}).sort((a, b) => {
|
|
2614
|
+
if (b.runs !== a.runs) return b.runs - a.runs;
|
|
2615
|
+
if (b.fightsLed !== a.fightsLed) return b.fightsLed - a.fightsLed;
|
|
2616
|
+
if (b.kills !== a.kills) return b.kills - a.kills;
|
|
2617
|
+
return a.account.localeCompare(b.account);
|
|
2618
|
+
});
|
|
2619
|
+
const playerRows = Array.from(players.values()).map((entry) => ({
|
|
2620
|
+
account: entry.account,
|
|
2621
|
+
characterNames: Array.from(entry.characterNames.values()).sort((a, b) => a.localeCompare(b)),
|
|
2622
|
+
profession: choosePrimaryProfession(entry.professionTimeMs),
|
|
2623
|
+
professionBreakdown: buildProfessionBreakdown(entry.professionRuns),
|
|
2624
|
+
runs: entry.runs,
|
|
2625
|
+
combatTimeMs: entry.combatTimeMs,
|
|
2626
|
+
squadTimeMs: entry.squadTimeMs,
|
|
2627
|
+
lastSeenTs: entry.lastSeenTs
|
|
2628
|
+
})).sort((a, b) => {
|
|
2629
|
+
if (b.runs !== a.runs) return b.runs - a.runs;
|
|
2630
|
+
if (b.lastSeenTs !== a.lastSeenTs) return b.lastSeenTs - a.lastSeenTs;
|
|
2631
|
+
if (b.squadTimeMs !== a.squadTimeMs) return b.squadTimeMs - a.squadTimeMs;
|
|
2632
|
+
return a.account.localeCompare(b.account);
|
|
2633
|
+
});
|
|
2634
|
+
return {
|
|
2635
|
+
commanderRows,
|
|
2636
|
+
playerRows,
|
|
2637
|
+
sourceReports: reports.length,
|
|
2638
|
+
uniqueRaids: includedRaidGroups,
|
|
2639
|
+
duplicateReportsCollapsed: Math.max(0, includedSourceReports - includedRaidGroups),
|
|
2640
|
+
raidsSkippedMissingRequiredData: Math.max(0, raidGroups.length - includedRaidGroups),
|
|
2641
|
+
reportsWithCommanderDetails,
|
|
2642
|
+
reportsMissingCommanderDetails,
|
|
2643
|
+
reportsWithAttendanceDetails,
|
|
2644
|
+
reportsMissingAttendanceDetails
|
|
2645
|
+
};
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
// src/reportMetrics.ts
|
|
2649
|
+
var ReportSchemaError = class extends Error {
|
|
2650
|
+
};
|
|
2651
|
+
var num = (value) => {
|
|
2652
|
+
const n = Number(value ?? 0);
|
|
2653
|
+
return Number.isFinite(n) ? n : 0;
|
|
2654
|
+
};
|
|
2655
|
+
var extractRunSummary = (report) => {
|
|
2656
|
+
const payload = report;
|
|
2657
|
+
const id = String(payload?.meta?.id ?? "").trim();
|
|
2658
|
+
if (!id) throw new ReportSchemaError("report has no meta.id \u2014 not an AxiBridge report.json");
|
|
2659
|
+
const stats = payload?.stats ?? {};
|
|
2660
|
+
const warnings = [];
|
|
2661
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
2662
|
+
const ensure = (row) => {
|
|
2663
|
+
const account = String(row?.account ?? "").trim();
|
|
2664
|
+
if (!account || account === "Unknown") return null;
|
|
2665
|
+
let entry = byAccount.get(account);
|
|
2666
|
+
if (!entry) {
|
|
2667
|
+
entry = {
|
|
2668
|
+
account,
|
|
2669
|
+
profession: String(row?.profession ?? "Unknown"),
|
|
2670
|
+
professionList: Array.isArray(row?.professionList) ? row.professionList.map(String) : [],
|
|
2671
|
+
combatTimeMs: 0,
|
|
2672
|
+
squadTimeMs: 0,
|
|
2673
|
+
classTimes: [],
|
|
2674
|
+
damage: 0,
|
|
2675
|
+
downContribution: 0,
|
|
2676
|
+
kills: 0,
|
|
2677
|
+
downsCaused: 0,
|
|
2678
|
+
strips: 0,
|
|
2679
|
+
cleanses: 0,
|
|
2680
|
+
resurrects: 0,
|
|
2681
|
+
healing: 0,
|
|
2682
|
+
barrier: 0,
|
|
2683
|
+
hasHealAddon: false,
|
|
2684
|
+
damageTaken: 0,
|
|
2685
|
+
downs: 0,
|
|
2686
|
+
deaths: 0,
|
|
2687
|
+
logsJoined: 0
|
|
2688
|
+
};
|
|
2689
|
+
byAccount.set(account, entry);
|
|
2690
|
+
}
|
|
2691
|
+
return entry;
|
|
2692
|
+
};
|
|
2693
|
+
const tables = ["offensePlayers", "supportPlayers", "healingPlayers", "defensePlayers", "generalPlayers", "attendanceData"];
|
|
2694
|
+
if (!tables.some((key) => Array.isArray(stats?.[key]) && stats[key].length > 0)) {
|
|
2695
|
+
warnings.push("no player tables in report");
|
|
2696
|
+
}
|
|
2697
|
+
for (const row of Array.isArray(stats?.offensePlayers) ? stats.offensePlayers : []) {
|
|
2698
|
+
const p = ensure(row);
|
|
2699
|
+
if (!p) continue;
|
|
2700
|
+
p.damage += num(row?.offenseTotals?.damage);
|
|
2701
|
+
p.downContribution += num(row?.offenseTotals?.downContribution);
|
|
2702
|
+
p.kills += num(row?.offenseTotals?.killed);
|
|
2703
|
+
p.downsCaused += num(row?.offenseTotals?.downed);
|
|
2704
|
+
p.strips = Math.max(p.strips, num(row?.offenseTotals?.boonStrips));
|
|
2705
|
+
}
|
|
2706
|
+
for (const row of Array.isArray(stats?.supportPlayers) ? stats.supportPlayers : []) {
|
|
2707
|
+
const p = ensure(row);
|
|
2708
|
+
if (!p) continue;
|
|
2709
|
+
p.cleanses += num(row?.supportTotals?.condiCleanse);
|
|
2710
|
+
p.strips = Math.max(p.strips, num(row?.supportTotals?.boonStrips));
|
|
2711
|
+
p.resurrects += num(row?.supportTotals?.resurrects);
|
|
2712
|
+
p.logsJoined = Math.max(p.logsJoined, num(row?.logsJoined));
|
|
2713
|
+
}
|
|
2714
|
+
for (const row of Array.isArray(stats?.healingPlayers) ? stats.healingPlayers : []) {
|
|
2715
|
+
const p = ensure(row);
|
|
2716
|
+
if (!p) continue;
|
|
2717
|
+
p.healing += num(row?.healingTotals?.squadHealing ?? row?.healingTotals?.healing);
|
|
2718
|
+
p.barrier += num(row?.healingTotals?.squadBarrier ?? row?.healingTotals?.barrier);
|
|
2719
|
+
if (row?.hasHealAddon === true) p.hasHealAddon = true;
|
|
2720
|
+
}
|
|
2721
|
+
for (const row of Array.isArray(stats?.defensePlayers) ? stats.defensePlayers : []) {
|
|
2722
|
+
const p = ensure(row);
|
|
2723
|
+
if (!p) continue;
|
|
2724
|
+
p.damageTaken += num(row?.defenseTotals?.damageTaken);
|
|
2725
|
+
p.downs += num(row?.defenseTotals?.downCount);
|
|
2726
|
+
p.deaths += num(row?.defenseTotals?.deadCount);
|
|
2727
|
+
}
|
|
2728
|
+
for (const row of Array.isArray(stats?.generalPlayers) ? stats.generalPlayers : []) {
|
|
2729
|
+
const p = ensure(row);
|
|
2730
|
+
if (!p) continue;
|
|
2731
|
+
p.combatTimeMs = Math.max(p.combatTimeMs, num(row?.squadActiveMs ?? row?.totalFightMs));
|
|
2732
|
+
p.logsJoined = Math.max(p.logsJoined, num(row?.logsJoined));
|
|
2733
|
+
}
|
|
2734
|
+
for (const row of Array.isArray(stats?.attendanceData) ? stats.attendanceData : []) {
|
|
2735
|
+
const p = ensure(row);
|
|
2736
|
+
if (!p) continue;
|
|
2737
|
+
p.combatTimeMs = Math.max(p.combatTimeMs, num(row?.combatTimeMs));
|
|
2738
|
+
p.squadTimeMs = Math.max(p.squadTimeMs, num(row?.squadTimeMs));
|
|
2739
|
+
if (Array.isArray(row?.classTimes)) {
|
|
2740
|
+
p.classTimes = row.classTimes.map((c) => ({ profession: String(c?.profession ?? ""), timeMs: num(c?.timeMs) })).filter((c) => c.profession && c.timeMs > 0);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
id,
|
|
2745
|
+
title: String(payload?.meta?.title ?? id),
|
|
2746
|
+
dateStart: payload?.meta?.dateStart ?? null,
|
|
2747
|
+
dateEnd: payload?.meta?.dateEnd ?? null,
|
|
2748
|
+
fights: num(stats?.total),
|
|
2749
|
+
wins: num(stats?.wins),
|
|
2750
|
+
losses: num(stats?.losses),
|
|
2751
|
+
avgSquadSize: typeof stats?.avgSquadSize === "number" ? stats.avgSquadSize : null,
|
|
2752
|
+
avgEnemies: typeof stats?.avgEnemies === "number" ? stats.avgEnemies : null,
|
|
2753
|
+
squadDeaths: num(stats?.totalSquadDeaths),
|
|
2754
|
+
squadDowns: num(stats?.totalSquadDowns),
|
|
2755
|
+
enemyDeaths: num(stats?.totalEnemyDeaths),
|
|
2756
|
+
enemyDowns: num(stats?.totalEnemyDowns),
|
|
2757
|
+
commanders: Array.isArray(payload?.meta?.commanders) ? payload.meta.commanders.map(String) : [],
|
|
2758
|
+
players: Array.from(byAccount.values()),
|
|
2759
|
+
warnings
|
|
2760
|
+
};
|
|
2761
|
+
};
|
|
2762
|
+
var aggregatePlayers = (summaries, accounts) => {
|
|
2763
|
+
const wanted = accounts && accounts.length > 0 ? new Set(accounts.map((a) => a.toLowerCase())) : null;
|
|
2764
|
+
const map = /* @__PURE__ */ new Map();
|
|
2765
|
+
for (const run of summaries) {
|
|
2766
|
+
for (const p of run.players) {
|
|
2767
|
+
if (wanted && !wanted.has(p.account.toLowerCase())) continue;
|
|
2768
|
+
let agg = map.get(p.account);
|
|
2769
|
+
if (!agg) {
|
|
2770
|
+
agg = {
|
|
2771
|
+
account: p.account,
|
|
2772
|
+
runsJoined: 0,
|
|
2773
|
+
combatTimeMs: 0,
|
|
2774
|
+
squadTimeMs: 0,
|
|
2775
|
+
professionTimeMs: {},
|
|
2776
|
+
damage: 0,
|
|
2777
|
+
dps: 0,
|
|
2778
|
+
downContribution: 0,
|
|
2779
|
+
kills: 0,
|
|
2780
|
+
strips: 0,
|
|
2781
|
+
cleanses: 0,
|
|
2782
|
+
resurrects: 0,
|
|
2783
|
+
healing: 0,
|
|
2784
|
+
barrier: 0,
|
|
2785
|
+
damageTaken: 0,
|
|
2786
|
+
downs: 0,
|
|
2787
|
+
deaths: 0,
|
|
2788
|
+
lastSeen: null
|
|
2789
|
+
};
|
|
2790
|
+
map.set(p.account, agg);
|
|
2791
|
+
}
|
|
2792
|
+
agg.runsJoined += 1;
|
|
2793
|
+
agg.combatTimeMs += p.combatTimeMs;
|
|
2794
|
+
agg.squadTimeMs += p.squadTimeMs;
|
|
2795
|
+
for (const c of p.classTimes) {
|
|
2796
|
+
agg.professionTimeMs[c.profession] = (agg.professionTimeMs[c.profession] || 0) + c.timeMs;
|
|
2797
|
+
}
|
|
2798
|
+
agg.damage += p.damage;
|
|
2799
|
+
agg.downContribution += p.downContribution;
|
|
2800
|
+
agg.kills += p.kills;
|
|
2801
|
+
agg.strips += p.strips;
|
|
2802
|
+
agg.cleanses += p.cleanses;
|
|
2803
|
+
agg.resurrects += p.resurrects;
|
|
2804
|
+
agg.healing += p.healing;
|
|
2805
|
+
agg.barrier += p.barrier;
|
|
2806
|
+
agg.damageTaken += p.damageTaken;
|
|
2807
|
+
agg.downs += p.downs;
|
|
2808
|
+
agg.deaths += p.deaths;
|
|
2809
|
+
const seen = run.dateEnd ?? run.dateStart;
|
|
2810
|
+
if (seen && (!agg.lastSeen || seen > agg.lastSeen)) agg.lastSeen = seen;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
const rows = Array.from(map.values());
|
|
2814
|
+
for (const row of rows) {
|
|
2815
|
+
row.dps = row.combatTimeMs > 0 ? row.damage / (row.combatTimeMs / 1e3) : 0;
|
|
2816
|
+
}
|
|
2817
|
+
return rows.sort((a, b) => b.damage - a.damage);
|
|
2818
|
+
};
|
|
2819
|
+
var RUN_SET_METRICS = [
|
|
2820
|
+
"fights",
|
|
2821
|
+
"wins",
|
|
2822
|
+
"losses",
|
|
2823
|
+
"squadDeaths",
|
|
2824
|
+
"squadDowns",
|
|
2825
|
+
"enemyDeaths",
|
|
2826
|
+
"enemyDowns"
|
|
2827
|
+
];
|
|
2828
|
+
var compareRunSets = (a, b) => {
|
|
2829
|
+
const total = (runs, metric) => runs.reduce((sum, run) => sum + num(run[metric]), 0);
|
|
2830
|
+
return {
|
|
2831
|
+
metrics: RUN_SET_METRICS.map((metric) => {
|
|
2832
|
+
const va = total(a, metric);
|
|
2833
|
+
const vb = total(b, metric);
|
|
2834
|
+
return { metric, a: va, b: vb, delta: vb - va, deltaPct: va !== 0 ? (vb - va) / va : null };
|
|
2835
|
+
})
|
|
2836
|
+
};
|
|
2837
|
+
};
|
|
2838
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2839
|
+
0 && (module.exports = {
|
|
2840
|
+
DEFAULT_DISRUPTION_METHOD,
|
|
2841
|
+
METRICS_SPEC,
|
|
2842
|
+
NON_DAMAGING_CONDITIONS,
|
|
2843
|
+
PROFESSION_COLORS,
|
|
2844
|
+
ROLLUP_SOURCES_VERSION,
|
|
2845
|
+
ReportSchemaError,
|
|
2846
|
+
aggregatePlayers,
|
|
2847
|
+
applySquadStabilityGeneration,
|
|
2848
|
+
buildConditionIconMap,
|
|
2849
|
+
buildRollupData,
|
|
2850
|
+
compareRunSets,
|
|
2851
|
+
computeDownContribution,
|
|
2852
|
+
computeIncomingDisruptions,
|
|
2853
|
+
computeOutgoingConditions,
|
|
2854
|
+
computeOutgoingCrowdControl,
|
|
2855
|
+
computePlayerAggregation,
|
|
2856
|
+
computeSquadBarrier,
|
|
2857
|
+
computeSquadHealing,
|
|
2858
|
+
createPlayerAggregationAccumulators,
|
|
2859
|
+
extractRollupSource,
|
|
2860
|
+
extractRunSummary,
|
|
2861
|
+
finalizePlayerAggregation,
|
|
2862
|
+
getDefaultConditionIcon,
|
|
2863
|
+
getFightDownsDeaths,
|
|
2864
|
+
getFightOutcome,
|
|
2865
|
+
getPlayerBlocked,
|
|
2866
|
+
getPlayerBreakbarDamage,
|
|
2867
|
+
getPlayerCleanses,
|
|
2868
|
+
getPlayerDamage,
|
|
2869
|
+
getPlayerDamageTaken,
|
|
2870
|
+
getPlayerDashboardTotals,
|
|
2871
|
+
getPlayerDeaths,
|
|
2872
|
+
getPlayerDistanceToTag,
|
|
2873
|
+
getPlayerDodges,
|
|
2874
|
+
getPlayerDownsTaken,
|
|
2875
|
+
getPlayerDps,
|
|
2876
|
+
getPlayerEvaded,
|
|
2877
|
+
getPlayerMissed,
|
|
2878
|
+
getPlayerOutgoingInterrupts,
|
|
2879
|
+
getPlayerResurrects,
|
|
2880
|
+
getPlayerStrips,
|
|
2881
|
+
getProfessionAbbrev,
|
|
2882
|
+
getProfessionAbbrevSuperscript,
|
|
2883
|
+
getProfessionBase,
|
|
2884
|
+
getProfessionColor,
|
|
2885
|
+
getProfessionEmoji,
|
|
2886
|
+
getTargetStatTotal,
|
|
2887
|
+
hexToRgba,
|
|
2888
|
+
ingestLogPlayerData,
|
|
2889
|
+
isResUtilitySkill,
|
|
2890
|
+
normalizeConditionLabel,
|
|
2891
|
+
parseFightTimestamp,
|
|
2892
|
+
parseRollupSourcesFile,
|
|
2893
|
+
precomputeGlobalEnemySkillStats,
|
|
2894
|
+
removeRollupSources,
|
|
2895
|
+
resolveBuffMetaById,
|
|
2896
|
+
resolveConditionNameFromEntry,
|
|
2897
|
+
resolveDisruptionValue,
|
|
2898
|
+
resolveFightTimestamp,
|
|
2899
|
+
resolveProfessionLabel,
|
|
2900
|
+
toSuperscript,
|
|
2901
|
+
updateRollupSourcesForPublish
|
|
2902
|
+
});
|