ui/phase-27: viewer layout pass + static cluster + duel layout
Layout reshuffle so the scene captures the maximum viewer area: - Header collapses three rows into one: `back to map` / `back to report` on the left, the centred title `Battle on planet <name> (#<number>)` (new i18n key `game.battle.header_title`), and the frame counter on the right. The wrapper `.active-view` no longer renders its own back-row; routes flow through props. - Viewer drops the `max-width: 880px` cap so on a wide monitor the scene scales up across the full active-view-host. - A drag-seek `<input type="range">` sits between the scene and the controls; dragging pauses playback and lands `frameIndex` on the chosen shot. - Speed control is one cycling button: `1x → 2x → 4x → 6x → 1x`. The label shows the current speed; the new 6x adds a 67 ms frame interval for skimming a long timeline. - The text protocol log is now collapsible behind a `Log ▲▼` toggle in the controls bar. The toggle is its own button; the default state stays expanded. Collapsing the log hands the remaining height to the scene. - Numerical list markers (`1. 2. 3.`) are dropped from the log; `list-style: none` keeps each row visually clean. Static cluster + visibility filter: - `staticBucketsByRace` now locks bucket order, mass, radius and local Vogel-spiral positions for the lifetime of the viewer; it only re-derives when `report` or the wasm `core` change. - `renderedByRace` overlays the per-frame `remaining` map and drops buckets whose `numLeft` hits zero. The surviving buckets keep their slots, so a class emptying never reshuffles the cluster — the empty bucket simply disappears. - A shot whose attacker or defender bucket is no longer visible draws no line (phantom shots into already-empty buckets are silently skipped, matching the user expectation that pup at 0 should stop attracting fire visually). - Race label clamps to a minimum y inside the SVG viewport so three-or-more-race layouts with a north anchor never clip the top race name off-canvas. Duel layout (user suggestion): - `layoutRaces` rotates the radial start angle by 90° when only two participants remain, so race 0 lands at 9 o'clock and race 1 at 3 o'clock. The pair faces off horizontally; neither label pushes against the SVG top edge. The existing test for two-race positions is updated accordingly. Tests: the existing `layoutRaces` two-race case is rewritten for the horizontal duel; the `game-shell-stubs` battle case checks the loading placeholder (back buttons now live in the loaded viewer, not the wrapper). 644 Vitest cases stay green; 4 Playwright battle-viewer cases stay green. Docs: `ui/docs/battle-viewer-ux.md` documents the static cluster / visibility filter, the duel layout, the scrubber, the cycling speed button and the collapsible log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+42
-16
@@ -48,13 +48,22 @@ across the underlying groups; per-tech detail is available in the
|
||||
Reports view (Foreign Ship Classes / My Ship Types).
|
||||
|
||||
Bucket order inside a cluster is **locked at battle start** by the
|
||||
initial ship count (`num` summed across tech variants, descending).
|
||||
As ships die during playback only the label number changes — every
|
||||
bucket keeps its slot in the Vogel spiral, so the user does not see
|
||||
the cluster reshuffle when a class empties. Vogel positions are
|
||||
then reassigned per rank by their inward distance toward the
|
||||
planet, so the rank-0 bucket (the largest at battle start) always
|
||||
sits at the most-inward spiral slot.
|
||||
initial ship count (`num` summed across tech variants, descending),
|
||||
together with mass, radius and local position. The static layout
|
||||
lives in `staticBucketsByRace`; the per-frame derivation
|
||||
`renderedByRace` overlays the live `NumberLeft` and drops buckets
|
||||
once they hit zero. The remaining buckets keep their slots in the
|
||||
cloud, so the cluster does not reshuffle when a class empties — the
|
||||
empty bucket simply disappears.
|
||||
|
||||
Vogel positions are reassigned per rank by their inward distance
|
||||
toward the planet, so the rank-0 bucket (the largest by initial
|
||||
ship count) always sits at the most-inward spiral slot.
|
||||
|
||||
When two races remain in battle the radial layout switches to the
|
||||
horizontal duel: race 0 at 9 o'clock, race 1 at 3 o'clock. This
|
||||
keeps both race labels clear of the SVG top edge and reads as the
|
||||
two sides facing off naturally.
|
||||
|
||||
Circle radius scales with per-ship FullMass (Empty + Carrying via
|
||||
the per-ship `LoadQuantity`). The viewer resolves a
|
||||
@@ -80,20 +89,25 @@ produces the "shot-shot-shot" pulse the user wanted.
|
||||
|
||||
## Playback controls
|
||||
|
||||
`lib/battle-player/playback-controls.svelte` ships the full set:
|
||||
`lib/battle-player/playback-controls.svelte` ships:
|
||||
|
||||
| Control | Effect |
|
||||
| ------------- | ------------------------------------------ |
|
||||
| ⏮ rewind | Stop, jump to frame 0 |
|
||||
| ◀︎ step back | Stop, frame ← frame − 1 |
|
||||
| ▶︎ / ⏸ play | Toggle continuous playback |
|
||||
| ▶︎▶︎ step fwd | Stop, frame ← frame + 1 |
|
||||
| 1x / 2x / 4x | Speed switch: 400 / 200 / 100 ms per frame |
|
||||
| Control | Effect |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| ⏮ rewind | Stop, jump to frame 0 |
|
||||
| ◀︎◀︎ step back | Stop, frame ← frame − 1 |
|
||||
| ▶︎ / ⏸ play | Toggle continuous playback |
|
||||
| ▶︎▶︎ step forward | Stop, frame ← frame + 1 |
|
||||
| `Nx` cycle speed | Single button, cycles 1x → 2x → 4x → 6x → 1x; the label shows the current speed (400 / 200 / 100 / 67 ms per frame) |
|
||||
| `Log ▲▼` toggle | Collapses / expands the always-visible text protocol so the user can give the scene the full viewer height |
|
||||
|
||||
When the timeline is at its end and the user hits play, the frame
|
||||
counter wraps to 0 and continues. Step buttons disable themselves at
|
||||
their boundary.
|
||||
|
||||
A drag-seek slider sits between the scene and the controls. Dragging
|
||||
pauses playback and lands `frameIndex` on the chosen shot — handy
|
||||
for jumping to the moment a particular race started losing ground.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Below the scene the viewer renders a static `<ol>` text protocol —
|
||||
@@ -130,12 +144,24 @@ the scene until its actual ships are gone. The phantom shots still
|
||||
draw a line during the frame they belong to; only the running
|
||||
counters are protected.
|
||||
|
||||
## Header + layout
|
||||
|
||||
The viewer header carries three rows of chrome in a single line:
|
||||
the back-navigation buttons (`back to map` / `back to report`) on
|
||||
the left, a centred title — `Battle on planet <name> (<#number>)`,
|
||||
i18n key `game.battle.header_title` — and the frame counter on the
|
||||
right. Pulling navigation into the header frees the entire viewer
|
||||
area for the scene; the `.viewer` container has no `max-width` cap,
|
||||
so on wide monitors the scene scales up while the log keeps its
|
||||
internal 30 dvh scroll.
|
||||
|
||||
## Height fit
|
||||
|
||||
The viewer is pinned to the viewport: `.active-view` uses
|
||||
`calc(100dvh − 80px)` so the in-game-shell header + optional
|
||||
HistoryBanner do not push the scene below the fold. Inside the
|
||||
viewer, the scene grows (`flex: 1`) and the log shrinks to a
|
||||
viewer, the scene grows (`flex: 1`), the scrubber + controls hold
|
||||
their natural height, and the log (when expanded) shrinks to a
|
||||
30 dvh ceiling with its own internal scroll, so the page itself
|
||||
never scrolls vertically. The 80 px allowance maps to the current
|
||||
Header + HistoryBanner total on desktop; mobile breakpoints reuse
|
||||
|
||||
Reference in New Issue
Block a user