ui/phase-27: skip phantom frames during play + freeze final layout

Two more KNNTS041 viewer fixes:

1. Phantom-frame fast-forward. `buildFrames` now flags every frame
   whose shot landed on an already-empty defender group as
   `phantom: true`. During play the BattleViewer effect detects a
   phantom frame and chains a 0 ms timer to the next non-phantom,
   so streaks of phantoms (the ~30 frames between shots 224 and
   255, and the 401..414 stretch) collapse from "the player just
   mots the timeline" into a single visual tick. Step controls and
   the scrubber can still land on a phantom deliberately for
   protocol inspection.

2. Final-frame layout freeze. `displayFrame` derives from the raw
   `frames[i]` and, on the very last frame when `activeRaceIds`
   shrinks vs the penultimate frame (the killing blow eliminates a
   race), substitutes the penultimate's `remaining` and
   `activeRaceIds` while keeping the current `shotIndex` and
   `lastAction`. The result: the surviving cluster no longer
   reflows onto the planet ring on the very last shot — the user
   sees the killing line + defender flash rendered against the
   picture they saw a moment earlier.

Tests: `phantom-destroy clamp` case extended with `frame.phantom`
flag assertions across the protocol; 644 Vitest cases stay green,
4 Playwright `battle-viewer` cases stay green.

Docs: `ui/docs/battle-viewer-ux.md` documents the fast-forward
behaviour and the final-frame freeze.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-13 18:16:11 +02:00
parent e2aba856b5
commit 2e7478f5ea
4 changed files with 107 additions and 33 deletions
+25 -8
View File
@@ -135,14 +135,31 @@ 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.
report path) occasionally log more `Destroyed` (and `Shields`)
lines against a ship-group bucket than the bucket's initial
population — the emitter keeps recording hits past the moment a
group emptied. `buildFrames` marks every such frame as
`phantom: true` and skips the race-total decrement so the race
stays on the scene until its actual ships are gone.
During play the BattleViewer fast-forwards through streaks of
phantom frames via a 0 ms timer so the user never sees a silent
gap (KNNTS041 had ~30 phantom frames between shots 224 and 255
right after the last `Nails:pup` died). Step controls and the
scrubber can still land on a phantom frame deliberately — useful
when inspecting the protocol entry that the engine emitted into
the void.
## Final-frame freeze
When the last protocol action eliminates a race, the surviving
side would otherwise reflow alone to the planet ring at the very
last shot — visually jarring and uninformative. `displayFrame`
freezes the layout-determining state (`remaining` and
`activeRaceIds`) at the penultimate frame's values while keeping
the final frame's `shotIndex` and `lastAction`, so the killing
shot still renders as a line + flash against the picture the user
saw a moment earlier.
## Header + layout