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:
@@ -0,0 +1,82 @@
|
||||
// Verify menu-card dino and in-game dino renderer produce matching art.
|
||||
'use strict';
|
||||
import { createRequire } from 'node:module';
|
||||
import { readdirSync, existsSync } from 'node:fs';
|
||||
const require = createRequire('/root/kidcraft/node_modules/');
|
||||
const { chromium } = require('playwright-core');
|
||||
function findBrowserExe() {
|
||||
const root = '/root/.cache/ms-playwright';
|
||||
for (const dir of readdirSync(root)) {
|
||||
if (!dir.startsWith('chromium')) continue;
|
||||
for (const sub of ['chrome-headless-shell-linux64/chrome-headless-shell', 'chrome-linux/headless_shell']) {
|
||||
const p = `${root}/${dir}/${sub}`;
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
let failures = 0;
|
||||
const ok = (c, l) => { console.log(`${c ? 'PASS' : 'FAIL'} ${l}`); if (!c) failures++; };
|
||||
|
||||
const browser = await chromium.launch({ executablePath: findBrowserExe(), args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://127.0.0.1:8095/', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
const res = await page.evaluate(() => {
|
||||
function avgColor(canvas) {
|
||||
const cx = canvas.getContext('2d');
|
||||
const d = cx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
let r = 0, g = 0, b = 0, n = 0;
|
||||
for (let i = 0; i < d.length; i += 4) {
|
||||
if (d[i + 3] < 40) continue; // skip transparent
|
||||
r += d[i]; g += d[i + 1]; b += d[i + 2]; n++;
|
||||
}
|
||||
return n ? [r / n, g / n, b / n] : null;
|
||||
}
|
||||
const out = {};
|
||||
// menu card canvases are the first canvas child of each .card
|
||||
const cards = document.querySelectorAll('.card');
|
||||
const keys = ['compy', 'raptor', 'trike', 'rex'];
|
||||
cards.forEach((card, idx) => {
|
||||
const cv = card.querySelector('canvas');
|
||||
out[keys[idx]] = avgColor(cv);
|
||||
});
|
||||
// render in-game style dino for each species via shared renderer
|
||||
out.game = {};
|
||||
for (const k of keys) {
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = 180; cv.height = 88;
|
||||
const c = cv.getContext('2d');
|
||||
// identical backdrop as drawCardPreview
|
||||
const g = c.createLinearGradient(0, 0, 0, 88);
|
||||
g.addColorStop(0, 'rgba(120,190,140,0.16)');
|
||||
g.addColorStop(1, 'rgba(30,60,45,0.28)');
|
||||
c.fillStyle = g;
|
||||
c.beginPath(); c.roundRect(0, 0, 180, 88, 8); c.fill();
|
||||
c.save();
|
||||
const scale = k === 'rex' ? 1.5 : k === 'compy' ? 0.95 : k === 'trike' ? 1.05 : 1.18;
|
||||
c.translate(90 - 6 * scale, 88 * 0.60); c.scale(scale, scale);
|
||||
paintDino(c, { x: 0, y: 4, dir: 0, r: 17, sp: k, stage: 2, phase: 1.15, moving: false, resting: false, bite: k === 'rex' ? 0.85 : 0, eat: 0, ci: 0, preview: true });
|
||||
c.restore();
|
||||
out.game[k] = avgColor(cv);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
for (const k of ['compy', 'raptor', 'trike', 'rex']) {
|
||||
const [r1, g1, b1] = res[k], [r2, g2, b2] = res.game[k];
|
||||
const d = Math.hypot(r1 - r2, g1 - g2, b1 - b2);
|
||||
ok(d < 12, `${k}: card vs game avg color Δ=${d.toFixed(1)} rgb(${r1.toFixed(0)},${g1.toFixed(0)},${b1.toFixed(0)}) vs (${r2.toFixed(0)},${g2.toFixed(0)},${b2.toFixed(0)})`);
|
||||
}
|
||||
|
||||
// screenshots for human review
|
||||
await page.screenshot({ path: '/tmp/shot-menu-v2.png' });
|
||||
await page.fill('#nameInput', 'RexCheck');
|
||||
await page.click('.card[data-key="rex"]');
|
||||
await page.click('#playBtn');
|
||||
await page.waitForTimeout(2200);
|
||||
await page.screenshot({ path: '/tmp/shot-game-rex.png' });
|
||||
|
||||
await browser.close();
|
||||
console.log(failures ? `\n${failures} CHECK(S) FAILED` : '\nMATCH CHECKS PASSED');
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in New Issue
Block a user