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:
@@ -76,20 +76,20 @@ describe("active-view stubs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("battle view stamps the battleId and renders the back-to-map link", () => {
|
||||
test("battle view stamps the battleId and shows the loading placeholder", () => {
|
||||
// Phase 27 replaces the Phase 10 stub with the Battle Viewer
|
||||
// wrapper. The wrapper mounts the loading copy until the
|
||||
// fetcher resolves (component test runs in jsdom without a
|
||||
// network); the back buttons and the data-battle-id stamp are
|
||||
// rendered unconditionally so the orchestrator scaffold is the
|
||||
// stable hook the active-view shell relies on.
|
||||
// wrapper. The latest layout iteration moved the back-
|
||||
// navigation buttons inside `BattleViewer` so they only mount
|
||||
// once the BattleReport finishes loading. The wrapper itself
|
||||
// always renders the `active-view-battle` host with the
|
||||
// `data-battle-id` stamp and a localized loading copy until
|
||||
// the fetcher resolves.
|
||||
const ui = render(BattleView, {
|
||||
props: { gameId: "synthetic-test", turn: 0, battleId: "b-42" },
|
||||
});
|
||||
const node = ui.getByTestId("active-view-battle");
|
||||
expect(node).toHaveAttribute("data-battle-id", "b-42");
|
||||
expect(ui.getByTestId("battle-back-to-map")).toBeInTheDocument();
|
||||
expect(ui.getByTestId("battle-back-to-report")).toBeInTheDocument();
|
||||
expect(ui.getByTestId("battle-loading")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("battle view surfaces the not-found state for an empty battleId", () => {
|
||||
|
||||
Reference in New Issue
Block a user