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
99 lines
4.9 KiB
JavaScript
99 lines
4.9 KiB
JavaScript
// ============ inputcheck.mjs — simulate keyboard & mouse against real handlers ============
|
|
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(); return typeof t[p] === 'function' ? t[p] : t[p]; },
|
|
set() { return true; },
|
|
apply() { return makeCtx(); },
|
|
});
|
|
const mkEl = (id = '') => {
|
|
const el = {
|
|
id, style: {}, dataset: {}, children: [],
|
|
_listeners: {},
|
|
addEventListener(ev, fn) { (el._listeners[ev] ??= []).push(fn); },
|
|
dispatch(ev, arg = {}) { for (const f of el._listeners[ev] || []) f({ preventDefault() {}, stopPropagation() {}, target: el, ...arg }); },
|
|
classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle() {}, contains: c => false },
|
|
appendChild() {}, remove() {}, setAttribute() {},
|
|
querySelectorAll: () => [], getBoundingClientRect: () => ({ left: 0, top: 0, width: 180, height: 180 }),
|
|
getContext: () => makeCtx(), width: 300, height: 150,
|
|
textContent: '',
|
|
};
|
|
Object.defineProperty(el, 'innerHTML', { get() { return ''; }, set(v) { if (!v) el.children = []; } });
|
|
return el;
|
|
};
|
|
|
|
globalThis.window = globalThis;
|
|
globalThis.innerWidth = 1280; globalThis.innerHeight = 800;
|
|
globalThis.__rafQueue = [];
|
|
globalThis.requestAnimationFrame = fn => { __rafQueue.push(fn); return __rafQueue.length; };
|
|
globalThis.cancelAnimationFrame = () => {};
|
|
globalThis.__winListeners = {};
|
|
globalThis.addEventListener = (ev, fn) => { (globalThis.__winListeners[ev] ??= []).push(fn); };
|
|
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 = {};
|
|
globalThis.document = {
|
|
getElementById(id) { return els[id] ?? (els[id] = mkEl(id)); },
|
|
createElement: t => mkEl(t),
|
|
createTextNode: t => ({ textContent: String(t) }),
|
|
querySelectorAll: () => [], querySelector: () => null,
|
|
addEventListener() {}, body: { appendChild() {} },
|
|
};
|
|
|
|
// start a game FIRST (so getState() exists like a real session)
|
|
const mainMod = await import('../js/main.js');
|
|
const cam = mainMod.cam;
|
|
const stateM = await import('../js/game/state.js');
|
|
const heroesM = await import('../js/game/heroes.js');
|
|
const cfg = await import('../js/core/config.js');
|
|
const st = stateM.newGame('sandbox');
|
|
heroesM.cacheScenario(st, cfg.SCENARIOS.find(s => s.id === 'sandbox'));
|
|
document.getElementById('main-menu').classList.remove ? null : null;
|
|
|
|
const winDispatch = (ev, arg = {}) => {
|
|
for (const f of globalThis.__winListeners[ev] || []) f({ preventDefault() {}, target: { tagName: 'CANVAS' }, ...arg });
|
|
};
|
|
|
|
// ---- KEYBOARD PAN DIRECTIONS (screen-relative) ----
|
|
function press(key, frames = 20) {
|
|
winDispatch('keydown', { key });
|
|
for (let i = 0; i < frames; i++) { const q = [...__rafQueue]; __rafQueue.length = 0; for (const f of q) f(performance.now()); }
|
|
winDispatch('keyup', { key });
|
|
}
|
|
const center = () => { cam.x = 30; cam.y = 40; };
|
|
let p;
|
|
center(); p = { ...cam }; press('w');
|
|
ok(cam.x < p.x && cam.y < p.y, `W pans screen-up (Δ${(cam.x - p.x).toFixed(1)},${(cam.y - p.y).toFixed(1)})`);
|
|
center(); p = { ...cam }; press('s');
|
|
ok(cam.x > p.x && cam.y > p.y, `S pans screen-down (+${(cam.x - p.x).toFixed(1)},+${(cam.y - p.y).toFixed(1)})`);
|
|
center(); p = { ...cam }; press('a');
|
|
ok(cam.x < p.x && cam.y > p.y, `A pans screen-left (${(cam.x - p.x).toFixed(1)},+${(cam.y - p.y).toFixed(1)})`);
|
|
center(); p = { ...cam }; press('d');
|
|
ok(cam.x > p.x && cam.y < p.y, `D pans screen-right (+${(cam.x - p.x).toFixed(1)},${(cam.y - p.y).toFixed(1)})`);
|
|
|
|
// ---- MOUSE DRAG PAN TEST ----
|
|
const canvas = document.getElementById('game');
|
|
const camBeforeDrag = { x: cam.x, y: cam.y };
|
|
canvas.dispatch('pointerdown', { button: 0, clientX: 600, clientY: 400 });
|
|
for (let s = 1; s <= 10; s++) canvas.dispatch('pointermove', { button: 0, clientX: 600 + s * 12, clientY: 400 - s * 6, shiftKey: false });
|
|
canvas.dispatch('pointerup', { button: 0, clientX: 720, clientY: 340 });
|
|
const dragMoved = Math.abs(cam.x - camBeforeDrag.x) + Math.abs(cam.y - camBeforeDrag.y);
|
|
ok(dragMoved > 1, `mouse drag pans camera (delta ${dragMoved.toFixed(1)} tiles)`);
|
|
|
|
// ---- WHEEL ZOOM TEST ----
|
|
const zBefore = cam.zoom;
|
|
canvas.dispatch('wheel', { deltaY: -120, clientX: 640, clientY: 400 });
|
|
ok(cam.zoom > zBefore, `wheel zooms (${zBefore.toFixed(2)} → ${cam.zoom.toFixed(2)})`);
|
|
canvas.dispatch('wheel', { deltaY: 120, clientX: 640, clientY: 400 });
|
|
|
|
// ---- MINIMAP CLICK TELEPORT ----
|
|
const mm = document.getElementById('minimap');
|
|
const mx = cam.x, my = cam.y;
|
|
mm.dispatch('pointerdown', { clientX: 90, clientY: 90, target: mm });
|
|
ok(Math.abs(cam.x - mx) + Math.abs(cam.y - my) > 3, `minimap click teleports camera (${cam.x.toFixed(0)},${cam.y.toFixed(0)})`);
|
|
|
|
console.log(fails ? `\n${fails} INPUT FAILURES` : '\nALL CAMERA INPUTS OK');
|
|
process.exit(fails ? 1 : 0);
|