forked from xziino/ff14-mitigator
- Replace fixed-window AoE bucketing with proximity clustering (1000ms from first event per ability) to prevent boundary splits - Show absorbed shield damage in blue next to player damage (e.g. 40k +20k) - Show overkill damage in red below job abbreviation on death - Phase dropdown in AoE Timeline for multi-phase fights (phaseTransitions) - Default phase selection: Ganzer Fight Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
202 lines
7.8 KiB
JavaScript
202 lines
7.8 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0, phases: [] };
|
|
|
|
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 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 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';
|
|
}
|
|
|
|
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;
|
|
window.App.phases = buildPhases(fight);
|
|
|
|
displayFight(fight);
|
|
explorerCard.style.display = 'block';
|
|
window.analysisTab?.onFightSelected?.();
|
|
loadAbilities(id, fight.startTime, fight.endTime);
|
|
});
|
|
|
|
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 = '<option value="">Lädt…</option>';
|
|
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,
|
|
}),
|
|
});
|
|
const json = await res.json();
|
|
|
|
exAbilitySelect.innerHTML = '<option value="">Alle</option>';
|
|
(json.abilities ?? []).forEach(ab => {
|
|
const opt = document.createElement('option');
|
|
opt.value = ab.id;
|
|
opt.textContent = ab.name;
|
|
exAbilitySelect.appendChild(opt);
|
|
});
|
|
|
|
exPlayerSelect.innerHTML = '<option value="">Alle</option>';
|
|
(json.players ?? []).forEach(p => {
|
|
const opt = document.createElement('option');
|
|
opt.value = p.name;
|
|
opt.textContent = p.name;
|
|
exPlayerSelect.appendChild(opt);
|
|
});
|
|
} catch {
|
|
exAbilitySelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
|
}
|
|
}
|
|
|
|
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,
|
|
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 = 'auth/start.php'; return; }
|
|
output.textContent = JSON.stringify(json, null, 2);
|
|
} catch (err) {
|
|
output.textContent = '// Fehler: ' + err.message;
|
|
}
|
|
});
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
initialHint.style.display = 'none';
|
|
outputCard.style.display = 'block';
|
|
output.textContent = '// fetching...';
|
|
fightSelectCard.style.display = 'none';
|
|
explorerCard.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.App.phases = [];
|
|
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 ↑';
|
|
});
|
|
});
|