From 4084cc33301237f2ecb3614d4782b0925ee4817f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:32:09 +0000 Subject: [PATCH] feat: add real-time kill feed UI - Server: track kill events in game.js for both bullet hits and ring damage - Server: include killFeed array in broadcast state payload - Server: clear kill feed on round start and reset - Client: add kill feed container in top-right corner with styled entries - Client: process incoming kill events with deduplication via event IDs - Client: auto-fade and remove entries after 5 seconds - Client: limit display to last 5 entries - Supports both combat kills (PlayerA eliminated PlayerB) and ring deaths (PlayerB was killed by the ring) - Works for both human players and NPC bots Co-Authored-By: bot_apk --- client/client.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++ client/index.html | 36 +++++++++++++++++++++ server/game.js | 26 ++++++++++++++++ 3 files changed, 141 insertions(+) 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 @@
Connecting...
+
diff --git a/server/game.js b/server/game.js index 2dc679f..ebf4c43 100644 --- a/server/game.js +++ b/server/game.js @@ -235,6 +235,8 @@ class Game { this.leaderboard = new Leaderboard(); this.registeredNicknames = new Map(); // nickname (lowercase) -> playerId this.machineGunPickup = null; // { x, y, collected, collectedBy } + this.killFeed = []; // array of { killer, victim, cause, timestamp } + this._killEventId = 0; } start() { @@ -544,6 +546,7 @@ class Game { this.ringStartTime = Date.now(); this.bullets = []; this.winnerId = null; + this.killFeed = []; // Spawn machine gun pickup at a random position inside the arena const pickupPos = randomPointInPolygon(this.arenaVertices, this.arenaCentroid, 0.7); @@ -654,6 +657,12 @@ class Game { if (player.hp <= 0) { player.hp = 0; player.alive = false; + const killer = this.players.get(bullet.ownerId); + this._addKillEvent( + killer ? killer.name : 'Unknown', + player.name, + 'combat' + ); } return false; // bullet consumed } @@ -694,11 +703,26 @@ class Game { if (player.hp <= 0) { player.hp = 0; player.alive = false; + this._addKillEvent(null, player.name, 'ring'); } } } } + _addKillEvent(killer, victim, cause) { + this.killFeed.push({ + id: ++this._killEventId, + killer, + victim, + cause, + timestamp: Date.now(), + }); + // Keep only the last 10 events server-side + if (this.killFeed.length > 10) { + this.killFeed.shift(); + } + } + checkWinCondition() { const alive = this.getAlivePlayers(); @@ -745,6 +769,7 @@ class Game { this.lobbyCountdownStart = 0; this.roundParticipants = 0; this.machineGunPickup = null; + this.killFeed = []; // Move spectators back to active players this.spectators.clear(); @@ -830,6 +855,7 @@ class Game { yourId: forPlayerId, isSpectator: this.spectators.has(forPlayerId), lobbyCountdown, + killFeed: this.killFeed, }; }