diff --git a/frontend/src/ReplayCanvas.jsx b/frontend/src/ReplayCanvas.jsx index 50a2cdf..df37e82 100644 --- a/frontend/src/ReplayCanvas.jsx +++ b/frontend/src/ReplayCanvas.jsx @@ -132,6 +132,7 @@ class ReplayCanvasEngine { _buildDOM() { this.container.innerHTML = '' + this._panelRows = [] const layout = document.createElement('div') layout.className = 'rc-layout' @@ -222,32 +223,28 @@ class ReplayCanvasEngine { _buildTeamPanel(panel, isWinner) { const teamEntities = this.entities.filter((e) => e.playerId > 0 && e.isWinner === isWinner) - const seen = new Set() - const unique = [] + // One row per player; a player may have several entities (one per spawn). + const byPlayer = new Map() for (const e of teamEntities) { - if (e.type === 'ground' && !seen.has(e.playerId)) { - seen.add(e.playerId) - unique.push(e) - } - } - for (const e of teamEntities) { - if (!seen.has(e.playerId)) { - seen.add(e.playerId) - unique.push(e) - } + if (!byPlayer.has(e.playerId)) byPlayer.set(e.playerId, []) + byPlayer.get(e.playerId).push(e) } + for (const list of byPlayer.values()) list.sort((a, b) => a.times[0] - b.times[0]) const color = isWinner ? RC.WIN : RC.LOSE - const firstPlayer = unique.length ? this.players[unique[0].playerId] : null - const clanTag = firstPlayer?.clan || '' - const label = clanTag ? `${esc(clanTag)}` : (isWinner ? 'Winners' : 'Losers') + const firstId = byPlayer.size ? byPlayer.keys().next().value : null + const firstPlayer = firstId ? this.players[firstId] : null + const teamSlot = firstPlayer ? firstPlayer.team : (isWinner ? this.data.teamWon : null) + const teamName = teamSlot != null ? this.data.teamNames?.[String(teamSlot)] : '' + const label = teamName ? esc(teamName) : (isWinner ? 'Winners' : 'Losers') let html = `
${label}
` - for (const ent of unique) { - const p = this.players[ent.playerId] + for (const [pid, list] of byPlayer) { + const p = this.players[pid] const name = p ? esc(p.name) : '?' + const ent = list[0] const veh = esc(ent.vehicleName) const panelIcon = ent.miniIcon ? ent.miniIcon.replace('mini:', '') : (ent.iconKey || 'medium') - html += `
+ html += `
${name} @@ -258,13 +255,51 @@ class ReplayCanvasEngine { } html += '
' panel.innerHTML = html - panel.querySelectorAll('.rc-row').forEach((row) => { + + const rows = panel.querySelectorAll('.rc-row') + let i = 0 + for (const [pid, list] of byPlayer) { + const row = rows[i++] + if (!row) continue + const pr = { + row, + playerId: pid, + entities: list, + vehEl: row.querySelector('.rc-row-veh'), + iconEl: row.querySelector('.rc-type-icon'), + statusEl: row.querySelector('.rc-row-status'), + shownEntityIndex: list[0].entityIndex, + currentEntityIndex: list[0].entityIndex, + } + this._panelRows.push(pr) row.addEventListener('mouseenter', () => { - const ent = this.entities.find((e) => e.entityIndex === Number(row.dataset.entityIndex)) - if (ent && !this._isEntityGone(ent, this.currentTime)) this._setHighlight(Number(row.dataset.playerId)) + const ent = pr.currentEntityIndex != null + ? this.entities.find((e) => e.entityIndex === pr.currentEntityIndex) + : null + if (ent && !this._isEntityGone(ent, this.currentTime)) this._setHighlight(pid) }) row.addEventListener('mouseleave', () => this._setHighlight(null)) - }) + } + } + + // Resolve which of a player's spawned vehicles is relevant at time t, and + // whether the player is currently alive, down (awaiting respawn), or gone. + _playerStateAtTime(entities, t) { + for (const e of entities) { + const first = e.times[0] + const last = e.times[e.times.length - 1] + if (t >= first && t <= last && !this._isEntityDead(e, t)) { + return { entity: e, dead: false, gone: false } + } + } + let recent = null + for (const e of entities) { + if (e.times[0] <= t && (!recent || e.times[0] >= recent.times[0])) recent = e + } + if (!recent) return { entity: entities[0] || null, dead: false, gone: false } + const hasFutureSpawn = entities.some((e) => e.times[0] > t) + const gone = !hasFutureSpawn && this._isEntityGone(recent, t) + return { entity: recent, dead: true, gone } } _buildEventList() { @@ -402,16 +437,25 @@ class ReplayCanvasEngine { _updatePanelDeathStates() { const t = this.currentTime - this.container.querySelectorAll('.rc-row').forEach((row) => { - const ent = this.entities.find((e) => e.entityIndex === Number(row.dataset.entityIndex)) - if (!ent) return - const dead = this._isEntityDead(ent, t) - const gone = this._isEntityGone(ent, t) - row.classList.toggle('rc-dead', dead) - row.classList.toggle('rc-gone', gone) - const status = row.querySelector('.rc-row-status') - status.textContent = dead || gone ? 'x' : '' - }) + if (!this._panelRows) return + for (const pr of this._panelRows) { + const st = this._playerStateAtTime(pr.entities, t) + pr.currentEntityIndex = st.entity ? st.entity.entityIndex : null + if (st.entity && st.entity.entityIndex !== pr.shownEntityIndex) { + pr.shownEntityIndex = st.entity.entityIndex + if (pr.vehEl) pr.vehEl.textContent = st.entity.vehicleName || '' + if (pr.iconEl) { + const panelIcon = st.entity.miniIcon + ? st.entity.miniIcon.replace('mini:', '') + : (st.entity.iconKey || 'medium') + pr.iconEl.src = `/api/icons/type/${panelIcon}` + pr.iconEl.style.display = '' + } + } + pr.row.classList.toggle('rc-dead', st.dead) + pr.row.classList.toggle('rc-gone', st.gone) + if (pr.statusEl) pr.statusEl.textContent = st.dead || st.gone ? 'x' : '' + } } _updateBattleLog() { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index a23c41c..1a83570 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -524,8 +524,7 @@ h3 { .rc-layout { display: grid; - grid-template-columns: minmax(190px, 230px) minmax(560px, 720px) minmax(190px, 230px); - width: min(100%, 1210px); + grid-template-columns: minmax(160px, 200px) min(720px, 60vh, 92vw) minmax(160px, 200px); gap: 0.5rem; align-items: start; justify-content: center;