small fix, stay on playner page on refresh and translate planner

This commit is contained in:
Akurosia Kamo 2026-05-22 12:32:50 +02:00
parent 14674d2842
commit c67b08737e
5 changed files with 175 additions and 24 deletions

View File

@ -212,11 +212,12 @@ foreach (MITIGATION_ABILITIES as $name => $meta) {
} }
} }
// statusId set for shield abilities — used to filter the buff timeline query // statusId set for tracked mitigations — used to resolve localized buff names
$shieldStatusIds = []; // from Buffs events and to build the shield fallback timeline.
$trackedStatusIds = [];
foreach (MITIGATION_ABILITIES as $meta) { foreach (MITIGATION_ABILITIES as $meta) {
if ($meta['buffType'] === 'shield' && isset($meta['statusId'])) { if (isset($meta['statusId'])) {
$shieldStatusIds[$meta['statusId']] = true; $trackedStatusIds[$meta['statusId']] = true;
} }
} }
@ -276,8 +277,9 @@ for ($page = 0; $page < 10; $page++) {
// Builds applybuff/removebuff intervals per target so we can detect shields // Builds applybuff/removebuff intervals per target so we can detect shields
// that were consumed by a hit (absent from the damage event's buffs snapshot). // that were consumed by a hit (absent from the damage event's buffs snapshot).
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...] $shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
$statusNames = []; // statusId → localized display name from Buffs events
if (!empty($shieldStatusIds)) { if (!empty($trackedStatusIds)) {
$nextPage = $startTime; $nextPage = $startTime;
for ($page = 0; $page < 10; $page++) { for ($page = 0; $page < 10; $page++) {
$bfResult = fflogs_gql(<<<GQL $bfResult = fflogs_gql(<<<GQL
@ -304,11 +306,19 @@ if (!empty($shieldStatusIds)) {
$bfEv = $bfResult['data']['reportData']['report']['events'] ?? []; $bfEv = $bfResult['data']['reportData']['report']['events'] ?? [];
foreach ($bfEv['data'] ?? [] as $ev) { foreach ($bfEv['data'] ?? [] as $ev) {
$abId = (int)($ev['abilityGameID'] ?? 0); $abId = (int)($ev['abilityGameID'] ?? 0);
if (!isset($shieldStatusIds[$abId])) continue; if (!isset($trackedStatusIds[$abId])) continue;
$evName = $ev['ability']['name'] ?? null;
if (is_string($evName) && $evName !== '') {
$statusNames[$abId] = $evName;
}
$tgtId = (int)($ev['targetID'] ?? 0); $tgtId = (int)($ev['targetID'] ?? 0);
$ts = (float)($ev['timestamp'] ?? 0); $ts = (float)($ev['timestamp'] ?? 0);
$type = $ev['type'] ?? ''; $type = $ev['type'] ?? '';
$meta = $mitigIdMap[$abId] ?? null;
if (($meta['buffType'] ?? null) !== 'shield') continue;
if ($type === 'applybuff') { if ($type === 'applybuff') {
$shieldTimeline[$tgtId][$abId][] = ['apply' => $ts, 'remove' => null]; $shieldTimeline[$tgtId][$abId][] = ['apply' => $ts, 'remove' => null];
@ -329,6 +339,20 @@ if (!empty($shieldStatusIds)) {
} }
} }
foreach ($statusNames as $statusId => $displayName) {
if (isset($mitigIdMap[$statusId])) {
$mitigIdMap[$statusId]['name'] = $displayName;
}
}
$mitigationNames = [];
foreach ($mitigIdMap as $meta) {
$key = $meta['key'] ?? null;
if ($key) {
$mitigationNames[$key] = $meta['name'] ?? $key;
}
}
// ── 3. AoE detection — proximity clustering ──────────────────────────────── // ── 3. AoE detection — proximity clustering ────────────────────────────────
// Group events by abilityId, then cluster by time proximity (≤ 1000ms from // Group events by abilityId, then cluster by time proximity (≤ 1000ms from
// the first event in the cluster) to avoid fixed-window boundary splits. // the first event in the cluster) to avoid fixed-window boundary splits.
@ -477,4 +501,5 @@ echo json_encode([
'players' => array_values($players), 'players' => array_values($players),
'aoe_events' => $aoeEvents, 'aoe_events' => $aoeEvents,
'fight_start' => (int)$startTime, 'fight_start' => (int)$startTime,
'mitigation_names' => $mitigationNames,
]); ]);

View File

@ -103,6 +103,7 @@
let currentPlayers = []; let currentPlayers = [];
let extFights = []; let extFights = [];
let extReportCode = ''; let extReportCode = '';
let mitigationNames = {};
// ── Player grid ────────────────────────────────────────────────────────── // ── Player grid ──────────────────────────────────────────────────────────
@ -683,6 +684,7 @@
populateRefFightSelect(); populateRefFightSelect();
setupPhases(window.App?.phases ?? []); setupPhases(window.App?.phases ?? []);
renderPlayers(json.players ?? []); renderPlayers(json.players ?? []);
mitigationNames = json.mitigation_names ?? {};
renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart); renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart);
document.getElementById('analysis-loading').style.display = 'none'; document.getElementById('analysis-loading').style.display = 'none';
@ -717,6 +719,9 @@
players: currentPlayers, players: currentPlayers,
fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`, fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`,
reportCode: window.App?.reportCode ?? '', reportCode: window.App?.reportCode ?? '',
fightId: window.App?.fightId ?? 0,
fightEnd: window.App?.fightEnd ?? 0,
mitigationNames,
}; };
}, },
reset() { reset() {
@ -726,6 +731,7 @@
refPlayers = []; refPlayers = [];
extFights = []; extFights = [];
extReportCode = ''; extReportCode = '';
mitigationNames = {};
document.getElementById('ref-player-section').style.display = 'none'; document.getElementById('ref-player-section').style.display = 'none';
refFightSelect.value = ''; refFightSelect.value = '';
refFightSelect.style.display = 'none'; refFightSelect.style.display = 'none';

View File

@ -137,6 +137,12 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelector('.tabs .tab[data-tab="analysis"]')?.click(); document.querySelector('.tabs .tab[data-tab="analysis"]')?.click();
} }
function shouldAutoOpenAnalysis() {
const params = new URLSearchParams(window.location.search);
const requestedTab = params.get('tab') || localStorage.getItem('ff14-mitigator-active-tab');
return requestedTab !== 'planner';
}
function selectFight(id, updateUrl = true) { function selectFight(id, updateUrl = true) {
const fight = allFights.find(f => f.id === id); const fight = allFights.find(f => f.id === id);
if (!fight) return false; if (!fight) return false;
@ -341,7 +347,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (initialUrlState.reportCode) { if (initialUrlState.reportCode) {
form.elements['report_code'].value = initialUrlState.reportCode; form.elements['report_code'].value = initialUrlState.reportCode;
loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => { loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => {
if (initialUrlState.fightId) { if (initialUrlState.fightId && shouldAutoOpenAnalysis()) {
openAnalysisTab(); openAnalysisTab();
} }
if (initialUrlState.compareFightId) { if (initialUrlState.compareFightId) {

View File

@ -1,6 +1,7 @@
// ── Storage ─────────────────────────────────────────────────────────────────── // ── Storage ───────────────────────────────────────────────────────────────────
const PLANNER_KEY = 'ff14-planner-plans'; const PLANNER_KEY = 'ff14-planner-plans';
const PLANNER_ACTIVE_KEY = 'ff14-planner-active-plan';
function loadPlans() { function loadPlans() {
try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); } try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); }
@ -20,6 +21,7 @@ function createPlan(name) {
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
source: null, source: null,
mitigationNames: {},
jobComposition: Array(8).fill(''), jobComposition: Array(8).fill(''),
mechanics: [] mechanics: []
}; };
@ -89,8 +91,22 @@ function fmtNumber(n) {
return Number(n).toLocaleString('de-DE'); return Number(n).toLocaleString('de-DE');
} }
function assignmentAbilityName(assignment) { function assignmentAbilityName(assignment, plan = null) {
return assignment?.abilityName ?? assignment?.ability ?? ''; const key = assignment?.ability ?? '';
return assignment?.abilityName ?? plan?.mitigationNames?.[key] ?? key;
}
function plannerLanguage() {
return window.App?.language || localStorage.getItem('ff14-mitigator-language') || 'en';
}
function sameMechanic(existing, incoming, source) {
const fightStart = source?.fightStart ?? 0;
const incomingRel = incoming.timestamp - fightStart;
if (existing.abilityId && incoming.abilityId && existing.abilityId === incoming.abilityId) {
return Math.abs(existing.timestamp - incomingRel) < 1500;
}
return Math.abs(existing.timestamp - incomingRel) < 1500;
} }
// ── Rendering: Plan List ────────────────────────────────────────────────────── // ── Rendering: Plan List ──────────────────────────────────────────────────────
@ -226,7 +242,7 @@ function renderMechanicListHtml(plan) {
: 'badge-assign-buff'; : 'badge-assign-buff';
const isMissing = !!a.job && !activeJobSet.has(a.job); const isMissing = !!a.job && !activeJobSet.has(a.job);
const icon = MITIG_ICONS[a.ability] ?? ''; const icon = MITIG_ICONS[a.ability] ?? '';
const ability = assignmentAbilityName(a); const ability = assignmentAbilityName(a, plan);
const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability); const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability);
const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : ''; const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : '';
return `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}> return `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}>
@ -384,8 +400,10 @@ function startRename(id, currentName) {
function openPlan(id) { function openPlan(id) {
activePlanId = id; activePlanId = id;
localStorage.setItem(PLANNER_ACTIVE_KEY, id);
renderPlanList(); renderPlanList();
renderPlanDetail(getPlan(id)); renderPlanDetail(getPlan(id));
refreshPlanLanguage(id);
} }
// ── New plan form ───────────────────────────────────────────────────────────── // ── New plan form ─────────────────────────────────────────────────────────────
@ -633,7 +651,7 @@ function mitigationKey(mitigation) {
// ── AoE Events → Plan Mechanics ─────────────────────────────────────────────── // ── AoE Events → Plan Mechanics ───────────────────────────────────────────────
function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations) { function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations, mitigationNames = {}) {
return aoeEvents.map(ev => { return aoeEvents.map(ev => {
const relTs = ev.timestamp - fightStart; const relTs = ev.timestamp - fightStart;
const phase = (phases ?? []).filter(p => p.id !== 0).find(p => const phase = (phases ?? []).filter(p => p.id !== 0).find(p =>
@ -657,7 +675,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
seen.add(key); seen.add(key);
assignments.push({ assignments.push({
ability: key, ability: key,
abilityName: mitigationDisplayName(m), abilityName: mitigationDisplayName(m) || mitigationNames[key],
job: guessJob(key, players), job: guessJob(key, players),
buffType: m.buffType ?? '', buffType: m.buffType ?? '',
}); });
@ -669,6 +687,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
return { return {
id: crypto.randomUUID(), id: crypto.randomUUID(),
name: ev.abilityName, name: ev.abilityName,
abilityId: ev.abilityId,
timestamp: relTs, timestamp: relTs,
phase: phase?.name ?? '', phase: phase?.name ?? '',
unmitigatedDamage: avgUnmit, unmitigatedDamage: avgUnmit,
@ -694,14 +713,16 @@ function extractJobComp(players) {
} }
function doImport(data, withMitigations, whereMode, mergeId, newName) { function doImport(data, withMitigations, whereMode, mergeId, newName) {
const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data; const { aoeEvents, fightStart, fightEnd, phases, players, fightName, reportCode, fightId, mitigationNames = {} } = data;
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations); const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations, mitigationNames);
const source = { reportCode, fightId, fightName, fightStart, fightEnd, language: plannerLanguage() };
if (whereMode === 'new') { if (whereMode === 'new') {
const plan = createPlan(newName || fightName || 'Importierter Plan'); const plan = createPlan(newName || fightName || 'Importierter Plan');
return updatePlan(plan.id, { return updatePlan(plan.id, {
mechanics, mechanics,
source: { reportCode, fightName }, source,
mitigationNames,
jobComposition: extractJobComp(players), jobComposition: extractJobComp(players),
}); });
} }
@ -719,7 +740,87 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
} }
merged.sort((a, b) => a.timestamp - b.timestamp); merged.sort((a, b) => a.timestamp - b.timestamp);
return updatePlan(mergeId, { mechanics: merged }); return updatePlan(mergeId, {
mechanics: merged,
source: { ...(plan.source ?? {}), ...source },
mitigationNames: { ...(plan.mitigationNames ?? {}), ...mitigationNames },
});
}
const refreshingPlans = new Set();
async function refreshPlanLanguage(planId) {
const plan = getPlan(planId);
const source = plan?.source ?? {};
const language = plannerLanguage();
if (!plan || refreshingPlans.has(planId)) return;
if (!source.reportCode || !source.fightId || !source.fightStart || !source.fightEnd) return;
if (source.language === language && plan.mitigationNames && Object.keys(plan.mitigationNames).length) return;
refreshingPlans.add(planId);
try {
const params = new URLSearchParams({
report_code: source.reportCode,
fight_id: source.fightId,
start_time: source.fightStart,
end_time: source.fightEnd,
language,
});
const res = await fetch('api/analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
});
const json = await res.json();
if (json.reauth) { window.location.href = window.App?.authStartUrl?.() ?? 'auth/start.php'; return; }
if (json.error) return;
const refreshed = (json.aoe_events ?? []);
const mechanics = plan.mechanics.map(mechanic => {
const match = refreshed.find(ev => sameMechanic(mechanic, ev, source));
if (!match) return mechanic;
const assignments = (mechanic.assignments ?? []).map(a => ({
...a,
abilityName: json.mitigation_names?.[a.ability] ?? a.abilityName,
}));
return {
...mechanic,
name: match.abilityName ?? mechanic.name,
abilityId: match.abilityId ?? mechanic.abilityId,
assignments,
};
});
let fightName = source.fightName;
try {
const fightRes = await fetch('api/fight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ report_code: source.reportCode, language }),
});
const fightJson = await fightRes.json();
const fight = (fightJson?.data?.reportData?.report?.fights ?? []).find(f => f.id === source.fightId);
if (fight?.name) fightName = fight.name;
} catch { }
const nextSource = { ...source, fightName, language };
const nextName = plan.name === source.fightName && fightName ? fightName : plan.name;
const updated = updatePlan(planId, {
name: nextName,
mechanics,
source: nextSource,
mitigationNames: json.mitigation_names ?? plan.mitigationNames ?? {},
});
if (updated && activePlanId === planId) {
renderPlanList();
renderPlanDetail(updated);
}
} catch { }
finally {
refreshingPlans.delete(planId);
}
} }
// ── Ability Assignment Modal ────────────────────────────────────────────────── // ── Ability Assignment Modal ──────────────────────────────────────────────────
@ -775,8 +876,8 @@ function renderAbilityModalContent() {
const otherClass = byOtherJob ? ' ability-chip--other-job' : ''; const otherClass = byOtherJob ? ' ability-chip--other-job' : '';
const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : ''; const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : '';
const icon = MITIG_ICONS[ab.name] ?? ''; const icon = MITIG_ICONS[ab.name] ?? '';
const assignedName = assigned ? assignmentAbilityName(assigned) : ''; const assignedName = assigned ? assignmentAbilityName(assigned, plan) : '';
const label = assignedName || ab.name; const label = assignedName || plan.mitigationNames?.[ab.name] || ab.name;
return `<button class="ability-chip ${cls}${activeClass}${otherClass}" return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
data-ability="${escHtml(ab.name)}" data-ability="${escHtml(ab.name)}"
data-job="${escHtml(job)}" data-job="${escHtml(job)}"
@ -809,7 +910,7 @@ function toggleAbilityAssignment(abilityName, job, buffType) {
mechanic.assignments[idx].job = job; mechanic.assignments[idx].job = job;
} }
} else { } else {
mechanic.assignments.push({ ability: abilityName, job, buffType }); mechanic.assignments.push({ ability: abilityName, abilityName: plan.mitigationNames?.[abilityName], job, buffType });
} }
updatePlan(abilityModalPlanId, { mechanics: plan.mechanics }); updatePlan(abilityModalPlanId, { mechanics: plan.mechanics });
@ -950,11 +1051,17 @@ document.addEventListener('DOMContentLoaded', () => {
initNewPlanForm(); initNewPlanForm();
initImportModal(); initImportModal();
initAbilityModal(); initAbilityModal();
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
renderPlanList(); renderPlanList();
if (activePlanId && getPlan(activePlanId)) {
renderPlanDetail(getPlan(activePlanId));
} else {
activePlanId = null;
renderPlanDetail(null); renderPlanDetail(null);
}
}); });
window.addEventListener('ff14-language-change', () => { window.addEventListener('ff14-language-change', () => {
if (!activePlanId) return; if (!activePlanId) return;
renderPlanDetail(getPlan(activePlanId)); refreshPlanLanguage(activePlanId);
}); });

View File

@ -1,8 +1,10 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const tabs = document.querySelectorAll('.tabs .tab'); const tabs = document.querySelectorAll('.tabs .tab');
const contents = document.querySelectorAll('.tab-content'); const contents = document.querySelectorAll('.tab-content');
const validTabs = new Set([...tabs].map(btn => btn.dataset.tab));
function showTab(name) { function showTab(name) {
if (!validTabs.has(name)) name = 'report';
contents.forEach(el => el.style.display = 'none'); contents.forEach(el => el.style.display = 'none');
tabs.forEach(btn => btn.classList.remove('active')); tabs.forEach(btn => btn.classList.remove('active'));
@ -14,8 +16,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (name === 'analysis') window.analysisTab?.onTabOpen?.(); if (name === 'analysis') window.analysisTab?.onTabOpen?.();
if (name === 'planner') window.plannerTab?.onTabOpen?.(); if (name === 'planner') window.plannerTab?.onTabOpen?.();
localStorage.setItem('ff14-mitigator-active-tab', name);
} }
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab))); tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
window.showTab = showTab; window.showTab = showTab;
const params = new URLSearchParams(window.location.search);
showTab(params.get('tab') || localStorage.getItem('ff14-mitigator-active-tab') || 'report');
}); });