fix(game): restore six-state target rotation

Replace the free-running 68x60 wheel animation with the original six-callback
91x90 DAT600 mechanism. Start WAVE 2011 at state one, advance once per selected
detail timer, and rotate the five per-player lock contact/item values exactly at
state six.

Decode the 17x17 DAT600 target image/mask pair and render all six recovered
position sets over the correct atlas or overlay-B base. Preserve filled targets
in their resting state after rotation and add a deterministic `targets` scenario
for trace and framebuffer validation.

Test Plan:
- `cargo test --all-targets` -- passed, 74 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed
- rendered all six target-rotation states -- visually verified
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 17:53:40 +02:00
parent 4b84460c93
commit 096d923c79
7 changed files with 151 additions and 43 deletions
+52 -8
View File
@@ -268,7 +268,7 @@ pub struct Game {
flipper_inputs: Flippers,
pub claw: Claw,
pub bumper_flash: [f32; 3],
pub wheel_animation: f32,
pub target_rotation_state: Option<u8>,
pub panel_frame: Option<u16>,
pub nudge_shake: f32,
pub launcher_charge: f32,
@@ -312,7 +312,7 @@ impl Game {
flipper_inputs: Flippers::default(),
claw: Claw::default(),
bumper_flash: [0.0; 3],
wheel_animation: 0.0,
target_rotation_state: None,
panel_frame: None,
nudge_shake: 0.0,
launcher_charge: 0.0,
@@ -456,6 +456,7 @@ impl Game {
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
self.update_claw(elapsed, events);
self.update_target_rotation(events);
let panel_active = self.update_panel_completion(events);
if !self.claw.ball_suspended && !panel_active {
for _ in 0..substeps {
@@ -493,6 +494,29 @@ impl Game {
self.apply_flipper_kicks();
}
fn update_target_rotation(&mut self, events: &mut Vec<Event>) {
let Some(state) = self.target_rotation_state else {
return;
};
if state >= 6 {
self.target_rotation_state = None;
return;
}
let next = state + 1;
self.target_rotation_state = Some(next);
if next == 1 {
events.push(Event::Sound(2011));
}
if next == 6 {
self.wheel_holes.rotate_left(1);
let first = self.record_contacts[129];
for record_id in 129..133 {
self.record_contacts[record_id] = self.record_contacts[record_id + 1];
}
self.record_contacts[133] = first;
}
}
fn update_panel_completion(&mut self, events: &mut Vec<Event>) -> bool {
let Some(frame) = self.panel_frame else {
return false;
@@ -591,7 +615,6 @@ impl Game {
#[allow(clippy::too_many_lines)]
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
self.wheel_animation = (self.wheel_animation - dt).max(0.0);
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
for flash in &mut self.bumper_flash {
*flash = (*flash - dt).max(0.0);
@@ -898,9 +921,11 @@ impl Game {
}
}
}
if wall.flags & 0x0400 != 0 {
self.wheel_animation = 0.65;
events.push(Event::Sound(2011));
if wall.flags & 0x0400 != 0
&& self.target_rotation_state.is_none()
&& self.panel_frame.is_none()
{
self.target_rotation_state = Some(0);
}
if wall.flags & 0x0004 != 0 {
events.push(Event::Sound(2012));
@@ -1081,7 +1106,6 @@ impl Game {
CaptureStep::Complete => {}
}
self.wheel_holes.fill(false);
self.wheel_animation = 0.65;
self.multiball_state = MultiballState::Ready;
self.reset_ball_to_launcher();
events.push(Event::Wheel);
@@ -1384,7 +1408,7 @@ impl Game {
self.tilt_counter = 0;
self.tilt_counter_accumulator = 0.0;
self.bumper_flash.fill(0.0);
self.wheel_animation = 0.0;
self.target_rotation_state = None;
self.panel_frame = None;
self.claw = Claw::default();
self.flipper_inputs = Flippers::default();
@@ -2475,6 +2499,26 @@ mod tests {
);
}
#[test]
fn target_rotation_uses_six_callbacks_and_rotates_player_state() {
let mut game = Game::new(1);
game.wheel_holes = [true, false, true, false, false];
game.record_contacts[129..=133].copy_from_slice(&[11, 22, 33, 44, 55]);
game.target_rotation_state = Some(0);
let mut events = Vec::new();
for expected in 1..=6 {
game.timer_tick(0.030, 0, &mut events);
assert_eq!(game.target_rotation_state, Some(expected));
}
assert_eq!(events, [Event::Sound(2011)]);
assert_eq!(game.wheel_holes, [false, true, false, false, true]);
assert_eq!(&game.record_contacts[129..=133], &[22, 33, 44, 55, 11]);
game.timer_tick(0.030, 0, &mut events);
assert_eq!(game.target_rotation_state, None);
}
#[test]
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
for (frame, expected_position, expected_velocity) in [