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:
+121
@@ -0,0 +1,121 @@
|
||||
// ---------------------------------------------------------------
|
||||
// Dino Isle Online — world generation (deterministic, seed based)
|
||||
// Tiles: 0 deep water, 1 shallow water, 2 sand, 3 grassland, 4 forest
|
||||
// ---------------------------------------------------------------
|
||||
'use strict';
|
||||
|
||||
const MAP_W = 160;
|
||||
const MAP_H = 160;
|
||||
const TILE = 48;
|
||||
const WORLD_W = MAP_W * TILE;
|
||||
const WORLD_H = MAP_H * TILE;
|
||||
|
||||
function hash2(ix, iy, seed) {
|
||||
let h = ix * 374761393 + iy * 668265263 + seed * 2246822519;
|
||||
h = (h ^ (h >>> 13)) >>> 0;
|
||||
h = Math.imul(h, 1274126177) >>> 0;
|
||||
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
||||
}
|
||||
function smooth(t) { return t * t * (3 - 2 * t); }
|
||||
function valueNoise(x, y, seed) {
|
||||
const ix = Math.floor(x), iy = Math.floor(y);
|
||||
const fx = x - ix, fy = y - iy;
|
||||
const a = hash2(ix, iy, seed), b = hash2(ix + 1, iy, seed);
|
||||
const c = hash2(ix, iy + 1, seed), d = hash2(ix + 1, iy + 1, seed);
|
||||
const u = smooth(fx), v = smooth(fy);
|
||||
return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v;
|
||||
}
|
||||
function fbm(x, y, seed, octaves) {
|
||||
let amp = 1, freq = 1, sum = 0, norm = 0;
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
sum += amp * valueNoise(x * freq, y * freq, seed + i * 1013);
|
||||
norm += amp; amp *= 0.5; freq *= 2.03;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
function generateWorld(seed) {
|
||||
const tiles = new Uint8Array(MAP_W * MAP_H);
|
||||
for (let ty = 0; ty < MAP_H; ty++) {
|
||||
for (let tx = 0; tx < MAP_W; tx++) {
|
||||
// normalized coords centered
|
||||
const nx = tx / MAP_W - 0.5, ny = ty / MAP_H - 0.5;
|
||||
const d = Math.sqrt(nx * nx * 1.15 + ny * ny * 1.35) * 2; // 0 center -> ~1.4 corners
|
||||
const e = fbm(tx / 22, ty / 22, seed, 4) * 0.9 + 0.18 - Math.pow(Math.min(d, 1.45), 2.1) * 0.62;
|
||||
const m = fbm(tx / 14 + 100, ty / 14 + 100, seed + 7777, 3);
|
||||
let t;
|
||||
if (e < 0.30) t = 0; // deep water
|
||||
else if (e < 0.40) t = 1; // shallow water
|
||||
else if (e < 0.45) t = 2; // sand
|
||||
else if (m > 0.60 && e < 0.85) t = 4; // forest
|
||||
else t = 3; // grassland
|
||||
tiles[ty * MAP_W + tx] = t;
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
function makeRng(seed) {
|
||||
let s = (seed >>> 0) || 1;
|
||||
return () => {
|
||||
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
||||
return s / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function tileAt(tiles, wx, wy) {
|
||||
const tx = Math.floor(wx / TILE), ty = Math.floor(wy / TILE);
|
||||
if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) return 0;
|
||||
return tiles[ty * MAP_W + tx];
|
||||
}
|
||||
|
||||
// Static decoration: trees on forest, rocks scattered, shoreline foam points
|
||||
function generateDecor(tiles, seed) {
|
||||
const rng = makeRng(seed + 991);
|
||||
const trees = [], rocks = [], flowers = [], foam = [];
|
||||
for (let ty = 1; ty < MAP_H - 1; ty++) {
|
||||
for (let tx = 1; tx < MAP_W - 1; tx++) {
|
||||
const t = tiles[ty * MAP_W + tx];
|
||||
const cx = tx * TILE + TILE / 2, cy = ty * TILE + TILE / 2;
|
||||
if (t === 4 && rng() < 0.16) {
|
||||
trees.push({ x: cx + (rng() - 0.5) * 30, y: cy + (rng() - 0.5) * 30, s: 0.8 + rng() * 0.55 });
|
||||
} else if (t === 3 && rng() < 0.010) {
|
||||
rocks.push({ x: cx + (rng() - 0.5) * 34, y: cy + (rng() - 0.5) * 34, s: 0.6 + rng() * 0.9, r: rng() });
|
||||
} else if ((t === 3 || t === 2) && rng() < 0.05) {
|
||||
flowers.push({ x: cx + (rng() - 0.5) * 40, y: cy + (rng() - 0.5) * 40, c: Math.floor(rng() * 3) });
|
||||
}
|
||||
}
|
||||
}
|
||||
// foam: shallow tiles adjacent to sand
|
||||
for (let ty = 1; ty < MAP_H - 1; ty++) {
|
||||
for (let tx = 1; tx < MAP_W - 1; tx++) {
|
||||
if (tiles[ty * MAP_W + tx] !== 1) continue;
|
||||
let adjSand = false;
|
||||
for (let oy = -1; oy <= 1 && !adjSand; oy++)
|
||||
for (let ox = -1; ox <= 1; ox++) {
|
||||
if (tiles[(ty + oy) * MAP_W + tx + ox] === 2) { adjSand = true; break; }
|
||||
}
|
||||
if (adjSand) foam.push({ x: tx * TILE + TILE / 2, y: ty * TILE + TILE / 2, p: rng() * Math.PI * 2 });
|
||||
}
|
||||
}
|
||||
// cap decor for perf
|
||||
const cap = (arr, n) => arr.length > n ? arr.filter((_, i) => i % Math.ceil(arr.length / n) === 0) : arr;
|
||||
return { trees: cap(trees, 900), rocks: cap(rocks, 260), flowers: cap(flowers, 700), foam: cap(foam, 1400) };
|
||||
}
|
||||
|
||||
// Find land spawn points (grass/sand near coast preferred)
|
||||
function findSpawnPoints(tiles, count) {
|
||||
const rng = makeRng(4242);
|
||||
const pts = [];
|
||||
let guard = 0;
|
||||
while (pts.length < count && guard++ < 20000) {
|
||||
const tx = 4 + Math.floor(rng() * (MAP_W - 8));
|
||||
const ty = 4 + Math.floor(rng() * (MAP_H - 8));
|
||||
const t = tiles[ty * MAP_W + tx];
|
||||
if (t !== 2 && t !== 3) continue;
|
||||
pts.push({ x: tx * TILE + TILE / 2, y: ty * TILE + TILE / 2 });
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
module.exports = { MAP_W, MAP_H, TILE, WORLD_W, WORLD_H, generateWorld, generateDecor, findSpawnPoints, tileAt, makeRng };
|
||||
Reference in New Issue
Block a user