diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index e5363b2..175f221 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -14,6 +14,8 @@ 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. ## [1.2.0] - 2026-08-31 diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 8988eae..8156e3b 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -143,7 +143,7 @@ pub struct Claw { pub bank: ClawSpriteBank, pub ball_suspended: bool, captured_slot: Option, - capture_restricted: bool, + capture_restricted: Option, target_frame: u8, frame_accumulator: f32, } @@ -156,7 +156,7 @@ impl Default for Claw { bank: ClawSpriteBank::Opening, ball_suspended: false, captured_slot: None, - capture_restricted: false, + capture_restricted: None, target_frame: 10, frame_accumulator: 0.0, } @@ -979,10 +979,7 @@ impl Game { .secondary_ball .take() .expect("a multiball capture must leave the other slot active"); - if self.claw.captured_slot == Some(ClawBallSlot::Secondary) { - self.claw.captured_slot = Some(ClawBallSlot::Primary); - self.score_mode = ScoreMode::Normal; - } + self.promote_claw_secondary_to_primary(); return true; } } @@ -1049,7 +1046,7 @@ impl Game { ) { CaptureStep::Complete => { let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted); + self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1599,7 +1596,7 @@ impl Game { CaptureStep::Holding | CaptureStep::Outside => {} CaptureStep::Complete => { let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted); + self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1663,6 +1660,12 @@ impl Game { || predicted_position.y < center.y.wrapping_sub(broadphase_margin) || predicted_position.y > center.y.wrapping_add(broadphase_margin) { + if record_id == 89 + && ball.capture_age == 0 + && self.record_contacts[usize::from(record_id)] == 0 + { + self.claw.capture_restricted = None; + } return CaptureStep::Outside; } let dx = center.x.wrapping_sub(current_position.x); @@ -1680,11 +1683,11 @@ impl Game { && contact_allowed && !(record_id == 148 && self.special_hole_gate == SpecialHoleGate::Suppressed) { - if record_id == 89 && ball.capture_age == 0 { + if record_id == 89 && ball.capture_age == 0 && self.claw.capture_restricted.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 = ball_count > 1; + self.claw.capture_restricted = Some(ball_count > 1); } if ball.capture_age < 300 { let mut velocity = MilliVec::from_velocity_per_second(ball.velocity); @@ -1744,7 +1747,7 @@ impl Game { self.record_contacts[contact_index] = 0; ball.capture_age = 0; if record_id == 89 { - self.claw.capture_restricted = false; + self.claw.capture_restricted = None; } } let velocity = MilliVec::from_velocity_per_second(ball.velocity); @@ -2123,6 +2126,16 @@ impl Game { self.claw.ball_suspended && self.claw.captured_slot == Some(slot) } + fn promote_claw_secondary_to_primary(&mut self) { + if self.claw.captured_slot == Some(ClawBallSlot::Secondary) { + self.claw.captured_slot = Some(ClawBallSlot::Primary); + if self.record_contacts[89] == 2 { + self.record_contacts[89] = 1; + } + self.score_mode = ScoreMode::Normal; + } + } + pub fn primary_ball_suspended(&self) -> bool { self.claw_holds_slot(ClawBallSlot::Primary) } @@ -2169,7 +2182,7 @@ impl Game { self.claw.bank = ClawSpriteBank::Closing; self.claw.ball_suspended = true; self.claw.captured_slot = Some(ClawBallSlot::Primary); - self.claw.capture_restricted = false; + self.claw.capture_restricted = None; self.claw.frame_accumulator = 0.0; self.ball.velocity = Vec2::ZERO; self.ball.spin = Real48::ZERO; @@ -2244,7 +2257,7 @@ impl Game { (ball.position, ball.velocity) = release; } else { (self.ball.position, self.ball.velocity) = release; - self.claw.captured_slot = Some(ClawBallSlot::Primary); + self.promote_claw_secondary_to_primary(); } } None => { @@ -2291,8 +2304,7 @@ impl Game { .secondary_ball .take() .expect("the claw-held secondary ball must remain in slot two"); - self.claw.captured_slot = Some(ClawBallSlot::Primary); - self.score_mode = ScoreMode::Normal; + self.promote_claw_secondary_to_primary(); events.push(Event::Drain); return; } @@ -4501,7 +4513,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert!(game.claw.capture_restricted); + assert_eq!(game.claw.capture_restricted, Some(true)); assert_eq!( game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), @@ -4565,6 +4577,14 @@ mod tests { ); game.secondary_ball = None; game.score_mode = ScoreMode::Normal; + + game.ball.velocity = vec2(100.0, 0.0); + assert_eq!( + game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), + BallAction::Keep + ); + assert_eq!(game.claw.capture_restricted, Some(true)); + game.ball.velocity = Vec2::ZERO; game.ball.capture_age = 300; @@ -4572,7 +4592,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert!(game.claw.capture_restricted); + assert_eq!(game.claw.capture_restricted, Some(true)); assert!(matches!(game.claw.target_frame, 1 | 6 | 7)); } @@ -4716,10 +4736,12 @@ mod tests { BallAction::Suspend ); game.secondary_ball = Some(secondary); + assert_eq!(game.record_contacts[89], 2); 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.score_mode, ScoreMode::Normal); assert!(events.contains(&Event::Drain)); } @@ -4762,7 +4784,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert!(game.claw.capture_restricted); + assert_eq!(game.claw.capture_restricted, Some(true)); game.secondary_ball = None; game.score_mode = ScoreMode::Normal; @@ -4786,7 +4808,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert!(!game.claw.capture_restricted); + assert_eq!(game.claw.capture_restricted, Some(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 bad9928..c9d0a06 100755 Binary files a/tdkpin-rs/web/tdkpin-rs.wasm and b/tdkpin-rs/web/tdkpin-rs.wasm differ