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,103 @@
|
||||
// ============ linkcheck.mjs — import EVERY module under a DOM stub ============
|
||||
// Catches missing/misnamed exports (module linking errors) without a browser.
|
||||
// Run: node tests/linkcheck.mjs
|
||||
let failures = 0;
|
||||
|
||||
const makeCtx = () => new Proxy({}, {
|
||||
get(t, p) {
|
||||
if (p === 'canvas') return {};
|
||||
if (!(p in t)) t[p] = (...args) => makeCtx();
|
||||
const v = t[p];
|
||||
return typeof v === 'function' ? v : t[p];
|
||||
},
|
||||
set() { return true; },
|
||||
});
|
||||
|
||||
const fakeEl = (id = '') => {
|
||||
const el = {
|
||||
id, style: {}, dataset: {}, children: [],
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains: () => true },
|
||||
setAttribute() {}, getAttribute: () => null,
|
||||
addEventListener() {}, removeEventListener() {},
|
||||
appendChild(c) { el.children.push(c); }, remove() {}, click() {},
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
|
||||
getContext: () => makeCtx(),
|
||||
focus() {}, select() {},
|
||||
files: [],
|
||||
text: '',
|
||||
_innerHTML: '',
|
||||
firstChild: null,
|
||||
};
|
||||
Object.defineProperty(el, 'innerHTML', {
|
||||
get() { return el._innerHTML; },
|
||||
set(v) {
|
||||
el._innerHTML = String(v);
|
||||
if (!el._innerHTML) el.children = [];
|
||||
el.firstChild = el.children[0] ?? null;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(el, 'textContent', {
|
||||
get() { return el._textContent ?? ''; },
|
||||
set(v) { el._textContent = String(v); },
|
||||
});
|
||||
let width = 300, height = 150;
|
||||
Object.defineProperty(el, 'width', { get: () => width, set: v => { width = v; } });
|
||||
Object.defineProperty(el, 'height', { get: () => height, set: v => { height = 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 = {
|
||||
getElementById: id => (globalThis.__els ??= {})[id] ?? ((globalThis.__els[id] = fakeEl(id))),
|
||||
createElement: tag => fakeEl(tag),
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
addEventListener() {},
|
||||
body: { appendChild() {} },
|
||||
};
|
||||
globalThis.AudioContext = undefined;
|
||||
globalThis.Blob = class { constructor() {} };
|
||||
globalThis.URL = { createObjectURL: () => 'blob:x', revokeObjectURL: () => {} };
|
||||
|
||||
async function load(label, path) {
|
||||
try {
|
||||
await import(path);
|
||||
console.log(`✔ ${label}`);
|
||||
} catch (e) {
|
||||
failures++;
|
||||
console.error(`✘ ${label}: ${e.message}`);
|
||||
console.error(e.stack?.split('\n').slice(1, 3).join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
await load('core/util', '../js/core/util.js');
|
||||
await load('core/config', '../js/core/config.js');
|
||||
await load('core/audio', '../js/core/audio.js');
|
||||
await load('world/map', '../js/world/map.js');
|
||||
await load('world/path', '../js/world/path.js');
|
||||
await load('game/economy', '../js/game/economy.js');
|
||||
await load('game/state', '../js/game/state.js');
|
||||
await load('game/guests', '../js/game/guests.js');
|
||||
await load('game/staff', '../js/game/staff.js');
|
||||
await load('game/rides', '../js/game/rides.js');
|
||||
await load('game/coaster', '../js/game/coaster.js');
|
||||
await load('game/heroes', '../js/game/heroes.js');
|
||||
await load('game/magic', '../js/game/magic.js');
|
||||
await load('game/research', '../js/game/research.js');
|
||||
await load('game/save', '../js/game/save.js');
|
||||
await load('render/renderer', '../js/render/renderer.js');
|
||||
await load('ui/ui', '../js/ui/ui.js');
|
||||
await load('ui/dialogs', '../js/ui/dialogs.js');
|
||||
await load('ui/povui', '../js/ui/povui.js');
|
||||
await load('main (full graph)', '../js/main.js');
|
||||
|
||||
console.log(failures ? `\n${failures} LINK FAILURES` : '\nALL MODULES LINK OK');
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in New Issue
Block a user