ui/synthetic-report: dev-only legacy report loader on lobby

Adds api/synthetic-report.ts, an in-memory registry + JSON->GameReport
decoder for synthetic-mode game sessions. The lobby grows a
import.meta.env.DEV-gated "Synthetic test reports" section with a
JSON file picker; loading a file registers the decoded report under
a synthetic-<uuid> id and navigates to /games/<id>/map.

The in-game shell layout detects the synthetic id range, takes the
report straight from the registry via gameState.initSynthetic, and
deliberately skips both galaxyClient.set and orderDraft.bindClient.
Order auto-sync stays silent: scheduleSync already short-circuits on
non-UUID game ids, and without a bound client the network path is
unreachable. applyOrderOverlay continues to project locally-valid
draft commands onto the rendered report so renames / production
choices / route edits are visible immediately.

A page reload loses the in-memory entry and redirects to /lobby —
synthetic mode is a debug affordance, not a session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-10 11:08:05 +02:00
parent 99962b295f
commit 8f320010c6
5 changed files with 647 additions and 0 deletions
+35
View File
@@ -56,6 +56,16 @@ export class GameStateStore {
* later phases (history mode, calc) will read it directly.
*/
currentTurn = $state(0);
/**
* synthetic is set by `initSynthetic` for DEV-only sessions backed
* by a hand-loaded report (lobby's "Load synthetic report"
* affordance). The flag travels through the layout so the order
* tab and any future server-bound features can short-circuit and
* stay local. The auto-sync pipeline already protects itself via
* the UUID guard on `OrderDraftStore.scheduleSync`, so flipping
* this flag is enough to keep the network silent.
*/
synthetic = $state(false);
private client: GalaxyClient | null = null;
private cache: Cache | null = null;
@@ -161,6 +171,31 @@ export class GameStateStore {
this.error = message;
}
/**
* initSynthetic seeds the store from a pre-loaded `GameReport`
* without touching the network. Used by the lobby's DEV-only
* "Load synthetic report" affordance: the layout invokes this
* instead of `init` when the route id is in the synthetic id
* range. The store ends up in `ready` immediately; no polling,
* no visibility-driven refresh, no client / cache-of-server
* binding.
*/
async initSynthetic(opts: {
cache: Cache;
gameId: string;
report: GameReport;
}): Promise<void> {
this.cache = opts.cache;
this.gameId = opts.gameId;
this.synthetic = true;
this.gameName = "Synthetic";
this.error = null;
this.wrapMode = await readWrapMode(opts.cache, opts.gameId);
this.report = opts.report;
this.currentTurn = opts.report.turn;
this.status = "ready";
}
dispose(): void {
this.destroyed = true;
if (this.visibilityListener !== null && typeof document !== "undefined") {