diff --git a/js/analysis.js b/js/analysis.js
index 4f54dc4..c02b3b3 100644
--- a/js/analysis.js
+++ b/js/analysis.js
@@ -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 = '';
+ 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 ``;
}).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
? `${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}`
@@ -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 ? ` Schild` : '')
+ : (t.absorbed > 0 ? ` +${fmtDmg(t.absorbed)}` : '');
+
return `