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:
@@ -208,6 +208,15 @@ describe("buildFrames phantom-destroy clamp", () => {
|
||||
// the only active race for the remainder of the protocol.
|
||||
expect(frames[5].remaining.get(10)).toBe(0);
|
||||
expect(frames[5].activeRaceIds).toEqual([1]);
|
||||
// Phantom flags: first two destroys land on a non-empty
|
||||
// group → real shots; the remaining three are phantoms.
|
||||
expect(frames[1].phantom).toBe(false);
|
||||
expect(frames[2].phantom).toBe(false);
|
||||
expect(frames[3].phantom).toBe(true);
|
||||
expect(frames[4].phantom).toBe(true);
|
||||
expect(frames[5].phantom).toBe(true);
|
||||
// The initial frame is never a phantom.
|
||||
expect(frames[0].phantom).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a race active while phantom destroys hit one of its empty groups", () => {
|
||||
|
||||
Reference in New Issue
Block a user