ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.
Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).
UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
radial scene that consumes a BattleReport prop. Planet at the
centre, races on the outer ring at equal angular spacing, race
clusters by (race, className) with <class>:<numLeft> labels;
observer groups (inBattle: false) are not drawn; eliminated
races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
on the next frame. Playback controls: play/pause + step ± +
rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
via api/battle-fetch.ts; synthetic-gameId prefix routes to a
fixture loader, otherwise REST through the gateway. Always-
visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
corners of the planet's circumscribed square (stroke width
clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
marker is a stroke-only ring (yellow when damaged, red when
wiped). Wired into state-binding.ts; click handler dispatches
battle clicks to the viewer and bombing clicks to the matching
Reports row.
- i18n keys for the viewer in en + ru.
Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).
Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+123
-27
@@ -2949,45 +2949,126 @@ Targeted tests:
|
||||
|
||||
Status: pending.
|
||||
|
||||
Goal: render battles as a dedicated view with playback controls
|
||||
(play, pause, step forward, step backward, rewind), driven by the
|
||||
server-side combat log; render battle and bombing markers on the map.
|
||||
Goal: ship a dedicated Battle Viewer rendering radial scenes from
|
||||
`BattleReport` data (planet centred, races on the outer ring, per
|
||||
ship-class clusters, animated shot lines), plus battle and bombing
|
||||
markers on the map. Battles and bombings stay strictly separate —
|
||||
bombings remain a static table in the Reports view, only battles
|
||||
get the animated viewer.
|
||||
|
||||
Artifacts:
|
||||
|
||||
- `ui/frontend/src/map/battle-markers.ts` renders markers on the map
|
||||
for current-turn battles and bombings within visibility, clickable
|
||||
to open the battle viewer
|
||||
- `ui/frontend/src/routes/games/[id]/battle/[battleId]/+page.svelte`
|
||||
view with the combatant list, the round-by-round log, and a player
|
||||
control bar
|
||||
- `ui/frontend/src/lib/battle-player/` round timeline, current-round
|
||||
highlight, per-shot animation
|
||||
- entry points to the viewer: marker on map, row in the report's
|
||||
battles section, push-event toast when a battle this turn involved
|
||||
the player
|
||||
- topic doc `ui/docs/battle-viewer-ux.md` covering playback
|
||||
semantics, accessibility (the combat log must be readable as text
|
||||
for users who skip animations)
|
||||
- engine: `game/internal/router/handler/battle.go` for
|
||||
`GET /api/v1/battle/:turn/:uuid` (handler pre-existed; Phase 27
|
||||
added the tests + openapi schemas)
|
||||
- engine wire: `pkg/model/report/battle.go` ships a new
|
||||
`BattleSummary{id, planet, shots}`; `Report.battle` carries a
|
||||
slice of these summaries so the map can place markers without
|
||||
fetching every full report
|
||||
- backend: `backend/internal/engineclient/client.go.FetchBattle`
|
||||
and `backend/internal/server/handlers_user_games.go.Battle`
|
||||
expose `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`
|
||||
- UI viewer: `ui/frontend/src/lib/battle-player/`
|
||||
(`radial-layout.ts`, `timeline.ts`, `battle-scene.svelte`,
|
||||
`playback-controls.svelte`, `battle-viewer.svelte`); SVG-based,
|
||||
one frame per protocol entry, full controls (play/pause + step
|
||||
back + step forward + rewind + 1x/2x/4x speed switch)
|
||||
- UI route + page wrapper:
|
||||
`ui/frontend/src/routes/games/[id]/battle/[[battleId]]/+page.svelte`
|
||||
feeds `gameId` / `turn` / `battleId` into
|
||||
`ui/frontend/src/lib/active-view/battle.svelte`, which loads the
|
||||
report via `api/battle-fetch.ts` (synthetic-fixture path + real
|
||||
engine fetch through the backend gateway)
|
||||
- UI report link: `lib/active-view/report/section-battles.svelte`
|
||||
now links every battle UUID into
|
||||
`/games/{id}/battle/{uuid}?turn={turn}`
|
||||
- UI map markers: `ui/frontend/src/map/battle-markers.ts` emits a
|
||||
yellow X cross per battle (two `LinePrim` through the planet's
|
||||
bounding-square diagonals; stroke width scales 1px..5px with
|
||||
protocol length) plus a stroke-only ring per bombing (yellow when
|
||||
damaged, red when wiped). Wired into `state-binding.ts`; the map
|
||||
click handler dispatches battle clicks to the viewer and bombing
|
||||
clicks to a scroll-into-view of the matching row in Reports.
|
||||
- topic doc `ui/docs/battle-viewer-ux.md` covers playback
|
||||
semantics, accessibility (the always-visible `<ol>` log), the
|
||||
radial layout, and the marker click behaviour
|
||||
- docs/FUNCTIONAL.md §6.5 (Battle viewer) + mirror in
|
||||
docs/FUNCTIONAL_ru.md
|
||||
|
||||
Dependencies: Phase 23.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- battle and bombing markers render on the map for the seeded
|
||||
current-turn report and are clickable to open the viewer;
|
||||
- the viewer plays back any battle in the seeded report including
|
||||
multi-round and one-sided battles;
|
||||
- step controls allow precise inspection;
|
||||
- the same data is accessible as a static text log for accessibility.
|
||||
current-turn report and are clickable: battle → Battle Viewer for
|
||||
the corresponding UUID, bombing → scroll to its row in Reports;
|
||||
- the Battle Viewer plays back any `BattleReport` end-to-end with
|
||||
step back / step forward / rewind / 1x-2x-4x speeds; observers
|
||||
(`inBattle === false`) are not drawn; eliminated races drop out
|
||||
and survivors re-distribute on the next frame;
|
||||
- the same protocol is mirrored as an always-visible text log under
|
||||
the scene for accessibility;
|
||||
- bombings keep their Phase 23 static table layout in Reports; no
|
||||
Battle Viewer entry-point is wired from them.
|
||||
|
||||
Targeted tests:
|
||||
|
||||
- Vitest unit tests for round-state transitions;
|
||||
- Vitest unit tests for marker rendering on torus and no-wrap
|
||||
fixtures;
|
||||
- Playwright e2e: click a battle marker on the map, play through,
|
||||
step backward, return to the report.
|
||||
- Vitest unit: radial layout (1/2/3 races) and timeline frame-
|
||||
builder (initial state, shot decrement, race-elimination drop-out)
|
||||
in `tests/battle-player.test.ts`
|
||||
- Vitest unit: marker primitives + stroke-width formula
|
||||
(1→1, 50→2.98, 100→5, 200→5) in `tests/battle-markers.test.ts`
|
||||
- Go unit: engine HTTP handler validations (400 / 404 / 500) in
|
||||
`game/internal/router/battle_test.go`
|
||||
- Go contract: openapi freezes for the new endpoint and schemas in
|
||||
`game/openapi_contract_test.go`
|
||||
- Playwright e2e: click battle marker → viewer; play / step back;
|
||||
click battle UUID in Reports → viewer; click bombing marker →
|
||||
Reports bombings row scrolled into view.
|
||||
|
||||
Decisions during stage:
|
||||
|
||||
1. **Bombings stay a static table.** `section-bombings.svelte`
|
||||
already covers the "who bombed, with what power, wiped or not"
|
||||
requirement; nothing in Phase 27 touches it. Bombings explicitly
|
||||
do not open the Battle Viewer.
|
||||
2. **Wire change.** `Report.Battle` switched from `[]uuid.UUID` to
|
||||
`[]BattleSummary{id, planet, shots}` so the map renderer can
|
||||
place markers without N extra fetches and so the cross-marker
|
||||
stroke can scale with protocol length.
|
||||
3. **Battle marker = yellow X cross** drawn as 2 `LinePrim` through
|
||||
the corners of the planet's circumscribed square; stroke width
|
||||
`clamp(1 + (shots - 1) * 4 / 99, 1, 5)` px.
|
||||
4. **Bombing marker = stroke-only ring** slightly larger than the
|
||||
planet circle. Yellow when damaged, red when wiped. Click =
|
||||
scroll to the matching row in Reports (not the viewer).
|
||||
5. **Viewer URL** `/games/[id]/battle/[battleId]?turn=N`. Turn is a
|
||||
query param so the same route works in history mode.
|
||||
6. **SVG, not PixiJS** for the radial scene — isolated component,
|
||||
no need for WebGL; PixiJS stays as the map renderer.
|
||||
7. **Playback controls full set**: play / pause + step back + step
|
||||
forward + rewind + 1x / 2x / 4x switch. 1x = 400 ms per frame.
|
||||
8. **Observer groups (`inBattle: false`)** are filtered out of both
|
||||
the scene and the text log.
|
||||
9. **Cluster aggregation by `(race, className)`** so a race with
|
||||
multiple groups of the same class collapses to one labelled
|
||||
circle. Stable target for shot-line endpoints.
|
||||
10. **Page loader switches on `synthetic-` gameId prefix** —
|
||||
synthetic mode uses `api/synthetic-battle.ts` fixtures; live
|
||||
games hit `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`.
|
||||
BattleViewer component itself is a logically isolated prop sink.
|
||||
11. **Always-visible `<ol>` text protocol** under the scene satisfies
|
||||
the accessibility requirement without a separate "skip
|
||||
animation" toggle.
|
||||
|
||||
TODO carried into Phase 27 deferred items
|
||||
(see Phase 27 of this PLAN's deferred-followups list, near the
|
||||
bottom):
|
||||
|
||||
- push event `game.battle.new` + toast deep-link;
|
||||
- richer ship-class visuals derived from class characteristics;
|
||||
- animated transitions when survivors re-distribute after an
|
||||
elimination (currently hard-jumps).
|
||||
|
||||
## Phase 28. Diplomatic Mail View
|
||||
|
||||
@@ -3459,3 +3540,18 @@ phase listed in the parenthesis when that phase lands.
|
||||
exercises a unary Connect call and a server-streaming Connect call
|
||||
through `testenv.Bootstrap`. (Phase 7+, fold into the phase that
|
||||
needs it.)
|
||||
- **Battle viewer — push event `game.battle.new`** — when a battle
|
||||
involving the current player lands, emit a backend notification
|
||||
intent (idempotency `battle-new:<game_id>:<turn>:<battle_id>`,
|
||||
payload `{game_id, turn, battle_id}`) so the in-game shell
|
||||
surfaces a toast with a deep link into the Battle Viewer.
|
||||
(Phase 27 deferred; needs an engine emit-side change.)
|
||||
- **Battle viewer — richer ship-class visuals** — current MVP draws
|
||||
one small circle plus `<class>:<numLeft>` label per `(race,
|
||||
className)` pair. Future work derives shape / scale from mass,
|
||||
armament, shields, and the number of ships in the group.
|
||||
(Phase 27 deferred.)
|
||||
- **Battle viewer — animated re-distribution on elimination** —
|
||||
current implementation hard-jumps to the new spacing on the next
|
||||
frame; replace with an easing so the survivors visibly slide
|
||||
along the outer ring. (Phase 27 deferred.)
|
||||
|
||||
Reference in New Issue
Block a user