document.addEventListener('DOMContentLoaded', () => { window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0, language: 'en', phases: [], fights: [] }; const form = document.getElementById('report-form'); const output = document.getElementById('output'); const outputCard = document.getElementById('output-card'); const initialHint = document.getElementById('initial-hint'); const fightSelectCard = document.getElementById('fight-select-card'); const fightSelect = document.getElementById('fight-select'); const fflogsReportLink = document.getElementById('fflogs-report-link'); const languageSelect = document.getElementById('language-select'); const explorerCard = document.getElementById('event-explorer-card'); const exLoadBtn = document.getElementById('ex-load-btn'); const exAbilitySelect = document.getElementById('ex-ability'); const exPlayerSelect = document.getElementById('ex-player-name'); let allFights = []; function getUrlState() { const params = new URLSearchParams(window.location.search); const pick = (...names) => { for (const name of names) { const value = params.get(name); if (value !== null && value !== '') return value; } return ''; }; return { reportCode: pick('report_code', 'reportCode', 'report'), fightId: parseInt(pick('fightid', 'fight_id', 'fightId'), 10) || 0, compareReportCode: pick('compare_report_code', 'compareReportCode', 'compare_report', 'ref_report'), compareFightId: parseInt(pick('comparefightid', 'compare_fight_id', 'compareFightId', 'ref_fight_id'), 10) || 0, language: pick('language', 'lang'), translate: pick('translate'), }; } function normalizeLanguage(value, fallback = 'en') { const lang = String(value || '').toLowerCase(); return ['en', 'de', 'fr', 'jp'].includes(lang) ? lang : fallback; } function setUrlState(updates) { const url = new URL(window.location.href); const setOrDelete = (name, value) => { if (value === null || value === undefined || value === '') url.searchParams.delete(name); else url.searchParams.set(name, value); }; if ('reportCode' in updates) setOrDelete('report_code', updates.reportCode); if ('fightId' in updates) setOrDelete('fightid', updates.fightId); if ('compareReportCode' in updates) setOrDelete('compare_report_code', updates.compareReportCode); if ('compareFightId' in updates) setOrDelete('comparefightid', updates.compareFightId); if ('language' in updates) { setOrDelete('language', normalizeLanguage(updates.language)); url.searchParams.delete('translate'); } window.history.replaceState(null, '', url); } window.App.setUrlState = setUrlState; function authStartUrl() { return 'auth/start.php?return=' + encodeURIComponent(window.location.pathname + window.location.search); } window.App.authStartUrl = authStartUrl; function fflogsReportUrl(reportCode, fightId = 0) { const code = String(reportCode || '').trim(); if (!code) return '#'; const host = { de: 'de.fflogs.com', fr: 'fr.fflogs.com', jp: 'ja.fflogs.com', }[window.App.language] ?? 'www.fflogs.com'; const fight = parseInt(fightId, 10) || 0; return `https://${host}/reports/${encodeURIComponent(code)}${fight ? `#fight=${fight}` : ''}`; } window.App.fflogsReportUrl = fflogsReportUrl; function updateFflogsReportLink() { if (!window.App.reportCode) { fflogsReportLink.style.display = 'none'; fflogsReportLink.href = '#'; return; } fflogsReportLink.href = fflogsReportUrl(window.App.reportCode, window.App.fightId); fflogsReportLink.style.display = ''; } const initialUrlState = getUrlState(); const storedLanguage = localStorage.getItem('ff14-mitigator-language'); const legacyTranslateLanguage = initialUrlState.translate === '1' ? 'de' : 'en'; languageSelect.value = normalizeLanguage( initialUrlState.language || storedLanguage, initialUrlState.translate !== '' ? legacyTranslateLanguage : 'en' ); window.App.language = languageSelect.value; languageSelect.addEventListener('change', () => { window.App.language = normalizeLanguage(languageSelect.value); localStorage.setItem('ff14-mitigator-language', window.App.language); setUrlState({ language: window.App.language }); window.dispatchEvent(new CustomEvent('ff14-language-change', { detail: { language: window.App.language } })); if (window.App.reportCode) { loadReport(window.App.reportCode, window.App.fightId); } }); const codeInput = form.elements['report_code']; codeInput.addEventListener('input', () => { const match = codeInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/); if (match) codeInput.value = match[1]; }); function formatDuration(ms) { const min = Math.floor(ms / 60000); const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0'); return `${min}:${sec}`; } function formatBossHp(fight) { if (fight.kill) return 'Kill'; const pct = fight.fightPercentage; if (pct == null) return '?'; return pct.toFixed(2) + '%'; } function displayFight(fight) { output.textContent = JSON.stringify(fight, null, 2); outputCard.style.display = 'block'; } function openAnalysisTab() { document.querySelector('.tabs .tab[data-tab="analysis"]')?.click(); } function selectFight(id, updateUrl = true) { const fight = allFights.find(f => f.id === id); if (!fight) return false; fightSelect.value = String(id); window.App.fightId = id; window.App.fightStart = fight.startTime; window.App.fightEnd = fight.endTime; window.App.phases = buildPhases(fight); updateFflogsReportLink(); displayFight(fight); explorerCard.style.display = 'block'; if (updateUrl) { setUrlState({ reportCode: window.App.reportCode, fightId: id, language: window.App.language, }); } window.analysisTab?.onFightSelected?.(); loadAbilities(id, fight.startTime, fight.endTime); return true; } fightSelect.addEventListener('change', () => { if (!fightSelect.value) return; selectFight(parseInt(fightSelect.value, 10)); }); function buildPhases(fight) { const transitions = fight.phaseTransitions ?? []; if (transitions.length === 0) return []; const phases = transitions.map((t, i) => ({ id: t.id, name: `Phase ${t.id}`, startTime: t.startTime, endTime: transitions[i + 1]?.startTime ?? fight.endTime, })); return [{ id: 0, name: 'Ganzer Fight', startTime: fight.startTime, endTime: fight.endTime }, ...phases]; } async function loadAbilities(fightId, startTime, endTime) { exAbilitySelect.innerHTML = ''; try { const res = await fetch('api/abilities.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: window.App.reportCode, fight_id: fightId, start_time: startTime, end_time: endTime, language: window.App.language, }), }); const json = await res.json(); exAbilitySelect.innerHTML = ''; (json.abilities ?? []).forEach(ab => { const opt = document.createElement('option'); opt.value = ab.id; opt.textContent = ab.name; exAbilitySelect.appendChild(opt); }); exPlayerSelect.innerHTML = ''; (json.players ?? []).forEach(p => { const opt = document.createElement('option'); opt.value = p.name; opt.textContent = p.name; exPlayerSelect.appendChild(opt); }); } catch { exAbilitySelect.innerHTML = ''; } } exLoadBtn.addEventListener('click', async () => { if (!window.App.fightId) return; output.textContent = '// loading events...'; outputCard.style.display = 'block'; const params = { report_code: window.App.reportCode, fight_id: window.App.fightId, start_time: window.App.fightStart, end_time: window.App.fightEnd, language: window.App.language, data_type: document.getElementById('ex-data-type').value, ability_id: exAbilitySelect.value, event_type: document.getElementById('ex-event-type').value.trim(), player_name: document.getElementById('ex-player-name').value.trim(), limit: document.getElementById('ex-limit').value, start_offset: document.getElementById('ex-start-offset').value, end_offset: document.getElementById('ex-end-offset').value, }; try { const res = await fetch('api/debug-events.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(params), }); const json = await res.json(); if (json.reauth) { window.location.href = authStartUrl(); return; } output.textContent = JSON.stringify(json, null, 2); } catch (err) { output.textContent = '// Fehler: ' + err.message; } }); async function loadReport(reportCode, preferredFightId = 0) { initialHint.style.display = 'none'; outputCard.style.display = 'block'; output.textContent = '// fetching...'; fightSelectCard.style.display = 'none'; explorerCard.style.display = 'none'; fightSelect.innerHTML = ''; fflogsReportLink.style.display = 'none'; fflogsReportLink.href = '#'; allFights = []; window.App.reportCode = reportCode; window.App.fightId = null; window.App.fightStart = 0; window.App.fightEnd = 0; window.App.language = normalizeLanguage(languageSelect.value); window.App.phases = []; window.App.fights = []; window.analysisTab?.reset?.(); localStorage.setItem('ff14-mitigator-language', window.App.language); setUrlState({ reportCode, fightId: '', compareReportCode: '', compareFightId: '', language: window.App.language, }); let response, json; try { response = await fetch('api/fight.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ report_code: reportCode, language: window.App.language }), }); json = await response.json(); } catch (err) { output.textContent = '// network error: ' + err.message; return; } if (json.reauth) { output.textContent = '// session expired — redirecting...'; setTimeout(() => { window.location.href = authStartUrl(); }, 1500); return; } if (json.errors) { output.textContent = '// GraphQL error:\n' + JSON.stringify(json.errors, null, 2); return; } const report = json?.data?.reportData?.report; if (!report) { output.textContent = JSON.stringify(json, null, 2); return; } allFights = report.fights ?? []; if (allFights.length === 0) { output.textContent = '// Keine Fights in diesem Report gefunden.'; return; } allFights.forEach(fight => { const duration = formatDuration(fight.endTime - fight.startTime); const hp = formatBossHp(fight); const opt = document.createElement('option'); opt.value = fight.id; opt.textContent = `${fight.name} — ${duration} — ${hp}`; fightSelect.appendChild(opt); }); window.App.fights = allFights; fightSelectCard.style.display = 'block'; updateFflogsReportLink(); window.analysisTab?.onFightsLoaded?.(allFights); if (preferredFightId && selectFight(preferredFightId, true)) return; output.textContent = '// Fight auswählen ↑'; } form.addEventListener('submit', async (e) => { e.preventDefault(); await loadReport(form.elements['report_code'].value.trim()); }); if (initialUrlState.reportCode) { form.elements['report_code'].value = initialUrlState.reportCode; loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => { if (initialUrlState.fightId) { openAnalysisTab(); } if (initialUrlState.compareFightId) { window.analysisTab?.selectSharedCompare?.(initialUrlState.compareFightId, initialUrlState.compareReportCode); } }); } });