fix(game): restore the 281-frame panel sequence

Replace the immediate five-hole cleanup with the original 281-callback panel
state machine. Suspend physics, clear contact/item state at the recovered
boundaries, and emit WAVE 2013, WAVE 2012, and stop events at frames
1/45/58/66/129/159/222/230/238 before resuming after cleanup.

Decode DAT600's four AND/OR mask-image pairs into transparent native textures
and render all nine phase formulas with the original source rectangles,
destination offsets, variable heights, and opaque wheel copies. Add a `panel`
simulation scenario for deterministic trace and framebuffer validation.

Test Plan:
- `cargo test --all-targets` -- passed, 72 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed
- rendered panel frames 1, 66, 159, 238, and completion -- visually verified
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 17:49:04 +02:00
parent 6337291461
commit 4b84460c93
7 changed files with 303 additions and 11 deletions
+67 -2
View File
@@ -66,6 +66,7 @@ pub enum Event {
Tilt,
Drain,
HighScoreCandidate(u32),
StopSound,
Sound(u16),
}
@@ -87,7 +88,8 @@ impl Event {
| Self::Nudge
| Self::Tilt
| Self::Drain
| Self::HighScoreCandidate(_) => None,
| Self::HighScoreCandidate(_)
| Self::StopSound => None,
}
}
}
@@ -267,6 +269,7 @@ pub struct Game {
pub claw: Claw,
pub bumper_flash: [f32; 3],
pub wheel_animation: f32,
pub panel_frame: Option<u16>,
pub nudge_shake: f32,
pub launcher_charge: f32,
pub finished: bool,
@@ -310,6 +313,7 @@ impl Game {
claw: Claw::default(),
bumper_flash: [0.0; 3],
wheel_animation: 0.0,
panel_frame: None,
nudge_shake: 0.0,
launcher_charge: 0.0,
finished: false,
@@ -452,7 +456,8 @@ impl Game {
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
self.update_claw(elapsed, events);
if !self.claw.ball_suspended {
let panel_active = self.update_panel_completion(events);
if !self.claw.ball_suspended && !panel_active {
for _ in 0..substeps {
self.fixed_update(STEP_SECONDS, events);
self.advance_secondary_ball(events);
@@ -488,6 +493,31 @@ impl Game {
self.apply_flipper_kicks();
}
fn update_panel_completion(&mut self, events: &mut Vec<Event>) -> bool {
let Some(frame) = self.panel_frame else {
return false;
};
if frame >= 281 {
self.panel_frame = None;
self.wheel_holes.fill(false);
return true;
}
if frame == 1 {
for record_id in 129..=133 {
self.record_contacts[record_id] = 0;
}
}
let next = frame + 1;
self.panel_frame = Some(next);
match next {
1 | 66 | 159 | 238 => events.push(Event::Sound(2013)),
45 | 129 | 222 => events.push(Event::StopSound),
58 | 230 => events.push(Event::Sound(2012)),
_ => {}
}
true
}
fn advance_tilt_counter(&mut self, elapsed: f32) {
self.tilt_counter_accumulator += elapsed;
while self.tilt_counter_accumulator >= self.claw_frame_seconds {
@@ -1025,6 +1055,7 @@ impl Game {
player.secondary_score = 0;
player.score_multiplier = player.score_multiplier.wrapping_add(1);
player.extra_balls = player.extra_balls.wrapping_add(1);
self.panel_frame = Some(0);
}
self.reset_ball_to_launcher();
events.push(Event::Lock);
@@ -1354,6 +1385,7 @@ impl Game {
self.tilt_counter_accumulator = 0.0;
self.bumper_flash.fill(0.0);
self.wheel_animation = 0.0;
self.panel_frame = None;
self.claw = Claw::default();
self.flipper_inputs = Flippers::default();
self.flippers = Flippers::default();
@@ -2410,6 +2442,39 @@ mod tests {
assert_eq!(game.capture_age, 0);
}
#[test]
fn panel_completion_uses_all_281_frames_and_original_sound_boundaries() {
let mut game = Game::new(1);
game.panel_frame = Some(0);
game.wheel_holes.fill(true);
for record_id in 129..=133 {
game.record_contacts[record_id] = 99;
}
let mut events = Vec::new();
for _ in 0..282 {
game.timer_tick(0.030, 0, &mut events);
}
assert_eq!(game.panel_frame, None);
assert_eq!(game.wheel_holes, [false; 5]);
assert!(game.record_contacts[129..=133].iter().all(|contact| *contact == 0));
assert_eq!(
events,
[
Event::Sound(2013),
Event::StopSound,
Event::Sound(2012),
Event::Sound(2013),
Event::StopSound,
Event::Sound(2013),
Event::StopSound,
Event::Sound(2012),
Event::Sound(2013),
]
);
}
#[test]
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
for (frame, expected_position, expected_velocity) in [