fix(rules): finish tilted type-three captures

The original capture handler does not request a launcher reset when a single
ball completes a type-three record during Tilt. It calls the normal ball-end
state machine instead. Rust returned Reset for every single-ball capture,
granting tilted lock-hole captures a free ball and bypassing record 148's
special-respawn decision.

Add an explicit Finish action for tilted single-ball type-three completions.
Lock holes now consume the ball with the tilted drain cue suppressed; record
148 still publishes its required contact first, so the same finish call can
consume it through the special-respawn branch without using a marker.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- 125 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed
- `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:36:29 +02:00
parent c1790c7ff6
commit 505bf0417a
3 changed files with 75 additions and 4 deletions
+71 -3
View File
@@ -247,6 +247,7 @@ enum BallAction {
Suspend,
Reset,
Remove,
Finish,
}
#[derive(Clone, Copy, Debug)]
@@ -885,6 +886,10 @@ impl Game {
self.reset_ball_to_launcher();
return true;
}
BallAction::Finish => {
self.drain(events);
return true;
}
BallAction::Remove => {
self.ball = self
.secondary_ball
@@ -1269,7 +1274,7 @@ impl Game {
}
match scan.action {
BallAction::Keep | BallAction::Suspend => self.secondary_ball = Some(ball),
BallAction::Reset | BallAction::Remove => return true,
BallAction::Reset | BallAction::Remove | BallAction::Finish => return true,
}
collided || scan.action == BallAction::Suspend
}
@@ -1434,6 +1439,10 @@ impl Game {
match scan.action {
BallAction::Keep | BallAction::Suspend => self.ball = ball,
BallAction::Reset => self.reset_ball_to_launcher(),
BallAction::Finish => {
self.ball = ball;
self.drain(events);
}
BallAction::Remove => {
self.ball = self
.secondary_ball
@@ -1727,7 +1736,11 @@ impl Game {
}
events.push(Event::Lock);
if ball_count == 1 {
BallAction::Reset
if self.tilted {
BallAction::Finish
} else {
BallAction::Reset
}
} else {
BallAction::Remove
}
@@ -1762,7 +1775,11 @@ impl Game {
self.multiball_state = MultiballState::Ready;
events.push(Event::Wheel);
Some(if ball_count == 1 {
BallAction::Reset
if self.tilted {
BallAction::Finish
} else {
BallAction::Reset
}
} else {
BallAction::Remove
})
@@ -2646,6 +2663,57 @@ mod tests {
assert!(events.is_empty());
}
#[test]
fn tilted_type_three_completion_finishes_or_special_respawns() {
let mut lock = Game::new(1);
lock.tilted = true;
lock.ball.in_launcher = false;
lock.ball.position = LOCK_HOLES[0].center;
lock.ball.velocity = Vec2::ZERO;
lock.ball.capture_age = 300;
let mut lock_events = Vec::new();
assert_eq!(
lock.check_sensor_objects(
MilliVec::from_position(LOCK_HOLES[0].center),
MilliVec::default(),
&mut lock_events,
),
BallAction::Finish
);
assert_eq!(lock.player().balls, 2);
assert_eq!(lock.player().secondary_score, 10_000);
assert!(lock.ball.in_launcher);
assert!(!lock.tilted);
assert!(lock_events.contains(&Event::Drain));
assert!(!lock_events.contains(&Event::Sound(2008)));
let mut special = Game::new(1);
special.tilted = true;
special.ball.in_launcher = false;
special.ball.position = SPECIAL_HOLE_SENSOR.center;
special.ball.velocity = Vec2::ZERO;
special.ball.capture_age = 300;
let balls = special.player().balls;
let mut special_events = Vec::new();
assert_eq!(
special.check_sensor_objects(
MilliVec::from_position(SPECIAL_HOLE_SENSOR.center),
MilliVec::default(),
&mut special_events,
),
BallAction::Finish
);
assert_eq!(special.player().balls, balls);
assert_eq!(special.ball.position, vec2(17.0, 23.0));
assert!(special.tilted);
assert_eq!(special.multiball_state, MultiballState::Unavailable);
assert_eq!(special.record_contacts[148], 0);
assert!(!special_events.contains(&Event::Drain));
assert!(!special_events
.iter()
.any(|event| event.sound_resource().is_some()));
}
#[test]
fn recovered_target_banks_deactivate_and_rearm_their_line_groups() {
let mut game = Game::new(1);