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
40 changes: 40 additions & 0 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,46 @@ function render() {
ctx.stroke();
}

// Draw machine gun pickup
if (gameState.machineGunPickup && !gameState.machineGunPickup.collected) {
const pkx = cx + gameState.machineGunPickup.x * scale;
const pky = cy + gameState.machineGunPickup.y * scale;
const pkRadius = 12 * scale;

// Outer glow
ctx.beginPath();
ctx.arc(pkx, pky, pkRadius * 1.5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 165, 0, 0.15)';
ctx.fill();

// Base circle
ctx.beginPath();
ctx.arc(pkx, pky, pkRadius, 0, Math.PI * 2);
ctx.fillStyle = '#1a1a2e';
ctx.fill();
ctx.strokeStyle = '#f80';
ctx.lineWidth = 2;
ctx.stroke();

// Gun icon (three horizontal lines like a minigun barrel)
ctx.strokeStyle = '#f80';
ctx.lineWidth = 2;
const barrelLen = pkRadius * 0.7;
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.moveTo(pkx - barrelLen * 0.3, pky + i * pkRadius * 0.25);
ctx.lineTo(pkx + barrelLen, pky + i * pkRadius * 0.25);
ctx.stroke();
}

// Label
const pkFontSize = Math.max(6, pkRadius * 0.4);
ctx.font = `bold ${pkFontSize}px monospace`;
ctx.fillStyle = '#f80';
ctx.textAlign = 'center';
ctx.fillText('MG', pkx, pky + pkRadius + pkFontSize + 2);
}

// Draw bullets
for (const bullet of gameState.bullets) {
const bx = cx + bullet.x * scale;
Expand Down
45 changes: 44 additions & 1 deletion server/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const BULLET_DAMAGE = 34; // ~3 hits to kill
const PLAYER_MAX_HP = 100;
const RING_DAMAGE_PER_SEC = 20;
const SHOOT_COOLDOWN_MS = 300;
const MACHINE_GUN_COOLDOWN_MS = 150;
const PICKUP_COLLECT_RADIUS = PLAYER_RADIUS;
const RING_SHRINK_DURATION_MS = 75000; // 75 seconds
const LOBBY_COUNTDOWN_MS = 5000; // 5 second countdown after 2+ players
const ROUND_END_DELAY_MS = 5000; // 5 seconds before resetting
Expand Down Expand Up @@ -232,6 +234,7 @@ class Game {
this.onBroadcast = null; // callback for broadcasting state
this.leaderboard = new Leaderboard();
this.registeredNicknames = new Map(); // nickname (lowercase) -> playerId
this.machineGunPickup = null; // { x, y, collected, collectedBy }
}

start() {
Expand All @@ -258,6 +261,7 @@ class Game {
lastShot: 0,
input: { up: false, down: false, left: false, right: false },
name: `Player ${id}`,
hasMachineGun: false,
};

if (this.state === STATE_ACTIVE || this.state === STATE_ROUND_END) {
Expand Down Expand Up @@ -480,7 +484,8 @@ class Game {

tryShoot(player) {
const now = Date.now();
if (now - player.lastShot < SHOOT_COOLDOWN_MS) return;
const cooldown = player.hasMachineGun ? MACHINE_GUN_COOLDOWN_MS : SHOOT_COOLDOWN_MS;
if (now - player.lastShot < cooldown) return;
player.lastShot = now;

const bullet = {
Expand Down Expand Up @@ -540,6 +545,10 @@ class Game {
this.bullets = [];
this.winnerId = null;

// Spawn machine gun pickup at a random position inside the arena
const pickupPos = randomPointInPolygon(this.arenaVertices, this.arenaCentroid, 0.7);
this.machineGunPickup = { x: pickupPos.x, y: pickupPos.y, collected: false, collectedBy: null };

// Respawn all non-spectator players inside the polygon
const totalPlayers = this.getNonSpectatorPlayers().length;
this.roundParticipants = totalPlayers;
Expand All @@ -554,6 +563,7 @@ class Game {
player.hp = PLAYER_MAX_HP;
player.alive = true;
player.lastShot = 0;
player.hasMachineGun = false;
index++;
}
}
Expand Down Expand Up @@ -581,6 +591,9 @@ class Game {
// Move bullets
this.updateBullets(dt, now);

// Check machine gun pickup collection
this.checkPickupCollection();

// Ring damage
this.applyRingDamage(dt);

Expand Down Expand Up @@ -653,6 +666,25 @@ class Game {
});
}

checkPickupCollection() {
if (!this.machineGunPickup || this.machineGunPickup.collected) return;

for (const player of this.players.values()) {
if (!player.alive || this.spectators.has(player.id)) continue;

const dx = player.x - this.machineGunPickup.x;
const dy = player.y - this.machineGunPickup.y;
const dist = Math.sqrt(dx * dx + dy * dy);

if (dist < PICKUP_COLLECT_RADIUS) {
this.machineGunPickup.collected = true;
this.machineGunPickup.collectedBy = player.id;
player.hasMachineGun = true;
break;
}
}
}

applyRingDamage(dt) {
for (const player of this.players.values()) {
if (!player.alive || this.spectators.has(player.id)) continue;
Expand Down Expand Up @@ -712,6 +744,7 @@ class Game {
this.winnerId = null;
this.lobbyCountdownStart = 0;
this.roundParticipants = 0;
this.machineGunPickup = null;

// Move spectators back to active players
this.spectators.clear();
Expand All @@ -726,6 +759,7 @@ class Game {
player.hp = PLAYER_MAX_HP;
player.alive = true;
player.lastShot = 0;
player.hasMachineGun = false;
player.input = { up: false, down: false, left: false, right: false };
index++;
}
Expand Down Expand Up @@ -785,6 +819,13 @@ class Game {
y: b.y,
ownerId: b.ownerId,
})),
machineGunPickup: this.machineGunPickup
? {
x: this.machineGunPickup.x,
y: this.machineGunPickup.y,
collected: this.machineGunPickup.collected,
}
: null,
winnerId: this.winnerId,
yourId: forPlayerId,
isSpectator: this.spectators.has(forPlayerId),
Expand All @@ -810,6 +851,8 @@ module.exports = {
PLAYER_MAX_HP,
RING_DAMAGE_PER_SEC,
SHOOT_COOLDOWN_MS,
MACHINE_GUN_COOLDOWN_MS,
PICKUP_COLLECT_RADIUS,
RING_SHRINK_DURATION_MS,
LOBBY_COUNTDOWN_MS,
ROUND_END_DELAY_MS,
Expand Down
1 change: 1 addition & 0 deletions server/npc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function createNPC(id, name) {
input: { up: false, down: false, left: false, right: false },
name: name,
isNPC: true,
hasMachineGun: false,
_wanderAngle: Math.random() * Math.PI * 2,
_wanderChangeTime: 0,
};
Expand Down
Loading
Loading