Spaces:
Sleeping
Sleeping
| 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}`); | |
| }); | |