226 lines
9.0 KiB
JavaScript
226 lines
9.0 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, 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`);
|
|
}
|