Two-tab app: report viewer + analysis tab with AoE timeline, per-player mitigation icons (local XIVAPI PNGs), and fight-wide buff/debuff window tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.9 KiB
JavaScript
113 lines
3.9 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
||
window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0 };
|
||
|
||
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');
|
||
|
||
let allFights = [];
|
||
|
||
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 '?';
|
||
// fightPercentage is 0–10000 (e.g. 5000 = 50.00%)
|
||
return (pct / 100).toFixed(2) + '%';
|
||
}
|
||
|
||
function displayFight(fight) {
|
||
output.textContent = JSON.stringify(fight, null, 2);
|
||
outputCard.style.display = 'block';
|
||
}
|
||
|
||
fightSelect.addEventListener('change', () => {
|
||
if (!fightSelect.value) return;
|
||
const id = parseInt(fightSelect.value, 10);
|
||
const fight = allFights.find(f => f.id === id);
|
||
if (!fight) return;
|
||
|
||
window.App.fightId = id;
|
||
window.App.fightStart = fight.startTime;
|
||
window.App.fightEnd = fight.endTime;
|
||
|
||
displayFight(fight);
|
||
window.analysisTab?.onFightSelected?.();
|
||
});
|
||
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
initialHint.style.display = 'none';
|
||
outputCard.style.display = 'block';
|
||
output.textContent = '// fetching...';
|
||
fightSelectCard.style.display = 'none';
|
||
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
||
allFights = [];
|
||
|
||
const reportCode = form.elements['report_code'].value.trim();
|
||
window.App.reportCode = reportCode;
|
||
window.App.fightId = null;
|
||
window.App.fightStart = 0;
|
||
window.App.fightEnd = 0;
|
||
window.analysisTab?.reset?.();
|
||
|
||
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 }),
|
||
});
|
||
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 = 'auth/start.php'; }, 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);
|
||
});
|
||
|
||
fightSelectCard.style.display = 'block';
|
||
output.textContent = '// Fight auswählen ↑';
|
||
});
|
||
});
|