ui/phase-23: turn-report view with twenty sections and TOC

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>
This commit is contained in:
Ilia Denisov
2026-05-11 14:33:56 +02:00
parent 81d8be08b2
commit c58027c034
48 changed files with 5368 additions and 103 deletions
+75
View File
@@ -2506,6 +2506,81 @@ Targeted tests:
- Playwright e2e: open the report, scroll to each section via anchor
navigation, assert content present.
Decisions during stage:
1. **Component decomposition.** The orchestrator
`lib/active-view/report.svelte` is one file; each of the twenty
sections is its own component under
`lib/active-view/report/section-<slug>.svelte`. Six distinct data
shapes (kv-list, races-style grid, planets-style grid, sub-table-
per-race, raw UUID list, fleet/group grids) sit too unevenly in one
monolith; per-section components also map directly onto the Vitest
targeted-test seam. No shared `<Section>` abstraction was extracted
— CLAUDE.md "wait for the third real caller" still holds with one
shape per section. Shared formatters live in `report/format.ts`.
2. **`races` vs `players`.** A parallel
`GameReport.players: ReportPlayer[]` was added (full roster, self
row included, extinct rows kept with `extinct: true`). The Phase 22
`races[]` (non-extinct, self excluded) stays untouched so no Phase
22 surface had to change. Extinct races are shown in Player Status
with a `RIP` marker; the orchestrator highlights the local row.
3. **Scroll save / restore.** Wired through SvelteKit's `Snapshot`
API on `routes/games/[id]/report/+page.svelte`. Captures
`window.scrollY` (the in-game shell layout expands its
`active-view-host` to fit content, so the document body is the real
scroll container) and restores via a `requestAnimationFrame` poll
that waits for `documentElement.scrollHeight` to catch up before
calling `window.scrollTo`. The earlier plan to track the host's
`scrollTop` did not survive contact with the layout's
no-explicit-height contract; the change is contained to the route
file. No new context plumbing was introduced.
4. **Active-section highlight.** `IntersectionObserver` rooted on the
viewport (`root: null`) with `rootMargin: "-30% 0px -60% 0px"`
tracks which section sits in the upper third of the visible area
and updates the TOC. Cheaper than a scroll handler and degrades
gracefully where IO is not available.
5. **Mobile TOC.** A sticky `<select>` at the top of the report body
replaces the desktop anchor sidebar on viewports below 768 px. No
new overlay primitive is introduced; the existing layout-owned
bottom-tab bar stays unobstructed. Picking an option scrolls the
chosen section into view.
6. **Battles section.** Battle UUIDs render as inactive monospace
`<span>` rows until Phase 27 lights up `/games/:id/battle/:battleId`.
The earlier plan to link them now was reverted: a dead link is a
worse experience than a plain identifier, and the rewire when
Phase 27 lands is one line.
7. **Foreign sciences / ship classes layout.** One sub-table per race
with a `{race} sciences` / `{race} ship classes` sub-header. The
`(race, name)` decoder sort produces stable groups; cross-race
sorting is intentionally avoided (it would be semantically
meaningless across races).
8. **Bombings wiped state.** Wiped rows get a `.wiped` CSS class plus
a dedicated `report-bombing-wiped-badge` element so the boolean is
visually explicit and easy to assert in e2e.
9. **Ships in production `prodUsed` derivation (Go side).** The legacy
text reports do not carry the engine's per-turn `ProdUsed` field —
only `Cost`, `Percent`, `Free`. The legacy parser derives an
approximation as `ShipBuildCost(shipMass, material, resources) * percent`
using a new shared helper `pkg/calc.ShipBuildCost`. The engine's
`controller.ProduceShip` was refactored to call the same helper
(behavior-preserving — engine tests stay unchanged and pass). The
approximation is documented in
`tools/local-dev/legacy-report/README.md`; live engine reports come
over FBS and never flow through this parser.
10. **Legacy parser scope.** Per user direction, the parser was
extended to populate `LocalScience`, `OtherScience`,
`OthersShipClass`, `Bombing`, and `ShipProduction` from their
legacy text sections. Battles stay in the parser's Skipped list:
the legacy text carries per-battle rosters with no stable UUID,
and synthesising IDs would invent data Phase 27 would have to
drop. `OtherGroup[]`, `UnidentifiedGroup[]`, and cargo routes
remain skipped (no legacy section).
11. **i18n namespace.** All Phase 23 strings live under
`game.report.section.<slug>.*`; the duplicate-looking entries
(sciences / ship classes columns) are deliberately separate from
`game.table.*` so the two surfaces evolve independently. ≈90 new
keys, en + ru in lockstep.
## Phase 24. Push Events — Turn-Ready
Status: pending.