forked from xziino/ff14-mitigator
Merge akus_schabernack3: Lokalisierung + Tab-Persistenz
- Lokalisierte Ability-Namen in Badges und Modal - Aktiver Plan wird per localStorage über Reload hinweg gespeichert - Aktiver Tab wird per localStorage gespeichert - refreshPlanLanguage() aktualisiert Namen beim Sprachwechsel - api/analysis.php gibt mitigation_names zurück - ff14-language-change Event in app.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
4801148a8c
@ -212,11 +212,12 @@ foreach (MITIGATION_ABILITIES as $name => $meta) {
|
||||
}
|
||||
}
|
||||
|
||||
// statusId set for shield abilities — used to filter the buff timeline query
|
||||
$shieldStatusIds = [];
|
||||
// statusId set for tracked mitigations — used to resolve localized buff names
|
||||
// from Buffs events and to build the shield fallback timeline.
|
||||
$trackedStatusIds = [];
|
||||
foreach (MITIGATION_ABILITIES as $meta) {
|
||||
if ($meta['buffType'] === 'shield' && isset($meta['statusId'])) {
|
||||
$shieldStatusIds[$meta['statusId']] = true;
|
||||
if (isset($meta['statusId'])) {
|
||||
$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
|
||||
// that were consumed by a hit (absent from the damage event's buffs snapshot).
|
||||
$shieldTimeline = []; // targetId → statusId → [[apply, remove|null], ...]
|
||||
$statusNames = []; // statusId → localized display name from Buffs events
|
||||
|
||||
if (!empty($shieldStatusIds)) {
|
||||
if (!empty($trackedStatusIds)) {
|
||||
$nextPage = $startTime;
|
||||
for ($page = 0; $page < 10; $page++) {
|
||||
$bfResult = fflogs_gql(<<<GQL
|
||||
@ -304,11 +306,19 @@ if (!empty($shieldStatusIds)) {
|
||||
$bfEv = $bfResult['data']['reportData']['report']['events'] ?? [];
|
||||
foreach ($bfEv['data'] ?? [] as $ev) {
|
||||
$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);
|
||||
$ts = (float)($ev['timestamp'] ?? 0);
|
||||
$type = $ev['type'] ?? '';
|
||||
$meta = $mitigIdMap[$abId] ?? null;
|
||||
|
||||
if (($meta['buffType'] ?? null) !== 'shield') continue;
|
||||
|
||||
if ($type === 'applybuff') {
|
||||
$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 ────────────────────────────────
|
||||
// Group events by abilityId, then cluster by time proximity (≤ 1000ms from
|
||||
// the first event in the cluster) to avoid fixed-window boundary splits.
|
||||
@ -474,7 +498,8 @@ foreach ($clusters as $group) {
|
||||
usort($aoeEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']);
|
||||
|
||||
echo json_encode([
|
||||
'players' => array_values($players),
|
||||
'aoe_events' => $aoeEvents,
|
||||
'fight_start' => (int)$startTime,
|
||||
'players' => array_values($players),
|
||||
'aoe_events' => $aoeEvents,
|
||||
'fight_start' => (int)$startTime,
|
||||
'mitigation_names' => $mitigationNames,
|
||||
]);
|
||||
|
||||
@ -103,6 +103,7 @@
|
||||
let currentPlayers = [];
|
||||
let extFights = [];
|
||||
let extReportCode = '';
|
||||
let mitigationNames = {};
|
||||
|
||||
// ── Player grid ──────────────────────────────────────────────────────────
|
||||
|
||||
@ -683,6 +684,7 @@
|
||||
populateRefFightSelect();
|
||||
setupPhases(window.App?.phases ?? []);
|
||||
renderPlayers(json.players ?? []);
|
||||
mitigationNames = json.mitigation_names ?? {};
|
||||
renderTimeline(json.aoe_events ?? [], json.fight_start ?? fightStart);
|
||||
|
||||
document.getElementById('analysis-loading').style.display = 'none';
|
||||
@ -717,6 +719,9 @@
|
||||
players: currentPlayers,
|
||||
fightName: fight?.name ?? `Fight ${window.App?.fightId ?? '?'}`,
|
||||
reportCode: window.App?.reportCode ?? '',
|
||||
fightId: window.App?.fightId ?? 0,
|
||||
fightEnd: window.App?.fightEnd ?? 0,
|
||||
mitigationNames,
|
||||
};
|
||||
},
|
||||
reset() {
|
||||
@ -726,6 +731,7 @@
|
||||
refPlayers = [];
|
||||
extFights = [];
|
||||
extReportCode = '';
|
||||
mitigationNames = {};
|
||||
document.getElementById('ref-player-section').style.display = 'none';
|
||||
refFightSelect.value = '';
|
||||
refFightSelect.style.display = 'none';
|
||||
|
||||
@ -103,6 +103,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.App.language = normalizeLanguage(languageSelect.value);
|
||||
localStorage.setItem('ff14-mitigator-language', window.App.language);
|
||||
setUrlState({ language: window.App.language });
|
||||
window.dispatchEvent(new CustomEvent('ff14-language-change', { detail: { language: window.App.language } }));
|
||||
if (window.App.reportCode) {
|
||||
loadReport(window.App.reportCode, window.App.fightId);
|
||||
}
|
||||
@ -136,6 +137,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) {
|
||||
const fight = allFights.find(f => f.id === id);
|
||||
if (!fight) return false;
|
||||
@ -340,7 +347,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (initialUrlState.reportCode) {
|
||||
form.elements['report_code'].value = initialUrlState.reportCode;
|
||||
loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => {
|
||||
if (initialUrlState.fightId) {
|
||||
if (initialUrlState.fightId && shouldAutoOpenAnalysis()) {
|
||||
openAnalysisTab();
|
||||
}
|
||||
if (initialUrlState.compareFightId) {
|
||||
|
||||
154
js/planner.js
154
js/planner.js
@ -1,6 +1,7 @@
|
||||
// ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const PLANNER_KEY = 'ff14-planner-plans';
|
||||
const PLANNER_ACTIVE_KEY = 'ff14-planner-active-plan';
|
||||
const FOLDERS_KEY = 'ff14-planner-folders';
|
||||
|
||||
function loadPlans() {
|
||||
@ -39,6 +40,7 @@ function createPlan(name) {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
source: null,
|
||||
mitigationNames: {},
|
||||
folderId: null,
|
||||
jobComposition: Array(8).fill(''),
|
||||
mechanics: []
|
||||
@ -118,6 +120,24 @@ function fmtNumber(n) {
|
||||
return Number(n).toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
function assignmentAbilityName(assignment, plan = null) {
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
function planItemHtml(p) {
|
||||
@ -354,7 +374,8 @@ function renderMechanicListHtml(plan) {
|
||||
: 'badge-assign-buff';
|
||||
const isMissing = !!a.job && !activeJobSet.has(a.job);
|
||||
const icon = MITIG_ICONS[a.ability] ?? '';
|
||||
const label = a.job ? `${escHtml(a.job)} · ${escHtml(a.ability)}` : escHtml(a.ability);
|
||||
const ability = assignmentAbilityName(a, plan);
|
||||
const label = a.job ? `${escHtml(a.job)} · ${escHtml(ability)}` : escHtml(ability);
|
||||
const title = isMissing ? `${escHtml(a.job)} nicht in Jobaufstellung` : '';
|
||||
const suggestions = isMissing ? findEquivSuggestions(a.ability, activeJobSet) : [];
|
||||
const badgeHtml = `<span class="badge badge-assign ${cls}${isMissing ? ' badge-assign--missing-job' : ''}"${title ? ` title="${title}"` : ''}>
|
||||
@ -576,8 +597,10 @@ function startRename(id, currentName) {
|
||||
|
||||
function openPlan(id) {
|
||||
activePlanId = id;
|
||||
localStorage.setItem(PLANNER_ACTIVE_KEY, id);
|
||||
renderPlanList();
|
||||
renderPlanDetail(getPlan(id));
|
||||
refreshPlanLanguage(id);
|
||||
}
|
||||
|
||||
// ── New plan form ─────────────────────────────────────────────────────────────
|
||||
@ -928,9 +951,17 @@ function guessJob(abilityName, players) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function mitigationDisplayName(mitigation) {
|
||||
return mitigation?.name ?? mitigation?.key ?? '';
|
||||
}
|
||||
|
||||
function mitigationKey(mitigation) {
|
||||
return mitigation?.key ?? mitigation?.name ?? '';
|
||||
}
|
||||
|
||||
// ── AoE Events → Plan Mechanics ───────────────────────────────────────────────
|
||||
|
||||
function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations) {
|
||||
function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations, mitigationNames = {}) {
|
||||
return aoeEvents.map(ev => {
|
||||
const relTs = ev.timestamp - fightStart;
|
||||
const phase = (phases ?? []).filter(p => p.id !== 0).find(p =>
|
||||
@ -949,10 +980,15 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
||||
const seen = new Set();
|
||||
for (const t of ev.targets) {
|
||||
for (const m of (t.mitigations ?? [])) {
|
||||
const key = m.key ?? m.name;
|
||||
const key = mitigationKey(m);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
assignments.push({ ability: key, job: guessJob(key, players), buffType: m.buffType ?? '' });
|
||||
assignments.push({
|
||||
ability: key,
|
||||
abilityName: mitigationDisplayName(m) || mitigationNames[key],
|
||||
job: guessJob(key, players),
|
||||
buffType: m.buffType ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -961,6 +997,7 @@ function aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitiga
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: ev.abilityName,
|
||||
abilityId: ev.abilityId,
|
||||
timestamp: relTs,
|
||||
phase: phase?.name ?? '',
|
||||
unmitigatedDamage: avgUnmit,
|
||||
@ -986,14 +1023,16 @@ function extractJobComp(players) {
|
||||
}
|
||||
|
||||
function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
||||
const { aoeEvents, fightStart, phases, players, fightName, reportCode } = data;
|
||||
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations);
|
||||
const { aoeEvents, fightStart, fightEnd, phases, players, fightName, reportCode, fightId, mitigationNames = {} } = data;
|
||||
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations, mitigationNames);
|
||||
const source = { reportCode, fightId, fightName, fightStart, fightEnd, language: plannerLanguage() };
|
||||
|
||||
if (whereMode === 'new') {
|
||||
const plan = createPlan(uniquePlanName(newName || fightName || 'Importierter Plan'));
|
||||
return updatePlan(plan.id, {
|
||||
mechanics,
|
||||
source: { reportCode, fightName },
|
||||
source,
|
||||
mitigationNames,
|
||||
jobComposition: extractJobComp(players),
|
||||
});
|
||||
}
|
||||
@ -1011,7 +1050,87 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
|
||||
}
|
||||
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 ──────────────────────────────────────────────────
|
||||
@ -1067,12 +1186,14 @@ function renderAbilityModalContent() {
|
||||
const otherClass = byOtherJob ? ' ability-chip--other-job' : '';
|
||||
const title = byOtherJob ? `Bereits von ${escHtml(assigned.job)} zugewiesen` : '';
|
||||
const icon = MITIG_ICONS[ab.name] ?? '';
|
||||
const assignedName = assigned ? assignmentAbilityName(assigned, plan) : '';
|
||||
const label = assignedName || plan.mitigationNames?.[ab.name] || ab.name;
|
||||
return `<button class="ability-chip ${cls}${activeClass}${otherClass}"
|
||||
data-ability="${escHtml(ab.name)}"
|
||||
data-job="${escHtml(job)}"
|
||||
data-buff-type="${escHtml(ab.buffType)}"
|
||||
${title ? `title="${title}"` : ''}
|
||||
>${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(ab.name)}</button>`;
|
||||
>${icon ? `<img class="badge-icon" src="${escHtml(icon)}" alt="">` : ''}${escHtml(label)}</button>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
@ -1099,7 +1220,7 @@ function toggleAbilityAssignment(abilityName, job, buffType) {
|
||||
mechanic.assignments[idx].job = job;
|
||||
}
|
||||
} else {
|
||||
mechanic.assignments.push({ ability: abilityName, job, buffType });
|
||||
mechanic.assignments.push({ ability: abilityName, abilityName: plan.mitigationNames?.[abilityName], job, buffType });
|
||||
}
|
||||
|
||||
updatePlan(abilityModalPlanId, { mechanics: plan.mechanics });
|
||||
@ -1241,6 +1362,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initNewFolderForm();
|
||||
initImportModal();
|
||||
initAbilityModal();
|
||||
activePlanId = localStorage.getItem(PLANNER_ACTIVE_KEY);
|
||||
renderPlanList();
|
||||
renderPlanDetail(null);
|
||||
if (activePlanId && getPlan(activePlanId)) {
|
||||
renderPlanDetail(getPlan(activePlanId));
|
||||
} else {
|
||||
activePlanId = null;
|
||||
renderPlanDetail(null);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('ff14-language-change', () => {
|
||||
if (!activePlanId) return;
|
||||
refreshPlanLanguage(activePlanId);
|
||||
});
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabs = document.querySelectorAll('.tabs .tab');
|
||||
const contents = document.querySelectorAll('.tab-content');
|
||||
const validTabs = new Set([...tabs].map(btn => btn.dataset.tab));
|
||||
|
||||
function showTab(name) {
|
||||
if (!validTabs.has(name)) name = 'report';
|
||||
contents.forEach(el => el.style.display = 'none');
|
||||
tabs.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
@ -14,8 +16,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (name === 'analysis') window.analysisTab?.onTabOpen?.();
|
||||
if (name === 'planner') window.plannerTab?.onTabOpen?.();
|
||||
|
||||
localStorage.setItem('ff14-mitigator-active-tab', name);
|
||||
}
|
||||
|
||||
tabs.forEach(btn => btn.addEventListener('click', () => showTab(btn.dataset.tab)));
|
||||
window.showTab = showTab;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
showTab(params.get('tab') || localStorage.getItem('ff14-mitigator-active-tab') || 'report');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user