c58027c034
Replaces the Phase 10 report stub with a scrollable orchestrator that renders every FBS array as a dedicated section (galaxy summary, votes, player status, my/foreign sciences, my/foreign ship classes, battles, bombings, approaching groups, my/foreign/uninhabited/unknown planets, ships in production, cargo routes, my fleets, my/foreign/unidentified ship groups). A sticky table of contents (a <select> on mobile), "back to map" affordance, IntersectionObserver-driven active-section highlight, and SvelteKit Snapshot-based scroll save/restore round out the view. GameReport gains six new fields (players, otherScience, otherShipClass, battleIds, bombings, shipProductions); decodeReport, the synthetic- report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend in lockstep. ~90 new i18n keys land in en + ru together. The legacy-report parser is extended to populate the new sections from the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship Types, Bombings, Ships In Production). Ships-in-production prod_used is derived through a new pkg/calc.ShipBuildCost helper; the engine's controller.ProduceShip refactors to call the same helper without any behaviour change (engine tests stay unchanged and green). Battles remain in the parser's Skipped list — the legacy text carries no stable per-battle UUID. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
210 lines
11 KiB
Markdown
210 lines
11 KiB
Markdown
# In-game shell — navigation model
|
||
|
||
This doc covers the chrome that wraps every in-game view: the
|
||
responsive layout shell, the active-view router built on SvelteKit's
|
||
file-system routes, the sidebar with three tools and its
|
||
state-preservation rule, and the mobile bottom-tabs. The user-facing
|
||
spec — view list, breakpoint diagrams, history-mode plans — lives in
|
||
[`../PLAN.md`](../PLAN.md), section
|
||
`Information Architecture and Navigation`. This doc is the source of
|
||
truth for how those rules are implemented.
|
||
|
||
## Active-view model
|
||
|
||
The client renders **one active view at a time**. Every active view is
|
||
a SvelteKit route under `routes/games/[id]/`; the route file is a
|
||
two-line wrapper that mounts the matching content component from
|
||
`src/lib/active-view/<name>.svelte`. The "view router" mentioned in
|
||
the plan is the file system plus those wrappers — there is no
|
||
separate dispatch component.
|
||
|
||
| URL | Active view component | Phase that fills it |
|
||
| ------------------------------------- | ---------------------------------------------- | ----------------------- |
|
||
| URL | Active view component | Phase that fills it |
|
||
| ------------------------------------------ | ---------------------------------------------- | ----------------------- |
|
||
| `/games/:id/map` | `lib/active-view/map.svelte` | Phase 11 |
|
||
| `/games/:id/table/:entity` | `lib/active-view/table.svelte` | Phase 11 / 17 / 19 / 22 |
|
||
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) | Phase 23 |
|
||
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` | Phase 27 |
|
||
| `/games/:id/mail` | `lib/active-view/mail.svelte` | Phase 28 |
|
||
| `/games/:id/designer/ship-class/:classId?` | `lib/active-view/designer-ship-class.svelte` | Phase 17 (CRUD) / 18 (calc preview) |
|
||
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` | Phase 21 |
|
||
|
||
`/games/:id` (no trailing view) redirects to `/games/:id/map`. The
|
||
optional `:classId?` / `:scienceId?` segments on the designer
|
||
routes match SvelteKit's `[[classId]]` syntax — `/designer/ship-class`
|
||
opens the empty new-class form, `/designer/ship-class/{name}`
|
||
opens the read-only view of the named class with the Delete
|
||
affordance. Phase 17 lights up the ship-class CRUD path; Phase 18
|
||
adds the live `pkg/calc/`-backed preview pane on top.
|
||
|
||
The `entity` slug on the table route is kebab-case (`planets`,
|
||
`ship-classes`, `ship-groups`, `fleets`, `sciences`, `races`).
|
||
`table.svelte` is the active-view router: it dispatches by slug to
|
||
the per-entity component (`ship-classes` → `table-ship-classes.svelte`
|
||
in Phase 17; the others fall back to the Phase 10 stub copy until
|
||
their respective phases land).
|
||
|
||
## Sidebar tools and state preservation
|
||
|
||
The desktop sidebar hosts three tools:
|
||
|
||
| Tool | Component | Phase that fills it |
|
||
| ---------- | -------------------------------------- | -------------------- |
|
||
| Calculator | `lib/sidebar/calculator-tab.svelte` | Phase 30 |
|
||
| Inspector | `lib/sidebar/inspector-tab.svelte` | Phase 13 / 19 |
|
||
| Order | `lib/sidebar/order-tab.svelte` | Phase 12 / 14 |
|
||
|
||
The selected-tab state is a `$state` rune in
|
||
`routes/games/[id]/+layout.svelte`, bound into
|
||
`lib/sidebar/sidebar.svelte` via `$bindable()`. The layout owns the
|
||
rune so external events — Phase 13's planet click, future similar
|
||
flows — can drive the active tab from outside the sidebar without
|
||
plumbing callbacks. The component is mounted by the layout, and
|
||
SvelteKit keeps that layout instance alive while the user navigates
|
||
between child routes (`/games/:id/map` → `/games/:id/report` → …),
|
||
so the rune survives every active-view switch automatically with no
|
||
URL coupling needed. The URL seed and the history-mode reset
|
||
described below still live inside the sidebar — they mutate the
|
||
bindable in place; the layout sees the change through the binding.
|
||
|
||
A `?sidebar=calc|calculator|inspector|order` URL param is read once
|
||
on mount and seeds the initial tab. Later phases that want to land
|
||
the user on a particular tool (for example, Phase 14's first
|
||
end-to-end command flow) can set it on navigation.
|
||
|
||
The Order entry is hidden when the layout's `historyMode` flag is
|
||
true. Phase 12 plumbs the flag end-to-end as a prop —
|
||
`+layout.svelte` passes a constant `false` to `Sidebar`, which
|
||
forwards `hideOrder` to its `TabBar`; the same flag goes to
|
||
`BottomTabs` so the mobile `Order` button is also suppressed. A
|
||
`?sidebar=order` URL seed that arrives while the flag is true falls
|
||
back to `inspector`, and an `$effect` on the sidebar resets
|
||
`activeTab` away from `order` if the flag flips on mid-session.
|
||
Phase 26 introduces `lib/history-mode.ts` and replaces the constant
|
||
with the live signal; the order draft survives the toggle because
|
||
`OrderDraftStore` lives one level above the sidebar in the layout
|
||
hierarchy. See [`order-composer.md`](order-composer.md) for the
|
||
draft-store side of the flow.
|
||
|
||
## Layout breakpoints
|
||
|
||
Three discrete CSS modes matched to the IA section diagrams:
|
||
|
||
- **≥ 1024 px (desktop)** — the sidebar sits beside the active view
|
||
and is always rendered. The header view-menu trigger uses the
|
||
dropdown icon (▾). Bottom-tabs and the tablet sidebar-toggle are
|
||
CSS-hidden.
|
||
- **768–1024 px (tablet)** — the sidebar collapses behind a click
|
||
toggle in the header right corner. Tapping the toggle slides the
|
||
sidebar in as a fixed overlay above the active view; a close
|
||
button on the sidebar dismisses it. The full swipe-from-right
|
||
gesture in the IA section is deferred to Phase 35 polish — the
|
||
click toggle satisfies the "layout switches at 768 px" acceptance
|
||
criterion on Phase 10.
|
||
- **< 768 px (mobile)** — the sidebar is hidden entirely and the
|
||
bottom-tabs row appears at the bottom of the viewport. The
|
||
view-menu trigger swaps to a hamburger icon (☰) that opens the
|
||
drop-down as a full-width drawer below the header.
|
||
|
||
On mobile the bottom tab row does not include `Inspector`. The
|
||
inspector content is reached by tapping a map object instead, which
|
||
raises a bottom-sheet — see [Planet selection](#planet-selection-phase-13).
|
||
|
||
## Mobile bottom-tabs and tool overlay
|
||
|
||
The bottom-tabs row is `[Map, Calc, Order, More]`. Map navigates to
|
||
`/games/:id/map` and clears any tool overlay. Calc and Order navigate
|
||
to `/games/:id/map` too — but they also flip the layout's
|
||
`mobileTool` state to `calc` / `order`, which the layout uses to
|
||
swap the active-view slot for the Calculator / Order tool component.
|
||
|
||
The tool overlay only applies when the URL is `/map`. Navigating to
|
||
any other view through the More drawer or the header view-menu makes
|
||
the layout's derived `effectiveTool` collapse back to `map`, so the
|
||
user always sees the URL's active view rather than a stale overlay.
|
||
The next time the user taps a Calc or Order bottom-tab, the
|
||
navigation re-routes them to `/map` and re-applies the overlay.
|
||
|
||
The `More` button opens a drawer that mirrors the header view-menu
|
||
content. The IA section's narrower "More" list (Mail, Battle log,
|
||
Tables, History, Settings, Logout) is the polish target for Phase 35
|
||
— Phase 10 keeps a single source of truth for destinations.
|
||
|
||
## Transient map overlays
|
||
|
||
Some views can push a transient overlay onto `/map` with a back
|
||
affordance — for example, the ship-class designer pushes a
|
||
range-preview overlay onto the map. The transient overlay clears
|
||
when the user navigates to any other view via the header or the
|
||
bottom-tabs.
|
||
|
||
Phase 10 documents this concept but does not implement the
|
||
back-stack mechanism. Phase 34 lands the back-stack alongside its
|
||
first user (multi-turn projection, range circles in the ship-class
|
||
designer).
|
||
|
||
## Planet selection (Phase 13)
|
||
|
||
The map view turns into the entry point for the inspector by
|
||
translating a renderer click into a planet selection. The flow:
|
||
|
||
1. The renderer (`src/map/render.ts`) exposes `onClick(cb)` next to
|
||
the existing `hitAt(cursor)`. It is built on `pixi-viewport`'s
|
||
`clicked` event, which already differentiates a click from a
|
||
pan-drag, so a click handler will not race the pan plugin.
|
||
2. `lib/active-view/map.svelte` wires that callback after a successful
|
||
`mountRenderer`. On a click it asks the renderer for the hit
|
||
primitive, looks the planet up by `number` in the live
|
||
`GameStateStore.report`, and calls `SelectionStore.selectPlanet(number)`.
|
||
3. `SelectionStore` (`lib/selection.svelte.ts`) is a runes store
|
||
instantiated by the layout and exposed via Svelte context under
|
||
`SELECTION_CONTEXT_KEY`. It carries a discriminated union — Phase
|
||
13 only models `{ kind: "planet"; id: number }`; Phase 19 widens
|
||
it for ship groups. Selection is in-memory only: it survives the
|
||
layout's lifetime (active-view switches inside `/games/:id/*`)
|
||
but does not persist across reloads — that contrast with the
|
||
order draft is intentional.
|
||
4. The layout watches the selection rune and, on the null → planet
|
||
transition, flips its bound `activeTab` to `inspector` and
|
||
`sidebarOpen` to `true`. Desktop already has the sidebar pinned;
|
||
tablet needs the drawer to surface; mobile is unaffected by the
|
||
tab rune because the sidebar is CSS-hidden there.
|
||
5. `lib/sidebar/inspector-tab.svelte` and
|
||
`lib/inspectors/planet-sheet.svelte` both read the selection
|
||
store, resolve it against the live report, and either render
|
||
`lib/inspectors/planet.svelte` or fall back to the empty state.
|
||
A selection that points at a planet missing from the current
|
||
report (visibility lost between turns) collapses to the empty
|
||
state instead of holding stale rows.
|
||
|
||
The mobile bottom-sheet is mounted alongside `<BottomTabs />` in the
|
||
layout. Its visibility is conditional on `effectiveTool === "map"` so
|
||
it does not stack on top of the calc / order overlays. Phase 13 ships
|
||
the minimal dismissal surface: a close button (`✕`) that calls
|
||
`SelectionStore.clear()`. Tap-outside and swipe-down dismissal from
|
||
the IA section are deferred to Phase 35 polish. A click that lands on
|
||
empty space is a no-op — selection is mutated only by an explicit
|
||
planet click or by the close button.
|
||
|
||
The planet inspector itself is a presentational component: it takes
|
||
a `ReportPlanet` snapshot as a prop and renders the documented field
|
||
set per planet kind. The wrapper in `api/game-state.ts` exposes every
|
||
field the FBS schema carries (`industryStockpile` for `capital`,
|
||
`materialsStockpile` for `material`, `industry`, `population`,
|
||
`colonists`, `production`, `freeIndustry`, plus `owner` for `other`).
|
||
Fields the FBS table does not project for a given kind read as `null`
|
||
and the inspector simply omits the row.
|
||
|
||
The selected-planet visual on the map (a ring or halo) is **not**
|
||
shipped in Phase 13. It rolls into Phase 35 polish together with the
|
||
sheet's swipe-to-dismiss gesture.
|
||
|
||
## Auth gate
|
||
|
||
The root `+layout.svelte` redirects `anonymous → /login` for any
|
||
non-`/__debug/` path; the in-game shell inherits that gate without
|
||
any extra check. When a session is revoked while the user is in the
|
||
shell, the same redirect fires through the existing
|
||
revocation watcher.
|