fix(ui): render partial target-bank states

Records 90 through 104 and 109 through 120 deactivate in linked three-line
groups. For each group, the flag-bearing head switches its recovered bounds
from overlay B to overlay A until the complete bank rearms every record. Rust
updated collision activity but never drew these nine intermediate states.

Render the five bumper-bank and four TDK-bank head regions directly from the
175-record ledger. Extend the bank regression to prove partial activation and
post-completion clearing, and seed two partial groups in the deterministic
rules framebuffer.

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-banks.png` -- passed; partial bumper and TDK bank regions visually inspected at 640x460
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:12:45 +02:00
parent 71938c0062
commit f53cd5ab2b
5 changed files with 48 additions and 8 deletions
+3
View File
@@ -99,6 +99,9 @@ and this project adheres to
51-53 and targets 140-147/150-152. 51-53 and targets 140-147/150-152.
- Render the five cumulative bumper-value lamps from original player items 1-5 - 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. at their recovered Word-Quad bounds as the value rises from 1,000 to 6,000.
- Render the nine in-bank hit states for record heads 90/93/96/99/102 and
109/112/115/118 from their exact overlay-A bounds until bank completion
rearms each three-line group.
- Replace the free-running wheel animation with the original six-callback - Replace the free-running wheel animation with the original six-callback
91x90 target rotation, WAVE 2011 start, exact DAT600 frames/target positions, 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 and state-6 rotation of the five per-player contact/item values. It advances
+1 -1
View File
@@ -19,7 +19,7 @@ implementation.
| Subsystem | Rust status | Evidence and boundary | | 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. 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)`. | | 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. The nine partially completed bumper/TDK bank heads use their record-ledger overlay-A bounds until each three-line group rearms. 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. | | 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. | | 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. | | 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. |
+16
View File
@@ -26,6 +26,17 @@ const BUMPER_VALUE_REGIONS: [(i32, i32, i32, i32); 5] = [
(177, 280, 195, 299), (177, 280, 195, 299),
(193, 303, 211, 322), (193, 303, 211, 322),
]; ];
const BANK_TARGET_REGIONS: [(u8, i32, i32, i32, i32); 9] = [
(90, 282, 231, 306, 246),
(93, 282, 217, 306, 231),
(96, 282, 202, 306, 216),
(99, 282, 187, 306, 201),
(102, 282, 174, 306, 186),
(109, 192, 214, 206, 236),
(112, 178, 209, 191, 231),
(115, 163, 204, 177, 226),
(118, 149, 198, 163, 221),
];
fn bumper_value_indicator_count(value: u32) -> usize { fn bumper_value_indicator_count(value: u32) -> usize {
usize::try_from(value / 1_000) usize::try_from(value / 1_000)
@@ -742,6 +753,11 @@ impl App {
); );
} }
} }
for (record_id, left, top, right, bottom) in BANK_TARGET_REGIONS {
if game.bank_target_lit(record_id) {
self.draw_active_table_region(left, top, right - left + 1, bottom - top + 1);
}
}
if game.player().diamond_segments > 0 { if game.player().diamond_segments > 0 {
let index = usize::from(game.player().diamond_segments.min(9) - 1); let index = usize::from(game.player().diamond_segments.min(9) - 1);
draw_texture(&self.assets.diamond[index], 123.0, 300.0, WHITE); draw_texture(&self.assets.diamond[index], 123.0, 300.0, WHITE);
+27 -2
View File
@@ -452,6 +452,16 @@ impl Game {
} }
} }
pub(crate) fn begin_target_scenario(&mut self) {
self.ball.in_launcher = false;
self.ball.position = vec2(170.0, 230.0);
self.players[0].bumper_value = 6_000;
self.wheel_holes = [true, false, true, false, false];
self.target_rotation_state = Some(0);
self.object_active[90] = false;
self.object_active[109] = false;
}
fn fire_launcher(&mut self) { fn fire_launcher(&mut self) {
self.launcher_velocity_milli = self self.launcher_velocity_milli = self
.launcher_velocity_milli .launcher_velocity_milli
@@ -1937,6 +1947,11 @@ impl Game {
self.object_active[object_id] self.object_active[object_id]
} }
pub fn bank_target_lit(&self, object_id: u8) -> bool {
matches!(object_id, 90 | 93 | 96 | 99 | 102 | 109 | 112 | 115 | 118)
&& !self.object_active[usize::from(object_id)]
}
pub fn effect_target_active(&self) -> bool { pub fn effect_target_active(&self) -> bool {
self.object_active[usize::from(EFFECT_SENSOR.id)] self.object_active[usize::from(EFFECT_SENSOR.id)]
} }
@@ -2630,17 +2645,27 @@ mod tests {
let mut game = Game::new(1); let mut game = Game::new(1);
let mut events = Vec::new(); let mut events = Vec::new();
for object_id in [90, 93, 96, 99, 102] { for object_id in [90, 93, 96, 99] {
game.apply_wall_rule(object_id, &mut events); game.apply_wall_rule(object_id, &mut events);
} }
assert!(game.bank_target_lit(90));
assert!(game.bank_target_lit(99));
assert!(!game.bank_target_lit(102));
game.apply_wall_rule(102, &mut events);
assert_eq!(game.player().bumper_value, 2_000); assert_eq!(game.player().bumper_value, 2_000);
assert!(game.object_active[90..=104].iter().all(|active| *active)); assert!(game.object_active[90..=104].iter().all(|active| *active));
assert!(!game.bank_target_lit(90));
for object_id in [109, 112, 115, 118] { for object_id in [109, 112, 115] {
game.apply_wall_rule(object_id, &mut events); game.apply_wall_rule(object_id, &mut events);
} }
assert!(game.bank_target_lit(109));
assert!(game.bank_target_lit(115));
assert!(!game.bank_target_lit(118));
game.apply_wall_rule(118, &mut events);
assert_eq!(game.player().diamond_segments, 1); assert_eq!(game.player().diamond_segments, 1);
assert!(game.object_active[109..=120].iter().all(|active| *active)); assert!(game.object_active[109..=120].iter().all(|active| *active));
assert!(!game.bank_target_lit(109));
} }
#[test] #[test]
+1 -5
View File
@@ -230,11 +230,7 @@ impl Simulation {
game.wheel_holes.fill(true); game.wheel_holes.fill(true);
} }
if scenario == Scenario::Targets { if scenario == Scenario::Targets {
game.ball.in_launcher = false; game.begin_target_scenario();
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];
} }
if scenario == Scenario::Effect7 { if scenario == Scenario::Effect7 {
game.begin_effect_scenario(7); game.begin_effect_scenario(7);