Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,28 @@ python3 tools/zrcp_tail_profiler.py build-bench-std/attribute-raid.map \
30 /tmp/profile.txt
```

`PLAY_SECONDS 0` only drains an already recorded history. Pitfalls: run
one profiling cycle per emulator launch (toggling cpu-history can pop the
emulator menu); do not sample with `get-registers` polling (it aliases to
the command-service point); entry counts are not T-states — idle frames
have MORE entries, not fewer.
`PLAY_SECONDS 0` only drains an already recorded history.

Pitfalls, all of them learned the hard way:

- Run **one profiling cycle per emulator launch**. A second cycle on the
same instance reads whatever state the first one left behind, which shows
up as a trace that is 100% ROM.
- **Wait until the game is actually playing before starting the profiler.**
Every cpu-history command briefly enters cpu-step mode, which the emulator
refuses while a menu is open; the tool now clears that with
`close-all-menus`, and doing so *cancels a tape load in progress*, leaving
the machine in BASIC. Poll a game symbol (`player_x` changing) first.
- **`write-memory` silently drops the emulator into cpu-step.** Follow every
poke with `exit-cpu-step` or the machine freezes and nothing else happens.
- Reading a multi-byte value one byte at a time straddles the frame that
changes it and invents intermediate states. Read adjacent bytes in a single
`read-memory`.
- Do not sample with `get-registers` polling (it aliases to the
command-service point).
- Entry counts are not T-states — idle frames have MORE entries, not fewer.
- Two builds never play the same scene, so overrun percentages compare only
loosely; a difference of a point or so is scene noise, not a regression.

### tools/zrcp_bridge_screenshot.py — frozen visual verification

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ both TAPs to every tagged release.

### Added

- Bridges crumble instead of vanishing. The shot punches a hole the plane can
fly through, and the hole then eats outward from it two byte columns at a
time, one chunk every eight frames, each with its own explosion and blast so
the span goes with a rhythm rather than a fade. The wreck stays part of the
bridge model for the whole sequence, so it scrolls down with the world like
the road it still is, and the surviving stumps keep their road colour while
only the blown columns turn to river.

- Play-in-browser page: every push to `main` deploys the current 128K TAP to
GitHub Pages, embedded in the vendored JSSpeccy 3 emulator
(`site/`, `.github/workflows/pages.yml`), linked from the README.
Expand Down
37 changes: 30 additions & 7 deletions docs/gameplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,34 @@ Bridge placement alternates between the naturally reached river width and a
deliberately narrow section (at most roughly 112 pixels of water), so not every
crossing is generated over a broad river.

Destroying a bridge clears all 16 affected bitmap scanlines before rebuilding
the complete banks, island, and river; the normal dirty renderer updates only
bank edges and therefore cannot reconstruct a row which has been cleared
completely. The brown centre span becomes blue water, while the road
approaches remain on both banks and keep scrolling down with the world - as a
white band on Timex with no further bitmap work, and with their two black
edge lines maintained per frame on the standard build.
Destroying a bridge does not remove the span in one frame. The shot punches a
two-byte hole where the player aimed, wide enough to fly through, and the hole
then widens outward by `BRIDGE_CRUMBLE_CHUNK` byte columns on each side every
`BRIDGE_CRUMBLE_PAUSE` frames (defaults: two columns, eight frames - a 16x16
pixel chunk per side, the band being two character rows tall). Each stage
restarts the impact explosion and its AY burst, alternating ends, so the span
goes with a rhythm of separate blasts instead of a smooth dissolve. Both
constants live in `tools/build.py` and can be overridden with `DEFINES=` to
retune the rhythm without touching the source.

The span stays `bridge_active` for the whole crumble, and that is what keeps
the wreck scrolling with the world: the ordinary bridge machinery still moves
the band, fills the rows entering at its bottom, restores the rows leaving its
top, and repaints its colour cells. Only the hole columns are excluded, and
they are excluded consistently - the world model reports river for them, the
band writer steps over them, and the colour pass gives them water while the
surviving road keeps its own attribute. Colouring the whole span as water at
the moment of the hit is what used to make the abandoned road glare solid
green: an `0xff` road byte under water paper shows its green INK. A hole column
is rewritten from the world model rather than zero-filled, because the FUEL
depot belongs to that model and may share those columns.

Lethality is separate from existence. `bridge_lethal` is cleared by the hit, so
the wreck kills nothing and is no longer a target while it crumbles;
`check_player_background_pixels` tests that flag rather than `bridge_active`.
Once the hole reaches both ends of the span the bridge hands over to the plain
destroyed road, which needs no per-frame bitmap work: the brown centre is
already blue water, while the road approaches remain on both banks and keep
scrolling down with the world - as a white band on Timex, and with their two
black edge lines maintained per frame on the standard build.

Loading
Loading