Arcane Tycoon — Heroes & Magic theme park tycoon game

Complete browser game inspired by OpenRCT2 with fantasy twist:
- Custom roller coaster designer with physics-based ratings + on-ride POV
- 10 animated rides, 7 shops, 16 scenery items, path network & guest AI
- Heroes guild vs monster invasions (5 classes, XP/gear/bosses)
- Magic spell system (8 spells), research tree, economy/marketing/loans
- Day-night cycle, weather, park rating, awards, 4 scenarios
- Save/load slots + autosave, procedural WebAudio SFX/music
- Isometric canvas renderer, minimap, diagnostics overlay
- Test suites: smoke(13), linkcheck, inputcheck, framecheck, rendercheck, flow
This commit is contained in:
2026-08-23 06:59:21 +00:00
commit ac00687480
30 changed files with 6772 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
// ============ rendercheck.mjs — regression test for terrain culling ============
let fails = 0;
const ok = (cond, msg) => { console.log((cond ? '✔ ' : '✘ ') + msg); if (!cond) fails++; };
globalThis.window = globalThis;
globalThis.requestAnimationFrame = () => 0;
globalThis.document = {
getElementById: () => null, createElement: () => ({ style: {}, classList: { add() {}, remove() {} }, addEventListener() {}, appendChild() {} }),
addEventListener() {}, body: { appendChild() {} },
};
globalThis.localStorage = { getItem: () => null, setItem() {}, removeItem() {} };
const R = await import('../js/render/renderer.js');
const { visibleBounds, worldToScreen, screenToWorld } = R;
ok(typeof visibleBounds === 'function', 'visibleBounds exported');
// --- camera translation regression: panning MUST shift screen coords ---
const camA = { x: 20, y: 20, zoom: 1 };
const camB = { x: 30, y: 26, zoom: 1 };
const [ax] = worldToScreen(camA, 1280, 800, 10, 10);
const [bx] = worldToScreen(camB, 1280, 800, 10, 10);
ok(Math.abs(bx - ax) > 50, `worldToScreen shifts with cam.x (${ax.toFixed(0)}${bx.toFixed(0)})`);
const [, ay2] = worldToScreen(camA, 1280, 800, 10, 10);
const camC = { x: 20, y: 30, zoom: 1 };
const [, cy2] = worldToScreen(camC, 1280, 800, 10, 10);
ok(Math.abs(cy2 - ay2) > 50, `worldToScreen shifts with cam.y (${ay2.toFixed(0)}${cy2.toFixed(0)})`);
// --- roundtrip screen→world→screen identity ---
for (const cz of [0.5, 1, 1.7]) {
const c = { x: 25.3, y: 41.8, zoom: cz };
const [sx, sy] = worldToScreen(c, 1280, 800, 12.4, 33.9, 0); // ground plane
const [wx, wy] = screenToWorld(c, 1280, 800, sx, sy);
ok(wx === 12 && wy === 33,
`roundtrip zoom ${cz}: (${sx.toFixed(0)},${sy.toFixed(0)}) → (${wx},${wy}) expect (12,33)`);
}
// --- visibleBounds must follow the camera ---
const bA = visibleBounds({ x: 5, y: 5, zoom: 1 }, 1280, 800, 80);
const bB = visibleBounds({ x: 40, y: 40, zoom: 1 }, 1280, 800, 80);
ok(bB.x0 > bA.x0 + 10 && bB.y0 > bA.y0 + 10, `bounds track camera (x0 ${bA.x0}${bB.x0}, y0 ${bA.y0}${bB.y0})`);
const cases = [
{ name: 'center of 52-map, zoom 1', cam: { x: 26, y: 26, zoom: 1 }, cw: 1280, ch: 800 },
{ name: 'entrance area, zoom 1', cam: { x: 28, y: 44, zoom: 1 }, cw: 1280, ch: 800 },
{ name: 'zoomed out 0.5', cam: { x: 26, y: 26, zoom: 0.5 }, cw: 1280, ch: 800 },
{ name: 'zoomed in 2', cam: { x: 26, y: 26, zoom: 2 }, cw: 1920, ch: 1080 },
];
for (const c of cases) {
const b = visibleBounds(c.cam, c.cw, c.ch, 80);
const spanX = b.x1 - b.x0, spanY = b.y1 - b.y0;
// exact iso-diamond coverage: ((cw+2p)/(TW2·z) + (ch+2p)/(TH2·z)) / 2 per axis
const p = 80;
const expectSpan = ((c.cw + 2 * p) / (32 * c.cam.zoom) + (c.ch + 2 * p) / (16 * c.cam.zoom)) / 2;
ok(spanX >= expectSpan - 2 && spanY >= expectSpan - 2,
`${c.name}: span ${spanX}×${spanY} tiles (expect ≈${Math.round(expectSpan)})`);
}
// the old bug produced a y-span of roughly 10 rows on a 1280×800 @zoom1 view
const b = visibleBounds({ x: 26, y: 26, zoom: 1 }, 1280, 800, 80);
ok(b.y1 - b.y0 > 40, `vertical coverage fixed (${b.y1 - b.y0} rows, was ~10 before)`);
console.log(fails ? `\n${fails} RENDER FAILURES` : '\nRENDER CULLING OK');
process.exit(fails ? 1 : 0);