Skip to content

Blow a destroyed span apart in stages instead of erasing it - #4

Open
mpasternak wants to merge 1 commit into
mainfrom
feat/bridge-crumble
Open

Blow a destroyed span apart in stages instead of erasing it#4
mpasternak wants to merge 1 commit into
mainfrom
feat/bridge-crumble

Conversation

@mpasternak

Copy link
Copy Markdown
Contributor

A destroyed bridge used to disappear between one frame and the next. It now goes
in chunks: the shot punches a two-byte hole where the player aimed, wide enough
to fly through, and the hole eats outward two byte columns per side every eight
frames. Each stage restarts the impact explosion and its AY burst, alternating
ends, so the span goes with a rhythm of separate blasts rather than a fade.

Observed sequence for one span, straight from the emulator:

20-21 → 18-23 → 16-25 → 14-27 → 12-28 → 10-28 → 8-28 → 6-28 → 4-28

The two numbers are BRIDGE_CRUMBLE_CHUNK and BRIDGE_CRUMBLE_PAUSE in
tools/build.py, so the rhythm can be retuned without touching the assembly:

make OUTDIR=build-test DEFINES="-D BRIDGE_CRUMBLE_PAUSE=12 -D BRIDGE_CRUMBLE_CHUNK=3" standard

Why the span stays active

Nothing else repaints the wreck, so a span removed from the model freezes on
the screen rows it happened to occupy while the world scrolls out from under
it. The span therefore stays bridge_active for the whole crumble and the
ordinary bridge machinery keeps moving the band, filling the rows entering at
its bottom, restoring the rows leaving its top and repainting its colour cells.

Only the hole columns are excluded, and they are excluded everywhere at once:

  • the world model reports river for them, so the plane flying through the gap
    composes and restores against water instead of stamping road back into it
  • the band writer steps over them
  • the colour pass gives them water while the surviving stumps keep road colour

That last point is also what removes the green flash. Colouring the whole span
as water at the moment of the hit left 0xff road bytes under water paper, and
a lit pixel takes the INK, so the abandoned road glared solid green until a
column happened to be redrawn. Colour now never runs ahead of the bitmap.

Lethality is separate from existence: bridge_lethal is cleared by the hit, so
the wreck kills nothing and stops being a target while it crumbles. Player and
projectile collision test that flag instead of bridge_active. When the hole
reaches both ends the bridge hands over to the plain destroyed road, whose
per-frame cost is unchanged.

Verification

Several natural destructions on both builds in ZEsarUX:

  • the hole widens in two-column steps outward from the hit, on both sides
  • bridge_y increases monotonically throughout, so the wreck scrolls with the
    world (measured 38→166, 40→150, 48→144 across three spans)
  • bridge_lethal is 0 for every sample of every crumble
  • the handover leaves bridge_active=0 with destroyed_road_active=1
  • screenshots show red stumps over clean river, no green anywhere

Trace profiler over 30 s of autopilot play: 16.6 % frame overruns against
16.1 % on main
. Two builds never play the same scene, so that difference is
noise rather than a regression.

Tool repairs needed to get those numbers

  • tools/zrcp_tail_profiler.py: every cpu-history command briefly enters
    cpu-step mode, which the emulator refuses while a menu is open — and under
    --vo null that menu cannot be dismissed from the keyboard. The tool fell
    back to a 1 M-entry buffer without saying so and then died draining it. It
    now closes menus over the protocol and retries each step.
  • tools/zrcp_bridge_screenshot.py: read five state bytes as one block and was
    quietly shifted by the new bridge_lethal byte. Reads each symbol by name now.
  • AGENTS.md records these traps, plus two that cost real time here:
    write-memory silently freezes the CPU (needs exit-cpu-step), and closing
    menus cancels a tape load in progress, so only profile an already running game.

Handoff

Nothing here is blocked, but for whoever picks this up next:

  • Playtest the rhythm. Eight frames per chunk and two columns per side
    are a first guess. A wide span takes about a second end to end; if that
    drags, BRIDGE_CRUMBLE_PAUSE=6 is the first knob.
  • A shot into a surviving stump does nothing. Mid-crumble the bullet
    falls through to the destroyed-road path, which queries the course model
    and sees water. Making a second hit accelerate the crumble would be a
    small, self-contained follow-up.
  • The right-hand blast is offset by one column. It is placed at
    bridge_hole_right * 8, the left edge of the last blown column, so the
    16-pixel burst spills one column into the surviving road. It reads fine
    as an explosion eating the next section, but it is not centred.
  • The known open bug is still open: bottom rows of resident sprites
    (balloon, ships, helicopter, FUEL depot) erode island edges, because the
    safe X is computed from a single scanline for sprites 8–32 lines tall.
    Recorded as item 1 in docs/TODO.md with the wrong theory already
    disproved; the fix belongs in placement, not in the writers. Eaten land
    stays lethal while looking like water.

Test TAPs for this branch are staged at ~/attribute-raid-test/crumble-standard.tap
and crumble-timex.tap.

🤖 Generated with Claude Code

A destroyed bridge used to disappear between one frame and the next. It now
goes in chunks: the shot punches a two-byte hole where the player aimed, wide
enough to fly through, and the hole eats outward two byte columns per side
every eight frames. Each stage restarts the impact explosion and its AY burst,
alternating ends, so the span goes with a rhythm of separate blasts rather than
a smooth fade. Both numbers are BRIDGE_CRUMBLE_CHUNK and BRIDGE_CRUMBLE_PAUSE
in tools/build.py, so the rhythm can be retuned with DEFINES= alone.

The span stays bridge_active for the whole sequence. That is the part which
matters: nothing else repaints the wreck, so a span removed from the model
freezes on the screen rows it happened to occupy while the world scrolls out
from under it. Left active, the ordinary bridge machinery keeps moving the
band, filling the rows entering at its bottom, restoring the rows leaving its
top and repainting its colour cells; only the hole columns are excluded, and
they are excluded everywhere at once. The world model reports river for them,
so the plane flying through composes and restores against water instead of
punching road back into the gap; the band writer steps over them; and the
colour pass gives them water while the surviving stumps keep road colour.

That last point also removes the green flash. Colouring the whole span as water
at the moment of the hit left 0xff road bytes under water paper, and a lit
pixel takes the INK, so the abandoned road glared solid green until a column
happened to be redrawn. Colour now never runs ahead of the bitmap.

Lethality is now separate from existence: bridge_lethal is cleared by the hit,
so the wreck kills nothing and stops being a target while it crumbles. Player
and projectile collision test that flag instead of bridge_active. When the hole
reaches both ends the bridge hands over to the plain destroyed road, whose
per-frame cost is unchanged.

Verified on both builds in ZEsarUX over several natural destructions: the hole
widens in two-column steps from the hit outward, bridge_y increases
monotonically throughout (the wreck scrolls), the plane is never lethal to
itself against it, and the handover leaves bridge_active=0 with
destroyed_road_active=1. Screenshots confirm red stumps over clean river with
no green. Trace profiler over 30s of autopilot play: 16.6% frame overruns
against 16.1% on main, which is scene noise between two runs.

Two tools needed repair to get those measurements. The profiler's cpu-history
commands are refused while an emulator menu is open, which under --vo null
cannot be dismissed from the keyboard; it silently fell back to a 1M-entry
buffer and then failed to drain. It now closes menus over the protocol and
retries every step. The screenshot tool read five state bytes as one block and
was quietly shifted by the new bridge_lethal byte, so it now reads each symbol
by name. AGENTS.md records these traps along with the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant