forked from xziino/ff14-mitigator
Add cross-report comparison and fix delta/filter bugs
- Cross-report comparison: toggle panel to load fights from a second report code, select a fight to use as reference (mutually exclusive with same-report ref dropdown) - Fix ref-row player filter: use hiddenPlayerNames (name-based) so ext-report targets with different actor IDs are correctly hidden - Fix negative delta formatting: pass Math.abs(diff) to fmtDmg so negative values are abbreviated the same way as positive ones Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8fe057e15b
commit
76c5d80cc2
@ -65,6 +65,27 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── External report panel ───────────────────────────────────────────────── */
|
||||
.ref-ext-row {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#ref-ext-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ref-report-input {
|
||||
width: 200px;
|
||||
padding: 4px 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── AoE Timeline ────────────────────────────────────────────────────────── */
|
||||
.aoe-event {
|
||||
display: grid;
|
||||
|
||||
114
js/analysis.js
114
js/analysis.js
@ -55,13 +55,16 @@
|
||||
return `${min}:${sec}`;
|
||||
}
|
||||
|
||||
let hiddenPlayers = new Set();
|
||||
let hiddenPlayers = new Set();
|
||||
let hiddenPlayerNames = new Set();
|
||||
let lastEvents = [];
|
||||
let lastFightStart = 0;
|
||||
let playerFilter = '';
|
||||
let phaseFilter = { startTime: 0, endTime: Infinity };
|
||||
let refEvents = [];
|
||||
let refFightStart = 0;
|
||||
let extFights = [];
|
||||
let extReportCode = '';
|
||||
|
||||
// ── Player grid ──────────────────────────────────────────────────────────
|
||||
|
||||
@ -73,10 +76,11 @@
|
||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id));
|
||||
hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id));
|
||||
hiddenPlayerNames = new Set(players.filter(p => p.role === 'tank').map(p => p.name));
|
||||
|
||||
grid.innerHTML = players.map(p => `
|
||||
<div class="player-card ${hiddenPlayers.has(p.id) ? 'player-hidden' : ''}" data-player-id="${p.id}">
|
||||
<div class="player-card ${hiddenPlayers.has(p.id) ? 'player-hidden' : ''}" data-player-id="${p.id}" data-player-name="${p.name}">
|
||||
<div class="player-job-icon role-${p.role}">${abbr(p.type)}</div>
|
||||
<div>
|
||||
<div class="player-name">${p.name}</div>
|
||||
@ -89,12 +93,15 @@
|
||||
document.getElementById('player-grid').addEventListener('click', e => {
|
||||
const card = e.target.closest('.player-card');
|
||||
if (!card) return;
|
||||
const id = parseInt(card.dataset.playerId, 10);
|
||||
const id = parseInt(card.dataset.playerId, 10);
|
||||
const name = card.dataset.playerName;
|
||||
if (hiddenPlayers.has(id)) {
|
||||
hiddenPlayers.delete(id);
|
||||
hiddenPlayerNames.delete(name);
|
||||
card.classList.remove('player-hidden');
|
||||
} else {
|
||||
hiddenPlayers.add(id);
|
||||
hiddenPlayerNames.add(name);
|
||||
card.classList.add('player-hidden');
|
||||
}
|
||||
renderTimeline(lastEvents, lastFightStart);
|
||||
@ -147,6 +154,9 @@
|
||||
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
|
||||
if (!fight) return;
|
||||
|
||||
// Clear ext-report selection
|
||||
refExtFightSelect.value = '';
|
||||
|
||||
refFightSelect.disabled = true;
|
||||
try {
|
||||
const res = await fetch('api/analysis.php', {
|
||||
@ -181,6 +191,91 @@
|
||||
refFightSelect.style.display = '';
|
||||
}
|
||||
|
||||
// ── External report comparison ────────────────────────────────────────────
|
||||
|
||||
const refExtToggle = document.getElementById('ref-ext-toggle');
|
||||
const refExtPanel = document.getElementById('ref-ext-panel');
|
||||
const refReportInput = document.getElementById('ref-report-input');
|
||||
const refReportLoad = document.getElementById('ref-report-load');
|
||||
const refExtFightSelect = document.getElementById('ref-ext-fight-select');
|
||||
|
||||
refExtToggle.addEventListener('click', () => {
|
||||
const hidden = refExtPanel.style.display === 'none';
|
||||
refExtPanel.style.display = hidden ? '' : 'none';
|
||||
});
|
||||
|
||||
refReportLoad.addEventListener('click', async () => {
|
||||
const code = refReportInput.value.trim();
|
||||
if (!code) return;
|
||||
|
||||
refReportLoad.disabled = true;
|
||||
refReportLoad.textContent = 'Lädt…';
|
||||
|
||||
try {
|
||||
const res = await fetch('api/fight.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ report_code: code }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.reauth) { window.location.href = 'auth/start.php'; return; }
|
||||
|
||||
const fights = json?.data?.reportData?.report?.fights ?? [];
|
||||
extFights = fights;
|
||||
extReportCode = code;
|
||||
|
||||
refExtFightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
||||
fights.forEach(f => {
|
||||
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.id;
|
||||
opt.textContent = `${f.name} — ${fmtDur(f.endTime - f.startTime)} — ${hp}`;
|
||||
refExtFightSelect.appendChild(opt);
|
||||
});
|
||||
refExtFightSelect.style.display = fights.length ? '' : 'none';
|
||||
} catch { }
|
||||
|
||||
refReportLoad.disabled = false;
|
||||
refReportLoad.textContent = 'Laden';
|
||||
});
|
||||
|
||||
refExtFightSelect.addEventListener('change', async () => {
|
||||
const refId = parseInt(refExtFightSelect.value, 10);
|
||||
if (!refId) {
|
||||
refEvents = [];
|
||||
refFightStart = 0;
|
||||
renderTimeline(lastEvents, lastFightStart);
|
||||
return;
|
||||
}
|
||||
|
||||
const fight = extFights.find(f => f.id === refId);
|
||||
if (!fight) return;
|
||||
|
||||
// Clear same-report selection
|
||||
refFightSelect.value = '';
|
||||
|
||||
refExtFightSelect.disabled = true;
|
||||
try {
|
||||
const res = await fetch('api/analysis.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
report_code: extReportCode,
|
||||
fight_id: refId,
|
||||
start_time: fight.startTime,
|
||||
end_time: fight.endTime,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.error && !json.reauth) {
|
||||
refEvents = json.aoe_events ?? [];
|
||||
refFightStart = json.fight_start ?? fight.startTime;
|
||||
}
|
||||
} catch { }
|
||||
refExtFightSelect.disabled = false;
|
||||
renderTimeline(lastEvents, lastFightStart);
|
||||
});
|
||||
|
||||
// ── Timeline rendering ────────────────────────────────────────────────────
|
||||
|
||||
function renderTimeline(events, fightStart) {
|
||||
@ -271,7 +366,7 @@
|
||||
let refHtml = '';
|
||||
if (refEv) {
|
||||
const refVisible = refEv.targets.filter(t =>
|
||||
!hiddenPlayers.has(t.id) &&
|
||||
!hiddenPlayerNames.has(t.name) &&
|
||||
(!playerFilter || t.name.toLowerCase().includes(playerFilter))
|
||||
);
|
||||
if (refVisible.length) {
|
||||
@ -284,7 +379,7 @@
|
||||
const dead = t.hp === 0 && t.maxHp > 0;
|
||||
|
||||
const deltaHtml = diff !== 0
|
||||
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : ''}${fmtDmg(diff)}</span>`
|
||||
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
|
||||
: '';
|
||||
|
||||
const refMitigIcons = (t.mitigations ?? []).map(m => {
|
||||
@ -390,8 +485,13 @@
|
||||
lastFightId = null;
|
||||
refEvents = [];
|
||||
refFightStart = 0;
|
||||
refFightSelect.value = '';
|
||||
extFights = [];
|
||||
extReportCode = '';
|
||||
refFightSelect.value = '';
|
||||
refFightSelect.style.display = 'none';
|
||||
refExtFightSelect.value = '';
|
||||
refExtFightSelect.style.display = 'none';
|
||||
refExtPanel.style.display = 'none';
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
@ -18,6 +18,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div id="player-grid" class="player-grid"></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">
|
||||
<input type="text" id="ref-report-input" class="ref-report-input" placeholder="Report-Code">
|
||||
<button id="ref-report-load" class="btn btn-sm">Laden</button>
|
||||
<select id="ref-ext-fight-select" class="filter-input" style="display:none">
|
||||
<option value="">— Fight auswählen —</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user