168 lines
5.7 KiB
JavaScript
168 lines
5.7 KiB
JavaScript
'use strict';
|
|
/*
|
|
* Turbo Race - HTTP + WebSocket server
|
|
* Serveert de statische frontend en draait de spelengine.
|
|
* Host-eindpunt: verbind met ?host=<ADMIN_TOKEN> en druk op "s".
|
|
* De host is toeschouwer: rijdt NIET zelf mee, maar volgt de koplopers live.
|
|
*/
|
|
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': {
|
|
const isHost = !!(msg.hostToken && ADMIN_TOKEN && String(msg.hostToken) === String(ADMIN_TOKEN));
|
|
if (isHost) {
|
|
// Host = toeschouwer: neemt geen spelerslot in, toont de koplopers.
|
|
ws.isHost = true;
|
|
send(ws, {
|
|
type: 'joined',
|
|
host: true,
|
|
spectator: true,
|
|
self: { name: 'Host', color: '#ffd93d' },
|
|
snapshot: game.snapshot(),
|
|
track: game.track,
|
|
});
|
|
break;
|
|
}
|
|
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,
|
|
spectator: false,
|
|
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;
|
|
if (typeof msg.x === 'number') {
|
|
p.followX = Math.max(12, Math.min(408, Number(msg.x))); // vinger volgen
|
|
p.steer = 0;
|
|
} else {
|
|
p.steer = Math.max(-1, Math.min(1, Number(msg.steer) || 0));
|
|
p.followX = null; // toetsenbord
|
|
}
|
|
break;
|
|
}
|
|
case 'host_start': {
|
|
const allowed = ws.isHost || (() => {
|
|
const id = sockToId.get(ws);
|
|
const p = id && game.players.get(id);
|
|
return !!(p && p.host);
|
|
})();
|
|
if (!allowed) 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 (!ws.isHost && !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>/?host=${ADMIN_TOKEN} -> druk op S om te starten`);
|
|
});
|