Files
arcane-tycoon/tests/inputcheck.mjs
T
deepseek ac00687480 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
2026-08-23 06:59:21 +00:00

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);