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:
ddidderr committed 2026-08-23 20:06:27 +02:00
1 parent d8cb7591b5
commit 2d14823c04
3 files changed
+64 -22

No files matched your search

+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];