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>
180 lines
8.4 KiB
Markdown
180 lines
8.4 KiB
Markdown
# Report view — Phase 23
|
|
|
|
The Phase 23 in-game "turn report" view is a single scrollable
|
|
layout with twenty sections, one per array on the FBS `Report`
|
|
table. The route file is the standard two-line wrapper; the
|
|
orchestrator and the per-section components live under
|
|
`ui/frontend/src/lib/active-view/report/`.
|
|
|
|
## Component layout
|
|
|
|
`lib/active-view/report.svelte` is the orchestrator. It owns the
|
|
section list, instantiates `IntersectionObserver` to track which
|
|
section is active, and renders the table of contents alongside the
|
|
section column.
|
|
|
|
```
|
|
report.svelte
|
|
├── report/report-toc.svelte // anchor list + mobile <select>
|
|
├── report/section-galaxy-summary.svelte
|
|
├── report/section-votes.svelte
|
|
├── report/section-player-status.svelte
|
|
├── report/section-my-sciences.svelte
|
|
├── report/section-foreign-sciences.svelte
|
|
├── report/section-my-ship-classes.svelte
|
|
├── report/section-foreign-ship-classes.svelte
|
|
├── report/section-battles.svelte
|
|
├── report/section-bombings.svelte
|
|
├── report/section-approaching-groups.svelte
|
|
├── report/section-my-planets.svelte
|
|
├── report/section-ships-in-production.svelte
|
|
├── report/section-cargo-routes.svelte
|
|
├── report/section-foreign-planets.svelte
|
|
├── report/section-uninhabited-planets.svelte
|
|
├── report/section-unknown-planets.svelte
|
|
├── report/section-my-fleets.svelte
|
|
├── report/section-my-ship-groups.svelte
|
|
├── report/section-foreign-ship-groups.svelte
|
|
└── report/section-unidentified-groups.svelte
|
|
```
|
|
|
|
Each section component is self-contained:
|
|
- reads `RenderedReportSource` from context;
|
|
- renders the loading copy when `rendered.report === null`;
|
|
- renders the empty-state copy when its array is empty;
|
|
- otherwise emits a `<section id="report-<slug>" data-testid="report-section-<slug>">`
|
|
containing the relevant grid / list / kv-list.
|
|
|
|
No shared `<Section>` wrapper exists. The visual scaffolding (dark
|
|
grid CSS, header style, status paragraph) is inlined per
|
|
component. The CLAUDE.md "wait for the third real caller before
|
|
extracting an abstraction" rule applies; with one shape per
|
|
section, the per-section inline CSS is the smallest correct
|
|
solution.
|
|
|
|
Shared formatters live in `report/format.ts` (`formatPercent`,
|
|
`formatCount`, `formatFloat`, `formatVotes`, `planetLabel`).
|
|
|
|
## Section order, data sources, empty copy
|
|
|
|
| # | Slug | Data | Empty copy (en) |
|
|
|---|------|------|-----------------|
|
|
| 1 | `galaxy-summary` | header turn / size / planet count / race | never empty |
|
|
| 2 | `votes` | `myVotes`, `myVoteFor`, `races[].votesReceived` | "no votes cast yet" |
|
|
| 3 | `player-status` | `players[]` (full roster, self + extinct) | never empty |
|
|
| 4 | `my-sciences` | `localScience[]` | "no sciences defined yet" |
|
|
| 5 | `foreign-sciences` | `otherScience[]`, one sub-table per race | "no foreign sciences observed yet" |
|
|
| 6 | `my-ship-classes` | `localShipClass[]` | "no ship classes designed yet" |
|
|
| 7 | `foreign-ship-classes` | `otherShipClass[]`, one sub-table per race | "no foreign ship classes observed yet" |
|
|
| 8 | `battles` | `battleIds[]` (inactive monospace spans) | "no battles last turn" |
|
|
| 9 | `bombings` | `bombings[]`, wiped rows visually distinct | "no bombings last turn" |
|
|
| 10 | `approaching-groups` | `incomingShipGroups[]` | "no approaching groups" |
|
|
| 11 | `my-planets` | `planets.filter(kind==="local")` | "no planets owned yet" |
|
|
| 12 | `ships-in-production` | `shipProductions[]` | "no ships in production" |
|
|
| 13 | `cargo-routes` | `routes[]` (flattened to one row per entry) | "no cargo routes set" |
|
|
| 14 | `foreign-planets` | `planets.filter(kind==="other")` | "no foreign planets observed" |
|
|
| 15 | `uninhabited-planets` | `planets.filter(kind==="uninhabited")` | "no uninhabited planets observed" |
|
|
| 16 | `unknown-planets` | `planets.filter(kind==="unidentified")` | "no unknown planets" |
|
|
| 17 | `my-fleets` | `localFleets[]` | "no fleets created yet" |
|
|
| 18 | `my-ship-groups` | `localShipGroups[]` | "no ship groups yet" |
|
|
| 19 | `foreign-ship-groups` | `otherShipGroups[]` | "no foreign ship groups observed" |
|
|
| 20 | `unidentified-groups` | `unidentifiedShipGroups[]` | "no unidentified groups" |
|
|
|
|
The orchestrator iterates this list once for the TOC and once for
|
|
the body — both surfaces stay in sync by construction.
|
|
|
|
## Table of contents and active highlight
|
|
|
|
`report/report-toc.svelte` renders two surfaces driven by the same
|
|
entry list:
|
|
|
|
- **Desktop / tablet sidebar** — sticky `<aside>` with vertical
|
|
anchor list. The anchor for the currently-visible section gets
|
|
`aria-current="location"` and an `.active` CSS class.
|
|
- **Mobile (< 768 px)** — the desktop sidebar is hidden via CSS
|
|
and a sticky `<select>` takes its place at the top of the body.
|
|
Picking an option scrolls the matching section into view. The
|
|
mobile contract intentionally avoids stacking another overlay on
|
|
top of the existing layout-owned bottom-tabs.
|
|
|
|
Both surfaces also expose a "Back to map" affordance
|
|
(`report-back-to-map`) at the top.
|
|
|
|
The active slug is computed by an `IntersectionObserver` rooted on
|
|
the viewport (`root: null`) with `rootMargin: "-30% 0px -60% 0px"`.
|
|
The skew biases the active band toward the upper third of the
|
|
visible area so that scrolling down advances the highlight
|
|
naturally. The observer is created on mount and torn down on
|
|
unmount.
|
|
|
|
The in-game shell layout (`routes/games/[id]/+layout.svelte`)
|
|
expands `<main class="active-view-host">` to fit content rather
|
|
than constraining it, so the document body is the actual scroll
|
|
container — not the host. The IntersectionObserver root is `null`
|
|
to match.
|
|
|
|
## Scroll save / restore
|
|
|
|
`routes/games/[id]/report/+page.svelte` exports a SvelteKit
|
|
`Snapshot<{ scrollY: number }>`:
|
|
|
|
- `capture()` reads `window.scrollY` when SvelteKit's
|
|
`beforeNavigate` cycle runs.
|
|
- `restore(value)` schedules a short
|
|
`requestAnimationFrame` poll that waits for
|
|
`document.documentElement.scrollHeight` to grow tall enough to
|
|
honour the saved offset, then calls `window.scrollTo(0, value)`.
|
|
The poll caps at ~60 frames (≈ 1 second) so a never-tall-enough
|
|
body never pins a frame loop.
|
|
|
|
The capture / restore pair is keyed by route, so:
|
|
- Forward navigation from `/report` to `/map` lands `/map` at
|
|
scrollY 0 (no snapshot for `/map` to restore from).
|
|
- History-back from `/map` to `/report` restores the previously
|
|
captured scrollY — the user returns to the same section.
|
|
|
|
The Snapshot API does not capture the active sidebar slug; the
|
|
IntersectionObserver re-derives it from the restored scroll
|
|
position on the next animation frame, which keeps the TOC
|
|
highlight consistent without a second source of truth.
|
|
|
|
## i18n namespace
|
|
|
|
All Phase 23 strings live under `game.report.*`:
|
|
- `game.report.loading` — section loading placeholder.
|
|
- `game.report.back_to_map`, `game.report.toc.title`,
|
|
`game.report.toc.mobile_label` — shell-level strings.
|
|
- `game.report.section.<slug>.title` — section heading.
|
|
- `game.report.section.<slug>.empty` — empty-state copy (where
|
|
applicable).
|
|
- `game.report.section.<slug>.column.<column>` — column headings.
|
|
- A small number of section-specific keys (`bombings.wiped`,
|
|
`player_status.local_marker`, `player_status.extinct_marker`,
|
|
`foreign_sciences.race_header`, `foreign_ship_classes.race_header`,
|
|
`battles.id_label`, `votes.target_none`).
|
|
|
|
The namespace is intentionally separate from `game.table.*` even
|
|
where the data shape overlaps (e.g. sciences, ship classes); the
|
|
two surfaces evolve independently and a shared key set would
|
|
couple them silently.
|
|
|
|
## Test seams
|
|
|
|
- **Vitest** — four representative specs cover the four section
|
|
shapes: kv-list (`report-section-galaxy-summary.test.ts`), grid
|
|
with conditional row state (`report-section-bombings.test.ts`),
|
|
per-race sub-table (`report-section-foreign-sciences.test.ts`),
|
|
TOC (`report-toc.test.ts`). Each spec mounts the component
|
|
against a synthetic `RenderedReportSource`, so the orchestrator
|
|
/ IntersectionObserver are out of scope.
|
|
- **Playwright** — `tests/e2e/report-sections.spec.ts` exercises
|
|
the full integration: every TOC anchor lands its section in
|
|
view, the snapshot mechanism preserves `window.scrollY` on
|
|
history navigation, the back-to-map button reaches `/map`, the
|
|
mobile `<select>` scrolls to the chosen section on a narrow
|
|
viewport.
|
|
|
|
Test IDs follow the pattern `report-section-<slug>` for section
|
|
roots, `report-toc-<slug>` for TOC anchors, and per-section row
|
|
identifiers (e.g. `report-bombing-row`, `my-planets-row`).
|