fix(divergence): scope claw latch to captured slot
The Greifarm entry restriction was still shared across both multiball slots. An unrelated upper-playfield ball could clear the entering ball's latch during its own record-89 scan, while a shallow aborted entry could retain a stale choice. Store the latch owner with the capture-time restriction, permit only that slot to clear it when leaving the deep capture zone, and migrate the owner when a held secondary ball becomes primary. Test Plan: - `just --justfile tdkpin-rs/justfile test` -- passed (151 game, 8 service tests) - `just --justfile tdkpin-rs/justfile clippy` -- passed - `just --justfile tdkpin-rs/justfile build-production` -- passed - `just --justfile tdkpin-rs/justfile web-build` -- passed - `cargo +nightly fmt --manifest-path tdkpin-rs/Cargo.toml --all -- --check` -- passed - `rumdl check --flavor commonmark tdkpin-rs/CHANGELOG.md tdkpin-rs/AGENTS.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
+158
-23
@@ -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<ClawBallSlot>,
|
||||
capture_restricted: Option<bool>,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user