diff --git a/client/client.js b/client/client.js index d5136c4..e67da11 100644 --- a/client/client.js +++ b/client/client.js @@ -41,6 +41,14 @@ let myId = null; let gameState = null; let arenaRadius = 500; +// --- Kill feed state --- +const KILL_FEED_DURATION_MS = 5000; // entries visible for 5 seconds +const KILL_FEED_FADE_MS = 500; // fade-out duration +const KILL_FEED_MAX_ENTRIES = 5; +const killFeedContainer = document.getElementById('kill-feed'); +let killFeedEntries = []; // { id, killer, victim, cause, addedAt } +let lastSeenKillEventId = 0; + // --- Input state --- const keys = { up: false, down: false, left: false, right: false }; let mouseX = canvasSize / 2; @@ -103,6 +111,7 @@ function connect() { } else if (msg.type === 'state') { gameState = msg; updateHUD(); + processKillFeed(msg.killFeed); } else if (msg.type === 'name_error') { nicknameError.textContent = msg.error; nicknameError.style.display = 'block'; @@ -540,4 +549,74 @@ function renderLeaderboard(data) { } } +// --- Kill feed processing --- +function processKillFeed(serverFeed) { + if (!serverFeed || !Array.isArray(serverFeed)) return; + + for (const event of serverFeed) { + if (event.id <= lastSeenKillEventId) continue; + lastSeenKillEventId = event.id; + + const entry = { + id: event.id, + killer: event.killer, + victim: event.victim, + cause: event.cause, + addedAt: Date.now(), + }; + killFeedEntries.push(entry); + + // Keep only the most recent entries + if (killFeedEntries.length > KILL_FEED_MAX_ENTRIES) { + killFeedEntries.shift(); + } + } +} + +function updateKillFeed() { + const now = Date.now(); + + // Remove expired entries + killFeedEntries = killFeedEntries.filter( + (e) => now - e.addedAt < KILL_FEED_DURATION_MS + KILL_FEED_FADE_MS + ); + + // Rebuild DOM + killFeedContainer.innerHTML = ''; + for (const entry of killFeedEntries) { + const el = document.createElement('div'); + el.className = 'kill-feed-entry'; + + if (entry.cause === 'ring') { + el.innerHTML = + '' + escapeHtml(entry.victim) + '' + + ' was killed by ' + + 'the ring'; + } else { + el.innerHTML = + '' + escapeHtml(entry.killer) + '' + + ' eliminated ' + + '' + escapeHtml(entry.victim) + ''; + } + + // Fade out near expiry + const age = now - entry.addedAt; + if (age > KILL_FEED_DURATION_MS) { + const fadeProgress = (age - KILL_FEED_DURATION_MS) / KILL_FEED_FADE_MS; + el.style.opacity = Math.max(0, 1 - fadeProgress); + } + + killFeedContainer.appendChild(el); + } +} + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +// Update kill feed every frame +setInterval(updateKillFeed, 100); + requestAnimationFrame(render); diff --git a/client/index.html b/client/index.html index d4edc6e..ce4bea8 100644 --- a/client/index.html +++ b/client/index.html @@ -135,6 +135,41 @@ #controls-hint:hover { color: #aaa; } + #kill-feed { + position: absolute; + top: 44px; + right: 16px; + z-index: 10; + pointer-events: none; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + max-width: 300px; + } + .kill-feed-entry { + background: rgba(0, 0, 0, 0.7); + border-left: 3px solid #f44; + padding: 4px 10px; + font-family: monospace; + font-size: 12px; + color: #eee; + white-space: nowrap; + border-radius: 2px; + transition: opacity 0.5s ease-out; + } + .kill-feed-entry .killer-name { + color: #f44; + font-weight: bold; + } + .kill-feed-entry .victim-name { + color: #4fc; + font-weight: bold; + } + .kill-feed-entry .ring-cause { + color: #f80; + font-weight: bold; + } #nickname-container { position: absolute; top: 10px; @@ -271,6 +306,7 @@