diff --git a/AGENTS.md b/AGENTS.md index 51b63fa..536b370 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac5a20..b584785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/gameplay.md b/docs/gameplay.md index 2417b25..21a2086 100644 --- a/docs/gameplay.md +++ b/docs/gameplay.md @@ -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. diff --git a/src/entities.asm b/src/entities.asm index 023f7c7..079c8be 100644 --- a/src/entities.asm +++ b/src/entities.asm @@ -1240,6 +1240,7 @@ tank_on_right: ret update_bridge: + call update_bridge_crumble call update_destroyed_road ld a,(bridge_spawn_pending) or a @@ -1248,8 +1249,10 @@ update_bridge: ld (bridge_spawn_pending),a ld a,1 ld (bridge_active),a + ld (bridge_lethal),a xor a ld (destroyed_road_active),a + ld (bridge_crumble_active),a ; Anchor the band on the block boundary: the newest block occupies rows ; 0..phase, so the two latched flat blocks begin at exactly phase+1. ; On the latched byte-aligned banks block_right_col is the first LAND @@ -1331,6 +1334,8 @@ bridge_top_restored: call bridge_clear_road_attributes xor a ld (bridge_active),a + ld (bridge_lethal),a + ld (bridge_crumble_active),a ld (bridge_tank_mode),a ld (bridge_tank_active),a ret @@ -1369,6 +1374,9 @@ update_destroyed_road: ld a,(destroyed_road_active) or a ret z + ld a,(bridge_active) + or a + ret nz ; still crumbling: the bridge path scrolls it ld a,(speed_pixels) or a ret z @@ -1462,36 +1470,6 @@ standard_road_line_segment: ret #endif -bridge_fill_full_bitmap_row: - ; Input A=Y, C=byte. Road rows are inside a bridge band, so land plus - ; bridge makes the complete 256-pixel scanline solid. - cp 16 - ret c - cp PLAYFIELD_BOTTOM - ret nc - call calc_screen_line_addr - ld b,4 - ld a,c -bridge_full_bitmap_byte: - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - ld (hl),a - inc l - djnz bridge_full_bitmap_byte - ret - update_bullet: ld a,(fire_pending) or a @@ -1540,9 +1518,9 @@ move_bullet_up: ; Actor sprites are absent from the bitmap during collision, so the ; crossing aircraft is tested explicitly above. Bridge geometry is also ; explicit because a background hit on it must award points and destroy it. - ld a,(bridge_active) + ld a,(bridge_lethal) or a - jp z,bullet_checks_destroyed_road + jp z,bullet_checks_destroyed_road ; a wreck mid-crumble is no longer a target ld a,(bullet_y) add a,10 ; swept projectile bottom ld b,a @@ -1960,39 +1938,220 @@ destroy_bridge: call add_score_100 call add_score_100 call destroy_bridge_tank_bonus - call bridge_paint_destroyed_road_attributes - ld a,(bridge_y) - ld (bridge_restore_y),a - ld a,16 - ld (bridge_restore_rows),a -destroy_bridge_restore: - ld a,(bridge_restore_rows) - or a - jr z,destroy_bridge_done - ld a,(bridge_restore_y) - cp PLAYFIELD_BOTTOM - jr nc,destroy_bridge_done - ; render_v3_row normally assumes that all bank interiors are already set. - ; The solid span violates that invariant, so clear the complete scanline - ; and use the deliberately slower full-row world reconstruction here. - ld c,0 - call bridge_fill_full_bitmap_row - ld a,(bridge_restore_y) - call render_full_world_row - ld a,(bridge_restore_y) - inc a - ld (bridge_restore_y),a - ld a,(bridge_restore_rows) - dec a - ld (bridge_restore_rows),a - jr destroy_bridge_restore -destroy_bridge_done: + ; The span stops killing anything at the moment of the hit, but it stays + ; bridge_active: the wreck is still drawn, so it must keep scrolling through + ; the ordinary bridge machinery instead of freezing on the screen rows it + ; happened to occupy. Only the hole columns leave the road model. xor a - ld (bridge_active),a + ld (bridge_lethal),a ld a,1 ld (destroyed_road_active),a + call setup_bridge_crumble jp preserve_road_tank_after_bridge +setup_bridge_crumble: + ; Only the span crumbles. On the latched flush banks of a bridge zone it is + ; exactly the water columns, so the land approaches - and the black road + ; edge lines on band rows 1 and 14 - are left alone throughout. + ld a,(bridge_col) + ld (bridge_crumble_min),a + ld b,a + ld a,(bridge_width) + add a,b + dec a + ld (bridge_crumble_max),a + + ; Open the first hole at the column the shot went through, clamped into the + ; span, so the plane can always fly on through where the player aimed. + ld a,(bullet_x) + add a,7 + srl a + srl a + srl a + ld b,a + ld a,(bridge_crumble_min) + cp b + jr c,crumble_hit_above_min + ld b,a ; hit at or left of the span: clamp to min + jr crumble_hit_ready +crumble_hit_above_min: + ld a,(bridge_crumble_max) + cp b + jr nc,crumble_hit_ready + ld b,a ; hit right of the span: clamp to max +crumble_hit_ready: + ld a,b + ld (bridge_hole_left),a + ld (bridge_crumble_col),a + inc a ; a two-byte opening unless the span ends + ld b,a + ld a,(bridge_crumble_max) + cp b + jr nc,crumble_hole_right_ready + ld b,a +crumble_hole_right_ready: + ld a,b + ld (bridge_hole_right),a + ld (bridge_crumble_col_last),a + ld a,1 + ld (bridge_crumble_active),a + xor a + ld (bridge_crumble_phase),a + ld a,BRIDGE_CRUMBLE_PAUSE + ld (bridge_crumble_timer),a + call bridge_dissolve_run ; punch the hole in this very frame + jp bridge_paint_road_attributes ; hole cells turn to water, the rest stays road + +update_bridge_crumble: + ; A chunk goes every BRIDGE_CRUMBLE_PAUSE frames, never one column per + ; frame: the point is a rhythm of separate blasts, not a smooth dissolve. + ; Each blast takes BRIDGE_CRUMBLE_CHUNK bytes off both ends of the hole, + ; which is two 8x8 cells wide by the band's two cells tall. + ld a,(bridge_crumble_active) + or a + ret z + ld a,(bridge_crumble_timer) + dec a + ld (bridge_crumble_timer),a + ret nz + ld a,BRIDGE_CRUMBLE_PAUSE + ld (bridge_crumble_timer),a + + ; Left chunk: the run ends just left of the hole and starts CHUNK further, + ; clamped at the span's first column. + ld a,(bridge_hole_left) + ld b,a + ld a,(bridge_crumble_min) + cp b + jr z,bridge_crumble_left_done + dec b + ld a,b + ld (bridge_crumble_col_last),a + sub BRIDGE_CRUMBLE_CHUNK-1 + ld b,a + ld a,(bridge_crumble_min) + jr c,bridge_crumble_left_clamped ; borrowed past column zero: the span end wins + cp b + jr nc,bridge_crumble_left_clamped + ld a,b +bridge_crumble_left_clamped: + ld (bridge_crumble_col),a + ld (bridge_hole_left),a + call bridge_dissolve_run +bridge_crumble_left_done: + ld a,(bridge_hole_right) + ld b,a + ld a,(bridge_crumble_max) + cp b + jr z,bridge_crumble_right_done + inc b + ld a,b + ld (bridge_crumble_col),a + add a,BRIDGE_CRUMBLE_CHUNK-1 + ld b,a + ld a,(bridge_crumble_max) + cp b + jr c,bridge_crumble_right_clamped + ld a,b +bridge_crumble_right_clamped: + ld (bridge_crumble_col_last),a + ld (bridge_hole_right),a + call bridge_dissolve_run +bridge_crumble_right_done: + call bridge_crumble_blast + call bridge_paint_road_attributes + + ; The span is gone once the hole reaches both ends. Handing over to the + ; plain destroyed road from here keeps every later frame on the cheap path. + ld a,(bridge_hole_left) + ld b,a + ld a,(bridge_crumble_min) + cp b + ret nz + ld a,(bridge_hole_right) + ld b,a + ld a,(bridge_crumble_max) + cp b + ret nz + xor a + ld (bridge_crumble_active),a + ld (bridge_active),a + jp bridge_paint_destroyed_road_attributes + +bridge_crumble_blast: + ; One burst per stage, alternating ends, so the blasts read as walking out + ; from the hit instead of one continuous roar in the middle. + ld a,(bridge_crumble_phase) + xor 1 + ld (bridge_crumble_phase),a + or a + jr z,bridge_crumble_blast_right + ld a,(bridge_hole_left) + jr bridge_crumble_blast_at +bridge_crumble_blast_right: + ld a,(bridge_hole_right) +bridge_crumble_blast_at: + add a,a + add a,a + add a,a + ld c,a + ld a,(bridge_y) + add a,2 + ld b,a + ld a,c + jp start_hit_explosion + +bridge_dissolve_run: + ; Input bridge_crumble_col..bridge_crumble_col_last inclusive. Rewrite those + ; columns over the whole sixteen-line band with true world bytes, which is + ; what turns road back into river. The world query is used rather than a + ; plain zero fill because the FUEL depot belongs to that model and may share + ; these columns. +bridge_dissolve_run_column: + call bridge_dissolve_column + ld a,(bridge_crumble_col) + ld b,a + ld a,(bridge_crumble_col_last) + cp b + ret c ; a one-column run, or a malformed one + ret z + ld a,b + inc a + ld (bridge_crumble_col),a + jr bridge_dissolve_run_column + +bridge_dissolve_column: + ; Rewrite bridge_crumble_col over the band's sixteen scanlines, taken from + ; the band's live top edge so the hole scrolls with everything else. + ld a,(bridge_y) + ld (bridge_crumble_row),a + ld b,16 +bridge_dissolve_row: + ld a,(bridge_crumble_row) + cp PLAYFIELD_BOTTOM + ret nc ; the rest of the band is below the playfield + cp 16 + jr c,bridge_dissolve_next ; a band hit while still entering the screen + push bc + ld a,(bridge_crumble_col) + ld c,a + ld a,(bridge_crumble_row) + call get_world_background_byte + ld c,a + ld a,(bridge_crumble_row) + call calc_screen_line_addr + ld a,(bridge_crumble_col) + add a,l + ld l,a + ld (hl),c + pop bc +bridge_dissolve_next: + ld a,(bridge_crumble_row) + inc a + ld (bridge_crumble_row),a + djnz bridge_dissolve_row + ret + preserve_road_tank_after_bridge: ; destroy_bridge_tank_bonus has already removed a mode-1 tank if it was on ; the span. A crossing tank still on an approach stops there and becomes a @@ -2323,7 +2482,7 @@ check_player_background_pixels: ld a,6 ld (player_core_rows),a - ld a,(bridge_active) + ld a,(bridge_lethal) or a jr z,player_world_prepare ld a,(player_core_y) diff --git a/src/sprite_renderer.asm b/src/sprite_renderer.asm index 9df7be4..51dcb19 100644 --- a/src/sprite_renderer.asm +++ b/src/sprite_renderer.asm @@ -118,6 +118,8 @@ init_entities: ld (bridge_tank_side),a ld (bridge_tank_move_phase),a ld (bridge_active),a + ld (bridge_lethal),a + ld (bridge_crumble_active),a ld (destroyed_road_active),a ld (player_move),a ld (player_bank),a @@ -2291,6 +2293,9 @@ get_world_terrain_byte: sub b cp 16 jr nc,world_background_course + ld a,(background_query_col) + call bridge_column_still_road + jr z,world_background_course ; blown out of the span: plain river again ld a,255 ret #else @@ -2319,6 +2324,9 @@ world_background_band_solid: ld a,(bridge_active) or a jp z,world_background_course ; destroyed road: course model serves it + ld a,(background_query_col) + call bridge_column_still_road + jp z,world_background_course ; blown out of the span: plain river again ld a,255 ret #endif @@ -2477,6 +2485,9 @@ load_world_terrain_triplet: sub b cp 16 jr nc,world_triplet_course + ld a,(bridge_crumble_active) + or a + jr nz,world_triplet_band_crumbling ld a,255 ld (world_background_byte_0),a ld (world_background_byte_1),a @@ -2503,11 +2514,17 @@ load_world_terrain_triplet: ld a,(bridge_active) or a jr z,world_triplet_band_course_base + ld a,(bridge_crumble_active) + or a + jr nz,world_triplet_band_crumble_base ld a,255 ld (world_background_byte_0),a ld (world_background_byte_1),a ld (world_background_byte_2),a jr world_triplet_band_overlay +world_triplet_band_crumble_base: + call world_triplet_band_crumbling + jr world_triplet_band_overlay world_triplet_band_course_base: call world_triplet_course world_triplet_band_overlay: @@ -2548,6 +2565,30 @@ world_triplet_overlay_byte_1: ret #endif +world_triplet_band_crumbling: + ; Mid-crumble the band is a mixture, so the course model provides the base + ; and only the columns still standing are put back to solid road. Doing it + ; this way round keeps the hole exactly as wide as the renderer drew it. + call world_triplet_course + ld hl,world_background_byte_0 ; the three slots are consecutive + ld a,(background_query_col) + ld c,a + ld b,3 +world_triplet_crumble_byte: + push bc + push hl + ld a,c + call bridge_column_still_road + pop hl + pop bc + jr z,world_triplet_crumble_next + ld (hl),255 +world_triplet_crumble_next: + inc hl + inc c + djnz world_triplet_crumble_byte + ret + world_triplet_course: call resolve_course_block_index ld b,a @@ -3320,6 +3361,79 @@ write_world_shifted_2_addr_ready: ld (world_write_addr),hl jp write_world_shifted_2_row +bridge_paint_attr_span: + ; Input HL=the span's first colour cell; returns HL one cell past its last. + ; Colour must never run ahead of the bitmap: an 0xff road byte under water + ; paper shows its INK, which is what used to make an abandoned span glare + ; solid green. Only columns whose bitmap has genuinely become river here + ; take the water colour. + ld a,(bridge_crumble_active) + or a + jr nz,bridge_attr_span_crumbled + ld a,(bridge_width) + ld b,a + ld a,(bridge_center_attr) ; brown intact span or normal broken water + jr bridge_paint_attr_run +bridge_attr_span_crumbled: + ld a,(bridge_col) + ld c,a + ld a,(bridge_hole_left) + sub c + jr z,bridge_attr_span_hole + ld b,a + ld a,(bridge_center_attr) + call bridge_paint_attr_run +bridge_attr_span_hole: + ld a,(bridge_hole_left) + ld c,a + ld a,(bridge_hole_right) + sub c + inc a + ld b,a + ld a,0x4c ; the blown columns are ordinary water + call bridge_paint_attr_run + ld a,(bridge_col) + ld c,a + ld a,(bridge_width) + add a,c + ld c,a ; first column past the span + ld a,(bridge_hole_right) + inc a + ld b,a + ld a,c + sub b + ret z + ld b,a + ld a,(bridge_center_attr) +bridge_paint_attr_run: + ld (hl),a + inc l + djnz bridge_paint_attr_run + ret + +bridge_column_still_road: + ; Input A=byte column. Returns NZ while that column is road the world model + ; must serve as solid, and Z once the crumble has blown it away. Columns + ; outside the span are approaches and can never be in the hole. + ld b,a + ld a,(bridge_crumble_active) + or a + jr z,bridge_column_road + ld a,(bridge_hole_left) + cp b + jr z,bridge_column_gone + jr nc,bridge_column_road ; left of the hole + ld a,(bridge_hole_right) + cp b + jr c,bridge_column_road ; right of the hole +bridge_column_gone: + xor a + ret +bridge_column_road: + ld a,1 + or a + ret + bridge_fill_rows: ; Input A=start Y, B=row count, C=byte value. The bridge is maintained as ; a persistent bitmap band: only rows entering or leaving its 16-line @@ -3364,12 +3478,16 @@ bridge_fill_row: ld a,(bridge_col) add a,l ld l,a + ld a,(bridge_crumble_active) + or a + jr nz,bridge_fill_row_crumbled ld a,(bridge_width) ld b,a bridge_fill_byte: ld (hl),c inc l djnz bridge_fill_byte +bridge_fill_row_done: ld a,(bridge_rows_left) dec a ld (bridge_rows_left),a @@ -3378,6 +3496,46 @@ bridge_fill_byte: ei ret +bridge_fill_row_crumbled: + ; The hole is river the course renderer already keeps correct, so the band + ; writer steps over it rather than stamping road back into it. Only two + ; runs remain: the span up to the hole, and the span beyond it. + ld a,(bridge_col) + ld d,a + ld a,(bridge_width) + add a,d + ld d,a ; first column past the span + ld a,(bridge_col) + ld e,a + ld a,(bridge_hole_left) + sub e + jr z,bridge_fill_skip_hole + ld b,a +bridge_fill_left_byte: + ld (hl),c + inc l + djnz bridge_fill_left_byte +bridge_fill_skip_hole: + ld a,(bridge_hole_left) + ld e,a + ld a,(bridge_hole_right) + sub e + inc a + add a,l + ld l,a + ld a,(bridge_hole_right) + inc a + ld e,a + ld a,d + sub e + jr z,bridge_fill_row_done + ld b,a +bridge_fill_right_byte: + ld (hl),c + inc l + djnz bridge_fill_right_byte + jr bridge_fill_row_done + bridge_refresh_edges: ; The bank pass touches only speed_pixels residue classes modulo eight. ; A 16-line bridge therefore has two affected rows per residue: four rows @@ -3422,6 +3580,9 @@ bridge_refresh_edge_row: cp PLAYFIELD_BOTTOM ret nc call calc_screen_line_addr + ld a,(bridge_crumble_active) + or a + jr nz,bridge_refresh_edge_crumbled ld a,(bridge_col) add a,l ld l,a @@ -3437,6 +3598,38 @@ bridge_refresh_edge_row: ld (hl),255 ret +bridge_refresh_edge_crumbled: + ; The bank pass only ever reaches the span's outermost columns, and the + ; hole reaches them last, so each of the four is repaired individually + ; rather than as a pair - one end can go a stage before the other. + ld a,(bridge_col) + ld e,a + call bridge_refresh_edge_byte + inc e + call bridge_refresh_edge_byte + ld a,(bridge_col) + ld b,a + ld a,(bridge_width) + add a,b + sub 2 + ld e,a + call bridge_refresh_edge_byte + inc e + jp bridge_refresh_edge_byte + +bridge_refresh_edge_byte: + ; Input HL=row base, E=byte column; preserves both. + ld a,e + call bridge_column_still_road + ret z + push hl + ld a,e + add a,l + ld l,a + ld (hl),255 + pop hl + ret + bridge_paint_road_attributes: ; A 16-line bridge covers two attribute rows when aligned and three while ; between cells. White-on-green road attributes fill the land on both @@ -3546,13 +3739,7 @@ bridge_attr_left_byte: inc l djnz bridge_attr_left_byte bridge_attr_paint_span: - ld a,(bridge_width) - ld b,a - ld a,(bridge_center_attr) ; brown intact span or normal broken water -bridge_attr_span_byte: - ld (hl),a - inc l - djnz bridge_attr_span_byte + call bridge_paint_attr_span ; road colour, minus whatever has crumbled ld a,(bridge_col) ld b,a @@ -3761,13 +3948,7 @@ timex_bridge_attr_left_byte: inc l djnz timex_bridge_attr_left_byte timex_bridge_attr_span: - ld a,(bridge_width) - ld b,a - ld a,(bridge_center_attr) -timex_bridge_attr_center_byte: - ld (hl),a - inc l - djnz timex_bridge_attr_center_byte + call bridge_paint_attr_span ; road colour, minus whatever has crumbled ld a,(bridge_col) ld b,a ld a,(bridge_width) diff --git a/src/state.asm b/src/state.asm index abc71bf..5777d7a 100644 --- a/src/state.asm +++ b/src/state.asm @@ -119,6 +119,7 @@ tank_shell_dir: db 1 ; 1=right, 255=left tank_shell_target_x: db 120 ; safe water point selected when fired tank_splash_timer: db 0 bridge_active: db 0 ; bridge bitmap persists between frames +bridge_lethal: db 0 ; cleared by the hit: the wreck kills nobody destroyed_road_active: db 0 ; white approaches remain after span removal bridge_y: db 0 bridge_col: db 0 @@ -126,6 +127,20 @@ bridge_width: db 0 bridge_rows_left: db 0 bridge_restore_y: db 0 bridge_restore_rows: db 0 +; A hit blows a hole through the span and the hole then widens outward in +; two-byte steps, one step every BRIDGE_CRUMBLE_PAUSE frames. The span stays +; bridge_active throughout, so the wreck keeps scrolling with the world through +; the ordinary bridge machinery; only bridge_lethal and the hole columns change. +bridge_crumble_active: db 0 +bridge_crumble_timer: db 0 ; frames left before the next chunk goes +bridge_crumble_phase: db 0 ; alternates which side gets the burst +bridge_crumble_min: db 0 ; first span column +bridge_crumble_max: db 0 ; last span column +bridge_hole_left: db 0 ; lowest span column already blown away +bridge_hole_right: db 0 ; highest span column already blown away +bridge_crumble_col: db 0 ; scratch for the column being dissolved +bridge_crumble_col_last: db 0 ; scratch for the last column of the run +bridge_crumble_row: db 0 ; scratch for its current scanline bridge_attr_row: db 0 bridge_attr_rows: db 0 bridge_center_attr: db 0x4a ; BRIGHT red intact span or normal water diff --git a/tools/build.py b/tools/build.py index 92e2b66..8430923 100644 --- a/tools/build.py +++ b/tools/build.py @@ -796,6 +796,10 @@ def main() -> int: "AUTOPILOT": 0, "AUTOPILOT_NOFIRE": 0, "PLAYFIELD_BOTTOM": 168, + # Bridge crumble rhythm: frames between blasts, and the number of byte + # columns each blast takes off both ends of the hole. + "BRIDGE_CRUMBLE_PAUSE": 8, + "BRIDGE_CRUMBLE_CHUNK": 2, } for item in args.define: if "=" in item: diff --git a/tools/zrcp_bridge_screenshot.py b/tools/zrcp_bridge_screenshot.py index 456ee9a..168d94b 100644 --- a/tools/zrcp_bridge_screenshot.py +++ b/tools/zrcp_bridge_screenshot.py @@ -135,7 +135,9 @@ def capture(z, syms, prefix): z.cmd("enter-cpu-step") try: dump = z.peek(0x4000, 0x1B00) - st = z.peek(syms["bridge_active"], 5) + # Read each symbol on its own: the state block's layout is not a + # contract, and a byte inserted into it used to silently shift these. + st = [z.peek(syms[n])[0] for n in SYMBOLS[:5]] live = [n for n in ENTITY_FLAGS if z.peek(syms[n])[0]] finally: render(dump, prefix + ".png") @@ -163,8 +165,9 @@ def main(): return 0 deadline = time.time() + 600 while time.time() < deadline: - st = z.peek(syms["bridge_active"], 3) - active, destroyed, by = st[0], st[1], st[2] + active = z.peek(syms["bridge_active"])[0] + destroyed = z.peek(syms["destroyed_road_active"])[0] + by = z.peek(syms["bridge_y"])[0] want = ( (mode == "intact" and active) or (mode == "destroyed" and destroyed) diff --git a/tools/zrcp_tail_profiler.py b/tools/zrcp_tail_profiler.py index 68e71cf..e33ca4a 100644 --- a/tools/zrcp_tail_profiler.py +++ b/tools/zrcp_tail_profiler.py @@ -84,17 +84,33 @@ def sym_for(pc): buf = b"" _, buf = read_until_prompt(sock, buf) # banner + # Every command that briefly enters cpu-step mode is refused while any + # emulator menu (the tape-load progress window, say) is open. Under + # --vo null no keypress can dismiss it, so close it over the protocol and + # retry - otherwise the setup commands fail silently and the run drains the + # 1M-entry default buffer, or the drain reads a buffer that is still moving. + # Closing menus cancels a tape load, so only profile an already running game. + reply = {} + + def with_menu_retry(cmd, buf, tries=6): + for _ in range(tries): + resp, buf = command(sock, buf, cmd) + text = resp.decode().strip() + reply[cmd] = resp + print(f"{cmd.decode()}: {text or 'ok'}", flush=True) + if "Can not enter cpu step" not in text: + return buf + _, buf = command(sock, buf, b"close-all-menus") + time.sleep(1) + raise SystemExit(f"{cmd.decode()}: blocked by an emulator menu") + # PLAY_SECONDS=0 means drain-only: the emulator already holds a recorded # buffer (for example after a previous run died mid-drain in step mode). if play_seconds > 0: - # set-max-size works only while history is enabled; starting the - # recorder briefly enters cpu-step mode, which fails while any menu - # (like the tape-load progress window) is open - retry until closed. for cmd in (b"cpu-history enabled yes", b"cpu-history set-max-size %d" % MAX_SIZE, b"cpu-history get-max-size"): - resp, buf = command(sock, buf, cmd) - print(f"{cmd.decode()}: {resp.decode().strip() or 'ok'}", flush=True) + buf = with_menu_retry(cmd, buf) text = "" for _ in range(30): @@ -107,19 +123,15 @@ def sym_for(pc): else: raise SystemExit(f"could not start cpu history: {text}") - resp, buf = command(sock, buf, b"cpu-history clear") - print(f"cpu-history clear: {resp.decode().strip() or 'ok'}", flush=True) + buf = with_menu_retry(b"cpu-history clear", buf) print(f"silent for {play_seconds:.0f}s of play", flush=True) time.sleep(play_seconds) - resp, buf = command(sock, buf, b"enter-cpu-step") - text = resp.decode().strip() - if text: - print(f"enter-cpu-step: {text}", flush=True) # may already be stepped + buf = with_menu_retry(b"enter-cpu-step", buf) try: - resp, buf = command(sock, buf, b"cpu-history get-size") - size = int(resp.split()[0]) + buf = with_menu_retry(b"cpu-history get-size", buf) + size = int(reply[b"cpu-history get-size"].split()[0]) size = min(size, MAX_SIZE) print(f"draining {size} entries", flush=True) trace_new_to_old = []