/** * js/dev/BuildDiag.js — in-game diagnostics for the Build console. * * Installed by js/main.js on every real page load (index.html). It is * deliberately read-only and defensive: it observes the live game and * prints a report — it never mutates state. * * Console commands (DevTools → Console, in the RUNNING game): * * orbitDiag() → full report (also logged). Copy-paste the * returned string when asking for help. * orbitDiagBrief() → one-line summary. * * The Build console ALSO logs the brief line itself every time it opens * (`[orbit-diag v…] planet=… L1=… L2=…`), so you do not have to remember * the command. * * If `orbitDiag` is NOT DEFINED in your console, the browser served an * OLDER js/main.js (stale module cache) — hard-reload with the cache * bypassed (Ctrl+Shift+R, or DevTools → Network → Disable cache) and * check again. The version marker below changes with each revision. */ import { config } from '../config/Config.js'; import { defById, rowState, missingRequirements, isShipScoped } from '../build/BuildModel.js'; export const DIAG_V = 4; const _errors = []; const _errCap = (msg) => { if (_errors.length < 8) _errors.push(String(msg).slice(0, 300)); }; function sceneOf(key) { const g = globalThis.window?.game; try { return g?.scene?.getScene(key) ?? null; } catch { return null; } } function builtMapOf(gs) { const b = gs?.buildState?.built; if (!b || typeof b.entries !== 'function') return null; return Object.fromEntries([...b.entries()].map(([k, v]) => [k, [...v]])); } function homeNameOf(gs) { return gs?.planet?.discoveryName ?? gs?.homeWorldName ?? null; } function tetherFor(gs, name) { const list = gs?.tetherField?.tethers; if (!Array.isArray(list) || !name) return null; const hit = list.find((t) => t.label === name) ?? list.find((t) => t.x === 0 && t.y === 0); return hit ? { id: hit.id, level: hit.level, label: hit.label } : null; } /** * The exact row state the Build window paints for one build id, using * the same pure functions and context as js/ui/BuildWindow.js (SHIP- * scoped builds — the mining arms/storage — read isBuiltAnywhere, the * window's ctx does too). */ function rowFor(gs, ss, id) { const planet = ss?.planetName ?? homeNameOf(gs); const def = defById(id); if (!def || !planet) return 'NO DEF/PLANET'; const shipScoped = isShipScoped(def); const ctx = { isResearchUnlocked: (c, n) => gs?.researchState?.isUnlocked?.(c, n) ?? false, tetherLevel: () => gs?.tetherLevelFor?.(planet) ?? 0, isBuilt: () => shipScoped ? gs?.buildState?.isBuiltAnywhere?.(id) ?? false : gs?.buildState?.isBuilt?.(planet, id) ?? false, }; const active = gs?.buildState?.getActive?.(); const activeOnPlanet = active && active.planet === planet ? active : null; const st = rowState(def, ctx, activeOnPlanet, id); const missing = missingRequirements(def, ctx); return st.toUpperCase() + (missing.length ? ` (needs: ${missing.join(' + ')})` : ''); } function saveBank() { try { const raw = globalThis.localStorage?.getItem?.('orbit.saves.v1'); if (!raw) return null; const bank = JSON.parse(raw); const slots = Object.entries(bank.slots ?? {}).map(([slot, rec]) => ({ slot, seed: rec?.record?.seed ?? rec?.seed ?? null, savedAt: rec?.savedAt ?? rec?.record?.savedAt ?? null, builds: rec?.record?.builds ?? null, })); return { key: 'orbit.saves.v1', slots }; } catch (e) { return { error: String(e.message ?? e) }; } } function pendingRestore() { try { const raw = globalThis.localStorage?.getItem?.('orbit.pendingRestore'); if (!raw) return null; const rec = JSON.parse(raw); return { present: true, seed: rec?.seed ?? null, builds: rec?.builds ?? null }; } catch (e) { return { error: String(e.message ?? e) }; } } function dataFacts() { const out = { buildsLoaded: !!config.get('builds.categories', null) }; const l1 = defById('tether-l1'); const l2 = defById('tether-l2'); out['tether-l1'] = l1 ? { starting: l1.starting ?? null, repeatable: l1.repeatable ?? false } : 'NOT IN DATA (stale data/builds.json?)'; out['tether-l2'] = l2 ? { cost: l2.cost ?? null, duration: l2.duration ?? null, requires: l2.requires ?? null, planetRequires: l2.planetRequires ?? null, } : 'NOT IN DATA (stale data/builds.json?)'; for (const id of ['mining-arm-improved', 'mining-arm-advanced', 'mining-storage-improved']) { const d = defById(id); out[id] = d ? { cost: d.cost ?? null, duration: d.duration ?? null, requires: d.requires ?? null, targets: d.targets ?? null, effects: d.effects ?? null } : 'NOT IN DATA (stale data/builds.json?)'; } return out; } /** The ship's derived mining stats (the build effects land here). */ function miningStatsOf(gs) { const st = gs?.ship?.stats; return st ? { rate: st.miningSpeed ?? null, hold: gs?.ship?.minerals ?? null, cap: st.mineralStorage ?? null } : null; } /** One-line summary — the same facts a support request needs. */ export function brief() { const gs = sceneOf('GameScene'); const ss = sceneOf('SurfaceScene'); const home = homeNameOf(gs); const planet = ss?.planetName ?? home; const bm = builtMapOf(gs); const isHome = planet != null && planet === home; const t = tetherFor(gs, planet); const line = `[orbit-diag v${DIAG_V}] ` + `planet=${planet ?? '?'} home=${home ?? '?'} isHome=${isHome ? 'YES' : 'no'} ` + `builtMap=${JSON.stringify(bm ?? null)} ` + `tether=${t ? `${t.id}:L${t.level}` : 'NONE'} ` + `L1=${rowFor(gs, ss, 'tether-l1')} L2=${rowFor(gs, ss, 'tether-l2')} ` + `L3=${rowFor(gs, ss, 'tether-l3')} ` + `mining=${JSON.stringify(miningStatsOf(gs) ?? null)} ` + `jsSeed=${typeof gs?._seedStartingBuilds === 'function'} ` + `dataL1starting=${JSON.stringify(defById('tether-l1')?.starting ?? null)} ` + `errors=${_errors.length}`; console.info(line); return line; } /** Full report. Logs it, returns the string (copy-paste friendly). */ export function full() { const gs = sceneOf('GameScene'); const ss = sceneOf('SurfaceScene'); const home = homeNameOf(gs); const planet = ss?.planetName ?? home; const scenes = {}; for (const k of ['MenuScene', 'GameScene', 'SurfaceScene']) { const s = sceneOf(k); scenes[k] = s ? (s.status ?? 'present') : 'absent'; } const lines = [ `ORBIT BUILD DIAG v${DIAG_V}`, `[env] url=${globalThis.location?.href ?? '?'} ua=${String(globalThis.navigator?.userAgent ?? '?').slice(0, 90)}`, `[js] GameScene=${gs ? 'present' : 'ABSENT'} seedFn=${typeof gs?._seedStartingBuilds === 'function'} ` + `beginBuild=${typeof gs?.beginBuild === 'function'} buildState=${!!gs?.buildState}`, `[data] ${JSON.stringify(dataFacts())}`, `[run] home=${home ?? 'n/a'} currentPlanet=${planet ?? 'n/a (not on surface)'} ` + `ssGameSceneIsGs=${ss ? String(!!gs && ss.gameScene === gs) : 'n/a'} ` + `pendingRestore=${JSON.stringify(pendingRestore())}`, `[state] builtMap=${JSON.stringify(builtMapOf(gs) ?? null)}`, ` homeTether=${JSON.stringify(tetherFor(gs, home) ?? null)} ` + `planetTether=${JSON.stringify(tetherFor(gs, planet) ?? null)}`, ` miningStats=${JSON.stringify(miningStatsOf(gs) ?? null)}`, ` researchTetherL2=${gs?.researchState?.isUnlocked?.('exploration', 'tether_l2') ?? 'n/a'} ` + `researchTetherL3=${gs?.researchState?.isUnlocked?.('exploration', 'tether_l3') ?? 'n/a'} ` + `researchMining=${JSON.stringify({ improved_arm: gs?.researchState?.isUnlocked?.('mining', 'improved_arm') ?? 'n/a', advanced_arm: gs?.researchState?.isUnlocked?.('mining', 'advanced_arm') ?? 'n/a', improved_storage: gs?.researchState?.isUnlocked?.('mining', 'improved_storage') ?? 'n/a', })} ` + `activeBuild=${JSON.stringify(gs?.buildState?.getActive?.() ?? null)}`, `[rows] L1=${rowFor(gs, ss, 'tether-l1')}`, ` L2=${rowFor(gs, ss, 'tether-l2')}`, ` L3=${rowFor(gs, ss, 'tether-l3')}`, ` ARM-I=${rowFor(gs, ss, 'mining-arm-improved')}`, ` ARM-A=${rowFor(gs, ss, 'mining-arm-advanced')}`, ` STORE=${rowFor(gs, ss, 'mining-storage-improved')}`, `[saves] ${JSON.stringify(saveBank())}`, `[scenes] ${JSON.stringify(scenes)}`, `[errors since boot] ${_errors.length ? _errors.join(' | ') : 'none'}`, ]; const text = lines.join('\n'); console.info(text); return text; } /** Install the console commands. Idempotent. */ export function installBuildDiag() { if (typeof globalThis === 'undefined') return; if (!globalThis.addEventListener) return; globalThis.addEventListener('error', (e) => _errCap(e.message)); globalThis.addEventListener('unhandledrejection', (e) => _errCap(`rejection: ${e.reason}`)); globalThis.orbitDiag = () => full(); globalThis.orbitDiagBrief = () => brief(); console.info(`[orbit-diag v${DIAG_V}] installed — type orbitDiag() in the console for a full report`); }