ui/phase-27: viewer polish + phantom-destroy clamp

Nine BattleViewer refinements from the latest review pass:

1. Mass radii were uniform in synthetic mode because
   `+layout.svelte` skipped `loadCore()` on the synthetic branch.
   The wasm bridge to `pkg/calc/ship.go` now boots in both modes
   so `computeBattleGroupMass` resolves a real FullMass and
   `radiusForMass` produces a per-battle scale.

2. Phantom-destroy clamp in `buildFrames`. Legacy emitters
   (KNNTS041 planet #7) log many more `Destroyed` lines against a
   group than the group's initial population — at frame 406 of
   2317 the race totals previously hit zero on phantom shots and
   the scene blanked while playback continued silently. We now
   only shrink the per-group remaining count and the race totals
   when the group still has ships. The line still draws on
   phantom frames; only the counters stay sane.

3. Vogel sunflower positions are now reassigned by inward dot
   product before being handed to ranks: the rank-0 bucket — the
   one with the largest initial ship count — always lands at the
   most-inward spiral slot. The previous quarter-step anchor bias
   was too weak; ranks r ≥ 2 routinely overtook rank-0 toward
   the planet. The anchor offset is gone.

4. Bucket order inside a cluster is locked at battle start by
   each bucket's *initial* ship count (`num`), not its live
   `numLeft`. The position of every class circle stays put for
   the whole battle; only the label number changes as ships die.

5. Shot line + defender flash blink on a per-frame timer during
   play. The line stays on for the first 90 % of frame duration,
   off for the last 10 %, so two consecutive shots from the same
   attacker on the same defender look like two distinct pulses.
   On pause the line and flash stay drawn for inspection.

6. The defender's class circle now flashes red (destroyed) or
   green (shielded) in sync with the shot line, so the eye
   catches *who* was hit, not just where the line lands.

7. Battle log rows are buttons. Click / Enter / Space pauses
   playback and seeks to that shot. The list also auto-scrolls
   the current row into view so the highlight does not race off
   the bottom on long battles.

8. Race labels now sit above the cloud's bounding top instead of
   a fixed offset, so a dense cluster does not swallow its own
   race name.

9. Planet glyph + label switch to neutral grey
   (`#2a2f40` / `#4a5066` / `#6d7388`), keeping the planet "in the
   background" rather than competing with the combatants.

Step-back icon switched to `◀︎◀︎` to mirror step-forward.

Tests: two new Vitest cases cover the phantom-destroy clamp
(single-race wipe, mixed-class race survives a class wipe). The
existing 642 Vitest tests stay green; all four `battle-viewer`
Playwright cases pass.

Docs: `ui/docs/battle-viewer-ux.md` rewrites the cluster section
(locked order + Vogel reassignment), adds Playback Details (blink
+ flash semantics), and a Phantom Destroys section explaining the
clamp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-13 16:44:46 +02:00
parent 8c260f8715
commit 17a3afd5e9
7 changed files with 384 additions and 70 deletions
+35
View File
@@ -47,6 +47,15 @@ hull. The per-bucket label `<className>:<numLeft>` sums NumberLeft
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.
Circle radius scales with per-ship FullMass (Empty + Carrying via
the per-ship `LoadQuantity`). The viewer resolves a
`(race, className) → ShipClassRef` lookup from the surrounding
@@ -95,6 +104,32 @@ the log instead of watching the SVG. The list is always present
and never hidden, satisfying the original Phase 27 acceptance "the
same data is accessible as a static text log".
Each log row is also a `<button>`: a click or Enter/Space jumps
playback to that shot (pauses and seeks). The list auto-scrolls
the current row into view as the timeline advances, so the user
does not have to chase the highlight on long battles.
## Playback details
On play, the shot line + the defender circle's colour flash gate
on a per-frame timer that blinks them off during the last 10 % of
the frame's duration. Two consecutive shots from the same attacker
on the same defender therefore look like two distinct pulses
rather than one continuous line. On pause the line and flash
stay drawn so the user can study the current shot.
## Phantom destroys
Legacy emitters (the `dg` engine format that feeds the synthetic-
report path) occasionally log more `Destroyed` lines against a
ship-group bucket than the bucket's initial population — the
emitter keeps recording hits past the moment the group emptied.
`buildFrames` clamps each per-group remaining count at zero and
only decrements race totals on a real shrink, so a race stays on
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.
## Height fit
The viewer is pinned to the viewport: `.active-view` uses