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
88 lines
4.1 KiB
JavaScript
88 lines
4.1 KiB
JavaScript
// ============ framecheck.mjs — run the REAL game loop for thousands of frames ============
|
|
let fails = 0;
|
|
const ok = (cond, msg) => { console.log((cond ? '✔ ' : '✘ ') + msg); if (!cond) fails++; };
|
|
|
|
const makeCtx = () => new Proxy(function () {}, {
|
|
get(t, p) {
|
|
if (!(p in t)) t[p] = (...a) => makeCtx();
|
|
const v = t[p];
|
|
return typeof v === 'function' ? v : v;
|
|
},
|
|
set() { return true; },
|
|
apply() { return makeCtx(); },
|
|
});
|
|
const mkEl = (id = '') => ({
|
|
id, style: {}, dataset: {}, children: [], _listeners: {},
|
|
addEventListener(ev, fn) { (this._listeners[ev] ??= []).push(fn); },
|
|
classList: { _s: new Set(['hidden']), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle() {}, contains(c) { return this._s.has(c); } },
|
|
appendChild() {}, remove() {}, setAttribute() {},
|
|
querySelectorAll: () => [], getBoundingClientRect: () => ({ left: 0, top: 0, width: 180, height: 180 }),
|
|
getContext: () => makeCtx(), width: 300, height: 150, textContent: '',
|
|
});
|
|
Object.defineProperty(mkEl.prototype ?? {}, 'x', { value: 0 });
|
|
|
|
globalThis.window = globalThis;
|
|
globalThis.innerWidth = 1280; globalThis.innerHeight = 800;
|
|
globalThis.__rafQ = [];
|
|
globalThis.requestAnimationFrame = fn => { globalThis.__rafQ.push(fn); return 1; };
|
|
globalThis.cancelAnimationFrame = () => {};
|
|
globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {};
|
|
globalThis.localStorage = { _m: {}, getItem(k) { return this._m[k] ?? null; }, setItem(k, v) { this._m[k] = String(v); }, removeItem(k) { delete this._m[k]; } };
|
|
const els = {};
|
|
const REAL_IDS = new Set(['game','minimap','topbar','toolbar','palette','pal-body','pal-title','pal-close','context-panel','tool-hint','toasts','modal-root','main-menu','mm-new','mm-how','mm-continue','stat-cash','stat-guests','stat-rating','mana-fill','mana-num','stat-weather','stat-date','btn-pause','btn-research','btn-finance','btn-heroes','btn-objectives','btn-park','btn-save','btn-help','minimap-wrap']);
|
|
globalThis.document = {
|
|
getElementById(id) { if (!REAL_IDS.has(id)) return null; return els[id] ?? (els[id] = mkEl(id)); },
|
|
createElement: () => mkEl(),
|
|
createTextNode: t => ({ textContent: String(t) }),
|
|
querySelectorAll: () => [], querySelector: () => null,
|
|
addEventListener() {}, body: { appendChild() {} },
|
|
};
|
|
|
|
await import('../js/main.js');
|
|
const stateM = await import('../js/game/state.js');
|
|
const heroesM = await import('../js/game/heroes.js');
|
|
const cfg = await import('../js/core/config.js');
|
|
|
|
for (const scenId of ['meadows', 'sandbox']) {
|
|
// fresh state through the REAL entry path
|
|
document.getElementById('main-menu'); // ensure element exists
|
|
const st = stateM.newGame(scenId);
|
|
heroesM.cacheScenario(st, cfg.SCENARIOS.find(s => s.id === scenId));
|
|
st._speed = 3; // fast-forward
|
|
st._paused = false;
|
|
// give the park some content to exercise more code paths
|
|
const m = st.map;
|
|
const ex = m.entranceX, ey = m.entranceY;
|
|
for (let y = ey - 12; y < ey; y++) for (let x = ex - 8; x <= ex + 8; x++)
|
|
if (m.isBuildable(x, y) && !m.objects[m.idx(x, y)]) m.pathType[m.idx(x, y)] = 1;
|
|
stateM.addShopObj(st, 'food', ex - 4, ey - 8);
|
|
stateM.addShopObj(st, 'drinks', ex - 2, ey - 8);
|
|
stateM.addShopObj(st, 'toilet', ex + 4, ey - 8);
|
|
const ride = stateM.addRideObj(st, 'carousel', ex - 7, ey - 11, {});
|
|
ride.status = 'open'; ride.price = 2;
|
|
|
|
let frames = 0, crashed = null, lastErrFrame = -1;
|
|
let tNow = performance.now();
|
|
for (; frames < 4000; frames++) {
|
|
const q = [...globalThis.__rafQ];
|
|
globalThis.__rafQ.length = 0;
|
|
tNow += 33; // ~30fps
|
|
try {
|
|
for (const f of q) f(tNow);
|
|
} catch (e) {
|
|
crashed = e;
|
|
lastErrFrame = frames;
|
|
break;
|
|
}
|
|
}
|
|
if (crashed) {
|
|
ok(false, `[${scenId}] crashed at frame ${lastErrFrame}: ${crashed.message}`);
|
|
console.error(crashed.stack?.split('\n').slice(0, 6).join('\n'));
|
|
} else {
|
|
ok(true, `[${scenId}] ${frames} frames clean · guests=${st.guests.length} cash=${Math.round(st.cash)} hour=${st.time.hour.toFixed(1)}`);
|
|
}
|
|
}
|
|
|
|
console.log(fails ? `\n${fails} FRAME FAILURES` : '\nREAL LOOP STABLE');
|
|
process.exit(fails ? 1 : 0);
|