/** * Dev-only: the BUILD system self-check — plays the real flow * (menu → NEW GAME → land on the home world → open the Build console) * and paints a plain-text report (top-left, outside the canvas) that * answers "why is my home world's Tether - Level 1 not BUILT?": * * 1. Is the browser running the current JS? (stale-module probe — * a hard cache-bypass reload is the usual cure) * 2. Which world is home, and what does the build state say about it? * 3. What tether does the home world hold (L2's planet gate)? * 4. The console's actual row states — before and after the * tether_l2 research — L1 must read BUILT; L2 must flip from * LOCKED (research) to BUILDABLE (home already holds a L1 tether). * * node dev/slow-server.mjs 8080 # or any static server on the repo * → open http://127.0.0.1:8080/dev/build-check.html * * Headless screenshot: * node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/build-check.html" \ * out.png "document.getElementById('report').textContent.includes('SELF-CHECK DONE')" */ import Phaser from '../js/vendor/phaser.js'; import { config } from '../js/config/Config.js'; import { ConfigLoader } from '../js/config/ConfigLoader.js'; import { createGameConfig } from '../js/config/GameConfig.js'; import { MenuScene } from '../js/scenes/MenuScene.js'; import { GameScene } from '../js/scenes/GameScene.js'; import { SurfaceScene } from '../js/scenes/SurfaceScene.js'; const data = await ConfigLoader.load(); config.init(data); const errors = []; const origErr = console.error.bind(console); console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); }; window.addEventListener('error', (e) => errors.push(String(e.message))); window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`)); const game = new Phaser.Game({ ...createGameConfig(), scene: [MenuScene, GameScene, SurfaceScene] }); window.game = game; const report = document.createElement('pre'); report.id = 'report'; report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:72%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;'; document.body.appendChild(report); const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); }; setReport('SELF-CHECK RUNNING… (menu → new game → home → build console)'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function waitFor(fn, tries = 150, everyMs = 100) { for (let i = 0; i < tries; i++) { const v = fn(); if (v) return v; await sleep(everyMs); } return null; } try { // 0) Stale-JS probe — does the loaded GameScene carry the build system // at all? (A browser module cache from an older session is the // classic cause of "I cleared the JSON cache but it's still wrong.") const GS = GameScene; const jsProbe = { buildStateSeam: typeof GS.prototype._seedStartingBuilds === 'function', beginBuild: typeof GS.prototype.beginBuild === 'function', buildsConfig: Object.keys(data.builds?.builds ?? {}).join(',') || '(none — stale data/builds.json?)', }; // 1) The real menu flow: NEW GAME (fresh seed, registry reset). const menu = await waitFor(() => { const m = game.scene.getScene('MenuScene'); return m?.newGameBtn ? m : null; }); if (!menu) throw new Error('menu never booted'); menu.startNewGame(); const gs = await waitFor(() => { const g = game.scene.getScene('GameScene'); return g?.ship && g?.buildState ? g : null; }, 200); if (!gs) throw new Error('GameScene never booted'); await sleep(800); const home = gs.planet.discoveryName; const builtMap = Object.fromEntries([...gs.buildState.built.entries()].map(([k, v]) => [k, [...v]])); const homeTether = gs.tetherField.tethers.filter((t) => t.label === home || (t.x === 0 && t.y === 0)).map((t) => `${t.id}:L${t.level}`); // 2) Land on the home world the game's own way. gs.startLanding(gs.commsTargetFor(gs.planet)); const ss = await waitFor(() => { const c = game.scene.getScene('SurfaceScene'); return c?.buildWindow ? c : null; }); if (!ss) throw new Error('surface never booted'); await sleep(700); // 3) The console's truth — the same pure functions the window paints. const { rowState, missingRequirements } = await import('../js/build/BuildModel.js'); const win = ss.buildWindow; win.open(); await sleep(400); const row = (id) => { const r = win.lists.get('planet')?.rows?.find((x) => x.id === id); if (!r) return 'NO ROW'; const ctx = win.ctxFor(id); const missing = missingRequirements(r.def, ctx); return `${rowState(r.def, ctx, win._activeOnPlanet(), id).toUpperCase()}${missing.length ? ' (needs: ' + missing.join(' + ') + ')' : ''}`; }; const before = `L1: ${row('tether-l1')} L2: ${row('tether-l2')}`; // 4) The research is "complete" → L2's only remaining gate on home is // the planet's own tether level (it holds L1, so it must pass). gs.researchState.unlock('exploration', 'tether_l2'); win.refresh(); await sleep(200); const after = `L1: ${row('tether-l1')} L2: ${row('tether-l2')}`; win.close(); const l1ok = builtMap[home]?.includes('tether-l1'); const l2ok = after.includes('L2: AVAILABLE'); const lines = [ 'SELF-CHECK DONE — the home world starts with its Tether - Level 1 BUILT.', `JS probe: seed=${jsProbe.buildStateSeam} beginBuild=${jsProbe.beginBuild} builds=[${jsProbe.buildsConfig}]`, `home world: ${home}`, `built records: ${JSON.stringify(builtMap)}`, `home tether: ${homeTether.join(', ') || 'NONE'}`, `console rows (fresh): ${before}`, `console rows (research done): ${after}`, l1ok && l2ok ? 'RESULT: OK — if YOUR game differs, the browser is running stale JS' + ' (hard reload, Ctrl+Shift+R) or you are on a DIFFERENT world (home is the one with the' + ' tether barrier ring, tagged "Home World" in comms).' : `RESULT: PROBLEM — L1 built=${l1ok}, L2 available=${l2ok}`, errors.length ? `ERRORS:\n${errors.slice(0, 3).join('\n')}` : 'no console errors', ]; setReport(lines); window.__BUILD_CHECK = { ready: true, lines, errors }; console.info('build-check: report painted'); } catch (err) { // Even a failed run should hand back the useful facts: which JS is // actually loaded (stale-cache probe), what state exists, what broke. const gs = game.scene.getScene('GameScene'); const seedFn = typeof gs?._seedStartingBuilds === 'function'; setReport([ `FATAL: ${err.message}`, `JS probe: seed=${seedFn} beginBuild=${typeof gs?.beginBuild === 'function'}` + (seedFn ? '' : ' ← STALE JS — the browser served a cached older module. Use `node dev/server.mjs 8080`' + ' (sends Cache-Control: no-store) and/or DevTools → Network → Disable cache, then reload.'), `home=${gs?.planet?.discoveryName ?? 'n/a'} builtMap=${gs?.buildState ? JSON.stringify(Object.fromEntries([...gs.buildState.built.entries()].map(([k, v]) => [k, [...v]]))) : 'n/a'}`, errors.length ? `ERRORS:\n${errors.slice(0, 3).join('\n')}` : 'no console errors', ]); window.__BUILD_CHECK = { ready: true, fatal: String(err.message), errors }; console.error('build-check: fatal', err); }