Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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 =
'<span class="victim-name">' + escapeHtml(entry.victim) + '</span>' +
' was killed by ' +
'<span class="ring-cause">the ring</span>';
} else {
el.innerHTML =
'<span class="killer-name">' + escapeHtml(entry.killer) + '</span>' +
' eliminated ' +
'<span class="victim-name">' + escapeHtml(entry.victim) + '</span>';
}

// 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);
36 changes: 36 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -271,6 +306,7 @@
<div class="status" id="status-text">Connecting...</div>
<div class="info" id="info-text"></div>
</div>
<div id="kill-feed"></div>
<div id="nickname-container">
<input type="text" id="nickname-input" maxlength="16" placeholder="Enter nickname...">
<button id="nickname-set">Set</button>
Expand Down
26 changes: 26 additions & 0 deletions server/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -830,6 +855,7 @@ class Game {
yourId: forPlayerId,
isSpectator: this.spectators.has(forPlayerId),
lobbyCountdown,
killFeed: this.killFeed,
};
}

Expand Down
Loading