orbit/js/dev/BuildDiag.js

193 lines
7.3 KiB
JavaScript

/**
* 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 } from '../build/BuildModel.js';
export const DIAG_V = 3;
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.
*/
function rowFor(gs, ss, id) {
const planet = ss?.planetName ?? homeNameOf(gs);
const def = defById(id);
if (!def || !planet) return 'NO DEF/PLANET';
const ctx = {
isResearchUnlocked: (c, n) => gs?.researchState?.isUnlocked?.(c, n) ?? false,
tetherLevel: () => gs?.tetherLevelFor?.(planet) ?? 0,
isBuilt: () => 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?)';
return out;
}
/** 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')} ` +
`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)}`,
` researchTetherL2=${gs?.researchState?.isUnlocked?.('exploration', 'tether_l2') ?? 'n/a'} ` +
`activeBuild=${JSON.stringify(gs?.buildState?.getActive?.() ?? null)}`,
`[rows] L1=${rowFor(gs, ss, 'tether-l1')}`,
` L2=${rowFor(gs, ss, 'tether-l2')}`,
`[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`);
}