forked from xziino/ff14-mitigator
Merge pull request 'akus_schabanack' (#1) from akus_schabanack into main
Reviewed-on: xziino/ff14-mitigator#1
This commit is contained in:
commit
5f0bdb3504
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
@ -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
130
js/app.js
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user