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;
|
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 Timeline ────────────────────────────────────────────────────────── */
|
||||||
.aoe-event {
|
.aoe-event {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
114
js/analysis.js
114
js/analysis.js
@ -55,13 +55,16 @@
|
|||||||
return `${min}:${sec}`;
|
return `${min}:${sec}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hiddenPlayers = new Set();
|
let hiddenPlayers = new Set();
|
||||||
|
let hiddenPlayerNames = new Set();
|
||||||
let lastEvents = [];
|
let lastEvents = [];
|
||||||
let lastFightStart = 0;
|
let lastFightStart = 0;
|
||||||
let playerFilter = '';
|
let playerFilter = '';
|
||||||
let phaseFilter = { startTime: 0, endTime: Infinity };
|
let phaseFilter = { startTime: 0, endTime: Infinity };
|
||||||
let refEvents = [];
|
let refEvents = [];
|
||||||
let refFightStart = 0;
|
let refFightStart = 0;
|
||||||
|
let extFights = [];
|
||||||
|
let extReportCode = '';
|
||||||
|
|
||||||
// ── Player grid ──────────────────────────────────────────────────────────
|
// ── Player grid ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -73,10 +76,11 @@
|
|||||||
return roleCmp !== 0 ? roleCmp : a.name.localeCompare(b.name);
|
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 => `
|
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 class="player-job-icon role-${p.role}">${abbr(p.type)}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="player-name">${p.name}</div>
|
<div class="player-name">${p.name}</div>
|
||||||
@ -89,12 +93,15 @@
|
|||||||
document.getElementById('player-grid').addEventListener('click', e => {
|
document.getElementById('player-grid').addEventListener('click', e => {
|
||||||
const card = e.target.closest('.player-card');
|
const card = e.target.closest('.player-card');
|
||||||
if (!card) return;
|
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)) {
|
if (hiddenPlayers.has(id)) {
|
||||||
hiddenPlayers.delete(id);
|
hiddenPlayers.delete(id);
|
||||||
|
hiddenPlayerNames.delete(name);
|
||||||
card.classList.remove('player-hidden');
|
card.classList.remove('player-hidden');
|
||||||
} else {
|
} else {
|
||||||
hiddenPlayers.add(id);
|
hiddenPlayers.add(id);
|
||||||
|
hiddenPlayerNames.add(name);
|
||||||
card.classList.add('player-hidden');
|
card.classList.add('player-hidden');
|
||||||
}
|
}
|
||||||
renderTimeline(lastEvents, lastFightStart);
|
renderTimeline(lastEvents, lastFightStart);
|
||||||
@ -147,6 +154,9 @@
|
|||||||
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
|
const fight = (window.App?.fights ?? []).find(f => f.id === refId);
|
||||||
if (!fight) return;
|
if (!fight) return;
|
||||||
|
|
||||||
|
// Clear ext-report selection
|
||||||
|
refExtFightSelect.value = '';
|
||||||
|
|
||||||
refFightSelect.disabled = true;
|
refFightSelect.disabled = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('api/analysis.php', {
|
const res = await fetch('api/analysis.php', {
|
||||||
@ -181,6 +191,91 @@
|
|||||||
refFightSelect.style.display = '';
|
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 ────────────────────────────────────────────────────
|
// ── Timeline rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderTimeline(events, fightStart) {
|
function renderTimeline(events, fightStart) {
|
||||||
@ -271,7 +366,7 @@
|
|||||||
let refHtml = '';
|
let refHtml = '';
|
||||||
if (refEv) {
|
if (refEv) {
|
||||||
const refVisible = refEv.targets.filter(t =>
|
const refVisible = refEv.targets.filter(t =>
|
||||||
!hiddenPlayers.has(t.id) &&
|
!hiddenPlayerNames.has(t.name) &&
|
||||||
(!playerFilter || t.name.toLowerCase().includes(playerFilter))
|
(!playerFilter || t.name.toLowerCase().includes(playerFilter))
|
||||||
);
|
);
|
||||||
if (refVisible.length) {
|
if (refVisible.length) {
|
||||||
@ -284,7 +379,7 @@
|
|||||||
const dead = t.hp === 0 && t.maxHp > 0;
|
const dead = t.hp === 0 && t.maxHp > 0;
|
||||||
|
|
||||||
const deltaHtml = diff !== 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 => {
|
const refMitigIcons = (t.mitigations ?? []).map(m => {
|
||||||
@ -390,8 +485,13 @@
|
|||||||
lastFightId = null;
|
lastFightId = null;
|
||||||
refEvents = [];
|
refEvents = [];
|
||||||
refFightStart = 0;
|
refFightStart = 0;
|
||||||
refFightSelect.value = '';
|
extFights = [];
|
||||||
|
extReportCode = '';
|
||||||
|
refFightSelect.value = '';
|
||||||
refFightSelect.style.display = 'none';
|
refFightSelect.style.display = 'none';
|
||||||
|
refExtFightSelect.value = '';
|
||||||
|
refExtFightSelect.style.display = 'none';
|
||||||
|
refExtPanel.style.display = 'none';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -18,6 +18,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="player-grid" class="player-grid"></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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user