ui/phase-26: history mode (turn navigator + read-only banner)
Split GameStateStore into currentTurn (server's latest) and viewedTurn (displayed snapshot) so history excursions don't corrupt the resume bookmark or the live-turn bound. Add viewTurn / returnToCurrent / historyMode rune, plus a game-history cache namespace that stores past-turn reports for fast re-entry. OrderDraftStore.bindClient takes a getHistoryMode getter and short-circuits add / remove / move while the user is viewing a past turn; RenderedReportSource skips the order overlay in the same case. Header replaces the static "turn N" with a clickable triplet (TurnNavigator), the layout mounts HistoryBanner under the header, and visibility-refresh is a no-op while history is active. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+64
-11
@@ -37,6 +37,10 @@ The store exposes:
|
||||
| `gameId` | `string` | active game id |
|
||||
| `status` | `idle / loading / ready / error` | current lifecycle state |
|
||||
| `report` | `GameReport \| null` | latest decoded report, `null` until first fetch |
|
||||
| `currentTurn` | `number` | server's authoritative current turn (live snapshot) |
|
||||
| `viewedTurn` | `number` | turn whose snapshot is in `report`; equals `currentTurn` in live mode |
|
||||
| `historyMode` | `boolean` (derived) | true while `status === "ready"` and `viewedTurn < currentTurn` |
|
||||
| `pendingTurn` | `number \| null` | latest server turn the user has not yet opened |
|
||||
| `wrapMode` | `torus / no-wrap` | per-game preference, persisted via `Cache` |
|
||||
| `error` | `string \| null` | localised error message when `status === "error"` |
|
||||
|
||||
@@ -45,8 +49,15 @@ The store exposes:
|
||||
- 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 26 splits `currentTurn` from the turn whose snapshot is
|
||||
displayed (`viewedTurn`) and adds `viewTurn(turn)` /
|
||||
`returnToCurrent()` for history navigation. The derived
|
||||
`historyMode` rune flips automatically when `viewedTurn <
|
||||
currentTurn`; the layout passes it to Phase 12's sidebar /
|
||||
bottom-tabs wiring (which hides the order tab) and to
|
||||
`OrderDraftStore.bindClient` (which gates `add` / `remove` /
|
||||
`move`). See "History mode" below for the cache and refresh
|
||||
rules.
|
||||
- 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.
|
||||
@@ -88,17 +99,59 @@ 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.
|
||||
`refresh()` re-fetches the current-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.
|
||||
In history mode `refresh()` is a no-op — forcing a reload would
|
||||
silently bump the user back onto the current turn while they are
|
||||
intentionally viewing a past one. Push events (Phase 24) still
|
||||
deliver new-turn notifications asynchronously while the user
|
||||
explores history, so the pending-turn toast continues to work.
|
||||
|
||||
`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.
|
||||
|
||||
## History mode
|
||||
|
||||
Phase 26 lets the user step backward through the report timeline
|
||||
without losing the live snapshot. The store keeps two turn runes:
|
||||
|
||||
- `currentTurn` — the server's authoritative latest. Only
|
||||
`setGame` and `advanceToPending` write to it.
|
||||
- `viewedTurn` — the turn currently rendered. `viewTurn(N)` flips
|
||||
this rune and the underlying `report` to `N` without touching
|
||||
`currentTurn`. `returnToCurrent()` is a one-line wrapper that
|
||||
navigates back to live.
|
||||
|
||||
The derived `historyMode` rune (`status === "ready" && viewedTurn
|
||||
< currentTurn`) drives every history-aware consumer:
|
||||
|
||||
- the layout passes it to `Sidebar` / `BottomTabs` so the order
|
||||
tab vanishes (Phase 12 prop wiring);
|
||||
- the layout passes a `getHistoryMode` getter to
|
||||
`OrderDraftStore.bindClient` so `add` / `remove` / `move` are
|
||||
no-ops while the user is looking at a past turn;
|
||||
- `RenderedReportSource` returns the raw report (no order overlay)
|
||||
because the draft is composed against the current turn;
|
||||
- the new `HistoryBanner` component renders the sticky "Viewing
|
||||
turn N · read-only" strip when the flag is true.
|
||||
|
||||
`last-viewed-turn` semantics keep their Phase 11 meaning: "the
|
||||
latest turn the user was caught up on". `loadTurn` only writes the
|
||||
cache row when called with `isCurrent === true` (i.e. when the
|
||||
load matches `currentTurn`). Historical excursions are therefore
|
||||
ephemeral: closing the tab and reopening the game resumes on the
|
||||
last caught-up turn, not on the last clicked one.
|
||||
|
||||
Past-turn reports are cached in the `game-history` namespace
|
||||
(`{gameId}/turn/{N}` → `GameReport`). The cache is written by
|
||||
`loadTurn` on every successful historical fetch and read first by
|
||||
`viewTurn(N)` before falling back to the network. Past turns are
|
||||
immutable, so the cache has no TTL and no eviction in Phase 26.
|
||||
The current-turn snapshot is deliberately *not* cached — it is
|
||||
mutable until the next engine tick.
|
||||
|
||||
Reference in New Issue
Block a user