Files
deepseek 764da2ee6c Dino Isle Online: multiplayer dino survival game (The Isle-like)
- 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
2026-08-23 07:00:08 +00:00

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!`);
});