akus_schabanack #1

Merged
xziino merged 3 commits from akus_schabanack into main 2026-05-21 16:23:38 +02:00
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); $fightId = (int)($_POST['fight_id'] ?? 0);
$startTime = (float)($_POST['start_time'] ?? 0); $startTime = (float)($_POST['start_time'] ?? 0);
$endTime = (float)($_POST['end_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; } if (!$reportCode || !$fightId || !$endTime) { http_response_code(400); echo json_encode(['error' => 'Missing params']); exit; }
$token = $_SESSION['access_token']; $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 { function ab_gql(string $query): array {
global $token; global $token, $language;
$ch = curl_init(GRAPHQL_URI); $acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]), CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
CURLOPT_RETURNTRANSFER => true, 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, CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]); ]);
$body = curl_exec($ch); $body = curl_exec($ch);
@ -39,7 +52,7 @@ $mdResult = ab_gql(<<<GQL
reportData { reportData {
report(code: "$reportCode") { report(code: "$reportCode") {
playerDetails(fightIDs: [$fightId]) playerDetails(fightIDs: [$fightId])
masterData { masterData(translate: $translate) {
abilities { gameID name } 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); $fightId = (int)($_POST['fight_id'] ?? 0);
$startTime = (float)($_POST['start_time'] ?? 0); $startTime = (float)($_POST['start_time'] ?? 0);
$endTime = (float)($_POST['end_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) { if (!$reportCode || !$fightId || !$endTime) {
http_response_code(400); http_response_code(400);
@ -33,9 +36,19 @@ if (!$reportCode || !$fightId || !$endTime) {
$token = $_SESSION['access_token']; $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 { function fflogs_gql(string $query): array {
global $token; global $token, $language;
$ch = curl_init(GRAPHQL_URI); $acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]), CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
@ -43,6 +56,7 @@ function fflogs_gql(string $query): array {
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
'Content-Type: application/json', 'Content-Type: application/json',
'Authorization: Bearer ' . $token, 'Authorization: Bearer ' . $token,
'Accept-Language: ' . $acceptLanguage,
], ],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE, CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]); ]);
@ -126,6 +140,7 @@ function resolveMitigations(string $buffStr, array $mitigIdMap): array {
if (isset($seen[$name])) continue; if (isset($seen[$name])) continue;
$seen[$name] = true; $seen[$name] = true;
$result[] = [ $result[] = [
'key' => $mitigIdMap[$id]['key'] ?? $name,
'name' => $name, 'name' => $name,
'dr' => $mitigIdMap[$id]['dr'], 'dr' => $mitigIdMap[$id]['dr'],
'buffType' => $mitigIdMap[$id]['buffType'], '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 ($iv['apply'] <= $ts && ($iv['remove'] === null || $iv['remove'] >= $ts - 200)) {
if (isset($mitigIdMap[$statusId])) { if (isset($mitigIdMap[$statusId])) {
$m = $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; break;
} }
@ -161,7 +176,7 @@ $pdResult = fflogs_gql(<<<GQL
reportData { reportData {
report(code: "$reportCode") { report(code: "$reportCode") {
playerDetails(fightIDs: [$fightId]) playerDetails(fightIDs: [$fightId])
masterData { masterData(translate: $translate) {
abilities { abilities {
gameID gameID
name name
@ -175,7 +190,7 @@ GQL);
if (isset($pdResult['_reauth'])) { echo json_encode(['reauth' => true]); exit; } 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; } 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 = []; $abilityNames = [];
foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) { foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) {
$abilityNames[(int)$ab['gameID']] = $ab['name']; $abilityNames[(int)$ab['gameID']] = $ab['name'];
@ -187,12 +202,13 @@ foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ??
$mitigIdMap = []; $mitigIdMap = [];
foreach ($abilityNames as $gameId => $name) { foreach ($abilityNames as $gameId => $name) {
if (isset(MITIGATION_ABILITIES[$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) { foreach (MITIGATION_ABILITIES as $name => $meta) {
if (isset($meta['statusId']) && !isset($mitigIdMap[$meta['statusId']])) { 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'] ?? ''); $eventType = trim($_POST['event_type'] ?? '');
$abilityId = (int)($_POST['ability_id'] ?? 0); $abilityId = (int)($_POST['ability_id'] ?? 0);
$limit = max(1, min(500, (int)($_POST['limit'] ?? 20))); $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 $startOffset = (float)($_POST['start_offset'] ?? 0) * 1000; // s → ms
$endOffset = isset($_POST['end_offset']) && $_POST['end_offset'] !== '' $endOffset = isset($_POST['end_offset']) && $_POST['end_offset'] !== ''
? (float)$_POST['end_offset'] * 1000 ? (float)$_POST['end_offset'] * 1000
@ -33,14 +36,24 @@ $queryEnd = min($queryEnd, $endTime);
$token = $_SESSION['access_token']; $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 { function dbg_gql(string $query): array {
global $token; global $token, $language;
$ch = curl_init(GRAPHQL_URI); $acceptLanguage = $language === 'jp' ? 'ja' : $language;
$ch = curl_init(localized_graphql_uri($language));
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['query' => $query]), CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
CURLOPT_RETURNTRANSFER => true, 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, CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]); ]);
$body = curl_exec($ch); $body = curl_exec($ch);
@ -85,6 +98,7 @@ $result = dbg_gql(<<<GQL
fightIDs: [$fightId], fightIDs: [$fightId],
dataType: $dataType, dataType: $dataType,
$includeResources $includeResources
translate: $translate,
startTime: $queryStart, startTime: $queryStart,
endTime: $queryEnd endTime: $queryEnd
) { ) {

View File

@ -12,6 +12,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
} }
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? ''); $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) { if (strlen($reportCode) < 1) {
http_response_code(400); http_response_code(400);
@ -64,7 +66,17 @@ $payload = json_encode([
'variables' => ['reportCode' => $reportCode], '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, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload, CURLOPT_POSTFIELDS => $payload,
@ -72,6 +84,7 @@ curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
'Content-Type: application/json', 'Content-Type: application/json',
'Authorization: Bearer ' . $_SESSION['access_token'], 'Authorization: Bearer ' . $_SESSION['access_token'],
'Accept-Language: ' . $acceptLanguage,
], ],
CURLOPT_SSL_VERIFYPEER => !DEV_MODE, CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
]); ]);

View File

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

130
js/app.js
View File

@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => { 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 form = document.getElementById('report-form');
const output = document.getElementById('output'); const output = document.getElementById('output');
@ -7,6 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
const initialHint = document.getElementById('initial-hint'); const initialHint = document.getElementById('initial-hint');
const fightSelectCard = document.getElementById('fight-select-card'); const fightSelectCard = document.getElementById('fight-select-card');
const fightSelect = document.getElementById('fight-select'); const fightSelect = document.getElementById('fight-select');
const languageSelect = document.getElementById('language-select');
const explorerCard = document.getElementById('event-explorer-card'); const explorerCard = document.getElementById('event-explorer-card');
const exLoadBtn = document.getElementById('ex-load-btn'); const exLoadBtn = document.getElementById('ex-load-btn');
const exAbilitySelect = document.getElementById('ex-ability'); const exAbilitySelect = document.getElementById('ex-ability');
@ -14,6 +15,68 @@ document.addEventListener('DOMContentLoaded', () => {
let allFights = []; 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']; const codeInput = form.elements['report_code'];
codeInput.addEventListener('input', () => { codeInput.addEventListener('input', () => {
const match = codeInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/); const match = codeInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/);
@ -38,12 +101,15 @@ document.addEventListener('DOMContentLoaded', () => {
outputCard.style.display = 'block'; outputCard.style.display = 'block';
} }
fightSelect.addEventListener('change', () => { function openAnalysisTab() {
if (!fightSelect.value) return; document.querySelector('.tabs .tab[data-tab="analysis"]')?.click();
const id = parseInt(fightSelect.value, 10); }
const fight = allFights.find(f => f.id === id);
if (!fight) return;
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.fightId = id;
window.App.fightStart = fight.startTime; window.App.fightStart = fight.startTime;
window.App.fightEnd = fight.endTime; window.App.fightEnd = fight.endTime;
@ -51,8 +117,21 @@ document.addEventListener('DOMContentLoaded', () => {
displayFight(fight); displayFight(fight);
explorerCard.style.display = 'block'; explorerCard.style.display = 'block';
if (updateUrl) {
setUrlState({
reportCode: window.App.reportCode,
fightId: id,
language: window.App.language,
});
}
window.analysisTab?.onFightSelected?.(); window.analysisTab?.onFightSelected?.();
loadAbilities(id, fight.startTime, fight.endTime); loadAbilities(id, fight.startTime, fight.endTime);
return true;
}
fightSelect.addEventListener('change', () => {
if (!fightSelect.value) return;
selectFight(parseInt(fightSelect.value, 10));
}); });
function buildPhases(fight) { function buildPhases(fight) {
@ -78,6 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
fight_id: fightId, fight_id: fightId,
start_time: startTime, start_time: startTime,
end_time: endTime, end_time: endTime,
language: window.App.language,
}), }),
}); });
const json = await res.json(); const json = await res.json();
@ -113,6 +193,7 @@ document.addEventListener('DOMContentLoaded', () => {
fight_id: window.App.fightId, fight_id: window.App.fightId,
start_time: window.App.fightStart, start_time: window.App.fightStart,
end_time: window.App.fightEnd, end_time: window.App.fightEnd,
language: window.App.language,
data_type: document.getElementById('ex-data-type').value, data_type: document.getElementById('ex-data-type').value,
ability_id: exAbilitySelect.value, ability_id: exAbilitySelect.value,
event_type: document.getElementById('ex-event-type').value.trim(), event_type: document.getElementById('ex-event-type').value.trim(),
@ -136,9 +217,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
form.addEventListener('submit', async (e) => { async function loadReport(reportCode, preferredFightId = 0) {
e.preventDefault();
initialHint.style.display = 'none'; initialHint.style.display = 'none';
outputCard.style.display = 'block'; outputCard.style.display = 'block';
output.textContent = '// fetching...'; output.textContent = '// fetching...';
@ -147,21 +226,29 @@ document.addEventListener('DOMContentLoaded', () => {
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>'; fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
allFights = []; allFights = [];
const reportCode = form.elements['report_code'].value.trim();
window.App.reportCode = reportCode; window.App.reportCode = reportCode;
window.App.fightId = null; window.App.fightId = null;
window.App.fightStart = 0; window.App.fightStart = 0;
window.App.fightEnd = 0; window.App.fightEnd = 0;
window.App.language = normalizeLanguage(languageSelect.value);
window.App.phases = []; window.App.phases = [];
window.App.fights = []; window.App.fights = [];
window.analysisTab?.reset?.(); window.analysisTab?.reset?.();
localStorage.setItem('ff14-mitigator-language', window.App.language);
setUrlState({
reportCode,
fightId: '',
compareReportCode: '',
compareFightId: '',
language: window.App.language,
});
let response, json; let response, json;
try { try {
response = await fetch('api/fight.php', { response = await fetch('api/fight.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 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(); json = await response.json();
} catch (err) { } catch (err) {
@ -204,7 +291,26 @@ document.addEventListener('DOMContentLoaded', () => {
window.App.fights = allFights; window.App.fights = allFights;
fightSelectCard.style.display = 'block'; fightSelectCard.style.display = 'block';
output.textContent = '// Fight auswählen ↑';
window.analysisTab?.onFightsLoaded?.(allFights); 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 required
> >
</div> </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> <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> <a class="btn" href="auth/start.php" style="align-self:flex-end;text-decoration:none">Reconnect</a>
</div> </div>