diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 175f221..2739218 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -14,8 +14,9 @@ and this project adheres to latch the two-ball capture rule at each entry, exclude the launcher release while that capture is active, and let later single-ball re-entries use all four original release choices. -- Preserve that entry decision through the complete pull phase and migrate the - Greifarm contact owner when a held secondary ball becomes the primary ball. +- Preserve that entry decision through the complete pull phase, scope it to the + entering slot until that ball exits the capture zone, and migrate Greifarm + state when a held secondary ball becomes the primary ball. ## [1.2.0] - 2026-08-31 diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 8156e3b..6a6a177 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -136,6 +136,16 @@ enum ClawBallSlot { Secondary, } +impl ClawBallSlot { + const fn from_ball_number(ball_number: u16) -> Self { + if ball_number == 2 { + Self::Secondary + } else { + Self::Primary + } + } +} + #[derive(Clone, Copy, Debug)] pub struct Claw { pub active: bool, @@ -143,7 +153,7 @@ pub struct Claw { pub bank: ClawSpriteBank, pub ball_suspended: bool, captured_slot: Option, - capture_restricted: Option, + capture_entry: Option<(ClawBallSlot, bool)>, target_frame: u8, frame_accumulator: f32, } @@ -156,7 +166,7 @@ impl Default for Claw { bank: ClawSpriteBank::Opening, ball_suspended: false, captured_slot: None, - capture_restricted: None, + capture_entry: None, target_frame: 10, frame_accumulator: 0.0, } @@ -1045,8 +1055,8 @@ impl Game { events, ) { CaptureStep::Complete => { - let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); + let terminal_frame = self + .next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1595,8 +1605,8 @@ impl Game { ) { CaptureStep::Holding | CaptureStep::Outside => {} CaptureStep::Complete => { - let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); + let terminal_frame = self + .next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1664,7 +1674,7 @@ impl Game { && ball.capture_age == 0 && self.record_contacts[usize::from(record_id)] == 0 { - self.claw.capture_restricted = None; + self.clear_claw_capture_entry_for(ball_number); } return CaptureStep::Outside; } @@ -1683,11 +1693,12 @@ impl Game { && contact_allowed && !(record_id == 148 && self.special_hole_gate == SpecialHoleGate::Suppressed) { - if record_id == 89 && ball.capture_age == 0 && self.claw.capture_restricted.is_none() { + if record_id == 89 && ball.capture_age == 0 && self.claw.capture_entry.is_none() { // Latch the restriction when the ball first enters the deep // capture zone, before the pull/hold phase can outlive the // other active ball. - self.claw.capture_restricted = Some(ball_count > 1); + self.claw.capture_entry = + Some((ClawBallSlot::from_ball_number(ball_number), ball_count > 1)); } if ball.capture_age < 300 { let mut velocity = MilliVec::from_velocity_per_second(ball.velocity); @@ -1743,12 +1754,12 @@ impl Game { return CaptureStep::Complete; } + if record_id == 89 && surface_distance >= -11_000 { + self.clear_claw_capture_entry_for(ball_number); + } if surface_distance > -11_000 && contact == ball_number { self.record_contacts[contact_index] = 0; ball.capture_age = 0; - if record_id == 89 { - self.claw.capture_restricted = None; - } } let velocity = MilliVec::from_velocity_per_second(ball.velocity); if surface_distance <= milli_distance(velocity) @@ -2126,9 +2137,27 @@ impl Game { self.claw.ball_suspended && self.claw.captured_slot == Some(slot) } + fn claw_capture_restricted_for(&self, ball_number: u16) -> bool { + self.claw.capture_entry == Some((ClawBallSlot::from_ball_number(ball_number), true)) + } + + fn clear_claw_capture_entry_for(&mut self, ball_number: u16) { + let slot = ClawBallSlot::from_ball_number(ball_number); + if self + .claw + .capture_entry + .is_some_and(|(owner, _)| owner == slot) + { + self.claw.capture_entry = None; + } + } + fn promote_claw_secondary_to_primary(&mut self) { if self.claw.captured_slot == Some(ClawBallSlot::Secondary) { self.claw.captured_slot = Some(ClawBallSlot::Primary); + if let Some((ClawBallSlot::Secondary, restricted)) = self.claw.capture_entry { + self.claw.capture_entry = Some((ClawBallSlot::Primary, restricted)); + } if self.record_contacts[89] == 2 { self.record_contacts[89] = 1; } @@ -2182,7 +2211,7 @@ impl Game { self.claw.bank = ClawSpriteBank::Closing; self.claw.ball_suspended = true; self.claw.captured_slot = Some(ClawBallSlot::Primary); - self.claw.capture_restricted = None; + self.claw.capture_entry = None; self.claw.frame_accumulator = 0.0; self.ball.velocity = Vec2::ZERO; self.ball.spin = Real48::ZERO; @@ -2205,11 +2234,7 @@ impl Game { self.claw.target_frame = terminal_frame; self.claw.bank = ClawSpriteBank::Closing; self.claw.ball_suspended = true; - self.claw.captured_slot = Some(if ball_number == 2 { - ClawBallSlot::Secondary - } else { - ClawBallSlot::Primary - }); + self.claw.captured_slot = Some(ClawBallSlot::from_ball_number(ball_number)); self.claw.frame_accumulator = 0.0; ball.velocity = Vec2::ZERO; ball.spin = Real48::ZERO; @@ -4513,7 +4538,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert_eq!( game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), @@ -4583,7 +4608,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); game.ball.velocity = Vec2::ZERO; game.ball.capture_age = 300; @@ -4592,10 +4617,112 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert!(matches!(game.claw.target_frame, 1 | 6 | 7)); } + #[test] + fn other_multiball_slot_cannot_clear_the_claw_entry_latch() { + let mut game = Game::new_with_seed(1, 7); + game.ball.in_launcher = false; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + game.secondary_ball = Some(Ball { + position: vec2(210.0, 100.0), + velocity: Vec2::ZERO, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }); + game.object_active.fill(false); + game.score_mode = ScoreMode::Multiball; + let mut events = Vec::new(); + + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + + let mut secondary = game.secondary_ball.take().expect("secondary ball"); + let secondary_position = MilliVec::from_position(secondary.position); + assert_eq!( + game.check_sensor_objects_for_ball( + &mut secondary, + 2, + 2, + secondary_position, + MilliVec::default(), + &mut events, + ) + .action, + BallAction::Keep + ); + game.secondary_ball = Some(secondary); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + } + + #[test] + fn shallow_claw_exit_clears_only_the_owning_entry_latch() { + let mut game = Game::new_with_seed(1, 7); + game.ball.in_launcher = false; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + game.secondary_ball = Some(Ball { + position: vec2(210.0, 240.0), + velocity: Vec2::ZERO, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }); + game.object_active.fill(false); + game.score_mode = ScoreMode::Multiball; + let mut events = Vec::new(); + + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + + game.ball.position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0); + game.ball.velocity = Vec2::ZERO; + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(game.ball.position), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, None); + + game.secondary_ball = None; + game.score_mode = ScoreMode::Normal; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Primary, false)) + ); + } + #[test] fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() { let mut game = Game::new_with_seed(1, 7); @@ -4737,11 +4864,16 @@ mod tests { ); game.secondary_ball = Some(secondary); assert_eq!(game.record_contacts[89], 2); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Secondary, true)) + ); game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events); assert!(game.secondary_ball.is_none()); assert!(game.primary_ball_suspended()); assert_eq!(game.record_contacts[89], 1); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert_eq!(game.score_mode, ScoreMode::Normal); assert!(events.contains(&Event::Drain)); } @@ -4784,7 +4916,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); game.secondary_ball = None; game.score_mode = ScoreMode::Normal; @@ -4808,7 +4940,10 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(false)); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Primary, false)) + ); assert_eq!( game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend diff --git a/tdkpin-rs/web/tdkpin-rs.wasm b/tdkpin-rs/web/tdkpin-rs.wasm index c9d0a06..7d8f9ce 100755 Binary files a/tdkpin-rs/web/tdkpin-rs.wasm and b/tdkpin-rs/web/tdkpin-rs.wasm differ