@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.
Files changed (86) hide show
  1. package/dist/aggregationTypes.cjs +18 -0
  2. package/dist/aggregationTypes.d.cts +21 -0
  3. package/dist/aggregationTypes.d.ts +21 -0
  4. package/dist/aggregationTypes.js +1 -0
  5. package/dist/boonGeneration.cjs +308 -0
  6. package/dist/boonGeneration.d.cts +65 -0
  7. package/dist/boonGeneration.d.ts +65 -0
  8. package/dist/boonGeneration.js +16 -0
  9. package/dist/chunk-42DVJJLC.js +106 -0
  10. package/dist/chunk-4F55Q6K2.js +0 -0
  11. package/dist/chunk-ELKDB763.js +349 -0
  12. package/dist/chunk-FU74LJEM.js +77 -0
  13. package/dist/chunk-J3YCFY3C.js +37 -0
  14. package/dist/chunk-JP2ZL44R.js +18 -0
  15. package/dist/chunk-K2SRAMGC.js +0 -0
  16. package/dist/chunk-KRHODGVU.js +48 -0
  17. package/dist/chunk-LIGIXSSA.js +1383 -0
  18. package/dist/chunk-M2WR3JBQ.js +0 -0
  19. package/dist/chunk-PMVLNDZZ.js +279 -0
  20. package/dist/chunk-R5EJF5AW.js +147 -0
  21. package/dist/chunk-RGXSI3AI.js +298 -0
  22. package/dist/chunk-UFJJ6WLD.js +197 -0
  23. package/dist/chunk-WW5XFXGC.js +234 -0
  24. package/dist/chunk-ZFZS7JFU.js +10 -0
  25. package/dist/combatMetrics.cjs +251 -0
  26. package/dist/combatMetrics.d.cts +26 -0
  27. package/dist/combatMetrics.d.ts +26 -0
  28. package/dist/combatMetrics.js +21 -0
  29. package/dist/computePlayerAggregation.cjs +2042 -0
  30. package/dist/computePlayerAggregation.d.cts +249 -0
  31. package/dist/computePlayerAggregation.d.ts +249 -0
  32. package/dist/computePlayerAggregation.js +30 -0
  33. package/dist/conditionsMetrics.cjs +328 -0
  34. package/dist/conditionsMetrics.d.cts +67 -0
  35. package/dist/conditionsMetrics.d.ts +67 -0
  36. package/dist/conditionsMetrics.js +18 -0
  37. package/dist/constants.cjs +36 -0
  38. package/dist/constants.d.cts +18 -0
  39. package/dist/constants.d.ts +18 -0
  40. package/dist/constants.js +10 -0
  41. package/dist/dashboardMetrics.cjs +226 -0
  42. package/dist/dashboardMetrics.d.cts +29 -0
  43. package/dist/dashboardMetrics.d.ts +29 -0
  44. package/dist/dashboardMetrics.js +42 -0
  45. package/dist/dpsReportTypes.cjs +18 -0
  46. package/dist/dpsReportTypes.d.cts +294 -0
  47. package/dist/dpsReportTypes.d.ts +294 -0
  48. package/dist/dpsReportTypes.js +1 -0
  49. package/dist/index.cjs +2902 -0
  50. package/dist/index.d.cts +13 -0
  51. package/dist/index.d.ts +13 -0
  52. package/dist/index.js +152 -0
  53. package/dist/metrics-methods.json +38 -0
  54. package/dist/metricsSettings.cjs +75 -0
  55. package/dist/metricsSettings.d.cts +23 -0
  56. package/dist/metricsSettings.d.ts +23 -0
  57. package/dist/metricsSettings.js +8 -0
  58. package/dist/professionUtils.cjs +265 -0
  59. package/dist/professionUtils.d.cts +10 -0
  60. package/dist/professionUtils.d.ts +10 -0
  61. package/dist/professionUtils.js +20 -0
  62. package/dist/reportMetrics.cjs +224 -0
  63. package/dist/reportMetrics.d.cts +85 -0
  64. package/dist/reportMetrics.d.ts +85 -0
  65. package/dist/reportMetrics.js +12 -0
  66. package/dist/resUtility.cjs +52 -0
  67. package/dist/resUtility.d.cts +5 -0
  68. package/dist/resUtility.d.ts +5 -0
  69. package/dist/resUtility.js +8 -0
  70. package/dist/roles.cjs +18 -0
  71. package/dist/roles.d.cts +17 -0
  72. package/dist/roles.d.ts +17 -0
  73. package/dist/roles.js +1 -0
  74. package/dist/rollup.cjs +378 -0
  75. package/dist/rollup.d.cts +103 -0
  76. package/dist/rollup.d.ts +103 -0
  77. package/dist/rollup.js +16 -0
  78. package/dist/statsMetrics.cjs +153 -0
  79. package/dist/statsMetrics.d.cts +39 -0
  80. package/dist/statsMetrics.d.ts +39 -0
  81. package/dist/statsMetrics.js +22 -0
  82. package/dist/timestampUtils.cjs +63 -0
  83. package/dist/timestampUtils.d.cts +12 -0
  84. package/dist/timestampUtils.d.ts +12 -0
  85. package/dist/timestampUtils.js +9 -0
  86. package/package.json +44 -0
@@ -0,0 +1,1383 @@
1
+ import {
2
+ isResUtilitySkill
3
+ } from "./chunk-JP2ZL44R.js";
4
+ import {
5
+ resolveFightTimestamp
6
+ } from "./chunk-J3YCFY3C.js";
7
+ import {
8
+ DEFENSE_METRICS,
9
+ OFFENSE_METRICS,
10
+ SUPPORT_METRICS
11
+ } from "./chunk-42DVJJLC.js";
12
+ import {
13
+ getPlayerBlocked,
14
+ getPlayerBreakbarDamage,
15
+ getPlayerCleanses,
16
+ getPlayerDamageTaken,
17
+ getPlayerEvaded,
18
+ getPlayerMissed,
19
+ getPlayerOutgoingInterrupts,
20
+ getPlayerStrips,
21
+ getTargetStatTotal
22
+ } from "./chunk-FU74LJEM.js";
23
+ import {
24
+ applySquadStabilityGeneration,
25
+ computeDownContribution,
26
+ computeOutgoingCrowdControl,
27
+ computeSquadBarrier,
28
+ computeSquadHealing
29
+ } from "./chunk-R5EJF5AW.js";
30
+ import {
31
+ NON_DAMAGING_CONDITIONS,
32
+ buildConditionIconMap,
33
+ computeOutgoingConditions,
34
+ normalizeConditionLabel,
35
+ resolveBuffMetaById,
36
+ resolveConditionNameFromEntry
37
+ } from "./chunk-RGXSI3AI.js";
38
+ import {
39
+ PROFESSION_COLORS
40
+ } from "./chunk-WW5XFXGC.js";
41
+
42
+ // src/computePlayerAggregation.ts
43
+ var getFightDownsDeaths = (details) => {
44
+ const players = details?.players || [];
45
+ const squadPlayers = players.filter((p) => !p.notInSquad);
46
+ let squadDownsDeaths = 0;
47
+ let enemyDownsDeaths = 0;
48
+ let squadDeaths = 0;
49
+ let enemyDeaths = 0;
50
+ squadPlayers.forEach((p) => {
51
+ const defenses = p.defenses?.[0];
52
+ if (!defenses) return;
53
+ const downCount = Number(defenses.downCount || 0);
54
+ const deadCount = Number(defenses.deadCount || 0);
55
+ squadDownsDeaths += downCount + deadCount;
56
+ squadDeaths += deadCount;
57
+ });
58
+ squadPlayers.forEach((p) => {
59
+ if (!p.statsTargets || p.statsTargets.length === 0) return;
60
+ p.statsTargets.forEach((targetStats) => {
61
+ if (!Array.isArray(targetStats)) return;
62
+ targetStats.forEach((phaseStats) => {
63
+ if (!phaseStats) return;
64
+ const downed = Number(phaseStats.downed || 0);
65
+ const killed = Number(phaseStats.killed || 0);
66
+ enemyDownsDeaths += downed + killed;
67
+ enemyDeaths += killed;
68
+ });
69
+ });
70
+ });
71
+ return { squadDownsDeaths, enemyDownsDeaths, squadDeaths, enemyDeaths };
72
+ };
73
+ var getFightOutcome = (details) => {
74
+ const { squadDownsDeaths, enemyDownsDeaths } = getFightDownsDeaths(details);
75
+ if (squadDownsDeaths > 0 || enemyDownsDeaths > 0) {
76
+ return enemyDownsDeaths > squadDownsDeaths;
77
+ }
78
+ if (typeof details?.success === "boolean") return details.success;
79
+ return false;
80
+ };
81
+ var knownProfessionNames = new Set(Object.keys(PROFESSION_COLORS));
82
+ var knownProfessionList = Object.keys(PROFESSION_COLORS).filter((name) => name && name !== "Unknown").sort((a, b) => b.length - a.length);
83
+ var baseProfessionNames = [
84
+ "Guardian",
85
+ "Revenant",
86
+ "Warrior",
87
+ "Engineer",
88
+ "Ranger",
89
+ "Thief",
90
+ "Elementalist",
91
+ "Mesmer",
92
+ "Necromancer"
93
+ ];
94
+ var resolveProfessionLabel = (name) => {
95
+ if (!name) return "Unknown";
96
+ const cleaned = String(name).replace(/\s*\([^)]*\)\s*$/, "").replace(/\s*\[[^\]]*\]\s*$/, "").replace(/\s\d+$/, "").trim();
97
+ if (knownProfessionNames.has(cleaned)) return cleaned;
98
+ const lower = cleaned.toLowerCase();
99
+ for (const prof of knownProfessionList) {
100
+ if (lower.includes(prof.toLowerCase())) return prof;
101
+ }
102
+ const baseMatch = baseProfessionNames.find((prof) => lower.includes(prof.toLowerCase()));
103
+ return baseMatch || cleaned || "Unknown";
104
+ };
105
+ var supportTimeSanityFields = /* @__PURE__ */ new Set(["boonStripsTime", "condiCleanseTime", "condiCleanseTimeSelf"]);
106
+ var isBoon = (meta) => {
107
+ if (!meta?.classification) return true;
108
+ return meta.classification === "Boon";
109
+ };
110
+ var createMitigationTotals = () => ({
111
+ totalHits: 0,
112
+ blocked: 0,
113
+ evaded: 0,
114
+ glanced: 0,
115
+ missed: 0,
116
+ invulned: 0,
117
+ interrupted: 0,
118
+ totalMitigation: 0,
119
+ minMitigation: 0
120
+ });
121
+ var readNumber = (value) => {
122
+ const numeric = Number(value);
123
+ return Number.isFinite(numeric) ? numeric : 0;
124
+ };
125
+ var parseMitigationKey = (rawKey) => {
126
+ const parts = String(rawKey).split("|");
127
+ const name = parts[0] || "Unknown";
128
+ const profession = resolveProfessionLabel(parts[1] || "Unknown");
129
+ const account = parts[2] || name || "Unknown";
130
+ return { name, profession, account };
131
+ };
132
+ var ensureMitigationRow = (map, key, base) => {
133
+ let row = map.get(key);
134
+ if (!row) {
135
+ row = {
136
+ account: base.account,
137
+ name: base.name,
138
+ profession: base.profession,
139
+ professionList: base.profession ? [base.profession] : [],
140
+ activeMs: 0,
141
+ mitigationTotals: createMitigationTotals()
142
+ };
143
+ map.set(key, row);
144
+ } else if (base.profession && !row.professionList.includes(base.profession)) {
145
+ row.professionList.push(base.profession);
146
+ }
147
+ return row;
148
+ };
149
+ var ensureMitigationMinionRow = (map, key, base) => {
150
+ let row = map.get(key);
151
+ if (!row) {
152
+ row = {
153
+ account: base.account,
154
+ name: base.name,
155
+ profession: base.profession,
156
+ professionList: base.profession ? [base.profession] : [],
157
+ activeMs: 0,
158
+ mitigationTotals: createMitigationTotals(),
159
+ minion: base.minion
160
+ };
161
+ map.set(key, row);
162
+ } else if (base.profession && !row.professionList.includes(base.profession)) {
163
+ row.professionList.push(base.profession);
164
+ }
165
+ return row;
166
+ };
167
+ var addMitigationTotals = (totals, entry) => {
168
+ totals.totalHits += readNumber(entry?.skill_hits ?? entry?.skillHits);
169
+ totals.blocked += readNumber(entry?.blocked);
170
+ totals.evaded += readNumber(entry?.evaded);
171
+ totals.glanced += readNumber(entry?.glanced);
172
+ totals.missed += readNumber(entry?.missed);
173
+ totals.invulned += readNumber(entry?.invulned);
174
+ totals.interrupted += readNumber(entry?.interrupted);
175
+ totals.totalMitigation += readNumber(entry?.avoided_damage ?? entry?.avoidedDamage);
176
+ totals.minMitigation += readNumber(entry?.min_avoided_damage ?? entry?.minAvoidedDamage);
177
+ };
178
+ var resolveGlobalEnemyStats = (globalEnemySkillStats, skillId) => {
179
+ const bucket = globalEnemySkillStats.get(skillId);
180
+ if (!bucket) {
181
+ return { hasSkill: false, avg: 1, min: 1, hits: 0 };
182
+ }
183
+ const hits = bucket.connectedHits || 0;
184
+ const avg = hits > 0 ? bucket.totalDamage / hits : 0;
185
+ const min = bucket.minCount > 0 ? bucket.minTotal / bucket.minCount : 0;
186
+ return { hasSkill: true, avg, min, hits };
187
+ };
188
+ var updateCumulativeCounts = (map, key, delta) => {
189
+ const existing = map.get(key) || createMitigationTotals();
190
+ existing.totalHits += delta.totalHits;
191
+ existing.blocked += delta.blocked;
192
+ existing.evaded += delta.evaded;
193
+ existing.glanced += delta.glanced;
194
+ existing.missed += delta.missed;
195
+ existing.invulned += delta.invulned;
196
+ existing.interrupted += delta.interrupted;
197
+ map.set(key, existing);
198
+ return existing;
199
+ };
200
+ var getPlayerIdentity = (player, splitPlayersByClass) => {
201
+ const baseAccount = player?.account && player.account !== "Unknown" ? player.account : player?.name || "Unknown";
202
+ const profession = resolveProfessionLabel(player?.profession || "Unknown");
203
+ const isSplit = splitPlayersByClass && profession !== "Unknown";
204
+ const key = isSplit ? `${baseAccount}::${profession}` : baseAccount;
205
+ const accountLabel = baseAccount;
206
+ return { key, baseAccount, profession, accountLabel, isSplit };
207
+ };
208
+ var getMitigationIdentity = (account, profession, splitPlayersByClass) => {
209
+ const resolvedProfession = resolveProfessionLabel(profession || "Unknown");
210
+ const isSplit = splitPlayersByClass && resolvedProfession !== "Unknown";
211
+ const key = isSplit ? `${account}::${resolvedProfession}` : account;
212
+ const accountLabel = account;
213
+ return { key, accountLabel, profession: resolvedProfession };
214
+ };
215
+ var normalizeStatePairs = (states) => {
216
+ if (!Array.isArray(states)) return [];
217
+ return states.map((entry) => {
218
+ if (Array.isArray(entry)) return [Number(entry[0]), Number(entry[1])];
219
+ if (entry && typeof entry === "object") return [Number(entry.time), Number(entry.value)];
220
+ return null;
221
+ }).filter(
222
+ (entry) => !!entry && Number.isFinite(entry[0]) && Number.isFinite(entry[1]) && entry[0] >= 0
223
+ ).sort((a, b) => a[0] - b[0]);
224
+ };
225
+ var resolveNonStackingFactor = (rawValue) => {
226
+ const safeValue = Math.max(0, Number(rawValue || 0));
227
+ if (!Number.isFinite(safeValue) || safeValue <= 0) return 0;
228
+ if (safeValue <= 1) return safeValue;
229
+ if (safeValue <= 100) return safeValue / 100;
230
+ return 1;
231
+ };
232
+ var integrateSourceStates = (states, durationMs, stacking) => {
233
+ const normalized = normalizeStatePairs(states);
234
+ if (normalized.length === 0 || durationMs <= 0) return { totalMs: 0, uptimeMs: 0 };
235
+ const resolvedDurationMs = Math.max(1, Number(durationMs || 0));
236
+ const clampTime = (value) => Math.max(0, Math.min(resolvedDurationMs, Number(value || 0)));
237
+ let totalMs = 0;
238
+ let uptimeMs = 0;
239
+ const addSegment = (startMs, endMs, value) => {
240
+ const start = clampTime(startMs);
241
+ const end = clampTime(endMs);
242
+ if (end <= start) return;
243
+ const segmentMs = Math.max(0, end - start);
244
+ if (segmentMs <= 0) return;
245
+ const safeValue = Math.max(0, Number(value || 0));
246
+ if (safeValue <= 0) return;
247
+ uptimeMs += segmentMs;
248
+ if (stacking) {
249
+ totalMs += segmentMs * safeValue;
250
+ } else {
251
+ totalMs += segmentMs * resolveNonStackingFactor(safeValue);
252
+ }
253
+ };
254
+ let prevTime = clampTime(normalized[0][0]);
255
+ let prevValue = Math.max(0, Number(normalized[0][1] || 0));
256
+ for (let idx = 1; idx < normalized.length; idx += 1) {
257
+ const [timeMs, rawValue] = normalized[idx];
258
+ const currentTime = clampTime(timeMs);
259
+ addSegment(prevTime, currentTime, prevValue);
260
+ prevTime = currentTime;
261
+ prevValue = Math.max(0, Number(rawValue || 0));
262
+ }
263
+ addSegment(prevTime, resolvedDurationMs, prevValue);
264
+ return { totalMs, uptimeMs };
265
+ };
266
+ var ensureSpecialBuffEntry = (map, buffId, key, account, profession) => {
267
+ if (!map.has(buffId)) {
268
+ map.set(buffId, /* @__PURE__ */ new Map());
269
+ }
270
+ const bucket = map.get(buffId);
271
+ let agg = bucket.get(key);
272
+ if (!agg) {
273
+ agg = {
274
+ key,
275
+ account,
276
+ profession: profession || "Unknown",
277
+ professions: /* @__PURE__ */ new Set(),
278
+ professionTimeMs: {},
279
+ totalMs: 0,
280
+ uptimeMs: 0,
281
+ durationMs: 0
282
+ };
283
+ bucket.set(key, agg);
284
+ }
285
+ return agg;
286
+ };
287
+ var createPlayerAggregationAccumulators = () => ({
288
+ playerStats: /* @__PURE__ */ new Map(),
289
+ skillDamageMap: {},
290
+ incomingSkillDamageMap: {},
291
+ playerSkillBreakdownMap: /* @__PURE__ */ new Map(),
292
+ healingBreakdownMap: /* @__PURE__ */ new Map(),
293
+ outgoingCondiTotals: {},
294
+ incomingCondiTotals: {},
295
+ enemyProfessionCounts: {},
296
+ specialBuffMeta: /* @__PURE__ */ new Map(),
297
+ specialBuffAgg: /* @__PURE__ */ new Map(),
298
+ specialBuffOutputAgg: /* @__PURE__ */ new Map(),
299
+ damageMitigationPlayersMap: /* @__PURE__ */ new Map(),
300
+ damageMitigationMinionsMap: /* @__PURE__ */ new Map(),
301
+ globalEnemySkillStats: /* @__PURE__ */ new Map(),
302
+ mitigationCumulativeCounts: /* @__PURE__ */ new Map(),
303
+ mitigationMinionCumulativeCounts: /* @__PURE__ */ new Map(),
304
+ wins: 0,
305
+ losses: 0,
306
+ totalSquadSizeAccum: 0,
307
+ totalEnemiesAccum: 0,
308
+ totalSquadDeaths: 0,
309
+ totalSquadKills: 0,
310
+ totalEnemyDeaths: 0,
311
+ totalEnemyKills: 0,
312
+ totalSquadDowns: 0,
313
+ totalEnemyDowns: 0
314
+ });
315
+ var precomputeGlobalEnemySkillStats = (log, acc) => {
316
+ const details = log.details;
317
+ if (!details) return;
318
+ const targets = details.targets || [];
319
+ targets.forEach((target) => {
320
+ const dist = target?.totalDamageDist || [];
321
+ const list = Array.isArray(dist) ? dist[0] : null;
322
+ list?.forEach((entry) => {
323
+ if (!entry?.id) return;
324
+ const skillId = Number(entry.id);
325
+ const globalBucket = acc.globalEnemySkillStats.get(skillId) || { totalDamage: 0, connectedHits: 0, minTotal: 0, minCount: 0 };
326
+ globalBucket.totalDamage += Number(entry.totalDamage || 0);
327
+ globalBucket.connectedHits += Number(entry.connectedHits || 0);
328
+ const minValueGlobal = Number.isFinite(Number(entry.min)) ? Number(entry.min) : 0;
329
+ globalBucket.minTotal += minValueGlobal;
330
+ globalBucket.minCount += 1;
331
+ acc.globalEnemySkillStats.set(skillId, globalBucket);
332
+ });
333
+ });
334
+ };
335
+ var ingestLogPlayerData = (log, acc, options) => {
336
+ const { method, skillDamageSource, splitPlayersByClass } = options;
337
+ const details = log.details;
338
+ if (!details) return;
339
+ const players = details.players;
340
+ const squadPlayers = players.filter((player) => !player?.notInSquad);
341
+ const allPlayers = Array.isArray(players) ? players : [];
342
+ const specialBuffSourceIdentityByName = /* @__PURE__ */ new Map();
343
+ const specialBuffSourceIdentityByInstanceId = /* @__PURE__ */ new Map();
344
+ const specialBuffSourceActiveMsByKey = /* @__PURE__ */ new Map();
345
+ const specialBuffOutputDurationSeen = /* @__PURE__ */ new Set();
346
+ const registerSpecialSourceIdentity = (value, identity) => {
347
+ const normalized = String(value || "").trim().toLowerCase();
348
+ if (!normalized) return;
349
+ if (!specialBuffSourceIdentityByName.has(normalized)) {
350
+ specialBuffSourceIdentityByName.set(normalized, identity);
351
+ }
352
+ };
353
+ allPlayers.forEach((player) => {
354
+ const identity = getPlayerIdentity(player, splitPlayersByClass);
355
+ const sourceActiveMs = Array.isArray(player?.activeTimes) && typeof player.activeTimes[0] === "number" ? Number(player.activeTimes[0] || 0) : Number(details?.durationMS || 0);
356
+ specialBuffSourceActiveMsByKey.set(identity.key, Math.max(0, sourceActiveMs));
357
+ const instanceId = Number(player?.instanceID);
358
+ if (Number.isFinite(instanceId) && instanceId > 0 && !specialBuffSourceIdentityByInstanceId.has(instanceId)) {
359
+ specialBuffSourceIdentityByInstanceId.set(instanceId, identity);
360
+ }
361
+ [player?.name, player?.display_name, player?.character_name, player?.account].forEach((candidate) => {
362
+ registerSpecialSourceIdentity(candidate, identity);
363
+ });
364
+ });
365
+ const targets = details.targets || [];
366
+ const replayMeta = details.combatReplayMetaData || {};
367
+ const inchesToPixel = replayMeta?.inchToPixel > 0 ? replayMeta.inchToPixel : 1;
368
+ const pollingRate = replayMeta?.pollingRate > 0 ? replayMeta.pollingRate : 1;
369
+ const RUN_BACK_RANGE = 5e3;
370
+ const toPairs = (value) => {
371
+ if (!Array.isArray(value)) return [];
372
+ return value.map((entry) => Array.isArray(entry) ? [Number(entry[0]), Number(entry[1])] : null).filter((entry) => !!entry && Number.isFinite(entry[0]));
373
+ };
374
+ let commanderTagPositions = [];
375
+ let deadTagMark = details.durationMS || 0;
376
+ let deadTag = false;
377
+ const commanderPlayer = players.find((player) => player?.hasCommanderTag && !player?.notInSquad);
378
+ if (commanderPlayer?.combatReplayData?.positions) {
379
+ commanderTagPositions = commanderPlayer.combatReplayData.positions;
380
+ const commanderDeaths = toPairs(commanderPlayer.combatReplayData.dead);
381
+ commanderDeaths.forEach(([deathTime]) => {
382
+ if (deathTime > 0) {
383
+ deadTag = true;
384
+ deadTagMark = Math.min(deadTagMark, deathTime);
385
+ }
386
+ });
387
+ }
388
+ const getDistanceToTag = (p) => {
389
+ const stats = p.statsAll?.[0];
390
+ if (stats?.distToCom !== void 0 && stats?.distToCom !== "Infinity") return Math.round(Number(stats.distToCom));
391
+ if (stats?.stackDist !== void 0) return Math.round(Number(stats.stackDist)) || 0;
392
+ if (p.hasCommanderTag) return 0;
393
+ const combatData = p.combatReplayData;
394
+ if (!combatData?.positions || !commanderTagPositions.length) return 0;
395
+ const playerPositions = combatData.positions;
396
+ const playerDeaths = toPairs(combatData.dead);
397
+ const playerDowns = toPairs(combatData.down);
398
+ const playerOffset = Math.floor((combatData.start || 0) / pollingRate);
399
+ let playerDistToTag = 0;
400
+ if (playerDeaths.length && playerDowns.length) {
401
+ for (const [deathKey] of playerDeaths) {
402
+ if (deathKey < 0) continue;
403
+ const positionMark = Math.max(0, Math.floor(deathKey / pollingRate)) - playerOffset;
404
+ for (const [downKey, downValue] of playerDowns) {
405
+ if (deathKey !== downValue) continue;
406
+ const playerDeadPoll = deadTag && downKey > deadTagMark ? Math.max(1, Math.floor(deadTagMark / pollingRate)) : positionMark;
407
+ const limit = Math.max(0, Math.min(playerDeadPoll, playerPositions.length, commanderTagPositions.length));
408
+ if (limit <= 0) continue;
409
+ let sum = 0;
410
+ for (let i = 0; i < limit; i++) {
411
+ const [px, py] = playerPositions[i];
412
+ const [tx, ty] = commanderTagPositions[i];
413
+ sum += Math.hypot(px - tx, py - ty);
414
+ }
415
+ playerDistToTag = Math.round(sum / limit / inchesToPixel);
416
+ }
417
+ }
418
+ }
419
+ return playerDistToTag;
420
+ };
421
+ acc.totalSquadSizeAccum += squadPlayers.length;
422
+ acc.totalEnemiesAccum += targets.filter((t) => !t.isFake).length;
423
+ applySquadStabilityGeneration(players, { durationMS: details.durationMS, buffMap: details.buffMap });
424
+ const { squadDownsDeaths, enemyDownsDeaths, squadDeaths, enemyDeaths } = getFightDownsDeaths(details);
425
+ acc.totalSquadDeaths += squadDeaths;
426
+ acc.totalSquadKills += enemyDeaths;
427
+ acc.totalEnemyKills += squadDeaths;
428
+ acc.totalEnemyDeaths += enemyDeaths;
429
+ acc.totalSquadDowns += Math.max(0, squadDownsDeaths - squadDeaths);
430
+ acc.totalEnemyDowns += Math.max(0, enemyDownsDeaths - enemyDeaths);
431
+ const isWin = getFightOutcome(details);
432
+ if (isWin) acc.wins++;
433
+ else acc.losses++;
434
+ const battleStandardSkillId = details.skillMap ? Number(Object.keys(details.skillMap).find((key) => details.skillMap?.[key]?.name === "Battle Standard")?.replace(/^s/, "")) : null;
435
+ const healAddonCharacters = /* @__PURE__ */ new Set();
436
+ const usedExtensions = Array.isArray(details.usedExtensions) ? details.usedExtensions : [];
437
+ usedExtensions.forEach((ext) => {
438
+ if (ext?.name !== "Healing Stats") return;
439
+ const running = Array.isArray(ext.runningExtension) ? ext.runningExtension : [];
440
+ running.forEach((charName) => {
441
+ if (typeof charName === "string" && charName.length > 0) healAddonCharacters.add(charName);
442
+ });
443
+ });
444
+ players.forEach((p, playerIndex) => {
445
+ if (p.notInSquad) return;
446
+ const identity = getPlayerIdentity(p, splitPlayersByClass);
447
+ const key = identity.key;
448
+ const name = p.name || "Unknown";
449
+ if (!acc.playerStats.has(key)) {
450
+ acc.playerStats.set(key, {
451
+ name,
452
+ account: identity.accountLabel,
453
+ characterNames: /* @__PURE__ */ new Set(),
454
+ downContrib: 0,
455
+ cleanses: 0,
456
+ strips: 0,
457
+ stab: 0,
458
+ healing: 0,
459
+ barrier: 0,
460
+ cc: 0,
461
+ interrupts: 0,
462
+ logsJoined: 0,
463
+ totalDist: 0,
464
+ distCount: 0,
465
+ stackedLogCount: 0,
466
+ dodges: 0,
467
+ downs: 0,
468
+ deaths: 0,
469
+ kills: 0,
470
+ enemyDowns: 0,
471
+ damageTaken: 0,
472
+ breakbar: 0,
473
+ blocks: 0,
474
+ evades: 0,
475
+ misses: 0,
476
+ totalFightMs: 0,
477
+ offenseTotals: {},
478
+ offenseRateWeights: {},
479
+ defenseActiveMs: 0,
480
+ defenseTotals: {},
481
+ defenseMinionDamageTaken: {},
482
+ supportActiveMs: 0,
483
+ supportTotals: {},
484
+ healingActiveMs: 0,
485
+ healingTotals: {},
486
+ hasHealAddon: false,
487
+ profession: identity.profession,
488
+ professions: /* @__PURE__ */ new Set(),
489
+ professionTimeMs: {},
490
+ squadActiveMs: 0,
491
+ firstSeenFightTs: 0,
492
+ lastSeenFightTs: 0,
493
+ lastSeenFightDurationMs: 0,
494
+ isCommander: false,
495
+ damage: 0,
496
+ dps: 0,
497
+ revives: 0,
498
+ outgoingConditions: {},
499
+ incomingConditions: {},
500
+ damageModTotals: {},
501
+ incomingDamageModTotals: {},
502
+ roleClassification: { role: "damage", supportScore: 0, confidenceScore: 0, threshold: 0, factors: [] }
503
+ });
504
+ }
505
+ const s = acc.playerStats.get(key);
506
+ if (p.hasCommanderTag) s.isCommander = true;
507
+ if (name && name !== "Unknown") s.characterNames.add(String(name));
508
+ if (p.profession && p.profession !== "Unknown") {
509
+ s.profession = p.profession;
510
+ s.professions.add(p.profession);
511
+ const activeMs2 = Array.isArray(p.activeTimes) && typeof p.activeTimes[0] === "number" ? p.activeTimes[0] : details.durationMS || 0;
512
+ s.professionTimeMs[p.profession] = (s.professionTimeMs[p.profession] || 0) + activeMs2;
513
+ }
514
+ s.logsJoined++;
515
+ s.downContrib += computeDownContribution(p);
516
+ s.cleanses += getPlayerCleanses(p);
517
+ s.strips += getPlayerStrips(p, method);
518
+ s.healing += computeSquadHealing(p);
519
+ s.barrier += computeSquadBarrier(p);
520
+ s.cc += computeOutgoingCrowdControl(p, method);
521
+ s.interrupts += getPlayerOutgoingInterrupts(p);
522
+ s.stab += p.stabGeneration || 0;
523
+ const dist = getDistanceToTag(p);
524
+ if (dist <= RUN_BACK_RANGE) {
525
+ s.totalDist += dist;
526
+ s.distCount++;
527
+ }
528
+ if (dist <= 600) {
529
+ s.stackedLogCount++;
530
+ }
531
+ if (p.defenses?.[0]) {
532
+ s.dodges += p.defenses[0].dodgeCount || 0;
533
+ s.downs += p.defenses[0].downCount || 0;
534
+ s.deaths += p.defenses[0].deadCount || 0;
535
+ }
536
+ s.damageTaken += getPlayerDamageTaken(p);
537
+ s.breakbar += getPlayerBreakbarDamage(p);
538
+ s.blocks += getPlayerBlocked(p);
539
+ s.evades += getPlayerEvaded(p);
540
+ s.misses += getPlayerMissed(p);
541
+ s.kills += getTargetStatTotal(p, "killed");
542
+ s.enemyDowns += getTargetStatTotal(p, "downed");
543
+ if (details.durationMS) s.totalFightMs += details.durationMS;
544
+ const activeMs = Array.isArray(p.activeTimes) && typeof p.activeTimes[0] === "number" ? p.activeTimes[0] : details.durationMS || 0;
545
+ s.squadActiveMs += activeMs;
546
+ const fightTs = resolveFightTimestamp(details, log);
547
+ const fightDurationMs = Number(details?.durationMS || 0);
548
+ if (fightTs > 0) {
549
+ if (s.firstSeenFightTs <= 0 || fightTs < s.firstSeenFightTs) {
550
+ s.firstSeenFightTs = fightTs;
551
+ }
552
+ if (s.lastSeenFightTs <= 0 || fightTs > s.lastSeenFightTs) {
553
+ s.lastSeenFightTs = fightTs;
554
+ s.lastSeenFightDurationMs = fightDurationMs;
555
+ } else if (fightTs === s.lastSeenFightTs && fightDurationMs > s.lastSeenFightDurationMs) {
556
+ s.lastSeenFightDurationMs = fightDurationMs;
557
+ }
558
+ }
559
+ s.defenseActiveMs += activeMs;
560
+ s.supportActiveMs += activeMs;
561
+ s.healingActiveMs += activeMs;
562
+ if (Array.isArray(p.buffUptimes) && p.buffUptimes.length > 0) {
563
+ const nonDamagingBuffs = p.buffUptimes.filter((buff) => {
564
+ if (typeof buff?.id !== "number") return false;
565
+ const buffId = `b${buff.id}`;
566
+ const meta = details.buffMap?.[buffId] || details.buffMap?.[String(buff.id)];
567
+ if (!meta || isBoon(meta)) return false;
568
+ return true;
569
+ });
570
+ nonDamagingBuffs.forEach((buff) => {
571
+ if (typeof buff?.id !== "number") return;
572
+ const buffId = `b${buff.id}`;
573
+ const meta = details.buffMap?.[buffId] || details.buffMap?.[String(buff.id)];
574
+ const uptime = Number(buff.buffData?.[0]?.uptime ?? 0);
575
+ const presence = Number(buff.buffData?.[0]?.presence ?? 0);
576
+ if ((!Number.isFinite(uptime) || uptime <= 0) && (!Number.isFinite(presence) || presence <= 0)) return;
577
+ const stacking = meta?.stacking ?? false;
578
+ const uptimeFactor = stacking ? Number.isFinite(uptime) ? uptime : 0 : Number.isFinite(uptime) ? uptime / 100 : 0;
579
+ const totalMs = uptimeFactor * activeMs;
580
+ const uptimePercentRaw = stacking ? presence : uptime;
581
+ const uptimePercentFactor = Math.min(Math.max(Number.isFinite(uptimePercentRaw) ? uptimePercentRaw : 0, 0), 100) / 100;
582
+ const uptimeMs = uptimePercentFactor * activeMs;
583
+ const hasTotalMs = Number.isFinite(totalMs) && totalMs > 0;
584
+ const hasUptimeMs = Number.isFinite(uptimeMs) && uptimeMs > 0;
585
+ if (!hasTotalMs && !hasUptimeMs) return;
586
+ const conditionName = normalizeConditionLabel(meta?.name);
587
+ if (conditionName && NON_DAMAGING_CONDITIONS.has(conditionName) && (!meta?.classification || meta.classification === "Condition")) {
588
+ const seconds = totalMs / 1e3;
589
+ const conditionIcon = meta?.icon;
590
+ const incomingSummary = acc.incomingCondiTotals[conditionName] || {
591
+ name: conditionName,
592
+ icon: conditionIcon,
593
+ applications: 0,
594
+ damage: 0
595
+ };
596
+ incomingSummary.applicationsFromUptime = (incomingSummary.applicationsFromUptime || 0) + seconds;
597
+ if (!incomingSummary.icon && conditionIcon) incomingSummary.icon = conditionIcon;
598
+ acc.incomingCondiTotals[conditionName] = incomingSummary;
599
+ const incomingEntry = s.incomingConditions[conditionName] || {
600
+ applications: 0,
601
+ damage: 0,
602
+ skills: {},
603
+ icon: conditionIcon
604
+ };
605
+ incomingEntry.applicationsFromUptime = (incomingEntry.applicationsFromUptime || 0) + seconds;
606
+ if (!incomingEntry.icon && conditionIcon) incomingEntry.icon = conditionIcon;
607
+ s.incomingConditions[conditionName] = incomingEntry;
608
+ }
609
+ if (!acc.specialBuffMeta.has(buffId)) {
610
+ acc.specialBuffMeta.set(buffId, { name: meta?.name, stacking, icon: meta?.icon });
611
+ }
612
+ const specialBucketKey = identity.key;
613
+ const agg = ensureSpecialBuffEntry(
614
+ acc.specialBuffAgg,
615
+ buffId,
616
+ specialBucketKey,
617
+ identity.accountLabel,
618
+ s.profession || p.profession || "Unknown"
619
+ );
620
+ const prof = p.profession || s.profession;
621
+ if (prof && prof !== "Unknown") {
622
+ agg.professions.add(prof);
623
+ agg.professionTimeMs[prof] = (agg.professionTimeMs[prof] || 0) + activeMs;
624
+ agg.profession = prof;
625
+ }
626
+ agg.totalMs += hasTotalMs ? totalMs : 0;
627
+ agg.uptimeMs += hasUptimeMs ? uptimeMs : 0;
628
+ agg.durationMs += activeMs;
629
+ const statesPerSource = buff?.statesPerSource && typeof buff.statesPerSource === "object" ? buff.statesPerSource : null;
630
+ if (!statesPerSource || Object.keys(statesPerSource).length === 0) {
631
+ } else {
632
+ Object.entries(statesPerSource).forEach(([sourceName, states]) => {
633
+ const sourceNameText = String(sourceName || "").trim();
634
+ const sourceNameLower = sourceNameText.toLowerCase();
635
+ const instanceMatch = sourceNameText.match(/\bpl-(\d+)\b/i);
636
+ const sourceIdentity = specialBuffSourceIdentityByName.get(sourceNameLower) || (instanceMatch ? specialBuffSourceIdentityByInstanceId.get(Number(instanceMatch[1])) : void 0) || {
637
+ key: `source:${sourceNameLower || "unknown"}`,
638
+ accountLabel: sourceNameText || "Unknown Source",
639
+ profession: resolveProfessionLabel(sourceNameText.replace(/\s+pl-\d+\s*$/i, "").trim() || "Unknown")
640
+ };
641
+ const sourceDurationMs = Number(specialBuffSourceActiveMsByKey.get(sourceIdentity.key) || details?.durationMS || 0);
642
+ if (!Number.isFinite(sourceDurationMs) || sourceDurationMs <= 0) return;
643
+ const sourceContribution = integrateSourceStates(states, sourceDurationMs, stacking);
644
+ const hasSourceTotal = Number.isFinite(sourceContribution.totalMs) && sourceContribution.totalMs > 0;
645
+ const hasSourceUptime = Number.isFinite(sourceContribution.uptimeMs) && sourceContribution.uptimeMs > 0;
646
+ if (!hasSourceTotal && !hasSourceUptime) return;
647
+ const sourceAgg = ensureSpecialBuffEntry(
648
+ acc.specialBuffOutputAgg,
649
+ buffId,
650
+ sourceIdentity.key,
651
+ sourceIdentity.accountLabel,
652
+ sourceIdentity.profession
653
+ );
654
+ const sourceProf = sourceIdentity.profession || "Unknown";
655
+ if (sourceProf && sourceProf !== "Unknown") {
656
+ sourceAgg.professions.add(sourceProf);
657
+ sourceAgg.profession = sourceProf;
658
+ }
659
+ sourceAgg.totalMs += hasSourceTotal ? sourceContribution.totalMs : 0;
660
+ sourceAgg.uptimeMs += hasSourceUptime ? sourceContribution.uptimeMs : 0;
661
+ const durationSeenKey = `${buffId}::${sourceIdentity.key}`;
662
+ if (!specialBuffOutputDurationSeen.has(durationSeenKey)) {
663
+ specialBuffOutputDurationSeen.add(durationSeenKey);
664
+ if (sourceProf && sourceProf !== "Unknown") {
665
+ sourceAgg.professionTimeMs[sourceProf] = (sourceAgg.professionTimeMs[sourceProf] || 0) + sourceDurationMs;
666
+ }
667
+ sourceAgg.durationMs += sourceDurationMs;
668
+ }
669
+ });
670
+ }
671
+ });
672
+ }
673
+ if (p.defenses?.[0]) {
674
+ const normalizeMinionName = (rawName) => {
675
+ let minionName = String(rawName || "Unknown").replace(/^Juvenile\s+/i, "") || "Unknown";
676
+ if (minionName.toUpperCase().includes("UNKNOWN")) minionName = "Unknown";
677
+ return minionName;
678
+ };
679
+ const getMinionDamageTaken = () => {
680
+ const minions = Array.isArray(p?.minions) ? p.minions : [];
681
+ if (minions.length === 0) return { total: 0, byMinion: {} };
682
+ let total = 0;
683
+ const byMinion = {};
684
+ minions.forEach((minion) => {
685
+ const dist2 = Array.isArray(minion?.totalDamageTakenDist) ? minion.totalDamageTakenDist : [];
686
+ const entries = Array.isArray(dist2[0]) ? dist2[0] : [];
687
+ const minionName = normalizeMinionName(minion?.name);
688
+ entries.forEach((entry) => {
689
+ const damage = Number(entry?.totalDamage ?? entry?.damageTaken ?? 0);
690
+ if (Number.isFinite(damage)) {
691
+ total += damage;
692
+ byMinion[minionName] = (byMinion[minionName] || 0) + damage;
693
+ }
694
+ });
695
+ });
696
+ return { total, byMinion };
697
+ };
698
+ const minionDamage = getMinionDamageTaken();
699
+ if (minionDamage.byMinion && typeof minionDamage.byMinion === "object") {
700
+ Object.entries(minionDamage.byMinion).forEach(([minionName, damage]) => {
701
+ const numeric = Number(damage || 0);
702
+ if (!Number.isFinite(numeric) || numeric <= 0) return;
703
+ s.defenseMinionDamageTaken[minionName] = (s.defenseMinionDamageTaken[minionName] || 0) + numeric;
704
+ });
705
+ }
706
+ DEFENSE_METRICS.forEach((m) => {
707
+ const val = m.id === "minionDamageTaken" ? minionDamage.total : Number(p.defenses[0][m.field] ?? 0);
708
+ if (Number.isFinite(val)) s.defenseTotals[m.id] = (s.defenseTotals[m.id] || 0) + val;
709
+ });
710
+ }
711
+ if (p.support?.[0]) {
712
+ SUPPORT_METRICS.forEach((m) => {
713
+ if (m.id === "boonStrips") return;
714
+ let val = Number(p.support[0][m.field] ?? 0);
715
+ if (Number.isFinite(val)) {
716
+ if (supportTimeSanityFields.has(m.field) && val > 999999) val = 0;
717
+ s.supportTotals[m.id] = (s.supportTotals[m.id] || 0) + val;
718
+ }
719
+ });
720
+ s.supportTotals.boonStrips = (s.supportTotals.boonStrips || 0) + getPlayerStrips(p, method);
721
+ }
722
+ const addHealing = (key2, val) => {
723
+ if (Number.isFinite(val)) s.healingTotals[key2] = (s.healingTotals[key2] || 0) + val;
724
+ };
725
+ const sumPhaseValue = (phases, field) => {
726
+ if (!Array.isArray(phases)) return 0;
727
+ return phases.reduce((sum, phase) => sum + Number(phase?.[field] ?? 0), 0);
728
+ };
729
+ const addHealingByCategory = (fieldBase, allyIdx, value) => {
730
+ if (!Number.isFinite(value) || value <= 0) return;
731
+ const ally = players[allyIdx];
732
+ const isSelf = allyIdx === playerIndex;
733
+ const isOffSquad = ally?.notInSquad;
734
+ const isSquad = !isOffSquad;
735
+ const isGroup = isSquad && ally?.group != null && ally.group === p.group;
736
+ addHealing(fieldBase, value);
737
+ if (isSquad) addHealing(`squad${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
738
+ if (isGroup) addHealing(`group${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
739
+ if (isSelf) addHealing(`self${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
740
+ if (isOffSquad) addHealing(`offSquad${fieldBase[0].toUpperCase()}${fieldBase.slice(1)}`, value);
741
+ };
742
+ if (Array.isArray(p.rotation)) {
743
+ let resCasts = 0;
744
+ p.rotation.forEach((rot) => {
745
+ if (!rot?.id || !isResUtilitySkill(rot.id, details.skillMap)) return;
746
+ const count = rot.skills?.length || 0;
747
+ if (count > 0) {
748
+ resCasts += count;
749
+ addHealing(`resUtility_s${rot.id}`, count);
750
+ }
751
+ });
752
+ if (resCasts > 0) addHealing("resUtility", resCasts);
753
+ }
754
+ const outgoingHealingAllies = p.extHealingStats?.outgoingHealingAllies;
755
+ if (Array.isArray(outgoingHealingAllies)) {
756
+ outgoingHealingAllies.forEach((allyPhases, allyIdx) => {
757
+ const healing = sumPhaseValue(allyPhases, "healing");
758
+ const downedHealing = sumPhaseValue(allyPhases, "downedHealing");
759
+ addHealingByCategory("healing", allyIdx, healing);
760
+ addHealingByCategory("downedHealing", allyIdx, downedHealing);
761
+ });
762
+ }
763
+ const outgoingBarrierAllies = p.extBarrierStats?.outgoingBarrierAllies;
764
+ if (Array.isArray(outgoingBarrierAllies)) {
765
+ outgoingBarrierAllies.forEach((allyPhases, allyIdx) => {
766
+ const barrier = sumPhaseValue(allyPhases, "barrier");
767
+ addHealingByCategory("barrier", allyIdx, barrier);
768
+ });
769
+ }
770
+ const statsAll = p.statsAll?.[0];
771
+ const dpsAll = p.dpsAll?.[0];
772
+ const support = p.support?.[0];
773
+ OFFENSE_METRICS.forEach((m) => {
774
+ if (m.id === "downContributionPercent" || m.id === "downContribution" || m.id === "boonStrips") return;
775
+ if (!m.field) return;
776
+ let val = 0;
777
+ let denom = 0;
778
+ if (m.source === "dpsAll" && dpsAll) val = Number(dpsAll[m.field] ?? 0);
779
+ else if (m.source === "statsAll" && statsAll) {
780
+ val = Number(statsAll[m.field] ?? 0);
781
+ denom = Number(statsAll[m.denomField || m.weightField || "connectedDamageCount"] ?? 0);
782
+ } else if (m.source === "support" && support) {
783
+ val = Number(support[m.field] ?? 0);
784
+ } else if (p.statsTargets) {
785
+ p.statsTargets.forEach((t) => {
786
+ if (!t?.[0]) return;
787
+ val += Number(t[0][m.field] ?? 0);
788
+ denom += Number(t[0][m.denomField || m.weightField || "connectedDamageCount"] ?? 0);
789
+ });
790
+ }
791
+ if (Number.isFinite(val)) {
792
+ s.offenseTotals[m.id] = (s.offenseTotals[m.id] || 0) + val;
793
+ if (m.isRate && denom > 0) s.offenseRateWeights[m.id] = (s.offenseRateWeights[m.id] || 0) + denom;
794
+ }
795
+ });
796
+ s.offenseTotals.downContribution = (s.offenseTotals.downContribution || 0) + computeDownContribution(p);
797
+ s.offenseTotals.boonStrips = (s.offenseTotals.boonStrips || 0) + getPlayerStrips(p, method);
798
+ if (p.targetDamageDist && Number.isFinite(battleStandardSkillId)) {
799
+ const connectedHits = p.targetDamageDist.flatMap((group) => group || []).flatMap((list) => list || []).reduce((sum, entry) => {
800
+ if (!entry?.id) return sum;
801
+ return entry.id === battleStandardSkillId ? sum + (entry?.connectedHits || 0) : sum;
802
+ }, 0);
803
+ s.offenseTotals.battleStandardHits = (s.offenseTotals.battleStandardHits || 0) + connectedHits;
804
+ }
805
+ s.revives += p.support?.[0]?.resurrects || 0;
806
+ if (dpsAll) {
807
+ s.damage += dpsAll.damage || 0;
808
+ s.dps += dpsAll.dps || 0;
809
+ }
810
+ const resolveSkillMeta = (entry) => {
811
+ let name2 = `Skill ${entry.id}`;
812
+ const mapped = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
813
+ let icon = mapped?.icon;
814
+ if (mapped?.name) name2 = mapped.name;
815
+ const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
816
+ if (name2.startsWith("Skill ") && buffMeta?.name) {
817
+ name2 = buffMeta.name;
818
+ icon = buffMeta.icon || icon;
819
+ }
820
+ if (name2.startsWith("Skill ")) {
821
+ const conditionName = resolveConditionNameFromEntry(name2, entry.id, details.buffMap);
822
+ if (conditionName) {
823
+ name2 = conditionName;
824
+ icon = buffMeta?.icon || icon;
825
+ }
826
+ }
827
+ return { name: name2, icon };
828
+ };
829
+ const playerProfession = p.profession || "Unknown";
830
+ const extractPhase0 = (dist2) => {
831
+ if (!Array.isArray(dist2)) return [];
832
+ const phase0 = dist2[0];
833
+ return Array.isArray(phase0) ? phase0 : [];
834
+ };
835
+ const healingPlayerKey = identity.key;
836
+ let healingBd = acc.healingBreakdownMap.get(healingPlayerKey);
837
+ if (!healingBd) {
838
+ healingBd = {
839
+ key: healingPlayerKey,
840
+ account: identity.accountLabel,
841
+ displayName: identity.accountLabel,
842
+ profession: playerProfession,
843
+ professionList: [playerProfession],
844
+ healingSkills: /* @__PURE__ */ new Map(),
845
+ barrierSkills: /* @__PURE__ */ new Map(),
846
+ hasHealAddon: false
847
+ };
848
+ acc.healingBreakdownMap.set(healingPlayerKey, healingBd);
849
+ }
850
+ if (playerProfession && !healingBd.professionList.includes(playerProfession)) {
851
+ healingBd.professionList.push(playerProfession);
852
+ }
853
+ if (typeof p.name === "string" && healAddonCharacters.has(p.name)) {
854
+ healingBd.hasHealAddon = true;
855
+ const ps = acc.playerStats.get(healingPlayerKey);
856
+ if (ps) ps.hasHealAddon = true;
857
+ }
858
+ const pushHealingSkillEntry = (entry, skillMap, totalField) => {
859
+ if (!entry?.id) return;
860
+ const amount = Number(entry[totalField] || 0);
861
+ if (!Number.isFinite(amount) || amount <= 0) return;
862
+ const { name: name2, icon } = resolveSkillMeta(entry);
863
+ const skillId = `s${entry.id}`;
864
+ let existing = skillMap.get(skillId);
865
+ if (!existing) {
866
+ existing = { id: skillId, name: name2, icon, total: 0, hits: 0, max: 0 };
867
+ skillMap.set(skillId, existing);
868
+ }
869
+ if (existing.name.startsWith("Skill ") && !name2.startsWith("Skill ")) existing.name = name2;
870
+ if (!existing.icon && icon) existing.icon = icon;
871
+ existing.total += amount;
872
+ existing.hits += Number(entry.hits || 0);
873
+ existing.max = Math.max(existing.max, Number(entry.max || 0));
874
+ };
875
+ extractPhase0(p.extHealingStats?.totalHealingDist).forEach((entry) => {
876
+ pushHealingSkillEntry(entry, healingBd.healingSkills, "totalHealing");
877
+ });
878
+ extractPhase0(p.extBarrierStats?.totalBarrierDist).forEach((entry) => {
879
+ pushHealingSkillEntry(entry, healingBd.barrierSkills, "totalBarrier");
880
+ });
881
+ const pushSkillDamageEntry = (entry) => {
882
+ if (!entry?.id) return;
883
+ const { name: name2, icon } = resolveSkillMeta(entry);
884
+ if (!acc.skillDamageMap[entry.id]) acc.skillDamageMap[entry.id] = { name: name2, icon, damage: 0, hits: 0, downContribution: 0 };
885
+ if (acc.skillDamageMap[entry.id].name.startsWith("Skill ") && !name2.startsWith("Skill ")) acc.skillDamageMap[entry.id].name = name2;
886
+ if (!acc.skillDamageMap[entry.id].icon && icon) acc.skillDamageMap[entry.id].icon = icon;
887
+ acc.skillDamageMap[entry.id].damage += entry.totalDamage;
888
+ acc.skillDamageMap[entry.id].hits += entry.connectedHits;
889
+ acc.skillDamageMap[entry.id].downContribution += Number(entry.downContribution || 0);
890
+ };
891
+ const playerKey = identity.key;
892
+ let playerBreakdown = acc.playerSkillBreakdownMap.get(playerKey);
893
+ if (!playerBreakdown) {
894
+ playerBreakdown = {
895
+ key: playerKey,
896
+ account: identity.accountLabel,
897
+ displayName: identity.accountLabel,
898
+ profession: playerProfession,
899
+ professionList: [playerProfession],
900
+ totalFightMs: 0,
901
+ skills: /* @__PURE__ */ new Map()
902
+ };
903
+ acc.playerSkillBreakdownMap.set(playerKey, playerBreakdown);
904
+ }
905
+ if (playerProfession && !playerBreakdown.professionList.includes(playerProfession)) {
906
+ playerBreakdown.professionList.push(playerProfession);
907
+ }
908
+ playerBreakdown.totalFightMs += details.durationMS || 0;
909
+ const pushPlayerSkillEntry = (entry) => {
910
+ if (!entry?.id) return;
911
+ const { name: name2, icon } = resolveSkillMeta(entry);
912
+ const skillId = `s${entry.id}`;
913
+ let skillEntry = playerBreakdown.skills.get(skillId);
914
+ if (!skillEntry) {
915
+ skillEntry = { id: skillId, name: name2, icon, damage: 0, downContribution: 0, hits: 0, casts: 0, min: Infinity, max: 0 };
916
+ playerBreakdown.skills.set(skillId, skillEntry);
917
+ }
918
+ if (skillEntry.name.startsWith("Skill ") && !name2.startsWith("Skill ")) skillEntry.name = name2;
919
+ if (!skillEntry.icon && icon) skillEntry.icon = icon;
920
+ skillEntry.damage += Number(entry.totalDamage || 0);
921
+ skillEntry.downContribution += Number(entry.downContribution || 0);
922
+ skillEntry.hits += Number(entry.hits || 0);
923
+ const entryMin = Number(entry.min);
924
+ if (Number.isFinite(entryMin) && entryMin > 0) {
925
+ skillEntry.min = Math.min(skillEntry.min, entryMin);
926
+ }
927
+ skillEntry.max = Math.max(skillEntry.max, Number(entry.max || 0));
928
+ };
929
+ if (skillDamageSource === "total") {
930
+ p.totalDamageDist?.forEach((list) => {
931
+ list?.forEach((entry) => {
932
+ pushSkillDamageEntry(entry);
933
+ pushPlayerSkillEntry(entry);
934
+ });
935
+ });
936
+ } else {
937
+ const targetSkillTotals = /* @__PURE__ */ new Map();
938
+ p.targetDamageDist?.forEach((targetGroup) => {
939
+ targetGroup?.forEach((list) => {
940
+ list?.forEach((entry) => {
941
+ const skillId = Number(entry?.id);
942
+ if (Number.isFinite(skillId)) {
943
+ const existing = targetSkillTotals.get(skillId) || { damage: 0, hits: 0, downContribution: 0 };
944
+ existing.damage += Number(entry?.totalDamage || 0);
945
+ existing.hits += Number(entry?.connectedHits || 0);
946
+ existing.downContribution += Number(entry?.downContribution || 0);
947
+ targetSkillTotals.set(skillId, existing);
948
+ }
949
+ pushSkillDamageEntry(entry);
950
+ pushPlayerSkillEntry(entry);
951
+ });
952
+ });
953
+ });
954
+ const allowTotalSupplement = !details?.detailedWvW;
955
+ if (allowTotalSupplement) {
956
+ p.totalDamageDist?.forEach((list) => {
957
+ list?.forEach((entry) => {
958
+ const skillId = Number(entry?.id);
959
+ if (!Number.isFinite(skillId)) return;
960
+ const target = targetSkillTotals.get(skillId);
961
+ if (!target) {
962
+ pushSkillDamageEntry(entry);
963
+ pushPlayerSkillEntry(entry);
964
+ return;
965
+ }
966
+ const totalDamage = Number(entry?.totalDamage || 0);
967
+ const totalHits = Number(entry?.connectedHits || 0);
968
+ const totalDownContribution = Number(entry?.downContribution || 0);
969
+ const deltaDamage = totalDamage - Number(target.damage || 0);
970
+ const deltaHits = totalHits - Number(target.hits || 0);
971
+ const deltaDownContribution = totalDownContribution - Number(target.downContribution || 0);
972
+ if (deltaDamage <= 0 && deltaHits <= 0 && deltaDownContribution <= 0) return;
973
+ const reconciledEntry = {
974
+ ...entry,
975
+ totalDamage: Math.max(0, deltaDamage),
976
+ connectedHits: Math.max(0, deltaHits),
977
+ downContribution: Math.max(0, deltaDownContribution),
978
+ hits: 0,
979
+ min: 0,
980
+ max: 0
981
+ };
982
+ pushSkillDamageEntry(reconciledEntry);
983
+ pushPlayerSkillEntry(reconciledEntry);
984
+ });
985
+ });
986
+ }
987
+ }
988
+ if (Array.isArray(p.rotation)) {
989
+ p.rotation.forEach((rot) => {
990
+ if (!rot?.id) return;
991
+ const count = rot.skills?.length || 0;
992
+ if (count <= 0) return;
993
+ const skillId = `s${rot.id}`;
994
+ const skillEntry = playerBreakdown.skills.get(skillId);
995
+ if (skillEntry) {
996
+ skillEntry.casts += count;
997
+ }
998
+ });
999
+ }
1000
+ if (p.totalDamageTaken) {
1001
+ p.totalDamageTaken.forEach((list) => {
1002
+ list?.forEach((entry) => {
1003
+ if (!entry?.id) return;
1004
+ let name2 = `Skill ${entry.id}`;
1005
+ const mapped = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
1006
+ let icon = mapped?.icon;
1007
+ if (mapped?.name) name2 = mapped.name;
1008
+ const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
1009
+ if (name2.startsWith("Skill ") && buffMeta?.name) {
1010
+ name2 = buffMeta.name;
1011
+ icon = buffMeta.icon || icon;
1012
+ }
1013
+ if (name2.startsWith("Skill ")) {
1014
+ const conditionName = resolveConditionNameFromEntry(name2, entry.id, details.buffMap);
1015
+ if (conditionName) {
1016
+ name2 = conditionName;
1017
+ icon = buffMeta?.icon || icon;
1018
+ }
1019
+ }
1020
+ if (!acc.incomingSkillDamageMap[entry.id]) acc.incomingSkillDamageMap[entry.id] = { name: name2, icon, damage: 0, hits: 0 };
1021
+ if (!acc.incomingSkillDamageMap[entry.id].name.startsWith("Skill ") || name2.startsWith("Skill ")) acc.incomingSkillDamageMap[entry.id].name = name2;
1022
+ if (!acc.incomingSkillDamageMap[entry.id].icon && icon) acc.incomingSkillDamageMap[entry.id].icon = icon;
1023
+ acc.incomingSkillDamageMap[entry.id].damage += entry.totalDamage;
1024
+ acc.incomingSkillDamageMap[entry.id].hits += entry.hits;
1025
+ });
1026
+ });
1027
+ }
1028
+ if (p.damageModifiers) {
1029
+ for (const entry of p.damageModifiers) {
1030
+ const phase0 = entry.damageModifiers?.[0];
1031
+ if (!phase0) continue;
1032
+ const key2 = `d${entry.id}`;
1033
+ const existing = s.damageModTotals[key2];
1034
+ if (existing) {
1035
+ existing.damageGain += phase0.damageGain;
1036
+ existing.hitCount += phase0.hitCount;
1037
+ existing.totalHitCount += phase0.totalHitCount;
1038
+ existing.totalDamage += phase0.totalDamage;
1039
+ } else {
1040
+ s.damageModTotals[key2] = {
1041
+ damageGain: phase0.damageGain,
1042
+ hitCount: phase0.hitCount,
1043
+ totalHitCount: phase0.totalHitCount,
1044
+ totalDamage: phase0.totalDamage
1045
+ };
1046
+ }
1047
+ }
1048
+ }
1049
+ if (p.incomingDamageModifiers) {
1050
+ for (const entry of p.incomingDamageModifiers) {
1051
+ const phase0 = entry.damageModifiers?.[0];
1052
+ if (!phase0) continue;
1053
+ const key2 = `d${entry.id}`;
1054
+ const existing = s.incomingDamageModTotals[key2];
1055
+ if (existing) {
1056
+ existing.damageGain += phase0.damageGain;
1057
+ existing.hitCount += phase0.hitCount;
1058
+ existing.totalHitCount += phase0.totalHitCount;
1059
+ existing.totalDamage += phase0.totalDamage;
1060
+ } else {
1061
+ s.incomingDamageModTotals[key2] = {
1062
+ damageGain: phase0.damageGain,
1063
+ hitCount: phase0.hitCount,
1064
+ totalHitCount: phase0.totalHitCount,
1065
+ totalDamage: phase0.totalDamage
1066
+ };
1067
+ }
1068
+ }
1069
+ }
1070
+ });
1071
+ targets.forEach((t) => {
1072
+ if (t?.isFake) return;
1073
+ const name = resolveProfessionLabel(t?.profession || t?.name || t?.id);
1074
+ if (!name) return;
1075
+ acc.enemyProfessionCounts[name] = (acc.enemyProfessionCounts[name] || 0) + 1;
1076
+ });
1077
+ const conditionResult = computeOutgoingConditions({
1078
+ players,
1079
+ targets,
1080
+ skillMap: details.skillMap,
1081
+ buffMap: details.buffMap,
1082
+ getPlayerKey: (pl) => getPlayerIdentity(pl, splitPlayersByClass).key
1083
+ });
1084
+ const conditionIconMap = buildConditionIconMap(details.buffMap);
1085
+ Object.entries(conditionResult.playerConditions).forEach(([k, totals]) => {
1086
+ const ps = acc.playerStats.get(k);
1087
+ if (!ps) return;
1088
+ Object.entries(totals).forEach(([cName, v]) => {
1089
+ const ex = ps.outgoingConditions[cName] || { applications: 0, damage: 0, skills: {}, icon: v.icon };
1090
+ ex.applications += Number(v.applications || 0);
1091
+ ex.damage += Number(v.damage || 0);
1092
+ if (v.applicationsFromBuffs) ex.applicationsFromBuffs = (ex.applicationsFromBuffs || 0) + v.applicationsFromBuffs;
1093
+ if (v.applicationsFromBuffsActive) ex.applicationsFromBuffsActive = (ex.applicationsFromBuffsActive || 0) + v.applicationsFromBuffsActive;
1094
+ if (v.uptimeMs) ex.uptimeMs = (ex.uptimeMs || 0) + v.uptimeMs;
1095
+ Object.entries(v.skills || {}).forEach(([sn, sv]) => {
1096
+ const sk = ex.skills[sn] || { name: sv.name, hits: 0, damage: 0, icon: sv.icon };
1097
+ sk.hits += Number(sv.hits || 0);
1098
+ sk.damage += Number(sv.damage || 0);
1099
+ if (!sk.icon && sv.icon) sk.icon = sv.icon;
1100
+ ex.skills[sn] = sk;
1101
+ });
1102
+ if (!ex.icon && v.icon) ex.icon = v.icon;
1103
+ ps.outgoingConditions[cName] = ex;
1104
+ });
1105
+ });
1106
+ Object.entries(conditionResult.summary).forEach(([cName, v]) => {
1107
+ const ex = acc.outgoingCondiTotals[cName] || { name: v.name || cName, icon: v.icon, applications: 0, damage: 0 };
1108
+ ex.applications += Number(v.applications || 0);
1109
+ ex.damage += Number(v.damage || 0);
1110
+ if (v.applicationsFromBuffs) ex.applicationsFromBuffs = (ex.applicationsFromBuffs || 0) + v.applicationsFromBuffs;
1111
+ if (v.applicationsFromBuffsActive) ex.applicationsFromBuffsActive = (ex.applicationsFromBuffsActive || 0) + v.applicationsFromBuffsActive;
1112
+ if (v.uptimeMs) ex.uptimeMs = (ex.uptimeMs || 0) + v.uptimeMs;
1113
+ if (!ex.icon && v.icon) ex.icon = v.icon;
1114
+ acc.outgoingCondiTotals[cName] = ex;
1115
+ });
1116
+ squadPlayers.forEach((p) => {
1117
+ const key = getPlayerIdentity(p, splitPlayersByClass).key;
1118
+ const ps = acc.playerStats.get(key);
1119
+ if (!ps || !p.totalDamageTaken) return;
1120
+ p.totalDamageTaken.forEach((list) => list?.forEach((entry) => {
1121
+ if (!entry?.id) return;
1122
+ let sName = `Skill ${entry.id}`;
1123
+ const sm = details.skillMap?.[`s${entry.id}`] || details.skillMap?.[`${entry.id}`];
1124
+ if (sm?.name) sName = sm.name;
1125
+ const buffMeta = resolveBuffMetaById(details.buffMap, entry.id);
1126
+ if (sName.startsWith("Skill ") && buffMeta?.name) sName = buffMeta.name;
1127
+ const finalName = resolveConditionNameFromEntry(sName, entry.id, details.buffMap);
1128
+ if (!finalName) return;
1129
+ const buffName = buffMeta?.name;
1130
+ const conditionIcon = conditionIconMap.get(finalName) || buffMeta?.icon;
1131
+ const skillIcon = sm?.icon || buffMeta?.icon || conditionIcon;
1132
+ if (sName.startsWith("Skill ") && (buffName || finalName)) {
1133
+ sName = buffName || finalName;
1134
+ }
1135
+ const hits = Number(entry.hits ?? 0);
1136
+ const dmg = Number(entry.totalDamage ?? 0);
1137
+ const summ = acc.incomingCondiTotals[finalName] || { name: finalName, icon: conditionIcon, applications: 0, damage: 0 };
1138
+ summ.applications += Number.isFinite(hits) ? hits : 0;
1139
+ summ.damage += Number.isFinite(dmg) ? dmg : 0;
1140
+ if (!summ.icon && conditionIcon) summ.icon = conditionIcon;
1141
+ acc.incomingCondiTotals[finalName] = summ;
1142
+ const pEntry = ps.incomingConditions[finalName] || { applications: 0, damage: 0, skills: {}, icon: conditionIcon };
1143
+ pEntry.applications += Number.isFinite(hits) ? hits : 0;
1144
+ pEntry.damage += Number.isFinite(dmg) ? dmg : 0;
1145
+ const skEntry = pEntry.skills[sName] || { name: sName, hits: 0, damage: 0, icon: skillIcon };
1146
+ skEntry.hits += Number.isFinite(hits) ? hits : 0;
1147
+ skEntry.damage += Number.isFinite(dmg) ? dmg : 0;
1148
+ if (!skEntry.icon && skillIcon) skEntry.icon = skillIcon;
1149
+ pEntry.skills[sName] = skEntry;
1150
+ if (!pEntry.icon && conditionIcon) pEntry.icon = conditionIcon;
1151
+ ps.incomingConditions[finalName] = pEntry;
1152
+ }));
1153
+ });
1154
+ const mitigationSource = details.player_damage_mitigation || details.playerDamageMitigation;
1155
+ const hasMitigationSource = mitigationSource && typeof mitigationSource === "object" && Object.keys(mitigationSource).length > 0;
1156
+ if (hasMitigationSource) {
1157
+ Object.entries(mitigationSource).forEach(([rawKey, skillMap]) => {
1158
+ if (!skillMap || typeof skillMap !== "object") return;
1159
+ const base = parseMitigationKey(rawKey);
1160
+ const identity = getMitigationIdentity(base.account, base.profession, splitPlayersByClass);
1161
+ const row = ensureMitigationRow(acc.damageMitigationPlayersMap, identity.key, { ...base, account: identity.accountLabel, profession: identity.profession });
1162
+ Object.values(skillMap).forEach((entry) => {
1163
+ const avoided = readNumber(entry?.avoided_damage ?? entry?.avoidedDamage);
1164
+ if (avoided <= 0) return;
1165
+ addMitigationTotals(row.mitigationTotals, entry);
1166
+ });
1167
+ });
1168
+ }
1169
+ const mitigationMinionSource = details.player_minion_damage_mitigation || details.playerMinionDamageMitigation;
1170
+ const hasMitigationMinionSource = mitigationMinionSource && typeof mitigationMinionSource === "object" && Object.keys(mitigationMinionSource).length > 0;
1171
+ if (hasMitigationMinionSource) {
1172
+ Object.entries(mitigationMinionSource).forEach(([rawKey, minionMap]) => {
1173
+ if (!minionMap || typeof minionMap !== "object") return;
1174
+ const base = parseMitigationKey(rawKey);
1175
+ const identity = getMitigationIdentity(base.account, base.profession, splitPlayersByClass);
1176
+ Object.entries(minionMap).forEach(([minionName, skillMap]) => {
1177
+ if (!skillMap || typeof skillMap !== "object") return;
1178
+ const rowKey = `${identity.key}::${minionName}`;
1179
+ const row = ensureMitigationMinionRow(acc.damageMitigationMinionsMap, rowKey, {
1180
+ ...base,
1181
+ account: identity.accountLabel,
1182
+ profession: identity.profession,
1183
+ minion: String(minionName || "Unknown")
1184
+ });
1185
+ Object.values(skillMap).forEach((entry) => {
1186
+ addMitigationTotals(row.mitigationTotals, entry);
1187
+ });
1188
+ });
1189
+ });
1190
+ }
1191
+ if (!hasMitigationSource || !hasMitigationMinionSource) {
1192
+ if (!hasMitigationSource) {
1193
+ squadPlayers.forEach((player) => {
1194
+ const entries = player?.totalDamageTaken || [];
1195
+ if (!Array.isArray(entries) || entries.length === 0) return;
1196
+ const identity = getPlayerIdentity(player, splitPlayersByClass);
1197
+ const base = {
1198
+ account: identity.accountLabel,
1199
+ name: player.name || identity.accountLabel,
1200
+ profession: identity.profession
1201
+ };
1202
+ ensureMitigationRow(acc.damageMitigationPlayersMap, identity.key, base);
1203
+ const list = Array.isArray(entries) ? entries[0] : null;
1204
+ list?.forEach((entry) => {
1205
+ if (!entry?.id) return;
1206
+ const blocked = readNumber(entry.blocked);
1207
+ const evaded = readNumber(entry.evaded);
1208
+ const glanced = readNumber(entry.glance ?? entry.glanced);
1209
+ const missed = readNumber(entry.missed);
1210
+ const invulned = readNumber(entry.invulned);
1211
+ const interrupted = readNumber(entry.interrupted);
1212
+ const skillHits = readNumber(entry.hits ?? entry.connectedHits);
1213
+ const skillId = Number(entry.id);
1214
+ updateCumulativeCounts(
1215
+ acc.mitigationCumulativeCounts,
1216
+ `${identity.key}::${skillId}`,
1217
+ {
1218
+ totalHits: skillHits,
1219
+ blocked,
1220
+ evaded,
1221
+ glanced,
1222
+ missed,
1223
+ invulned,
1224
+ interrupted,
1225
+ totalMitigation: 0,
1226
+ minMitigation: 0
1227
+ }
1228
+ );
1229
+ });
1230
+ });
1231
+ }
1232
+ if (!hasMitigationMinionSource) {
1233
+ squadPlayers.forEach((player) => {
1234
+ const minions = player?.minions || [];
1235
+ if (!Array.isArray(minions) || minions.length === 0) return;
1236
+ const identity = getPlayerIdentity(player, splitPlayersByClass);
1237
+ const base = {
1238
+ account: identity.accountLabel,
1239
+ name: player.name || identity.accountLabel,
1240
+ profession: identity.profession
1241
+ };
1242
+ minions.forEach((minion) => {
1243
+ const minionEntries = minion?.totalDamageTakenDist || [];
1244
+ if (!Array.isArray(minionEntries) || minionEntries.length === 0) return;
1245
+ let minionName = String(minion?.name || "Unknown").replace(/^Juvenile\s+/i, "") || "Unknown";
1246
+ if (minionName.toUpperCase().includes("UNKNOWN")) minionName = "Unknown";
1247
+ const rowKey = `${identity.key}::${minionName}`;
1248
+ ensureMitigationMinionRow(acc.damageMitigationMinionsMap, rowKey, { ...base, minion: minionName });
1249
+ const list = Array.isArray(minionEntries) ? minionEntries[0] : null;
1250
+ list?.forEach((entry) => {
1251
+ if (!entry?.id) return;
1252
+ const blocked = readNumber(entry.blocked);
1253
+ const evaded = readNumber(entry.evaded);
1254
+ const glanced = readNumber(entry.glance ?? entry.glanced);
1255
+ const missed = readNumber(entry.missed);
1256
+ const invulned = readNumber(entry.invulned);
1257
+ const interrupted = readNumber(entry.interrupted);
1258
+ const skillHits = readNumber(entry.hits ?? entry.connectedHits);
1259
+ const skillId = Number(entry.id);
1260
+ updateCumulativeCounts(
1261
+ acc.mitigationMinionCumulativeCounts,
1262
+ `${rowKey}::${skillId}`,
1263
+ {
1264
+ totalHits: skillHits,
1265
+ blocked,
1266
+ evaded,
1267
+ glanced,
1268
+ missed,
1269
+ invulned,
1270
+ interrupted,
1271
+ totalMitigation: 0,
1272
+ minMitigation: 0
1273
+ }
1274
+ );
1275
+ });
1276
+ });
1277
+ });
1278
+ }
1279
+ }
1280
+ };
1281
+ var recomputeMitigationTotals = (rows, cumulative, globalEnemySkillStats) => {
1282
+ const aggregate = /* @__PURE__ */ new Map();
1283
+ const ensureAggregate = (key) => {
1284
+ let bucket = aggregate.get(key);
1285
+ if (!bucket) {
1286
+ bucket = createMitigationTotals();
1287
+ aggregate.set(key, bucket);
1288
+ }
1289
+ return bucket;
1290
+ };
1291
+ cumulative.forEach((counts, key) => {
1292
+ const splitIndex = key.lastIndexOf("::");
1293
+ const rowKey = splitIndex > -1 ? key.slice(0, splitIndex) : key;
1294
+ const skillId = splitIndex > -1 ? Number(key.slice(splitIndex + 2)) : Number.NaN;
1295
+ const enemy = Number.isFinite(skillId) ? resolveGlobalEnemyStats(globalEnemySkillStats, skillId) : { hasSkill: false, avg: 0, min: 0, hits: 0 };
1296
+ if (!enemy.hasSkill || enemy.hits <= 0) return;
1297
+ const avg = enemy.avg;
1298
+ const min = enemy.min;
1299
+ const avoid = counts.glanced * avg / 2 + (counts.blocked + counts.evaded + counts.missed + counts.invulned + counts.interrupted) * avg;
1300
+ const avoidMin = counts.glanced * min / 2 + (counts.blocked + counts.evaded + counts.missed + counts.invulned + counts.interrupted) * min;
1301
+ if (avoid <= 0) return;
1302
+ const bucket = ensureAggregate(rowKey);
1303
+ bucket.totalHits += counts.totalHits;
1304
+ bucket.blocked += counts.blocked;
1305
+ bucket.evaded += counts.evaded;
1306
+ bucket.glanced += counts.glanced;
1307
+ bucket.missed += counts.missed;
1308
+ bucket.invulned += counts.invulned;
1309
+ bucket.interrupted += counts.interrupted;
1310
+ bucket.totalMitigation += avoid;
1311
+ bucket.minMitigation += avoidMin;
1312
+ });
1313
+ rows.forEach((row, rowKey) => {
1314
+ const totals = aggregate.get(rowKey) || createMitigationTotals();
1315
+ row.mitigationTotals.totalHits = totals.totalHits;
1316
+ row.mitigationTotals.blocked = totals.blocked;
1317
+ row.mitigationTotals.evaded = totals.evaded;
1318
+ row.mitigationTotals.glanced = totals.glanced;
1319
+ row.mitigationTotals.missed = totals.missed;
1320
+ row.mitigationTotals.invulned = totals.invulned;
1321
+ row.mitigationTotals.interrupted = totals.interrupted;
1322
+ row.mitigationTotals.totalMitigation = totals.totalMitigation;
1323
+ row.mitigationTotals.minMitigation = totals.minMitigation;
1324
+ });
1325
+ };
1326
+ var finalizePlayerAggregation = (acc) => {
1327
+ recomputeMitigationTotals(acc.damageMitigationPlayersMap, acc.mitigationCumulativeCounts, acc.globalEnemySkillStats);
1328
+ recomputeMitigationTotals(acc.damageMitigationMinionsMap, acc.mitigationMinionCumulativeCounts, acc.globalEnemySkillStats);
1329
+ };
1330
+ var computePlayerAggregation = ({
1331
+ validLogs,
1332
+ method,
1333
+ skillDamageSource,
1334
+ splitPlayersByClass
1335
+ }) => {
1336
+ const acc = createPlayerAggregationAccumulators();
1337
+ const options = { method, skillDamageSource, splitPlayersByClass };
1338
+ for (const log of validLogs) {
1339
+ precomputeGlobalEnemySkillStats(log, acc);
1340
+ }
1341
+ for (const log of validLogs) {
1342
+ ingestLogPlayerData(log, acc, options);
1343
+ }
1344
+ finalizePlayerAggregation(acc);
1345
+ return {
1346
+ playerStats: acc.playerStats,
1347
+ skillDamageMap: acc.skillDamageMap,
1348
+ incomingSkillDamageMap: acc.incomingSkillDamageMap,
1349
+ playerSkillBreakdownMap: acc.playerSkillBreakdownMap,
1350
+ healingBreakdownMap: acc.healingBreakdownMap,
1351
+ outgoingCondiTotals: acc.outgoingCondiTotals,
1352
+ incomingCondiTotals: acc.incomingCondiTotals,
1353
+ enemyProfessionCounts: acc.enemyProfessionCounts,
1354
+ specialBuffMeta: acc.specialBuffMeta,
1355
+ specialBuffAgg: acc.specialBuffAgg,
1356
+ specialBuffOutputAgg: acc.specialBuffOutputAgg,
1357
+ damageMitigationPlayersMap: acc.damageMitigationPlayersMap,
1358
+ damageMitigationMinionsMap: acc.damageMitigationMinionsMap,
1359
+ mitigationCumulativeCounts: acc.mitigationCumulativeCounts,
1360
+ mitigationMinionCumulativeCounts: acc.mitigationMinionCumulativeCounts,
1361
+ wins: acc.wins,
1362
+ losses: acc.losses,
1363
+ totalSquadSizeAccum: acc.totalSquadSizeAccum,
1364
+ totalEnemiesAccum: acc.totalEnemiesAccum,
1365
+ totalSquadDeaths: acc.totalSquadDeaths,
1366
+ totalSquadKills: acc.totalSquadKills,
1367
+ totalEnemyDeaths: acc.totalEnemyDeaths,
1368
+ totalEnemyKills: acc.totalEnemyKills,
1369
+ totalSquadDowns: acc.totalSquadDowns,
1370
+ totalEnemyDowns: acc.totalEnemyDowns
1371
+ };
1372
+ };
1373
+
1374
+ export {
1375
+ getFightDownsDeaths,
1376
+ getFightOutcome,
1377
+ resolveProfessionLabel,
1378
+ createPlayerAggregationAccumulators,
1379
+ precomputeGlobalEnemySkillStats,
1380
+ ingestLogPlayerData,
1381
+ finalizePlayerAggregation,
1382
+ computePlayerAggregation
1383
+ };