diff --git a/ui/docs/battle-viewer-ux.md b/ui/docs/battle-viewer-ux.md index 956933a..4a4d7e2 100644 --- a/ui/docs/battle-viewer-ux.md +++ b/ui/docs/battle-viewer-ux.md @@ -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 `
{i18n.t("game.battle.loading")}
{:else if state.kind === "ready"} -{i18n.t("game.battle.not_found")} @@ -153,44 +142,23 @@ component does not have to know about `RenderedReportSource`. /* * The in-game shell renders this active view inside an * `.active-view-host` with `flex: 1; overflow-y: auto`, but - * the surrounding `.game-shell` uses `min-height: 100vh`, - * so without a hard upper bound the viewer pushes the - * whole shell past the viewport. We pin the active view to - * `100dvh` minus a small allowance for the header chrome - * (in-game Header + optional HistoryBanner = ~66 px on - * desktop) so the internal flex chain can split the - * remaining height between the scene and the always- - * visible log without forcing a page-level scroll. + * the surrounding `.game-shell` uses `min-height: 100vh`, so + * without a hard upper bound the viewer pushes the whole + * shell past the viewport. We pin the active view to `100dvh` + * minus a small allowance for the header chrome (in-game + * Header + optional HistoryBanner ≈ 66 px on desktop) so the + * internal flex chain can split the remaining height between + * the scene, scrubber, controls and log without forcing a + * page-level scroll. */ height: calc(100dvh - 80px); max-height: calc(100dvh - 80px); min-height: 0; overflow: hidden; - padding: 1rem; box-sizing: border-box; font-family: system-ui, sans-serif; color: #d6dcf2; } - .back-row { - display: flex; - gap: 0.5rem; - max-width: 880px; - margin: 0 auto 1rem; - flex: 0 0 auto; - } - .back-btn { - appearance: none; - background: #1f2748; - color: #d6dcf2; - border: 1px solid #2c3568; - padding: 0.35rem 0.7rem; - border-radius: 3px; - cursor: pointer; - font-size: 0.85rem; - } - .back-btn:hover { - background: #2a3463; - } .status { margin: 2rem auto; max-width: 880px; diff --git a/ui/frontend/src/lib/battle-player/battle-scene.svelte b/ui/frontend/src/lib/battle-player/battle-scene.svelte index 206727d..bcc2292 100644 --- a/ui/frontend/src/lib/battle-player/battle-scene.svelte +++ b/ui/frontend/src/lib/battle-player/battle-scene.svelte @@ -5,19 +5,20 @@ Layout: planet at the centre, race anchors equally spaced on an outer ring, each race rendered as a *cloud* of class circles arranged on a Vogel sunflower spiral. Spiral positions are reassigned per rank by their inward distance toward the planet so -the rank-0 bucket (heaviest by NumberLeft) always sits at the -most-inward Vogel slot — the cloud visually leans toward the -planet without the cluster anchor needing a manual offset. +the rank-0 bucket (the bucket with the largest initial ship count) +always sits at the most-inward Vogel slot. Tech-variant groups of the same `(race, className)` collapse to one -visual node — the per-tech detail lives in Reports. Each circle's +visual node — per-tech detail lives in Reports. Each circle's radius scales with the per-ship FullMass (sqrt) so heavy ships -visually dominate. +visually dominate. Order, position, radius and mass are locked at +battle start; only NumberLeft (the label number) and per-bucket +visibility change per frame. Empty buckets are hidden so the +remaining ones keep their original spots without reshuffling. Observer groups (`inBattle === false`) are filtered out by -`buildFrames`, so they never appear here. Same-race opponents are -forbidden by the engine's combat filter, so a shot can never -collapse to a single visual node. +`buildFrames`. Same-race opponents are forbidden by the engine's +combat filter, so a shot never collapses to a single visual node. -->