Analyse-Tab: Plan als Referenz-Quelle

- Pläne aus localStorage als Ref-Quelle auswählbar ("+Plan als Referenz")
- planToRefEvents() konvertiert Plan-Mechaniken ins refEvents-Format
- PLAN-Label statt REF, kein Delta, kein Schadenswert
- Buff-Icons mit "fehlt"-Markierung, Debuffs im Header
- Shield-Assignments als "Schild"-Text mit Tooltip
- Schließt sich mit anderen Ref-Quellen gegenseitig aus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xziino 2026-05-23 08:30:52 +02:00
parent 6024560e61
commit fb58226be8
2 changed files with 146 additions and 11 deletions

View File

@ -161,6 +161,7 @@
let extFights = [];
let extReportCode = '';
let mitigationNames = {};
let planRefId = '';
// ── Player grid ──────────────────────────────────────────────────────────
@ -301,8 +302,11 @@
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
if (!fight) return;
// Clear ext-report selection
refExtFightSelect.value = '';
// Clear ext-report and plan selections
refExtFightSelect.value = '';
planRefId = '';
refPlanSelect.value = '';
refPlanPanel.style.display = 'none';
refFightSelect.disabled = true;
try {
@ -446,8 +450,11 @@
const fight = extFights.find(f => f.id === refId);
if (!fight) return;
// Clear same-report selection
refFightSelect.value = '';
// Clear same-report and plan selections
refFightSelect.value = '';
planRefId = '';
refPlanSelect.value = '';
refPlanPanel.style.display = 'none';
refExtFightSelect.disabled = true;
try {
@ -485,6 +492,115 @@
await loadExternalCompare(refId);
});
// ── Plan as reference ─────────────────────────────────────────────────────
const refPlanToggle = document.getElementById('ref-plan-toggle');
const refPlanPanel = document.getElementById('ref-plan-panel');
const refPlanSelect = document.getElementById('ref-plan-select');
const PLAN_JOB_ROLE = {
'PLD': 'tank', 'WAR': 'tank', 'DRK': 'tank', 'GNB': 'tank',
'WHM': 'healer', 'SCH': 'healer', 'AST': 'healer', 'SGE': 'healer',
};
function loadPlansForRef() {
try { return JSON.parse(localStorage.getItem('ff14-planner-plans') || '[]'); }
catch { return []; }
}
function planToRefEvents(plan) {
const roster = plan.playerRoster ?? [];
const jobComp = plan.jobComposition ?? [];
const fightStart = plan.source?.fightStart ?? 0;
const mitigNames = plan.mitigationNames ?? {};
const players = jobComp.map((job, i) => ({
job,
name: roster[i]?.name ?? '',
role: PLAN_JOB_ROLE[job] ?? 'dps',
})).filter(p => p.name && p.job);
return plan.mechanics.map(m => {
const mitigations = (m.assignments ?? []).map(a => ({
key: a.ability,
name: a.abilityName || mitigNames[a.ability] || a.ability,
buffType: a.buffType,
dr: 0,
}));
const targets = players.map(p => ({
id: 0,
name: p.name,
type: p.job,
role: p.role,
amount: 0,
absorbed: 0,
overkill: 0,
hp: 0,
maxHp: 0,
unmitigatedAmount: 0,
mitigations,
}));
return {
abilityName: m.name,
abilityId: m.abilityId ?? 0,
timestamp: fightStart + m.timestamp,
totalDamage: 0,
targets,
isPlanRef: true,
};
});
}
function populateRefPlanSelect() {
const plans = loadPlansForRef();
refPlanSelect.innerHTML = '<option value="">— Plan auswählen —</option>';
plans.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = `${p.name} (${p.mechanics.length} Mechaniken)`;
refPlanSelect.appendChild(opt);
});
refPlanSelect.value = planRefId || '';
}
refPlanToggle.addEventListener('click', () => {
const hidden = refPlanPanel.style.display === 'none';
refPlanPanel.style.display = hidden ? '' : 'none';
if (hidden) populateRefPlanSelect();
});
refPlanSelect.addEventListener('change', () => {
const id = refPlanSelect.value;
// Clear other ref sources
refFightSelect.value = '';
refExtFightSelect.value = '';
updateRefFflogsLink(0);
if (!id) {
planRefId = '';
refEvents = [];
refFightStart = 0;
refPlayers = [];
renderRefPlayers();
renderTimeline(lastEvents, lastFightStart);
return;
}
const plan = loadPlansForRef().find(p => p.id === id);
if (!plan) return;
planRefId = id;
refEvents = planToRefEvents(plan);
refFightStart = plan.source?.fightStart ?? 0;
refPlayers = [];
renderRefPlayers();
renderTimeline(lastEvents, lastFightStart);
});
// ── Timeline rendering ────────────────────────────────────────────────────
function renderTimeline(events, fightStart) {
@ -630,10 +746,12 @@
return `<img class="aoe-target-buff-icon" src="${iconSrc}" alt="${m.name}" title="${m.name}${dr}">`;
}).join('');
const isPlanRef = !!refEv.isPlanRef;
const refCards = refVisible.map(t => {
const curr = currentByName[t.name];
const diff = curr ? curr.amount - t.amount : 0;
const dead = t.hp === 0 && t.maxHp > 0;
const diff = (!isPlanRef && curr) ? curr.amount - t.amount : 0;
const dead = !isPlanRef && t.hp === 0 && t.maxHp > 0;
const deltaHtml = diff !== 0
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
@ -658,11 +776,15 @@
const k = s.key ?? s.name;
const jobs = ABILITY_JOBS[k];
const currentGroupHasJob = jobs ? jobs.some(j => currentFightJobSet.has(j)) : false;
const isMissing = currentGroupHasJob && !currentEventMitigKeys.has(k);
const isMissing = !isPlanRef && currentGroupHasJob && !currentEventMitigKeys.has(k);
return isMissing ? `${s.name} [fehlt im aktuellen Pull]` : s.name;
}).join('\n')
: null;
const absorbedHtml = isPlanRef
? (refShields.length ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? ''}">Schild</span>` : '')
: (t.absorbed > 0 ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? 'Keine erkannten Schilde'}">+${fmtDmg(t.absorbed)}</span>` : '');
return `
<div class="aoe-target-wrap">
<div class="aoe-ref-target${dead ? ' aoe-target--dead' : ''}">
@ -671,20 +793,21 @@
${deltaHtml}
</div>
<span class="aoe-target-name">${t.name}</span>
<span class="aoe-target-dmg">${fmtDmg(t.amount)}${t.absorbed > 0 ? ` <span class="aoe-target-absorbed" title="${refShieldTitle ?? 'Keine erkannten Schilde'}">+${fmtDmg(t.absorbed)}</span>` : ''}</span>
<span class="aoe-target-dmg">${isPlanRef ? '' : fmtDmg(t.amount)}${absorbedHtml}</span>
</div>
${refMitigIcons ? `<div class="aoe-target-buffs">${refMitigIcons}</div>` : ''}
</div>`;
}).join('');
const totalDiff = ev.totalDamage - refEv.totalDamage;
const totalDelta = totalDiff !== 0
const totalDelta = (!isPlanRef && totalDiff !== 0)
? `<span class="${totalDiff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${totalDiff > 0 ? '+' : ''}${fmtDmg(totalDiff)}</span>`
: '';
const refLabel = isPlanRef ? 'PLAN' : `REF ${fmtDmg(refEv.totalDamage)} ${totalDelta}`;
refHtml = `
<div class="aoe-ref-row">
<span class="aoe-ref-label">REF ${fmtDmg(refEv.totalDamage)} ${totalDelta} ${refDebuffIconsHtml}</span>
<span class="aoe-ref-label">${refLabel} ${refDebuffIconsHtml}</span>
<div class="aoe-targets">${refCards}</div>
</div>`;
}
@ -822,7 +945,7 @@
mitigationNames,
};
},
hasRefExport() { return refEvents.length > 0; },
hasRefExport() { return refEvents.length > 0 && !planRefId; },
reset() {
lastFightId = null;
refEvents = [];
@ -831,6 +954,7 @@
extFights = [];
extReportCode = '';
mitigationNames = {};
planRefId = '';
document.getElementById('ref-player-section').style.display = 'none';
refFightSelect.value = '';
refFightSelect.style.display = 'none';
@ -839,6 +963,8 @@
refFflogsLink.style.display = 'none';
refFflogsLink.href = '#';
refExtPanel.style.display = 'none';
refPlanPanel.style.display = 'none';
refPlanSelect.value = '';
const exportBtn = document.getElementById('export-to-planner-btn');
if (exportBtn) exportBtn.style.display = 'none';
},

View File

@ -22,6 +22,15 @@
<div class="ref-player-label">REF Spieler</div>
<div id="ref-player-grid" class="player-grid"></div>
</div>
<div class="ref-ext-row">
<button id="ref-plan-toggle" class="btn btn-sm">+ Plan als Referenz</button>
<div id="ref-plan-panel" style="display:none">
<select id="ref-plan-select" class="filter-input">
<option value=""> Plan auswählen </option>
</select>
</div>
</div>
<div class="ref-ext-row">
<button id="ref-ext-toggle" class="btn btn-sm">+ Anderer Report</button>
<div id="ref-ext-panel" style="display:none">