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:
@@ -0,0 +1,84 @@
|
||||
// ============ flow.mjs — simulate New Game click flow headlessly ============
|
||||
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 listenersOf = el => (el._listeners ??= {});
|
||||
const fakeEl = (id = '') => {
|
||||
const el = {
|
||||
id, style: {}, dataset: {}, children: [], files: [],
|
||||
classList: {
|
||||
_set: new Set(['hidden']),
|
||||
add(c) { this._set.add(c); }, remove(c) { this._set.delete(c); },
|
||||
toggle(c) { this._set.has(c) ? this._set.delete(c) : this._set.add(c); },
|
||||
contains(c) { return this._set.has(c); },
|
||||
},
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
|
||||
getContext: () => makeCtx(),
|
||||
appendChild(c) { el.children.push(c); c.parentNode = el; },
|
||||
remove() { const p = el.parentNode; if (p) p.children = p.children.filter(x => x !== el); },
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
addEventListener(ev, fn) { (listenersOf(el)[ev] ??= []).push(fn); },
|
||||
dispatch(ev, arg = {}) { for (const f of listenersOf(el)[ev] || []) f(arg); },
|
||||
setAttribute() {}, focus() {},
|
||||
textContent: '',
|
||||
};
|
||||
Object.defineProperty(el, 'innerHTML', {
|
||||
get() { return ''; },
|
||||
set(v) { if (!v) el.children = []; },
|
||||
});
|
||||
let w = 300, h = 150;
|
||||
Object.defineProperty(el, 'width', { get: () => w, set: v => { w = v; } });
|
||||
Object.defineProperty(el, 'height', { get: () => h, set: v => { h = v; } });
|
||||
return el;
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.innerWidth = 1280; globalThis.innerHeight = 800;
|
||||
globalThis.requestAnimationFrame = () => 0; 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]; } };
|
||||
globalThis.document = {
|
||||
els: {},
|
||||
getElementById(id) { return this.els[id] ?? (this.els[id] = fakeEl(id)); },
|
||||
createElement: tag => fakeEl(tag),
|
||||
createTextNode: t => ({ nodeType: 3, textContent: String(t) }),
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
addEventListener() {}, body: { appendChild(c) { document.bodyChildren ??= []; document.bodyChildren.push(c); } },
|
||||
};
|
||||
|
||||
const main = await import('../js/main.js');
|
||||
ok(true, 'main.js evaluated (full graph)');
|
||||
const dialogs = await import('../js/ui/dialogs.js');
|
||||
const stateM = await import('../js/game/state.js');
|
||||
|
||||
// 1. click New Game
|
||||
const mmNew = document.getElementById('mm-new');
|
||||
mmNew.dispatch('click');
|
||||
ok(document.getElementById('modal-root').classList.contains('hidden') === false, 'scenario picker modal visible after click');
|
||||
ok(document.getElementById('modal-root').children.length > 0, 'picker content rendered');
|
||||
|
||||
// find scenario cards among descendants
|
||||
const findCards = n => {
|
||||
let out = [];
|
||||
for (const c of n.children || []) {
|
||||
if ((c.className || '').includes && String(c.className).includes('scen-card')) out.push(c);
|
||||
out = out.concat(findCards(c));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const cards = findCards(document.getElementById('modal-root'));
|
||||
ok(cards.length === 4, `4 scenario cards rendered (${cards.length})`);
|
||||
|
||||
// 2. pick first scenario
|
||||
let picked = null;
|
||||
cards[0].dispatch('click'); // handler calls onPick(scenarioId) → main.startNewGame
|
||||
ok(stateM.getState()?.map != null, `game state created (${stateM.getState()?.scenario})`);
|
||||
ok(document.getElementById('main-menu').classList.contains('hidden'), 'main menu hidden after start');
|
||||
ok(document.getElementById('topbar').classList.contains('hidden') === false, 'HUD shown after start');
|
||||
|
||||
console.log(fails ? `\n${fails} FLOW FAILURES` : '\nNEW GAME FLOW OK');
|
||||
process.exit(fails ? 1 : 0);
|
||||
Reference in New Issue
Block a user