fix(ui): render armed record 148 item region

Record 148 publishes player item 20 and refreshes word-quad 20 through overlay
A. Its recovered bounds are (9,14)-(25,30). Rust tracked the armed special hole
but never rendered this visible 17x17 table state.

Expose the armed/active special-hole state and composite the exact DAT997 region
until the special respawn consumes record 148's contact. Make the deterministic
effect-seven scenario internally valid by seeding the required contact, so its
framebuffer now covers both the DAT407 effect and item 20.

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 effect-7 --step 0 --screenshot /tmp/tdkpin-effect7-special.png` -- passed; visually inspected at 640x460
- launcher/effect screenshot comparison -- 136 RGB pixels differ inside only the expected 17x17 item region
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:09:55 +02:00
parent 8d436b8a4b
commit ac254f9e45
4 changed files with 19 additions and 2 deletions
+2 -1
View File
@@ -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.
+1 -1
View File
@@ -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. |
+3
View File
@@ -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,
+13
View File
@@ -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);