'use strict'; /* * Turbo Race - HTTP + WebSocket server * Serveert de statische frontend en draait de spelengine. * Host-eindpunt: verbind met ?host= en druk op "s". */ const path = require('path'); const http = require('http'); const express = require('express'); const { WebSocketServer } = require('ws'); const Game = require('./engine'); const PORT = parseInt(process.env.PORT || '8080', 10); const MAX_PLAYERS = parseInt(process.env.MAX_PLAYERS || '20', 10); const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'dev-host'; const COUNTDOWN_S = parseInt(process.env.COUNTDOWN_S || '30', 10); const RACE_DURATION_S = parseInt(process.env.RACE_DURATION_S || '90', 10); const TRACK_LENGTH = parseInt(process.env.TRACK_LENGTH || '23000', 10); const app = express(); app.use(express.static(path.join(__dirname, '..', 'public'))); app.get('/healthz', (req, res) => res.status(200).json({ ok: true, phase: game.phase, players: game.players.size })); const server = http.createServer(app); const wss = new WebSocketServer({ server, path: '/ws' }); const game = new Game({ maxPlayers: MAX_PLAYERS, countdownS: COUNTDOWN_S, raceDurationS: RACE_DURATION_S, trackLength: TRACK_LENGTH, }); // socket -> playerId const sockToId = new Map(); function broadcast(obj, except) { const data = JSON.stringify(obj); for (const client of wss.clients) { if (client.readyState === client.OPEN && client !== except) client.send(data); } } function send(ws, obj) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(obj)); } /* ---------------- engine-events -> naar alle clients ---------------- */ game.on('join', (p) => broadcast({ type: 'player_joined', player: { id: p.id, name: p.name, color: p.color } })); game.on('leave', (id) => broadcast({ type: 'player_left', id })); game.on('countdown', () => broadcast({ type: 'phase', phase: 'countdown' })); game.on('race_start', (track) => { broadcast({ type: 'track', track }); broadcast(game.snapshot()); }); game.on('lobby_reset', () => broadcast({ type: 'phase', phase: 'lobby' })); game.on('hit', (e) => broadcast({ type: 'fx', id: e.id, kind: 'hit:' + e.kind })); game.on('pickup', (e) => broadcast({ type: 'fx', id: e.id, kind: 'pickup:' + e.kind })); game.on('finish', (e) => broadcast({ type: 'announce', text: `🏁 ${e.name} is over de finish!` })); /* ---------------- websocket ---------------- */ wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); ws.on('message', (buf) => { let msg; try { msg = JSON.parse(buf.toString()); } catch { return; } switch (msg.type) { case 'join': { let player; try { player = game.addPlayer(msg.name, msg.color, msg.hostToken, ADMIN_TOKEN); } catch (e) { return send(ws, { type: 'error', code: e.code || 'ERR', msg: e.message }); } sockToId.set(ws, player.id); send(ws, { type: 'joined', id: player.id, host: player.host, self: { name: player.name, color: player.color }, snapshot: game.snapshot(), track: game.track, }); break; } case 'input': { const id = sockToId.get(ws); if (!id) break; const p = game.players.get(id); if (!p) break; p.steer = Math.max(-1, Math.min(1, Number(msg.steer) || 0)); break; } case 'host_start': { const id = sockToId.get(ws); const p = id && game.players.get(id); if (!p || !p.host) break; if (game.hostStart()) broadcast({ type: 'phase', phase: 'countdown', hostStart: true }); break; } case 'ready_again': { const id = sockToId.get(ws); const p = id && game.players.get(id); if (!p) break; if (game.readyAgain()) broadcast({ type: 'phase', phase: 'lobby', reset: true }); break; } } }); ws.on('error', () => {}); ws.on('close', () => { const id = sockToId.get(ws); sockToId.delete(ws); if (id) game.removePlayer(id); }); }); /* ---------------- heartbeat + game-loop + broadcasts ---------------- */ setInterval(() => { for (const client of wss.clients) { if (!client.isAlive) { client.terminate(); continue; } client.isAlive = false; client.ping(); } }, 30000); setInterval(() => game.update(25), 25); setInterval(() => { if (wss.clients.size > 0) broadcast(game.snapshot()); }, 62.5); server.listen(PORT, () => { console.log(`🏎️ Turbo Race draait op http://0.0.0.0:${PORT}`); console.log(` Max spelers: ${MAX_PLAYERS} | countdown: ${COUNTDOWN_S}s | race: max ${RACE_DURATION_S}s | baan: ${TRACK_LENGTH}m`); console.log(` Host-modus (open in browser): http:///?host=${ADMIN_TOKEN} -> druk op S om te starten`); });