feat(ui): Phase 29 map visibility toggles
Adds the gear-icon popover on the map view with per-game persistence of every category toggle plus the wrap-mode radio. Hide-by-id and visibility-fog facilities land on the renderer so every flip applies within one frame without a Pixi remount; the wrap-mode toggle keeps its existing remount + camera-preserve path. A new server-side turn force-resets every flag to defaults so a hidden category never makes the player miss the next turn's news. Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go (plus the single Go caller); the TS side keeps duplicating the formula until a race-level WASM bridge lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,44 @@ explores history, so the pending-turn toast continues to work.
|
||||
map view's effect picks the change up and re-mounts the renderer
|
||||
with the new mode.
|
||||
|
||||
## Map visibility toggles
|
||||
|
||||
Phase 29 adds a `mapToggles: MapToggles` rune that drives the
|
||||
gear popover in the map view. Every flag defaults to `true` —
|
||||
including `unreachablePlanets` (showing every planet by default)
|
||||
and `visibilityFog` (the fog overlay on by default). The
|
||||
exhaustive shape lives in `src/lib/game-state.svelte.ts`; the
|
||||
gear popover (`src/lib/active-view/map-toggles.svelte`) is a
|
||||
thin view of the rune.
|
||||
|
||||
`setMapToggle(key, value)` flips one entry in place and
|
||||
persists the whole blob to `Cache` under the
|
||||
`game-map-toggles/{gameId}` key. The blob carries a companion
|
||||
`lastResetTurn` number — the turn at which the toggles were last
|
||||
reset to defaults — so the new-turn reset path (below) can detect
|
||||
a stale blob even across a cross-session gap.
|
||||
|
||||
### New-turn reset
|
||||
|
||||
A new server-side turn force-resets every toggle to defaults so a
|
||||
hidden category never makes the player miss what changed:
|
||||
|
||||
- `setGame` reads the persisted `{toggles, lastResetTurn}` blob.
|
||||
If `lastResetTurn < currentTurn`, the rune is overwritten with
|
||||
`DEFAULT_MAP_TOGGLES` and the blob is rewritten with
|
||||
`lastResetTurn = currentTurn` before the report load. Otherwise
|
||||
the persisted overrides are restored.
|
||||
- `advanceToPending` (the user's explicit jump onto the new turn)
|
||||
calls the same reset path after `loadTurn(currentTurn, …)`
|
||||
succeeds, updating `lastResetTurn` to the freshly-loaded
|
||||
current turn.
|
||||
- `viewTurn` (history mode) does NOT reset — toggles are a
|
||||
single shared state per game, not per turn.
|
||||
- `refresh()` does not advance turns, so it does not reset.
|
||||
|
||||
The cache namespace and blob shape are documented in
|
||||
`storage.md`.
|
||||
|
||||
## History mode
|
||||
|
||||
Phase 26 lets the user step backward through the report timeline
|
||||
|
||||
+60
-6
@@ -269,25 +269,79 @@ resolver that translates `sourcePlanetNumber` to the underlying
|
||||
current report). Inspector subsections call `service.pick(...)`
|
||||
and react to the resolved id.
|
||||
|
||||
## Hidden primitives
|
||||
|
||||
`RendererHandle.setHiddenPrimitiveIds(ids)` replaces the current
|
||||
hide-by-id set. Every primitive whose id sits in `ids` has its
|
||||
per-copy `Graphics.visible` flipped to `false` and is skipped by
|
||||
`hitAt`, so a click on its former area falls through to the next
|
||||
visible primitive. An empty set restores everything. Repeated
|
||||
calls are diff-free idempotent — `g.visible` assignments are
|
||||
cheap.
|
||||
|
||||
The hide set is propagated to `hitTest` through a new optional
|
||||
`hiddenIds` parameter so internal hit-test sites (pointer-move,
|
||||
clicked dispatcher) stay in lock-step with the visible scene.
|
||||
After `setExtraPrimitives` the hide set is re-applied so a
|
||||
freshly-pushed extras layer (cargo-route overlay, pending-Send
|
||||
tracks) does not silently un-hide a primitive whose id is in the
|
||||
current set.
|
||||
|
||||
The Phase 29 map view (`src/lib/active-view/map.svelte`) computes
|
||||
the set from the per-game `MapToggles` rune + the planet-cascade
|
||||
rule and pushes it on every effect run; toggling a checkbox
|
||||
flips visibility within one frame without a Pixi remount.
|
||||
|
||||
## Visibility fog
|
||||
|
||||
`RendererHandle.setVisibilityFog(circles)` draws (or removes) the
|
||||
Phase 29 fog overlay. Each entry describes a circle around a
|
||||
LOCAL planet where the player has scanner / visibility coverage:
|
||||
|
||||
- An empty list destroys the existing fog Graphics.
|
||||
- A non-empty list creates one fog `Graphics` per torus copy.
|
||||
Each fills the world rectangle with `FOG_COLOR` (two shades
|
||||
lighter than the dark theme background) and "cuts" every
|
||||
circle out of it via Pixi v8's `Graphics.cut()` path operator,
|
||||
so overlapping circles compose into a union hole (no
|
||||
even-odd-fill quirks). The fog is inserted at the bottom of
|
||||
each copy's z-order so primitives paint on top.
|
||||
- The fog never participates in hit-test. Planet glyphs sit on
|
||||
top of fog, so clicks on visible planets work unchanged.
|
||||
- Wrap mode is honoured for free — `applyMode` hides every
|
||||
non-origin copy in `no-wrap`, so the fog inherits the same
|
||||
behaviour because the fog Graphics is a child of each copy.
|
||||
|
||||
The map view recomputes the fog input only when the report or the
|
||||
fog toggle changes — per-frame cost stays at zero.
|
||||
|
||||
## Debug surface
|
||||
|
||||
The DEV-only `__galaxyDebug` object (defined in
|
||||
`routes/__debug/store/+page.svelte`) exposes
|
||||
`getMapPrimitives()` and `getMapPickState()` so e2e specs can
|
||||
assert the renderer's current state without scraping pixels:
|
||||
`getMapPrimitives()`, `getMapPickState()`, `getMapCamera()`, and
|
||||
`getMapFog()` so e2e specs can assert the renderer's current
|
||||
state without scraping pixels:
|
||||
|
||||
- `getMapPrimitives()` returns a snapshot of every primitive in
|
||||
the active world: id, kind, priority, current alpha
|
||||
(post-overlay), and the explicit fill / stroke colour from its
|
||||
`Style` (no theme fallback). Tests use this to count cargo
|
||||
arrows or to verify dim state during pick mode.
|
||||
(post-overlay), the explicit fill / stroke colour from its
|
||||
`Style` (no theme fallback), and the Phase 29 `visible` flag
|
||||
mirroring the renderer's hide set.
|
||||
- `getMapPickState()` returns `{ active, sourcePlanetNumber,
|
||||
reachableIds, hoveredId }` — the renderer's view of the
|
||||
current pick session.
|
||||
- `getMapCamera()` returns the current camera + viewport +
|
||||
canvas-origin snapshot, used by Phase 29 e2e specs to assert
|
||||
camera preservation across wrap-mode flips.
|
||||
- `getMapFog()` returns the most recent visibility-fog input
|
||||
(the list of circles last passed to `setVisibilityFog`).
|
||||
Empty when the fog toggle is off.
|
||||
|
||||
The active map view registers providers on mount via
|
||||
`registerMapPrimitivesProvider` / `registerMapPickStateProvider`
|
||||
in `src/lib/debug-surface.svelte.ts`, deregisters on dispose, and
|
||||
/ `registerMapCameraProvider` / `registerMapFogProvider` in
|
||||
`src/lib/debug-surface.svelte.ts`, deregisters on dispose, and
|
||||
the surface invokes them lazily on every read.
|
||||
|
||||
## Tests
|
||||
|
||||
+18
-7
@@ -112,13 +112,24 @@ wipes every namespace.
|
||||
|
||||
Namespaces in current use:
|
||||
|
||||
| Namespace | Key | Value type | Owner |
|
||||
|-----------------|--------------------------------|------------------|------------------------------------|
|
||||
| `session` | `device-session-id` | `string` | Phase 7+ |
|
||||
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | Phase 11+ (`game-state.md`) |
|
||||
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | Phase 11+ (`game-state.md`) |
|
||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | Phase 12+ (`order-composer.md`) |
|
||||
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | Phase 26+ (`game-state.md`) |
|
||||
| Namespace | Key | Value type | Owner |
|
||||
|--------------------|--------------------------------|-----------------------------------------------|------------------------------------|
|
||||
| `session` | `device-session-id` | `string` | Phase 7+ |
|
||||
| `game-prefs` | `{gameId}/wrap-mode` | `WrapMode` | Phase 11+ (`game-state.md`) |
|
||||
| `game-prefs` | `{gameId}/last-viewed-turn` | `number` | Phase 11+ (`game-state.md`) |
|
||||
| `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | Phase 12+ (`order-composer.md`) |
|
||||
| `game-history` | `{gameId}/turn/{N}` | `GameReport` | Phase 26+ (`game-state.md`) |
|
||||
| `game-map-toggles` | `{gameId}` | `{toggles: MapToggles; lastResetTurn: number}` | Phase 29+ (`game-state.md`) |
|
||||
|
||||
The `game-map-toggles` blob stores the gear popover's per-game
|
||||
visibility state plus a `lastResetTurn` companion field. Reading
|
||||
a missing or malformed entry falls back to `DEFAULT_MAP_TOGGLES`
|
||||
field-by-field, so a stale older client losing a field added
|
||||
later does not nuke the rest of the user's overrides. The
|
||||
`GameStateStore.setGame` path resets the blob to defaults
|
||||
whenever `lastResetTurn < currentTurn`, so a fresh server turn
|
||||
always greets the player with every map category visible (see
|
||||
`game-state.md` for the new-turn-reset contract).
|
||||
|
||||
Later phases will add more per-feature namespaces (fixtures, lobby
|
||||
snapshot, etc.). The contract is namespace-strings stay scoped to
|
||||
|
||||
Reference in New Issue
Block a user