diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index d89fc98..8b102ae 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -92,7 +92,8 @@ and this project adheres to upward from the shooter lane. - Treat record 148 as the special/multiball hole represented by player item 20; capturing it preserves all five lock-hole items and contacts instead of - inventing an immediate wheel reset. + inventing an immediate wheel reset. Its exact 17x17 overlay-A Word-Quad at + `(9,14)` remains visible until the required contact is consumed. - 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. diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index a28d2de..cebced5 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. | +| 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)`. | | 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 b3b92f1..24080b5 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -771,6 +771,9 @@ impl App { }, ); } + if game.special_hole_active() { + self.draw_active_table_region(9, 14, 17, 17); + } if game.ball.in_launcher { draw_texture_ex( &self.assets.plunger, diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index c37c404..ea9bbe7 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -446,6 +446,10 @@ impl Game { pub(crate) fn begin_effect_scenario(&mut self, effect: u8) { self.target_effect = effect.min(7); self.object_active[usize::from(EFFECT_SENSOR.id)] = self.target_effect != 0; + if self.target_effect == 7 { + self.multiball_state = MultiballState::Ready; + self.record_contacts[148] = 2; + } } fn fire_launcher(&mut self) { @@ -1937,6 +1941,13 @@ impl Game { self.object_active[usize::from(EFFECT_SENSOR.id)] } + pub fn special_hole_active(&self) -> bool { + matches!( + self.multiball_state, + MultiballState::Ready | MultiballState::Active + ) + } + pub fn player_effect(&self) -> u8 { self.target_effect.min(7) } @@ -2794,6 +2805,7 @@ mod tests { .secondary_ball .expect("effect seven should spawn a second ball"); assert_eq!(game.multiball_state, MultiballState::Active); + assert!(game.special_hole_active()); assert_eq!(spawned.position, vec2(17.0, 23.0)); assert_eq!( MilliVec::from_velocity_per_second(spawned.velocity), @@ -2822,6 +2834,7 @@ mod tests { MilliVec { x: 0, y: 3_040 } ); assert_eq!(game.multiball_state, MultiballState::Unavailable); + assert!(!game.special_hole_active()); assert_eq!(game.record_contacts[148], 0); assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);