fix(multiball): defer fifth-lock panel for survivor

The fifth type-three lock always pays and transfers its accumulated award, but
the original starts the DAT600 panel only when no multiball is active. Rust
started the panel unconditionally and suspended the surviving ball. A later
capture with all five contacts already set could also shift the award to
320,000 instead of retaining the binary's 160,000 cap.

Derive each lock award from the post-capture filled count capped at five. Keep
the transfer, multiplier increment, and extra ball during multiball, but defer
the panel until a single-ball completion. Cover the fifth multiball capture and
its survivor recapture end to end.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- 120 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:06:27 +02:00
parent d8cb7591b5
commit 2d14823c04
3 changed files with 64 additions and 22 deletions
+60 -20
View File
@@ -1683,30 +1683,42 @@ impl Game {
CaptureStep::Outside | CaptureStep::Holding => continue,
CaptureStep::Complete => {}
}
let filled_before = self.wheel_holes.iter().filter(|filled| **filled).count();
self.wheel_holes[index] = true;
let award = 10_000_u32 << u32::try_from(filled_before).unwrap_or_default();
let player = &mut self.players[self.current_player];
player.secondary_score = player.secondary_score.wrapping_add(award);
if self.wheel_holes.iter().all(|filled| *filled) {
let transfer = self.secondary_score_display();
self.add_score(transfer, events);
let player = &mut self.players[self.current_player];
player.secondary_score = 0;
player.score_multiplier = player.score_multiplier.wrapping_add(1);
player.extra_balls = player.extra_balls.wrapping_add(1);
self.panel_frame = Some(0);
}
events.push(Event::Lock);
return Some(if ball_count == 1 {
BallAction::Reset
} else {
BallAction::Remove
});
return Some(self.complete_lock_hole(index, ball_count, events));
}
None
}
fn complete_lock_hole(
&mut self,
index: usize,
ball_count: u16,
events: &mut Vec<Event>,
) -> BallAction {
self.wheel_holes[index] = true;
let filled = self.wheel_holes.iter().filter(|filled| **filled).count();
let shift = u32::try_from(filled.min(5)).unwrap_or(5);
let award = 5_000_u32 << shift;
let player = &mut self.players[self.current_player];
player.secondary_score = player.secondary_score.wrapping_add(award);
if filled == 5 {
let transfer = self.secondary_score_display();
self.add_score(transfer, events);
let player = &mut self.players[self.current_player];
player.secondary_score = 0;
player.score_multiplier = player.score_multiplier.wrapping_add(1);
player.extra_balls = player.extra_balls.wrapping_add(1);
if ball_count == 1 {
self.panel_frame = Some(0);
}
}
events.push(Event::Lock);
if ball_count == 1 {
BallAction::Reset
} else {
BallAction::Remove
}
}
#[allow(clippy::too_many_arguments)]
fn check_wheel_reset(
&mut self,
@@ -3903,6 +3915,34 @@ mod tests {
);
}
#[test]
fn fifth_multiball_lock_defers_panel_and_caps_survivor_reaward() {
let mut game = Game::new(1);
game.wheel_holes = [true, true, true, true, false];
game.players[0].secondary_score = 150_000;
game.score_mode = ScoreMode::Multiball;
let mut events = Vec::new();
assert_eq!(
game.complete_lock_hole(4, 2, &mut events),
BallAction::Remove
);
assert_eq!(game.player().score, 620_000);
assert_eq!(game.player().secondary_score, 0);
assert_eq!(game.player().score_multiplier, 2);
assert_eq!(game.panel_frame, None);
game.score_mode = ScoreMode::Normal;
assert_eq!(
game.complete_lock_hole(4, 1, &mut events),
BallAction::Reset
);
assert_eq!(game.player().score, 940_000);
assert_eq!(game.player().secondary_score, 0);
assert_eq!(game.player().score_multiplier, 3);
assert_eq!(game.panel_frame, Some(0));
}
#[test]
fn type_three_capture_pulls_holds_and_publishes_contact_sentinels() {
let sensor = LOCK_HOLES[0];