(function () { const { MITIG_ICONS, JOB_ABBR, ABILITY_JOBS, JOB_ROLE } = window.FF14_DATA; // Deduplicated list of all mitigations across all targets of a ref event function collectRefMitigs(refEvent) { if (!refEvent) return []; const seen = new Set(), result = []; for (const t of refEvent.targets ?? []) { for (const m of (t.mitigations ?? [])) { const k = m.key ?? m.name; if (!seen.has(k)) { seen.add(k); result.push(m); } } } return result; } function abbr(type) { return JOB_ABBR[type] ?? type.slice(0, 3).toUpperCase(); } function fmtTime(ms, start) { const rel = ms - start; const min = Math.floor(rel / 60000); const sec = String(Math.floor((rel % 60000) / 1000)).padStart(2, '0'); return `${min}:${sec}`; } function fmtDmg(n) { if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M'; if (n >= 1_000) return Math.round(n / 1_000) + 'k'; return String(n); } function fmtDur(ms) { const min = Math.floor(ms / 60000); const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0'); return `${min}:${sec}`; } function normalizeFightName(name) { return String(name ?? '').trim().toLowerCase(); } function fightEncounterId(fight) { return parseInt(fight?.encounterID ?? fight?.encounterId ?? 0, 10) || 0; } function currentFight() { return (window.App?.fights ?? []).find(f => f.id === window.App?.fightId) ?? null; } function isSameEncounter(fight) { const selectedFight = currentFight(); const selectedEncounterId = fightEncounterId(selectedFight); const encounterId = fightEncounterId(fight); if (selectedEncounterId && encounterId) { return encounterId === selectedEncounterId; } const name = normalizeFightName(selectedFight?.name); return name !== '' && normalizeFightName(fight?.name) === name; } let hiddenPlayers = new Set(); let hiddenPlayerNames = new Set(); let lastEvents = []; let lastFightStart = 0; let playerFilter = ''; let phaseFilter = { startTime: 0, endTime: Infinity }; let refEvents = []; let refFightStart = 0; let refPlayers = []; let currentPlayers = []; let extFights = []; let extReportCode = ''; let mitigationNames = {}; let planRefId = ''; let actionIconPromise = null; const actionIconsByName = {}; function mitigationIcon(m) { return MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name] ?? actionIconsByName[m.key] ?? actionIconsByName[m.name] ?? ''; } async function ensureActionIconCache() { if (actionIconPromise) return actionIconPromise; actionIconPromise = (async () => { try { const res = await fetch('assets/jsons/Action.json', { cache: 'no-cache' }); if (!res.ok) return; const actions = await res.json(); for (const action of Object.values(actions ?? {})) { if (action?.names?.en && action?.icon) actionIconsByName[action.names.en] = action.icon; } } catch { } })(); return actionIconPromise; } // ── Player grid ────────────────────────────────────────────────────────── function renderPlayers(players) { currentPlayers = players; const grid = document.getElementById('player-grid'); const order = { healer: 0, dps: 1, tank: 2 }; players.sort((a, b) => { const roleCmp = (order[a.role] ?? 2) - (order[b.role] ?? 2); return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name); }); hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id)); hiddenPlayerNames = new Set(players.filter(p => p.role === 'tank').map(p => p.name)); grid.innerHTML = players.map(p => `