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
This commit is contained in:
2026-08-23 07:00:08 +00:00
commit 764da2ee6c
14 changed files with 3509 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
// ---------------------------------------------------------------
// Minimal RFC6455 WebSocket server — zero dependencies
// ---------------------------------------------------------------
'use strict';
const crypto = require('crypto');
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function acceptKey(key) {
return crypto.createHash('sha1').update(key + GUID).digest('base64');
}
class WSConn {
constructor(socket) {
this.socket = socket;
this.alive = true;
this.fragOp = 0;
this.fragBufs = [];
this.onmessage = null;
this.onclose = null;
socket.setNoDelay(true);
socket.on('data', (buf) => this._onData(buf));
const end = () => this._closed();
socket.on('close', end);
socket.on('error', end);
}
get writable() {
return this.socket && this.socket.writable && this.alive;
}
_closed() {
if (!this.alive) return;
this.alive = false;
if (this.onclose) this.onclose();
try { this.socket.destroy(); } catch (_) {}
}
_onData(buf) {
let off = 0;
try {
while (off + 2 <= buf.length) {
const b0 = buf[off];
const b1 = buf[off + 1];
const fin = (b0 & 0x80) !== 0;
const op = b0 & 0x0f;
const masked = (b1 & 0x80) !== 0;
let len = b1 & 0x7f;
off += 2;
if (len === 126) {
if (off + 2 > buf.length) break;
len = buf.readUInt16BE(off); off += 2;
} else if (len === 127) {
if (off + 8 > buf.length) break;
const big = buf.readBigUInt64BE(off);
off += 8;
len = Number(big);
}
if (len > 10 * 1024 * 1024) { this.close(); return; } // sanity cap
let maskKey = null;
if (masked) {
if (off + 4 > buf.length) break;
maskKey = buf.subarray(off, off + 4); off += 4;
}
if (off + len > buf.length) break; // wait for more data (rare; messages are small)
let payload = buf.subarray(off, off + len);
off += len;
if (maskKey) {
payload = Buffer.from(payload); // copy so we can unmask
for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i & 3];
}
switch (op) {
case 0x0: // continuation
this.fragBufs.push(payload);
if (fin) {
const full = Buffer.concat(this.fragBufs);
this.fragBufs = [];
this._emit(this.fragOp, full);
}
break;
case 0x1: case 0x2: // text / binary
if (fin) this._emit(op, payload);
else { this.fragOp = op; this.fragBufs = [payload]; }
break;
case 0x8: this.close(); return; // close
case 0x9: this._sendFrame(0xA, payload); break; // ping -> pong
case 0xA: break; // pong
default: break;
}
}
} catch (_) { this.close(); }
}
_emit(op, payload) {
if (op === 0x1) {
const str = payload.toString('utf8');
if (this.onmessage) this.onmessage(str);
}
}
_sendFrame(op, payload) {
if (!this.writable) return;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.from([0x80 | op, len]);
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = 0x80 | op; header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = 0x80 | op; header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
try { this.socket.write(Buffer.concat([header, payload])); } catch (_) {}
}
send(str) { this._sendFrame(0x1, Buffer.from(str, 'utf8')); }
ping() { this._sendFrame(0x9, Buffer.alloc(0)); }
close() {
if (this.writable) { try { this._sendFrame(0x8, Buffer.alloc(0)); } catch (_) {} }
this._closed();
}
}
function attach(server, path, onConn) {
server.on('upgrade', (req, socket) => {
try {
const url = req.url.split('?')[0];
if (url !== path) { socket.destroy(); return; }
const key = req.headers['sec-websocket-key'];
if (!key) { socket.destroy(); return; }
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey(key)}`,
'\r\n',
].join('\r\n');
socket.write(headers);
onConn(new WSConn(socket));
} catch (_) {
try { socket.destroy(); } catch (_) {}
}
});
}
module.exports = { attach, WSConn };