Merge pull request 'akus_schabanack' (#1) from akus_schabanack into main

Reviewed-on: #1
This commit is contained in:
xziino 2026-05-21 16:23:37 +02:00
commit 5f0bdb3504
7 changed files with 264 additions and 55 deletions

View File

@ -13,19 +13,32 @@ $reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
$fightId = (int)($_POST['fight_id'] ?? 0);
$startTime = (float)($_POST['start_time'] ?? 0);
$endTime = (float)($_POST['end_time'] ?? 0);
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
$translate = 'true';
if (!$reportCode || !$fightId || !$endTime) { http_response_code(400); echo json_encode(['error' => 'Missing params']); exit; }
$token = $_SESSION['access_token'];
function localized_graphql_uri(string $language): string {
$host = [
'de' => 'de.fflogs.com',
'fr' => 'fr.fflogs.com',
'jp' => 'ja.fflogs.com',
][$language] ?? 'www.fflogs.com';
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
}
function ab_gql(string $query): array {
global $token;
$ch = curl_init(GRAPHQL_URI);
global $token, $language;
$acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token, 'Accept-Language: ' . $acceptLanguage],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]);
$body = curl_exec($ch);
@ -39,7 +52,7 @@ $mdResult = ab_gql(<<<GQL
reportData {
report(code: "$reportCode") {
playerDetails(fightIDs: [$fightId])
masterData {
masterData(translate: $translate) {
abilities { gameID name }
}
}

View File

@ -24,6 +24,9 @@ $reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
$fightId = (int)($_POST['fight_id'] ?? 0);
$startTime = (float)($_POST['start_time'] ?? 0);
$endTime = (float)($_POST['end_time'] ?? 0);
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
$translate = 'true';
if (!$reportCode || !$fightId || !$endTime) {
http_response_code(400);
@ -33,9 +36,19 @@ if (!$reportCode || !$fightId || !$endTime) {
$token = $_SESSION['access_token'];
function localized_graphql_uri(string $language): string {
$host = [
'de' => 'de.fflogs.com',
'fr' => 'fr.fflogs.com',
'jp' => 'ja.fflogs.com',
][$language] ?? 'www.fflogs.com';
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
}
function fflogs_gql(string $query): array {
global $token;
$ch = curl_init(GRAPHQL_URI);
global $token, $language;
$acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
@ -43,6 +56,7 @@ function fflogs_gql(string $query): array {
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
'Accept-Language: ' . $acceptLanguage,
],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]);
@ -126,6 +140,7 @@ function resolveMitigations(string $buffStr, array $mitigIdMap): array {
if (isset($seen[$name])) continue;
$seen[$name] = true;
$result[] = [
'key' => $mitigIdMap[$id]['key'] ?? $name,
'name' => $name,
'dr' => $mitigIdMap[$id]['dr'],
'buffType' => $mitigIdMap[$id]['buffType'],
@ -146,7 +161,7 @@ function shieldsActiveAt(array $shieldTimeline, int $targetId, float $ts, array
if ($iv['apply'] <= $ts && ($iv['remove'] === null || $iv['remove'] >= $ts - 200)) {
if (isset($mitigIdMap[$statusId])) {
$m = $mitigIdMap[$statusId];
$result[] = ['name' => $m['name'], 'dr' => $m['dr'], 'buffType' => $m['buffType']];
$result[] = ['key' => $m['key'] ?? $m['name'], 'name' => $m['name'], 'dr' => $m['dr'], 'buffType' => $m['buffType']];
}
break;
}
@ -161,7 +176,7 @@ $pdResult = fflogs_gql(<<<GQL
reportData {
report(code: "$reportCode") {
playerDetails(fightIDs: [$fightId])
masterData {
masterData(translate: $translate) {
abilities {
gameID
name
@ -175,7 +190,7 @@ GQL);
if (isset($pdResult['_reauth'])) { echo json_encode(['reauth' => true]); exit; }
if (isset($pdResult['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $pdResult['_curl_error']]); exit; }
// abilityGameID → display name
// abilityGameID/statusID → display name
$abilityNames = [];
foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) {
$abilityNames[(int)$ab['gameID']] = $ab['name'];
@ -187,12 +202,13 @@ foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ??
$mitigIdMap = [];
foreach ($abilityNames as $gameId => $name) {
if (isset(MITIGATION_ABILITIES[$name])) {
$mitigIdMap[$gameId] = array_merge(['name' => $name], MITIGATION_ABILITIES[$name]);
$mitigIdMap[$gameId] = array_merge(['key' => $name, 'name' => $name], MITIGATION_ABILITIES[$name]);
}
}
foreach (MITIGATION_ABILITIES as $name => $meta) {
if (isset($meta['statusId']) && !isset($mitigIdMap[$meta['statusId']])) {
$mitigIdMap[$meta['statusId']] = array_merge(['name' => $name], $meta);
$displayName = $abilityNames[(int)$meta['statusId']] ?? $name;
$mitigIdMap[$meta['statusId']] = array_merge(['key' => $name, 'name' => $displayName], $meta);
}
}

View File

@ -17,6 +17,9 @@ $playerName = trim($_POST['player_name'] ?? '');
$eventType = trim($_POST['event_type'] ?? '');
$abilityId = (int)($_POST['ability_id'] ?? 0);
$limit = max(1, min(500, (int)($_POST['limit'] ?? 20)));
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
$translate = 'true';
$startOffset = (float)($_POST['start_offset'] ?? 0) * 1000; // s → ms
$endOffset = isset($_POST['end_offset']) && $_POST['end_offset'] !== ''
? (float)$_POST['end_offset'] * 1000
@ -33,14 +36,24 @@ $queryEnd = min($queryEnd, $endTime);
$token = $_SESSION['access_token'];
function localized_graphql_uri(string $language): string {
$host = [
'de' => 'de.fflogs.com',
'fr' => 'fr.fflogs.com',
'jp' => 'ja.fflogs.com',
][$language] ?? 'www.fflogs.com';
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
}
function dbg_gql(string $query): array {
global $token;
$ch = curl_init(GRAPHQL_URI);
global $token, $language;
$acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token, 'Accept-Language: ' . $acceptLanguage],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]);
$body = curl_exec($ch);
@ -85,6 +98,7 @@ $result = dbg_gql(<<<GQL
fightIDs: [$fightId],
dataType: $dataType,
$includeResources
translate: $translate,
startTime: $queryStart,
endTime: $queryEnd
) {

View File

@ -12,6 +12,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
}
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
if (strlen($reportCode) < 1) {
http_response_code(400);
@ -64,7 +66,17 @@ $payload = json_encode([
'variables' => ['reportCode' => $reportCode],
]);
$ch = curl_init(GRAPHQL_URI);
function localized_graphql_uri(string $language): string {
$host = [
'de' => 'de.fflogs.com',
'fr' => 'fr.fflogs.com',
'jp' => 'ja.fflogs.com',
][$language] ?? 'www.fflogs.com';
return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI);
}
$acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
@ -72,6 +84,7 @@ curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $_SESSION['access_token'],
'Accept-Language: ' . $acceptLanguage,
],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]);

View File

@ -219,6 +219,9 @@
if (!refId) {
refEvents = [];
refFightStart = 0;
refPlayers = [];
window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' });
renderRefPlayers();
renderTimeline(lastEvents, lastFightStart);
return;
}
@ -239,25 +242,30 @@
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 = [];
window.App.setUrlState?.({
compareReportCode: '',
compareFightId: refId,
language: window.App.language,
});
}
} catch { }
refFightSelect.disabled = false;
renderRefPlayers();
renderTimeline(lastEvents, lastFightStart);
});
let allSameReportFights = [];
function populateRefFightSelect() {
const currentName = (window.App.fights ?? []).find(f => f.id === window.App.fightId)?.name;
const visible = allSameReportFights.filter(f =>
f.id !== window.App.fightId && (!currentName || f.name === currentName)
);
const visible = allSameReportFights.filter(f => f.id !== window.App.fightId);
refFightSelect.innerHTML = '<option value="">Kein Vergleich</option>';
visible.forEach(f => {
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
@ -292,8 +300,7 @@
refExtPanel.style.display = hidden ? '' : 'none';
});
refReportLoad.addEventListener('click', async () => {
const code = refReportInput.value.trim();
async function loadExternalReport(code, preferredFightId = 0) {
if (!code) return;
refReportLoad.disabled = true;
@ -303,7 +310,7 @@
const res = await fetch('api/fight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ report_code: code }),
body: new URLSearchParams({ report_code: code, language: window.App.language }),
});
const json = await res.json();
if (json.reauth) { window.location.href = 'auth/start.php'; return; }
@ -312,8 +319,7 @@
extFights = fights;
extReportCode = code;
const currentName = (window.App.fights ?? []).find(f => f.id === window.App.fightId)?.name;
const visibleExt = currentName ? fights.filter(f => f.name === currentName) : fights;
const visibleExt = fights;
refExtFightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
visibleExt.forEach(f => {
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
@ -323,18 +329,27 @@
refExtFightSelect.appendChild(opt);
});
refExtFightSelect.style.display = visibleExt.length ? '' : 'none';
refExtPanel.style.display = '';
if (preferredFightId) {
refExtFightSelect.value = String(preferredFightId);
await loadExternalCompare(preferredFightId);
}
} catch { }
refReportLoad.disabled = false;
refReportLoad.textContent = 'Laden';
}
refReportLoad.addEventListener('click', async () => {
await loadExternalReport(refReportInput.value.trim());
});
refExtFightSelect.addEventListener('change', async () => {
const refId = parseInt(refExtFightSelect.value, 10);
async function loadExternalCompare(refId) {
if (!refId) {
refEvents = [];
refFightStart = 0;
refPlayers = [];
window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' });
renderRefPlayers();
renderTimeline(lastEvents, lastFightStart);
return;
@ -356,6 +371,7 @@
fight_id: refId,
start_time: fight.startTime,
end_time: fight.endTime,
language: window.App.language,
}),
});
const json = await res.json();
@ -363,11 +379,20 @@
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 () => {
await loadExternalCompare(parseInt(refExtFightSelect.value, 10));
});
// ── Timeline rendering ────────────────────────────────────────────────────
@ -405,24 +430,25 @@
if (!visibleTargets.length) return '';
// Collect boss debuffs (Reprisal/Feint/Addle) once at event level
const seenDebuffNames = new Set();
const seenDebuffKeys = new Set();
const eventDebuffs = [];
for (const t of visibleTargets) {
for (const m of (t.mitigations ?? [])) {
if (m.buffType === 'debuff' && !seenDebuffNames.has(m.name)) {
seenDebuffNames.add(m.name);
const key = m.key ?? m.name;
if (m.buffType === 'debuff' && !seenDebuffKeys.has(key)) {
seenDebuffKeys.add(key);
eventDebuffs.push(m);
}
}
}
const eventMissingDebuffs = refEv
? (refEv.targets[0]?.mitigations ?? []).filter(m => m.buffType === 'debuff' && !seenDebuffNames.has(m.name))
? (refEv.targets[0]?.mitigations ?? []).filter(m => m.buffType === 'debuff' && !seenDebuffKeys.has(m.key ?? m.name))
: [];
const debuffIconsHtml = [
...eventDebuffs.map(m => ({ ...m, missing: false })),
...eventMissingDebuffs.map(m => ({ ...m, missing: true })),
].map(m => {
const iconSrc = MITIG_ICONS[m.name];
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return m.missing
@ -447,15 +473,15 @@
</div>`;
})() : '';
const currentMitigNames = new Set((t.mitigations ?? []).map(m => m.name));
const currentMitigKeys = new Set((t.mitigations ?? []).map(m => m.key ?? m.name));
const refTarget = refEv?.targets?.find(rt => rt.name === t.name);
const missingMitigs = refTarget
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigNames.has(m.name))
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigKeys.has(m.key ?? m.name))
: [];
// DR buff icons (shown below player box)
const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
const iconSrc = MITIG_ICONS[m.name];
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
@ -464,7 +490,7 @@
// Shield tooltip on absorbed value
const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
const missingShields = refTarget
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigNames.has(m.name))
? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigKeys.has(m.key ?? m.name))
: [];
const shieldLines = [
...activeShields.map(s => s.name),
@ -504,11 +530,11 @@
const currentByName = {};
ev.targets.forEach(t => { currentByName[t.name] = t; });
const seenRefDebuffNames = new Set();
const seenRefDebuffKeys = new Set();
const refDebuffIconsHtml = refVisible.flatMap(t => (t.mitigations ?? []))
.filter(m => m.buffType === 'debuff' && !seenRefDebuffNames.has(m.name) && seenRefDebuffNames.add(m.name))
.filter(m => m.buffType === 'debuff' && !seenRefDebuffKeys.has(m.key ?? m.name) && seenRefDebuffKeys.add(m.key ?? m.name))
.map(m => {
const iconSrc = MITIG_ICONS[m.name];
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
@ -523,13 +549,13 @@
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
: '';
const currMitigNames = new Set((curr?.mitigations ?? []).map(m => m.name));
const currMitigKeys = new Set((curr?.mitigations ?? []).map(m => m.key ?? m.name));
const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => {
const iconSrc = MITIG_ICONS[m.name];
const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name];
if (!iconSrc) return '';
const dr = m.dr > 0 ? ` ${m.dr}%` : '';
const missing = !currMitigNames.has(m.name);
const missing = !currMitigKeys.has(m.key ?? m.name);
const cls = missing ? ' aoe-buff-ref-unique' : '';
const titleSufx = missing ? ' (fehlt im aktuellen Pull)' : '';
return `<img class="aoe-target-buff-icon${cls}" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}${titleSufx}">`;
@ -537,7 +563,7 @@
const refShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield');
const refShieldTitle = refShields.length
? refShields.map(s => currMitigNames.has(s.name) ? s.name : `${s.name} [fehlt im aktuellen Pull]`).join('\n')
? refShields.map(s => currMitigKeys.has(s.key ?? s.name) ? s.name : `${s.name} [fehlt im aktuellen Pull]`).join('\n')
: null;
return `
@ -609,7 +635,7 @@
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 }),
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) {
@ -634,6 +660,18 @@
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'));
}
},
reset() {
lastFightId = null;
refEvents = [];

130
js/app.js
View File

@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0, phases: [], fights: [] };
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');
@ -7,6 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
const initialHint = document.getElementById('initial-hint');
const fightSelectCard = document.getElementById('fight-select-card');
const fightSelect = document.getElementById('fight-select');
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');
@ -14,6 +15,68 @@ document.addEventListener('DOMContentLoaded', () => {
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;
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 });
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]+)/);
@ -38,12 +101,15 @@ document.addEventListener('DOMContentLoaded', () => {
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;
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;
@ -51,8 +117,21 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
@ -78,6 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
fight_id: fightId,
start_time: startTime,
end_time: endTime,
language: window.App.language,
}),
});
const json = await res.json();
@ -113,6 +193,7 @@ document.addEventListener('DOMContentLoaded', () => {
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(),
@ -136,9 +217,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
async function loadReport(reportCode, preferredFightId = 0) {
initialHint.style.display = 'none';
outputCard.style.display = 'block';
output.textContent = '// fetching...';
@ -147,21 +226,29 @@ document.addEventListener('DOMContentLoaded', () => {
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.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 }),
body: new URLSearchParams({ report_code: reportCode, language: window.App.language }),
});
json = await response.json();
} catch (err) {
@ -204,7 +291,26 @@ document.addEventListener('DOMContentLoaded', () => {
window.App.fights = allFights;
fightSelectCard.style.display = 'block';
output.textContent = '// Fight auswählen ↑';
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);
}
});
}
});

View File

@ -13,6 +13,15 @@
required
>
</div>
<div class="fg">
<label>Namen</label>
<select name="language" id="language-select">
<option value="en">EN</option>
<option value="de">DE</option>
<option value="fr">FR</option>
<option value="jp">JP</option>
</select>
</div>
<button class="btn btn-gold" type="submit" style="align-self:flex-end">Fetch</button>
<a class="btn" href="auth/start.php" style="align-self:flex-end;text-decoration:none">Reconnect</a>
</div>