From 71938c00620b132a7f3c3a82a8824b8d6b5ecf0e Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 23 Aug 2026 20:11:06 +0200 Subject: [PATCH] fix(ui): render cumulative bumper-value items The five word quads at indices 1 through 5 are the visible progression for the recovered bumper-value byte. The original activates them cumulatively as the value rises from 1,000 to 6,000. Rust implemented the score progression but did not render any of these player items during gameplay. Share the exact quad bounds with the attract animation and draw the first value/1000-1 regions from DAT997. Seed the deterministic targets scenario with the maximum recovered value so framebuffer validation covers all five lamps. Test Plan: - `cargo test --workspace --all-targets --all-features` -- 120 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed - `cargo run -- --simulate targets --step 26 --screenshot /tmp/tdkpin-targets-bumper.png` -- passed; all five recovered regions visually inspected at 640x460 - `git diff --cached --check` -- passed --- tdkpin-rs/CHANGELOG.md | 2 ++ tdkpin-rs/RECONSTRUCTION.md | 2 +- tdkpin-rs/src/app.rs | 32 +++++++++++++++++++++++++------- tdkpin-rs/src/simulation.rs | 1 + 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 8b102ae..46acfc7 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -97,6 +97,8 @@ and this project adheres to - Replace invented bumper outline/cooldown effects with the original per-record 5/20/10 callback countdowns and exact overlay-A render rectangles for bumpers 51-53 and targets 140-147/150-152. +- Render the five cumulative bumper-value lamps from original player items 1-5 + at their recovered Word-Quad bounds as the value rises from 1,000 to 6,000. - Replace the free-running wheel animation with the original six-callback 91x90 target rotation, WAVE 2011 start, exact DAT600 frames/target positions, and state-6 rotation of the five per-player contact/item values. It advances diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index cebced5..b9238a9 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -19,7 +19,7 @@ implementation. | Subsystem | Rust status | Evidence and boundary | | --- | --- | --- | -| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. DAT400-407 expose the current per-player bonus/collect/release effect at `(55,79)` while preserving the saved 14x14 wheel overlap. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. Record 148's armed player-item-20 state uses its exact overlay-A Word-Quad `(9,14)-(25,30)`. | +| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. DAT400-407 expose the current per-player bonus/collect/release effect at `(55,79)` while preserving the saved 14x14 wheel overlap. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. Player items 1-5 cumulatively light the five recovered bumper-value Word-Quads from 2,000 through 6,000 points. Record 148's armed player-item-20 state uses its exact overlay-A Word-Quad `(9,14)-(25,30)`. | | Audio | Exact samples and recovered dispatch | All 16 mono PCM WAV resources are embedded unchanged. Playback is emitted at the reconstructed call sites, including multi-sound bank completions, nudge-then-tilt ordering, and Tilt suppression of the pre-reset WAVE-2008 drain cue. Like Win16 `SndPlaySound(SND_ASYNC|SND_NODEFAULT)`, each new resource stops the previous one and plays at the host mixer level without an application-side attenuation. WAV 2022 is loaded by the original generic resource loop but has no playback call and is therefore never played by Rust. | | Help and languages | Exact | Original resource images 1001-1005 are displayed directly. | | Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Every static record retains its `+0x49` layer mask; the scanner selects layer 1 below the old Y value 250,000 and layer 2 at or above it, while mask 3 records remain shared. Before detection, the raw `1000:9b69` predicted position must lie in the record bounds derived with the registered five-pixel ball margin. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Detection retains unresolved normal/material candidates so later scan-time motion changes can participate in the selected response. Raw stack slot `SS:...d8a2` makes the first detected record ID win; later nearer candidates are stored but never applied, and both C/Rust have two-candidate regression coverage for this quirk. Each flipper uses its exact two line records plus moving tip circle in both positions. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. | diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 24080b5..cad022e 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -19,6 +19,20 @@ const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [ [(21.0, 60.0), (9.0, 28.0), (35.0, 7.0), (62.0, 25.0), (54.0, 56.0)], [(8.0, 35.0), (26.0, 8.0), (59.0, 18.0), (59.0, 50.0), (27.0, 61.0)], ]; +const BUMPER_VALUE_REGIONS: [(i32, i32, i32, i32); 5] = [ + (103, 304, 121, 323), + (119, 280, 137, 299), + (148, 272, 166, 291), + (177, 280, 195, 299), + (193, 303, 211, 322), +]; + +fn bumper_value_indicator_count(value: u32) -> usize { + usize::try_from(value / 1_000) + .unwrap_or(0) + .saturating_sub(1) + .min(BUMPER_VALUE_REGIONS.len()) +} fn visible_score_digits(mut value: u32) -> Vec<(u8, u8)> { let mut digits = Vec::with_capacity(8); @@ -533,13 +547,7 @@ impl App { } if let Some(active_quad) = self.attract.active_word_quad() { - let (left, top, right, bottom) = [ - (103, 304, 121, 323), - (119, 280, 137, 299), - (148, 272, 166, 291), - (177, 280, 195, 299), - (193, 303, 211, 322), - ][active_quad]; + let (left, top, right, bottom) = BUMPER_VALUE_REGIONS[active_quad]; self.draw_active_table_region(left, top, right - left + 1, bottom - top + 1); } @@ -738,6 +746,12 @@ impl App { let index = usize::from(game.player().diamond_segments.min(9) - 1); draw_texture(&self.assets.diamond[index], 123.0, 300.0, WHITE); } + for &(left, top, right, bottom) in BUMPER_VALUE_REGIONS + .iter() + .take(bumper_value_indicator_count(game.player().bumper_value)) + { + self.draw_active_table_region(left, top, right - left + 1, bottom - top + 1); + } for (object_id, region) in [ (6, Rect::new(151.0, 389.0, 13.0, 49.0)), (153, Rect::new(11.0, 344.0, 13.0, 49.0)), @@ -1326,6 +1340,10 @@ mod tests { assert_eq!(displayed_player_markers(3, 0, true), 2); assert_eq!(displayed_player_markers(3, 0, false), 3); assert_eq!(displayed_player_markers(3, 4, true), 6); + assert_eq!(bumper_value_indicator_count(1_000), 0); + assert_eq!(bumper_value_indicator_count(2_000), 1); + assert_eq!(bumper_value_indicator_count(6_000), 5); + assert_eq!(bumper_value_indicator_count(u32::MAX), 5); } #[test] diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index e536425..1a7eaa4 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -232,6 +232,7 @@ impl Simulation { if scenario == Scenario::Targets { game.ball.in_launcher = false; game.ball.position = macroquad::prelude::vec2(170.0, 230.0); + game.players[0].bumper_value = 6_000; game.target_rotation_state = Some(0); game.wheel_holes = [true, false, true, false, false]; }