(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 = ''; // ── 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 => `
${abbr(p.type)}
${p.name}
${p.type}
`).join(''); } function renderRefPlayers() { const section = document.getElementById('ref-player-section'); const grid = document.getElementById('ref-player-grid'); if (!refPlayers.length) { section.style.display = 'none'; return; } const currentNames = new Set(currentPlayers.map(p => p.name)); if (!refPlayers.some(p => !currentNames.has(p.name))) { section.style.display = 'none'; return; } const order = { healer: 0, dps: 1, tank: 2 }; const sorted = [...refPlayers].sort((a, b) => { const roleCmp = (order[a.role] ?? 2) - (order[b.role] ?? 2); return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name); }); sorted.filter(p => p.role === 'tank').forEach(p => hiddenPlayerNames.add(p.name)); grid.innerHTML = sorted.map(p => `
${abbr(p.type)}
${p.name}
${p.type}
`).join(''); section.style.display = ''; } document.getElementById('ref-player-grid').addEventListener('click', e => { const card = e.target.closest('.player-card'); if (!card) return; const name = card.dataset.playerName; if (hiddenPlayerNames.has(name)) { hiddenPlayerNames.delete(name); card.classList.remove('player-hidden'); } else { hiddenPlayerNames.add(name); card.classList.add('player-hidden'); } renderTimeline(lastEvents, lastFightStart); }); document.getElementById('player-grid').addEventListener('click', e => { const card = e.target.closest('.player-card'); if (!card) return; const id = parseInt(card.dataset.playerId, 10); const name = card.dataset.playerName; if (hiddenPlayers.has(id)) { hiddenPlayers.delete(id); hiddenPlayerNames.delete(name); card.classList.remove('player-hidden'); } else { hiddenPlayers.add(id); hiddenPlayerNames.add(name); card.classList.add('player-hidden'); } renderTimeline(lastEvents, lastFightStart); }); document.getElementById('player-filter').addEventListener('input', e => { playerFilter = e.target.value.trim().toLowerCase(); renderTimeline(lastEvents, lastFightStart); }); // ── Phase select ───────────────────────────────────────────────────────── const phaseSelect = document.getElementById('phase-select'); phaseSelect.addEventListener('change', () => { const phases = window.App?.phases ?? []; const phase = phases.find(p => p.id === parseInt(phaseSelect.value, 10)); if (phase) { phaseFilter = { startTime: phase.startTime, endTime: phase.endTime }; renderTimeline(lastEvents, lastFightStart); } }); function setupPhases(phases) { if (!phases.length) { phaseSelect.style.display = 'none'; phaseFilter = { startTime: 0, endTime: Infinity }; return; } phaseSelect.innerHTML = phases.map(p => `` ).join(''); phaseSelect.value = 0; phaseFilter = { startTime: phases[0].startTime, endTime: phases[0].endTime }; phaseSelect.style.display = ''; } // ── Reference fight select ──────────────────────────────────────────────── const refFightSelect = document.getElementById('ref-fight-select'); refFightSelect.addEventListener('change', async () => { const refId = parseInt(refFightSelect.value, 10); if (!refId) { refEvents = []; refFightStart = 0; refPlayers = []; window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' }); renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); return; } const fight = (window.App?.fights ?? []).find(f => f.id === refId); if (!fight) return; // Clear ext-report and plan selections refExtFightSelect.value = ''; planRefId = ''; refPlanSelect.value = ''; refPlanPanel.style.display = 'none'; refFightSelect.disabled = true; try { const res = await fetch('api/analysis.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: window.App.reportCode, fight_id: refId, start_time: fight.startTime, end_time: fight.endTime, language: window.App.language, }), }); const json = await res.json(); if (!json.error && !json.reauth) { refEvents = json.aoe_events ?? []; refFightStart = json.fight_start ?? fight.startTime; refPlayers = json.players ?? []; window.App.setUrlState?.({ compareReportCode: '', compareFightId: refId, language: window.App.language, }); } } catch { } refFightSelect.disabled = false; renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); }); let allSameReportFights = []; function populateRefFightSelect() { const visible = allSameReportFights.filter(f => f.id !== window.App.fightId && isSameEncounter(f)); refFightSelect.innerHTML = ''; visible.forEach(f => { const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?'); const opt = document.createElement('option'); opt.value = f.id; opt.textContent = `${f.name} — ${fmtDur(f.endTime - f.startTime)} — ${hp}`; refFightSelect.appendChild(opt); }); refFightSelect.style.display = visible.length ? '' : 'none'; } function onFightsLoaded(fights) { allSameReportFights = fights; populateRefFightSelect(); } // ── External report comparison ──────────────────────────────────────────── const refExtToggle = document.getElementById('ref-ext-toggle'); const refExtPanel = document.getElementById('ref-ext-panel'); const refReportInput = document.getElementById('ref-report-input'); const refReportLoad = document.getElementById('ref-report-load'); const refFflogsLink = document.getElementById('ref-fflogs-report-link'); const refExtFightSelect = document.getElementById('ref-ext-fight-select'); function updateRefFflogsLink(fightId = 0) { if (!extReportCode) { refFflogsLink.style.display = 'none'; refFflogsLink.href = '#'; return; } refFflogsLink.href = window.App?.fflogsReportUrl ? window.App.fflogsReportUrl(extReportCode, fightId) : `https://www.fflogs.com/reports/${encodeURIComponent(extReportCode)}${fightId ? `#fight=${fightId}` : ''}`; refFflogsLink.style.display = ''; } refReportInput.addEventListener('input', () => { const match = refReportInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/); if (match) refReportInput.value = match[1]; }); refExtToggle.addEventListener('click', () => { const hidden = refExtPanel.style.display === 'none'; refExtPanel.style.display = hidden ? '' : 'none'; }); async function loadExternalReport(code, preferredFightId = 0) { if (!code) return; refReportLoad.disabled = true; refReportLoad.textContent = 'Lädt…'; try { const res = await fetch('api/fight.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: code, language: window.App.language }), }); const json = await res.json(); if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; } const fights = json?.data?.reportData?.report?.fights ?? []; extFights = fights; extReportCode = code; updateRefFflogsLink(); const visibleExt = fights.filter(isSameEncounter); refExtFightSelect.innerHTML = ''; visibleExt.forEach(f => { const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?'); const opt = document.createElement('option'); opt.value = f.id; opt.textContent = `${f.name} — ${fmtDur(f.endTime - f.startTime)} — ${hp}`; refExtFightSelect.appendChild(opt); }); refExtFightSelect.style.display = visibleExt.length ? '' : 'none'; refExtPanel.style.display = ''; if (preferredFightId && visibleExt.some(f => f.id === preferredFightId)) { refExtFightSelect.value = String(preferredFightId); updateRefFflogsLink(preferredFightId); await loadExternalCompare(preferredFightId); } } catch { } refReportLoad.disabled = false; refReportLoad.textContent = 'Laden'; } refReportLoad.addEventListener('click', async () => { await loadExternalReport(refReportInput.value.trim()); }); async function loadExternalCompare(refId) { if (!refId) { refEvents = []; refFightStart = 0; refPlayers = []; window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' }); renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); return; } const fight = extFights.find(f => f.id === refId); if (!fight) return; // Clear same-report and plan selections refFightSelect.value = ''; planRefId = ''; refPlanSelect.value = ''; refPlanPanel.style.display = 'none'; refExtFightSelect.disabled = true; try { const res = await fetch('api/analysis.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: extReportCode, fight_id: refId, start_time: fight.startTime, end_time: fight.endTime, language: window.App.language, }), }); const json = await res.json(); if (!json.error && !json.reauth) { refEvents = json.aoe_events ?? []; refFightStart = json.fight_start ?? fight.startTime; refPlayers = json.players ?? []; window.App.setUrlState?.({ compareReportCode: extReportCode, compareFightId: refId, language: window.App.language, }); } } catch { } refExtFightSelect.disabled = false; renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); } refExtFightSelect.addEventListener('change', async () => { const refId = parseInt(refExtFightSelect.value, 10) || 0; updateRefFflogsLink(refId); await loadExternalCompare(refId); }); // ── Plan as reference ───────────────────────────────────────────────────── const refPlanToggle = document.getElementById('ref-plan-toggle'); const refPlanPanel = document.getElementById('ref-plan-panel'); const refPlanSelect = document.getElementById('ref-plan-select'); function loadPlansForRef() { try { return JSON.parse(localStorage.getItem('ff14-planner-plans') || '[]'); } catch { return []; } } function planToRefEvents(plan) { const roster = plan.playerRoster ?? []; const jobComp = plan.jobComposition ?? []; const fightStart = plan.source?.fightStart ?? 0; const mitigNames = plan.mitigationNames ?? {}; const players = jobComp.map((job, i) => ({ job, name: roster[i]?.name ?? '', role: JOB_ROLE[job] ?? 'dps', })).filter(p => p.name && p.job); return plan.mechanics.map(m => { const mitigations = (m.assignments ?? []).map(a => ({ key: a.ability, name: a.abilityName || mitigNames[a.ability] || a.ability, buffType: a.buffType, dr: 0, })); const targets = players.map(p => ({ id: 0, name: p.name, type: p.job, role: p.role, amount: 0, absorbed: 0, overkill: 0, hp: 0, maxHp: 0, unmitigatedAmount: 0, mitigations, })); return { abilityName: m.name, abilityId: m.abilityId ?? 0, timestamp: fightStart + m.timestamp, totalDamage: 0, targets, isPlanRef: true, }; }); } function populateRefPlanSelect() { const plans = loadPlansForRef(); refPlanSelect.innerHTML = ''; plans.forEach(p => { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = `${p.name} (${p.mechanics.length} Mechaniken)`; refPlanSelect.appendChild(opt); }); refPlanSelect.value = planRefId || ''; } refPlanToggle.addEventListener('click', () => { const hidden = refPlanPanel.style.display === 'none'; refPlanPanel.style.display = hidden ? '' : 'none'; if (hidden) populateRefPlanSelect(); }); refPlanSelect.addEventListener('change', () => { const id = refPlanSelect.value; // Clear other ref sources refFightSelect.value = ''; refExtFightSelect.value = ''; updateRefFflogsLink(0); if (!id) { planRefId = ''; refEvents = []; refFightStart = 0; refPlayers = []; renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); return; } const plan = loadPlansForRef().find(p => p.id === id); if (!plan) return; planRefId = id; refEvents = planToRefEvents(plan); refFightStart = plan.source?.fightStart ?? 0; refPlayers = (plan.jobComposition ?? []) .map((job, i) => { const name = plan.playerRoster?.[i]?.name ?? ''; if (!name || !job) return null; return { name, type: job, role: JOB_ROLE[job] ?? 'dps' }; }) .filter(Boolean); renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); }); // ── Timeline rendering ──────────────────────────────────────────────────── function renderTimeline(events, fightStart) { lastEvents = events; lastFightStart = fightStart; const el = document.getElementById('aoe-timeline'); if (!events.length) { el.innerHTML = '

Keine AoE-Events gefunden

'; return; } const currentFightJobSet = new Set(currentPlayers.map(p => JOB_ABBR[p.type]).filter(Boolean)); // Build reference index: abilityName → [events in order] const refIndex = {}; for (const ev of refEvents) { (refIndex[ev.abilityName] = refIndex[ev.abilityName] ?? []).push(ev); } const abilityOccurrence = {}; const rows = events.map(ev => { if (ev.timestamp < phaseFilter.startTime || ev.timestamp >= phaseFilter.endTime) return ''; // Track occurrence for ref matching const occ = abilityOccurrence[ev.abilityName] ?? 0; abilityOccurrence[ev.abilityName] = occ + 1; const refEv = refEvents.length ? (refIndex[ev.abilityName]?.[occ] ?? null) : null; const allRefMitigs = collectRefMitigs(refEv); const currentEventMitigKeys = new Set(); for (const t of ev.targets) { for (const m of (t.mitigations ?? [])) currentEventMitigKeys.add(m.key ?? m.name); } const visibleTargets = ev.targets.filter(t => !hiddenPlayers.has(t.id) && (!playerFilter || t.name.toLowerCase().includes(playerFilter)) ); if (ev.isHeavyTankbuster && !visibleTargets.some(t => t.role === 'tank')) return ''; if (!visibleTargets.length) return ''; // Collect boss debuffs (Reprisal/Feint/Addle) once at event level const seenDebuffKeys = new Set(); const eventDebuffs = []; for (const t of visibleTargets) { for (const m of (t.mitigations ?? [])) { const key = m.key ?? m.name; if (m.buffType === 'debuff' && !seenDebuffKeys.has(key)) { seenDebuffKeys.add(key); eventDebuffs.push(m); } } } const eventMissingDebuffs = refEv ? allRefMitigs.filter(m => { if (m.buffType !== 'debuff' || seenDebuffKeys.has(m.key ?? m.name)) return false; const jobs = ABILITY_JOBS[m.key] ?? ABILITY_JOBS[m.name]; return jobs ? jobs.some(j => currentFightJobSet.has(j)) : false; }) : []; const debuffIconsHtml = [ ...eventDebuffs.map(m => ({ ...m, missing: false })), ...eventMissingDebuffs.map(m => ({ ...m, missing: true })), ].map(m => { const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return m.missing ? `${m.name}` : `${m.name}`; }).join(''); // Current targets const targets = visibleTargets.map(t => { const hpBar = (t.maxHp > 0) ? (() => { const afterPct = t.hp / t.maxHp * 100; const damagePct = t.amount / t.maxHp * 100; const hpColor = afterPct > 50 ? 'var(--green)' : afterPct > 25 ? '#e8a020' : 'var(--red)'; const missingBefore = Math.max(0, t.maxHp - t.hp - t.amount); const fmt = n => n.toLocaleString(); const hpPct = (t.hp / t.maxHp * 100).toFixed(1); const missingPct = (missingBefore / t.maxHp * 100).toFixed(1); const tooltip = `MaxHP: ${fmt(t.maxHp)}\nCurrentHP: ${fmt(t.hp)}\nHP-%: ${hpPct}%\nMissingBefore: ${fmt(missingBefore)}\nMissing-%: ${missingPct}%`; return `
`; })() : ''; // DR buff icons (shown below player box) const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => { const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return `${m.name}`; }).join(''); // Shield tooltip on absorbed value const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield'); const shieldLines = activeShields.map(s => s.name); const shieldTitle = shieldLines.length ? shieldLines.join('\n') : null; const dead = t.hp === 0 && t.maxHp > 0; return `
${abbr(t.type)} ${dead && t.overkill > 0 ? `-${fmtDmg(t.overkill)}` : ''}
${t.name} ${fmtDmg(t.amount)}${t.absorbed > 0 ? ` +${fmtDmg(t.absorbed)}` : ''}
${hpBar}
${mitigIcons ? `
${mitigIcons}
` : ''}
`; }).join(''); // Reference row let refHtml = ''; if (refEv) { const refVisible = refEv.targets.filter(t => !hiddenPlayerNames.has(t.name) && (!playerFilter || t.name.toLowerCase().includes(playerFilter)) ); if (refVisible.length) { const currentByName = {}; ev.targets.forEach(t => { currentByName[t.name] = t; }); const seenRefDebuffKeys = new Set(); const refDebuffIconsHtml = refVisible.flatMap(t => (t.mitigations ?? [])) .filter(m => m.buffType === 'debuff' && !seenRefDebuffKeys.has(m.key ?? m.name) && seenRefDebuffKeys.add(m.key ?? m.name)) .map(m => { const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return `${m.name}`; }).join(''); const isPlanRef = !!refEv.isPlanRef; const refCards = refVisible.map(t => { const curr = currentByName[t.name]; const diff = (!isPlanRef && curr) ? curr.amount - t.amount : 0; const dead = !isPlanRef && t.hp === 0 && t.maxHp > 0; const deltaHtml = diff !== 0 ? `${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}` : ''; const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => { const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; const k = m.key ?? m.name; const jobs = ABILITY_JOBS[k] ?? ABILITY_JOBS[m.name]; const currentGroupHasJob = jobs ? jobs.some(j => currentFightJobSet.has(j)) : false; const missing = currentGroupHasJob && !currentEventMitigKeys.has(k); const cls = missing ? ' aoe-buff-ref-unique' : ''; const titleSufx = missing ? ' (fehlt im aktuellen Pull)' : ''; return `${m.name}`; }).join(''); const refShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield'); const refShieldTitle = refShields.length ? refShields.map(s => { const k = s.key ?? s.name; const jobs = ABILITY_JOBS[k]; const currentGroupHasJob = jobs ? jobs.some(j => currentFightJobSet.has(j)) : false; const isMissing = !isPlanRef && currentGroupHasJob && !currentEventMitigKeys.has(k); return isMissing ? `${s.name} [fehlt im aktuellen Pull]` : s.name; }).join('\n') : null; const absorbedHtml = isPlanRef ? (refShields.length ? ` Schild` : '') : (t.absorbed > 0 ? ` +${fmtDmg(t.absorbed)}` : ''); return `
${abbr(t.type)} ${deltaHtml}
${t.name} ${isPlanRef ? '' : fmtDmg(t.amount)}${absorbedHtml}
${refMitigIcons ? `
${refMitigIcons}
` : ''}
`; }).join(''); const totalDiff = ev.totalDamage - refEv.totalDamage; const totalDelta = (!isPlanRef && totalDiff !== 0) ? `${totalDiff > 0 ? '+' : ''}${fmtDmg(totalDiff)}` : ''; const refLabel = isPlanRef ? 'PLAN' : `REF ${fmtDmg(refEv.totalDamage)} ${totalDelta}`; refHtml = `
${refLabel} ${refDebuffIconsHtml}
${refCards}
`; } } return `
${fmtTime(ev.timestamp, fightStart)}
${ev.abilityName} — ${fmtDmg(ev.totalDamage)} total ${debuffIconsHtml}
${targets}
${refHtml}
`; }).join(''); el.innerHTML = rows || '

Keine sichtbaren Targets

'; } function setEmpty(msg) { document.getElementById('analysis-loading').style.display = 'none'; document.getElementById('analysis-content').style.display = 'none'; document.getElementById('analysis-empty').style.display = 'block'; document.getElementById('analysis-empty-msg').textContent = msg; } let lastFightId = null; async function load() { const { reportCode, fightId, fightStart, fightEnd } = window.App ?? {}; if (!reportCode || !fightId) return; if (lastFightId === fightId) return; document.getElementById('analysis-loading').style.display = 'flex'; document.getElementById('analysis-empty').style.display = 'none'; document.getElementById('analysis-content').style.display = 'none'; let json; try { const res = await fetch('api/analysis.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: reportCode, fight_id: fightId, start_time: fightStart, end_time: fightEnd, language: window.App.language }), }); json = await res.json(); } catch (err) { setEmpty('Netzwerkfehler: ' + err.message); return; } if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; } if (json.error) { setEmpty('Fehler: ' + json.error); return; } lastFightId = fightId; populateRefFightSelect(); setupPhases(window.App?.phases ?? []); renderPlayers(json.players ?? []); mitigationNames = json.mitigation_names ?? {}; renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart); document.getElementById('analysis-loading').style.display = 'none'; document.getElementById('analysis-content').style.display = 'block'; const exportBtn = document.getElementById('export-to-planner-btn'); if (exportBtn) exportBtn.style.display = ''; } window.analysisTab = { onFightSelected: load, onTabOpen: load, onFightsLoaded: onFightsLoaded, async selectSharedCompare(fightId, reportCode = '') { if (!fightId) return; if (reportCode && reportCode !== window.App?.reportCode) { refReportInput.value = reportCode; await loadExternalReport(reportCode, fightId); return; } if ([...refFightSelect.options].some(opt => parseInt(opt.value, 10) === fightId)) { refFightSelect.value = String(fightId); refFightSelect.dispatchEvent(new Event('change')); } }, exportForPlanner() { const fight = (window.App?.fights ?? []).find(f => f.id === window.App?.fightId); return { aoeEvents: lastEvents, fightStart: lastFightStart, phases: window.App?.phases ?? [], players: currentPlayers, fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`, reportCode: window.App?.reportCode ?? '', fightId: window.App?.fightId ?? 0, fightEnd: window.App?.fightEnd ?? 0, mitigationNames, }; }, exportRefForPlanner() { const sameReportId = parseInt(refFightSelect.value, 10); const extId = parseInt(refExtFightSelect.value, 10); let fight = null, reportCode = '', fightId = 0; if (sameReportId) { fight = allSameReportFights.find(f => f.id === sameReportId); reportCode = window.App?.reportCode ?? ''; fightId = sameReportId; } else if (extId) { fight = extFights.find(f => f.id === extId); reportCode = extReportCode; fightId = extId; } const transitions = fight?.phaseTransitions ?? []; const phases = transitions.length === 0 ? [] : [ { id: 0, name: 'Ganzer Fight', startTime: fight.startTime, endTime: fight.endTime }, ...transitions.map((t, i) => ({ id: t.id, name: `Phase ${t.id}`, startTime: t.startTime, endTime: transitions[i + 1]?.startTime ?? fight.endTime, })), ]; return { aoeEvents: refEvents, fightStart: refFightStart, phases, players: refPlayers, fightName: fight?.name ?? 'Referenz-Fight', reportCode, fightId, fightEnd: fight?.endTime ?? 0, mitigationNames, }; }, hasRefExport() { return refEvents.length > 0 && !planRefId; }, reset() { lastFightId = null; refEvents = []; refFightStart = 0; refPlayers = []; extFights = []; extReportCode = ''; mitigationNames = {}; planRefId = ''; document.getElementById('ref-player-section').style.display = 'none'; refFightSelect.value = ''; refFightSelect.style.display = 'none'; refExtFightSelect.value = ''; refExtFightSelect.style.display = 'none'; refFflogsLink.style.display = 'none'; refFflogsLink.href = '#'; refExtPanel.style.display = 'none'; refPlanPanel.style.display = 'none'; refPlanSelect.value = ''; const exportBtn = document.getElementById('export-to-planner-btn'); if (exportBtn) exportBtn.style.display = 'none'; }, }; document.getElementById('export-to-planner-btn')?.addEventListener('click', (e) => { if (!refEvents.length) { window.plannerTab?.showImportModal(window.analysisTab.exportForPlanner()); return; } showExportChoiceMenu(e.currentTarget); }); function showExportChoiceMenu(anchor) { document.getElementById('export-choice-menu')?.remove(); const menu = document.createElement('div'); menu.id = 'export-choice-menu'; menu.className = 'export-choice-menu'; [ { label: 'Aktueller Fight', fn: () => window.analysisTab.exportForPlanner() }, { label: 'Referenz-Fight', fn: () => window.analysisTab.exportRefForPlanner() }, ].forEach(({ label, fn }) => { const btn = document.createElement('button'); btn.className = 'export-choice-item'; btn.textContent = label; btn.addEventListener('click', () => { menu.remove(); window.plannerTab?.showImportModal(fn()); }); menu.appendChild(btn); }); document.body.appendChild(menu); const rect = anchor.getBoundingClientRect(); menu.style.top = (rect.bottom + 4) + 'px'; menu.style.right = (window.innerWidth - rect.right) + 'px'; const close = (ev) => { if (!menu.contains(ev.target) && ev.target !== anchor) { menu.remove(); document.removeEventListener('click', close, true); } }; setTimeout(() => document.addEventListener('click', close, true), 0); } })();