// server.js // Simple WebSocket server for multiplayer demo // npm install ws const WebSocket = require('ws'); const port = process.env.PORT || 8080; const wss = new WebSocket.Server({ port }); console.log('Multiplayer server running on port', port); // State: map clientId -> { ws, username, lastSeen, pos, rot } const clients = new Map(); function broadcast(data, exceptWs = null) { const raw = JSON.stringify(data); for (const c of clients.values()) { if (c.ws.readyState === WebSocket.OPEN && c.ws !== exceptWs) { c.ws.send(raw); } } } wss.on('connection', (ws) => { const clientId = Math.random().toString(36).slice(2,10); clients.set(clientId, { ws, clientId, username: 'anon', lastSeen: Date.now() }); // Send welcome + assigned id ws.send(JSON.stringify({ t: 'welcome', id: clientId })); ws.on('message', (raw) => { let msg; try { msg = JSON.parse(raw); } catch (e) { return; } const me = clients.get(clientId); me.lastSeen = Date.now(); switch (msg.t) { case 'join': // {t:'join', username} me.username = (msg.username || 'anon').substring(0,32); // send current players to the new client const roster = []; for (const c of clients.values()) { if (c.clientId !== clientId && c.ws.readyState === WebSocket.OPEN) { roster.push({ id: c.clientId, username: c.username, pos: c.pos || null, rot: c.rot || null }); } } ws.send(JSON.stringify({ t:'roster', players: roster })); // tell others about me broadcast({ t:'player-joined', id: clientId, username: me.username }, ws); break; case 'update': // {t:'update', pos:{x,y,z}, rot:{x,y,z}} me.pos = msg.pos; me.rot = msg.rot; // broadcast to others broadcast({ t:'update', id: clientId, pos: msg.pos || null, rot: msg.rot || null }, ws); break; case 'place-block': // {t:'place-block', x,y,z, blockType} // broadcast action so others can replicate the block placement broadcast({ t:'place-block', id: clientId, x: msg.x, y: msg.y, z: msg.z, blockType: msg.blockType }, ws); break; case 'chat': // {t:'chat', text} const text = String(msg.text || '').slice(0,200); broadcast({ t:'chat', id: clientId, username: me.username || 'anon', text }); break; default: // ignore break; } }); ws.on('close', () => { clients.delete(clientId); broadcast({ t:'player-left', id: clientId }, ws); }); ws.on('error', (e) => { console.error('ws err', e); }); }); // Periodic cleanup of dead clients (in case) setInterval(() => { const now = Date.now(); for (const [id, c] of clients.entries()) { if (!c.ws || c.ws.readyState !== WebSocket.OPEN || (now - c.lastSeen) > (1000*60*5)) { try { c.ws.terminate(); } catch(e){} clients.delete(id); broadcast({ t:'player-left', id }); } } }, 30_000);