- Zero-dependency Node server: HTTP static + hand-rolled RFC6455 WebSocket + 20Hz authoritative simulation - Seeded island worldgen (tiles, forest decor, spawns) - 4 playable species, needs (food/water/stamina), rest, growth stages Hatchling->Apex - AI fauna: critters, fish shoals, Dryosaurus/Psittacosaurus herds; carcass feasting - PvP with knockback, kill feed, chat, leaderboard, minimap, day/night cycle - True top-down procedural dino renderer shared by menu cards and in-game - Developer mode (` key): god-mode, x6 XP, instant evolve, spawn AI, Alt+Click tp - Headless smoke tests, real-browser visual tests, pixel match checks
121 lines
3.6 KiB
JavaScript
121 lines
3.6 KiB
JavaScript
// ---------------------------------------------------------------
|
|
// Dino Isle Online — main server: static files + websocket + game loop
|
|
// Run: node server.js (PORT env optional, default 8090)
|
|
// ---------------------------------------------------------------
|
|
'use strict';
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const { Game, TICK_MS } = require('./sim');
|
|
const wsattach = require('./ws').attach;
|
|
|
|
const PORT = parseInt(process.env.PORT || '8095', 10);
|
|
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
const CHEATS = process.env.DEBUG_CHEATS === '1';
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.woff2': 'font/woff2',
|
|
};
|
|
|
|
const server = http.createServer((req, res) => {
|
|
let urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
if (urlPath === '/') urlPath = '/index.html';
|
|
const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
|
|
if (!filePath.startsWith(PUBLIC_DIR)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
fs.readFile(filePath, (err, data) => {
|
|
if (err) {
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not found');
|
|
return;
|
|
}
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
res.writeHead(200, {
|
|
'Content-Type': MIME[ext] || 'application/octet-stream',
|
|
'Cache-Control': 'no-store',
|
|
});
|
|
res.end(data);
|
|
});
|
|
});
|
|
|
|
const game = new Game();
|
|
|
|
let nextAllowedMsg = new WeakMap(); // conn -> timestamp budget
|
|
|
|
wsattach(server, '/ws', (conn) => {
|
|
conn.onmessage = (str) => {
|
|
let m;
|
|
try { m = JSON.parse(str); } catch (_) { return; }
|
|
if (!m || typeof m !== 'object') return;
|
|
|
|
// simple flood guard: max ~40 msgs/sec
|
|
const now = Date.now();
|
|
const budget = nextAllowedMsg.get(conn) || now;
|
|
if (now < budget - 1000) return; // way over -> drop
|
|
nextAllowedMsg.set(conn, Math.max(budget + 25, now));
|
|
|
|
const p = game.players.get(conn);
|
|
switch (m.t) {
|
|
case 'join': {
|
|
const player = game.join(conn, String(m.name || '').slice(0, 16), m.sp);
|
|
conn.send(JSON.stringify(game.welcomePayload(player)));
|
|
break;
|
|
}
|
|
case 'in':
|
|
if (p) game.onInput(p, m);
|
|
break;
|
|
case 'chat':
|
|
if (p && p.alive !== undefined) game.onChat(p, m.msg);
|
|
break;
|
|
case 'dev':
|
|
if (p) game.onDev(p, m);
|
|
break;
|
|
case 'devact':
|
|
if (p) game.onDevAct(p, String(m.act || ''));
|
|
break;
|
|
case 'ping': conn.send('{"t":"pong"}'); break;
|
|
case 'tp':
|
|
if (p && (CHEATS || p.dev)) {
|
|
p.x = Math.max(40, Math.min(7680 - 40, +m.x || p.x));
|
|
p.y = Math.max(40, Math.min(7680 - 40, +m.y || p.y));
|
|
if (typeof m.a === 'number') p.dir = m.a;
|
|
p.vx = 0; p.vy = 0;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
conn.onclose = () => game.leave(conn);
|
|
});
|
|
|
|
// heartbeat
|
|
setInterval(() => {
|
|
for (const p of [...game.players.keys()]) {
|
|
const c = p; // key is conn
|
|
if (c.writable) c.ping();
|
|
}
|
|
}, 25000);
|
|
|
|
// main loop with drift compensation
|
|
let last = Date.now();
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
let dt = (now - last) / 1000;
|
|
last = now;
|
|
dt = Math.min(dt, 0.25); // clamp big pauses
|
|
try { game.tick(dt); } catch (e) { console.error('tick error', e); }
|
|
}, TICK_MS);
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`[dino-isle] listening on http://localhost:${PORT} (seed=${game.seed})`);
|
|
console.log(`[dino-isle] open multiple browser tabs for multiplayer fun!`);
|
|
});
|