# Per-game state store This document describes the per-game state owned by the in-game shell layout. Phase 11 introduces the store and uses it for two consumers (the header turn counter and the map view); later phases plug inspector tabs, the order composer, and the calculator on top of the same instance. ## Lifecycle `routes/games/[id]/+layout.svelte` instantiates one `GameStateStore` per game (the layout remounts when the user navigates to a different game id, so each game gets a fresh store). The layout exposes the instance through Svelte context under `GAME_STATE_CONTEXT_KEY`; descendants read it via `getContext(GAME_STATE_CONTEXT_KEY)`. The layout's `onMount` builds the `GalaxyClient`, loads `Cache` through `loadStore()`, then calls `gameState.init({ client, cache, gameId })`. `init`: 1. installs a `visibilitychange` listener on `document` so the report is refreshed when the tab regains focus; 2. calls `setGame(gameId)`, which: - reads the per-game wrap-mode preference from `Cache` (`game-prefs / /wrap-mode`, default `torus`); - calls `lobby.my.games.list` and finds the game record (the Phase 11 wire schema extension on `GameSummary` adds `current_turn`); if the user is not a member, the store flips to `error`; - calls `user.games.report` for the discovered turn and decodes the FlatBuffers response into a TS-friendly `GameReport` shape. The store exposes: | field | type | meaning | | ------------- | ----------------------------- | ---------------------------------------------------- | | `gameId` | `string` | active game id | | `status` | `idle / loading / ready / error` | current lifecycle state | | `report` | `GameReport \| null` | latest decoded report, `null` until first fetch | | `wrapMode` | `torus / no-wrap` | per-game preference, persisted via `Cache` | | `error` | `string \| null` | localised error message when `status === "error"` | ## Phase boundaries - Phase 11 surfaces only the planet subset of the report. Later phases extend `GameReport` and `decodeReport` as their slice of the wire lands (ships, fleets, sciences, routes, battles, mail). - Phase 26 wires history mode through `setTurn(turn)`. The store already supports it; the navigator UI is what is missing. - Phase 24 replaces the tab-focus refresh with push-event-driven refreshes; the visibility listener stays as a fallback for background tabs that miss a push. - Phase 29 wires the wrap-mode toggle UI on top of `setWrapMode`. ## Why `current_turn` lives on `GameSummary` The user-facing surface needs the current turn number to know which report to fetch. Two alternatives were rejected: - a brand-new `user.games.state` message — adds a full wire-flow (fbs schema, transcoder, gateway routing, backend handler) for a one-field response; - hard-coding `turn=0` for all games — works for the dev sandbox (which never advances past turn zero) but renders the initial state for any real game past turn zero. Extending `GameSummary` reuses the existing lobby pipeline; the backend already tracks `current_turn` in its runtime projection (`backend/internal/server/handlers_user_lobby_helpers.go` `gameSummaryToWire` reads it from `g.RuntimeSnapshot.CurrentTurn`). The wire change touches Phase 8's already-shipped catalogue, but the `current_turn` field defaults to zero on the FB side, so existing tests and the dev sandbox flow continue to work unchanged. ## State binding `map/state-binding.ts::reportToWorld(report)` translates a `GameReport` into a renderer-ready `World`. Phase 11 emits one Point primitive per planet across all four kinds (local / other / uninhabited / unidentified). Each kind gets a distinct fill colour, fill alpha, and point radius so the four classes are visually-distinguishable at a glance; later phases will refine the colour palette as the visual language stabilises (Phase 35 polish). The planet engine number is reused as the primitive id so a hit-test result can resolve back to a planet without an extra lookup table. ## Refresh discipline `refresh()` re-fetches the same turn snapshot. It is called by the `visibilitychange` handler when `document.visibilityState === "visible"` and the store is already in `ready` state. The map view's mount effect skips a re-render when the new snapshot's turn matches the previously-mounted turn (and the wrap mode is unchanged), so a no-op refresh does not flicker the canvas. `setTurn(turn)` is the entry point for Phase 26 history mode: calling it on a different turn loads that snapshot and the same mount effect re-creates the renderer with the new world. `setWrapMode(mode)` writes to `Cache` and updates the rune; the map view's effect picks the change up and re-mounts the renderer with the new mode.