The client lifts its fog over a strike's disc from its launch until its blast ends #141

Owner

Tracker: #3775

Step 3 of plan #3560. Every client lifts its fog over the disc a launched nuke reveals, from the launch until its blast ends, and the player-facing text records the whole change.

What it delivers

  • The view builds its fog's friends in one exported function, fogFriends (src/client/render/view.ts): the player's units and owned cities through collectFriends, then the reveal's sight sources, which every player gets, spectators included. The reveal's sources go after the units' and cities', so a disc that comes or goes only changes the friend count: the live field rebuilds once, as on a unit's death, and no unit's or city's stamp shifts.
  • The sources: GameView.showReveals takes them for the next frame. main.ts and the Nuke yard (src/mock/nuke.ts) pass the replica's reveals() (step 2). The Map Yard (src/mock/map-yard.ts) passes its own strikes' through the shared addReveals, so ?fog&doom=nuke draws the reveal. Its at-rest strike landed CRATER_AGE (30 s) earlier, past its window, so the at-rest yard draws as before.
  • Player-facing text: a changelog.md "Current" bullet for the whole plan, and a sentence in the help's bomb paragraphs (src/client/menu.ts): from the launch until the blast's cloud has faded, 15 seconds after the impact, every player sees the ground within 13.4 tiles of the target as if a unit of theirs stood there. The units, buildings and wrecks there show, their units may fire on them, and the ground stays explored afterwards.

Tests

src/client/render/reveal.test.ts runs two clients, the launcher's and another player's. Neither has a unit or city with sight over the disc. Each is a Replica given the same frames (the strike in flight, then landed, then gone), each frame reaching it 3 ticks after the server's tick it carries. Each has its own Fog, fed fogFriends at every local tick with dt = 1/30. Read through the fog sheet's live and explored textures:

  • every texel within REVEAL_RADIUS − FOG_FEATHER of the impact reads fogged until the launch frame arrives;
  • it reads seen at every tick from then through due + BLAST_TICKS − 1;
  • it reads explored fog (live 255, explored 0) from due + BLAST_TICKS, before the frame that unlists the strike arrives;
  • no texel farther than REVEAL_RADIUS + FOG_FEATHER from the impact, outside the player's own scout's sight, is lit at any tick.

Mutation check (a scratch dir outside the checkout, vi.mock of src/shared/nuke.ts replacing revealing and addReveals, which calls it): the control passes. tick <= due + BLAST_TICKS fails with the disc seen through tick 770 (due + BLAST_TICKS), and tick < due + BLAST_TICKS − 1 fails with it ending at 768, against the expected 769. With frames applied on their own tick, the <= mutant passed, because the frame that unlists the strike lands on the very tick the window ends. That is why the test delivers its frames late, as a real client receives them.

Draw checks

Map Yard bench (src/bench/map.ts, Playwright WebKit, iPhone 15 landscape, on m5): 8 arms in one map.ts 7 … call per session, 5 sessions × 7 runs, 840 pages, no page errors. Base 6b8535d and head were built from git archive trees. The head's yard dist was built at 1249534 and is byte-identical to the final head 9ffb2a4's (diff -r), because the later commit touches only the test.

Frame times fall into two clusters on every arm (at 1× the base's quartiles are 1.10 / 1.43 / 1.70 ms), so the pooled medians land in either. The paired median takes each arm against its partner loaded in the same rotation (session, run, view). In ms a frame, with the phone estimate (7.5×) in brackets:

pair 1× pooled / paired 0.5× pooled / paired 0.25× pooled / paired
head − base, at rest −0.043 / −0.010 (−0.08) +0.140 / +0.047 (+0.35) −0.050 / +0.067 (+0.50)
head ?fog&doom=nuke − base ?fog&doom=nuke (the reveal's own cost) −0.007 / −0.007 (−0.05) −0.017 / −0.003 (−0.03) −0.073 / −0.010 (−0.08)
head ?doom=battle − &spare −0.007 / +0.040 (+0.30) +0.103 / +0.083 (+0.62) +0.533 / +0.220 (+1.65)
head ?doom=nuke − &spare +0.020 / +0.067 (+0.50) +0.017 / +0.070 (+0.53) +0.137 / +0.057 (+0.42)
  • Head and base draw the same calls and triangles at rest (67 / 65 / 41 calls) and on ?fog&doom=nuke (88 / 86 / 72 calls).
  • The at-rest arms are unfogged, and their only strike landed 30 s earlier, past its window, so nothing differs there but noise. Per-session at-rest medians spread ±0.19 ms.
  • The bench ran beside runs 514 (from 09:24) and 516 (from 09:52, with its sx full gate from 10:01) on the same runner, and another session's headless Chromium. The one-minute load reached 15–21. Sessions took 10–13 minutes.

Node fog cost. The yard's units never move, so the bench does not time the fog's per-move work. Fog.update was timed in Node (tsx, ESM scratch dir, the checkout's fog.ts, which this step leaves unchanged) at 192² with 40 friends of six unit types moving at jittered speeds (±15%), dt = 1/60. The friends were built as fogFriends builds them, through collectFriends and then the disc. view.ts itself does not load under tsx because of its asset imports. The disc was present from frame 300 to 1199, and a unit died at frame 1500. Medians over 9 sessions, under the same load:

  • median frame: disc absent 0.462 ms (the base's case), present 0.481 ms;
  • mean frame: absent 1.86 ms, present 1.86 ms;
  • the frame the disc enters: 1.70 ms; the frame it leaves: 1.62 ms; the frame a unit dies: 1.32 ms. Each is one rebuild of the live field, and single frames ranged 0.9–6.6 ms between sessions.

Renders. The Map Yard at ?fog&doom=nuke, 1×, 0.5× and 0.25×, in the four finishes, reduced motion. Per finish and view: head, base, then head again, from probe copies of both trees (outside the checkout). The probe copies publish the disc's outline on screen (REVEAL_RADIUS + FOG_FEATHER round the impact, projected) and count the disc's tiles outside player 1's own sight. Pixel counts where R+G+B differs by more than 12, inside / outside the outline:

  • The yard's impact (104,67): 44 of the disc's 561 tiles lie outside player 1's sight, so the lift is one region north of the blast. At 0.5× head against base, lighter 28.1–28.5k inside and 3.9–4.1k outside, against head-again controls of 8.8–9.4k and 3.7–3.8k. At 1× the outline covers the whole screen.
  • A strike whose disc lies in fog, impact=100,88: 335 of its 561 tiles lie outside player 1's sight. Lighter pixels inside the outline: 145k at 0.5× (control 0.5–0.6k), 36.5k at 0.25× (control 0.1k) and 71–72k at 1× (control 1.4–1.9k). Outside the outline the lighter and darker counts stay within the control's range at 0.5× (7.7–8.7k against 8.1–8.5k) and 0.25× (1.8–2.3k against 2.0–2.2k). At 1× they run up to 1.8k above that finish's control (lighter 13.1–13.8k against 12.0–13.2k), and the overlay puts every one of those pixels in speckle on the old crater's smoke at the top of the screen.
  • The overlay images (grey, green where the head is lighter, red where darker) show the lift as one solid green region inside the outline. Everything outside it is mixed red and green speckle on the burning crater's smoke and dust ring, which changes between loads.
  • Calls match in every load. At 1× with impact=100,88, the head draws 292 more triangles than the base in DieCast, Toybox and Voxel, and the same as its second load. That is something the lifted fog shows inside the disc; I did not trace which part.

Play

The step changes only the view, main.ts, the mocks, the help and the changelog. The esbuild bundles of src/ai/sim.ts and src/bench/net.ts built from the base and head trees are byte-identical (sim.js a3c510e5…, net.js 4ffd0007…), so the play code does not take in this change. The 14 AI matches and npm run bench were not rerun.

Gates

  • fast on 1249534 (this run's gate 1, tracker gate run 283): passed, npm test 980/980.
  • full on 9ffb2a4, the final head (this run's gate 2, tracker gate run 290): passed. npm test 980/980 in 68.6 s at a one-minute load of about 18, while run 516 worked beside it. test:phone 180/180.
Tracker: [#3775](https://sak.swipelab.com/tasks/3775) Step 3 of plan #3560. Every client lifts its fog over the disc a launched nuke reveals, from the launch until its blast ends, and the player-facing text records the whole change. ## What it delivers - **The view** builds its fog's friends in one exported function, `fogFriends` (src/client/render/view.ts): the player's units and owned cities through `collectFriends`, then the reveal's sight sources, which every player gets, spectators included. The reveal's sources go after the units' and cities', so a disc that comes or goes only changes the friend count: the live field rebuilds once, as on a unit's death, and no unit's or city's stamp shifts. - **The sources:** `GameView.showReveals` takes them for the next frame. main.ts and the Nuke yard (src/mock/nuke.ts) pass the replica's `reveals()` (step 2). The Map Yard (src/mock/map-yard.ts) passes its own strikes' through the shared `addReveals`, so `?fog&doom=nuke` draws the reveal. Its at-rest strike landed CRATER_AGE (30 s) earlier, past its window, so the at-rest yard draws as before. - **Player-facing text:** a changelog.md "Current" bullet for the whole plan, and a sentence in the help's bomb paragraphs (src/client/menu.ts): from the launch until the blast's cloud has faded, 15 seconds after the impact, every player sees the ground within 13.4 tiles of the target as if a unit of theirs stood there. The units, buildings and wrecks there show, their units may fire on them, and the ground stays explored afterwards. ## Tests src/client/render/reveal.test.ts runs two clients, the launcher's and another player's. Neither has a unit or city with sight over the disc. Each is a Replica given the same frames (the strike in flight, then landed, then gone), each frame reaching it 3 ticks after the server's tick it carries. Each has its own Fog, fed `fogFriends` at every local tick with dt = 1/30. Read through the fog sheet's `live` and `explored` textures: - every texel within REVEAL_RADIUS − FOG_FEATHER of the impact reads fogged until the launch frame arrives; - it reads seen at every tick from then through `due + BLAST_TICKS − 1`; - it reads explored fog (live 255, explored 0) from `due + BLAST_TICKS`, before the frame that unlists the strike arrives; - no texel farther than REVEAL_RADIUS + FOG_FEATHER from the impact, outside the player's own scout's sight, is lit at any tick. **Mutation check** (a scratch dir outside the checkout, `vi.mock` of src/shared/nuke.ts replacing `revealing` and `addReveals`, which calls it): the control passes. `tick <= due + BLAST_TICKS` fails with the disc seen through tick 770 (due + BLAST_TICKS), and `tick < due + BLAST_TICKS − 1` fails with it ending at 768, against the expected 769. With frames applied on their own tick, the `<=` mutant passed, because the frame that unlists the strike lands on the very tick the window ends. That is why the test delivers its frames late, as a real client receives them. ## Draw checks **Map Yard bench** (src/bench/map.ts, Playwright WebKit, iPhone 15 landscape, on m5): 8 arms in one `map.ts 7 …` call per session, 5 sessions × 7 runs, 840 pages, no page errors. Base 6b8535d and head were built from `git archive` trees. The head's yard dist was built at 1249534 and is byte-identical to the final head 9ffb2a4's (`diff -r`), because the later commit touches only the test. Frame times fall into two clusters on every arm (at 1× the base's quartiles are 1.10 / 1.43 / 1.70 ms), so the pooled medians land in either. The paired median takes each arm against its partner loaded in the same rotation (session, run, view). In ms a frame, with the phone estimate (7.5×) in brackets: | pair | 1× pooled / paired | 0.5× pooled / paired | 0.25× pooled / paired | |---|---|---|---| | head − base, at rest | −0.043 / −0.010 (−0.08) | +0.140 / +0.047 (+0.35) | −0.050 / +0.067 (+0.50) | | head `?fog&doom=nuke` − base `?fog&doom=nuke` (the reveal's own cost) | −0.007 / −0.007 (−0.05) | −0.017 / −0.003 (−0.03) | −0.073 / −0.010 (−0.08) | | head `?doom=battle` − `&spare` | −0.007 / +0.040 (+0.30) | +0.103 / +0.083 (+0.62) | +0.533 / +0.220 (+1.65) | | head `?doom=nuke` − `&spare` | +0.020 / +0.067 (+0.50) | +0.017 / +0.070 (+0.53) | +0.137 / +0.057 (+0.42) | - Head and base draw the same calls and triangles at rest (67 / 65 / 41 calls) and on `?fog&doom=nuke` (88 / 86 / 72 calls). - The at-rest arms are unfogged, and their only strike landed 30 s earlier, past its window, so nothing differs there but noise. Per-session at-rest medians spread ±0.19 ms. - The bench ran beside runs 514 (from 09:24) and 516 (from 09:52, with its sx `full` gate from 10:01) on the same runner, and another session's headless Chromium. The one-minute load reached 15–21. Sessions took 10–13 minutes. **Node fog cost.** The yard's units never move, so the bench does not time the fog's per-move work. `Fog.update` was timed in Node (tsx, ESM scratch dir, the checkout's fog.ts, which this step leaves unchanged) at 192² with 40 friends of six unit types moving at jittered speeds (±15%), dt = 1/60. The friends were built as `fogFriends` builds them, through `collectFriends` and then the disc. `view.ts` itself does not load under tsx because of its asset imports. The disc was present from frame 300 to 1199, and a unit died at frame 1500. Medians over 9 sessions, under the same load: - median frame: disc absent 0.462 ms (the base's case), present 0.481 ms; - mean frame: absent 1.86 ms, present 1.86 ms; - the frame the disc enters: 1.70 ms; the frame it leaves: 1.62 ms; the frame a unit dies: 1.32 ms. Each is one rebuild of the live field, and single frames ranged 0.9–6.6 ms between sessions. **Renders.** The Map Yard at `?fog&doom=nuke`, 1×, 0.5× and 0.25×, in the four finishes, reduced motion. Per finish and view: head, base, then head again, from probe copies of both trees (outside the checkout). The probe copies publish the disc's outline on screen (REVEAL_RADIUS + FOG_FEATHER round the impact, projected) and count the disc's tiles outside player 1's own sight. Pixel counts where R+G+B differs by more than 12, inside / outside the outline: - **The yard's impact (104,67):** 44 of the disc's 561 tiles lie outside player 1's sight, so the lift is one region north of the blast. At 0.5× head against base, lighter 28.1–28.5k inside and 3.9–4.1k outside, against head-again controls of 8.8–9.4k and 3.7–3.8k. At 1× the outline covers the whole screen. - **A strike whose disc lies in fog, `impact=100,88`:** 335 of its 561 tiles lie outside player 1's sight. Lighter pixels inside the outline: 145k at 0.5× (control 0.5–0.6k), 36.5k at 0.25× (control 0.1k) and 71–72k at 1× (control 1.4–1.9k). Outside the outline the lighter and darker counts stay within the control's range at 0.5× (7.7–8.7k against 8.1–8.5k) and 0.25× (1.8–2.3k against 2.0–2.2k). At 1× they run up to 1.8k above that finish's control (lighter 13.1–13.8k against 12.0–13.2k), and the overlay puts every one of those pixels in speckle on the old crater's smoke at the top of the screen. - The overlay images (grey, green where the head is lighter, red where darker) show the lift as one solid green region inside the outline. Everything outside it is mixed red and green speckle on the burning crater's smoke and dust ring, which changes between loads. - Calls match in every load. At 1× with `impact=100,88`, the head draws 292 more triangles than the base in DieCast, Toybox and Voxel, and the same as its second load. That is something the lifted fog shows inside the disc; I did not trace which part. ## Play The step changes only the view, main.ts, the mocks, the help and the changelog. The esbuild bundles of src/ai/sim.ts and src/bench/net.ts built from the base and head trees are byte-identical (sim.js a3c510e5…, net.js 4ffd0007…), so the play code does not take in this change. The 14 AI matches and `npm run bench` were not rerun. ## Gates - `fast` on 1249534 (this run's gate 1, tracker gate run 283): passed, `npm test` 980/980. - `full` on 9ffb2a4, the final head (this run's gate 2, tracker gate run 290): passed. `npm test` 980/980 in 68.6 s at a one-minute load of about 18, while run 516 worked beside it. `test:phone` 180/180.
agra merged commit bc281f211e into task/3560-a-launched-nuke-reveals-where-it-lands-t 2026-10-24 11:38:45 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
agra/mini-alert!141
No description provided.