const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); const url = require('url'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); app.use(express.static(path.join(__dirname, 'public'))); const PORT = process.env.PORT || 7860; // ---- Game settings ---- const MAX_HP = 5; // 5 laser hits and you're out — no time limit, unlimited ammo // id -> { id, name, color, position:[x,y,z], rotationY, score, hp } const players = new Map(); const COLORS = [ 0xff5555, 0x55ff55, 0x5599ff, 0xffdd55, 0xff55dd, 0x55ffdd, 0xffa500, 0xaa55ff, ]; let colorIndex = 0; let nextId = 1; function scoreList() { return Array.from(players.values()).map((p) => ({ id: p.id, name: p.name, score: p.score })); } function broadcast(data, exceptWs) { const msg = JSON.stringify(data); wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN && client !== exceptWs) { client.send(msg); } }); } wss.on('connection', (ws, req) => { const id = 'player-' + (nextId++); const color = COLORS[colorIndex % COLORS.length]; colorIndex++; const query = url.parse(req.url, true).query; const name = (query.name || '').toString().replace(/[<>]/g, '').trim().slice(0, 16) || 'Player ' + (nextId - 1); const initialState = { id, name, color, position: [0, 1.6, 0], rotationY: 0, score: 0, hp: MAX_HP, }; players.set(id, initialState); ws.playerId = id; ws.lastHitAt = 0; // Tell the new client who they are + who's already here ws.send(JSON.stringify({ type: 'init', id, name, color, hp: MAX_HP, maxHp: MAX_HP, players: Array.from(players.values()).filter((p) => p.id !== id), scores: scoreList(), })); // Tell everyone else someone new joined broadcast({ type: 'join', ...initialState }, ws); console.log(`[+] ${id} "${name}" connected (${wss.clients.size} online)`); ws.on('message', (raw) => { let data; try { data = JSON.parse(raw); } catch (e) { return; } if (data.type === 'update' && Array.isArray(data.position)) { const p = players.get(id); if (!p) return; p.position = data.position; p.rotationY = data.rotationY || 0; broadcast({ type: 'update', id, position: p.position, rotationY: p.rotationY }, ws); } else if (data.type === 'shoot' && Array.isArray(data.from) && Array.isArray(data.to)) { // Relay the laser shot so everyone else can draw/hear it broadcast({ type: 'shoot', id, from: data.from, to: data.to }, ws); } else if (data.type === 'hit' && typeof data.target === 'string') { const shooter = players.get(id); const target = players.get(data.target); if (!shooter || !target || data.target === id) return; const now = Date.now(); if (now - ws.lastHitAt < 150) return; // simple spam guard ws.lastHitAt = now; shooter.score++; target.hp--; if (target.hp <= 0) { // 5 hits — the player is out of the game broadcast({ type: 'eliminated', target: data.target, targetName: target.name, by: id, byName: shooter.name, scores: scoreList(), }); // Kick the eliminated player's socket shortly after so everyone sees them leave let targetWs = null; wss.clients.forEach((c) => { if (c.playerId === data.target) targetWs = c; }); if (targetWs) { setTimeout(() => { try { targetWs.close(); } catch (e) {} }, 500); } } else { broadcast({ type: 'tagged', target: data.target, targetName: target.name, targetHp: target.hp, by: id, byName: shooter.name, scores: scoreList(), }); } } else if (data.type === 'signal' && data.target) { // Relay a WebRTC signaling message (offer/answer/ICE) to one specific peer. let targetWs = null; wss.clients.forEach((c) => { if (c.playerId === data.target && c.readyState === WebSocket.OPEN) targetWs = c; }); if (targetWs) { targetWs.send(JSON.stringify({ type: 'signal', from: id, data: data.data })); } } }); ws.on('close', () => { players.delete(id); broadcast({ type: 'leave', id, scores: scoreList() }); console.log(`[-] ${id} disconnected (${wss.clients.size} online)`); }); ws.on('error', () => { // swallow - close event will still fire }); }); server.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });