fix(scoring): preserve bumper sound precedence

Bumper flags are dispatched before the variable bumper award in the original
collision response. Rust duplicated the primary- and secondary-ball rule path
and appended WAVE 2006 after score mutation, which hid WAVE 2007 whenever that
award crossed a media/extra-ball threshold.

Share one bumper-rule helper for both ball slots, arm the five-callback visual
countdown, dispatch WAVE 2006, and only then add the weak or kicked bumper score.
An exact event-order regression keeps the threshold sound as the final audible
sample.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- 116 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 19:56:41 +02:00
parent 50764f6151
commit df66530b86
3 changed files with 51 additions and 31 deletions
+47 -28
View File
@@ -835,20 +835,8 @@ impl Game {
}
}
if let Some(index) = hit_circle
.and_then(|circle_id| BUMPERS.iter().position(|bumper| bumper.id == circle_id))
&& !self.tilted
{
let points = self
.player()
.bumper_value
.saturating_sub(if auxiliary_fired { 0 } else { 1_000 });
if points != 0 {
self.add_score(points, events);
}
self.record_countdowns[usize::from(BUMPERS[index].id)] = 5;
events.push(Event::Bumper);
events.push(Event::Sound(2006));
if let Some(circle_id) = hit_circle {
self.apply_bumper_rule(circle_id, auxiliary_fired, events);
}
match scan.action {
@@ -1237,20 +1225,8 @@ impl Game {
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
events.push(Event::Sound(2019));
}
} else if let Some((object_id, false)) = hit
&& let Some(index) = BUMPERS.iter().position(|bumper| bumper.id == object_id)
&& !self.tilted
{
let points = self
.player()
.bumper_value
.saturating_sub(if auxiliary_fired { 0 } else { 1_000 });
if points != 0 {
self.add_score(points, events);
}
self.record_countdowns[usize::from(BUMPERS[index].id)] = 5;
events.push(Event::Bumper);
events.push(Event::Sound(2006));
} else if let Some((object_id, false)) = hit {
self.apply_bumper_rule(object_id, auxiliary_fired, events);
}
match scan.action {
BallAction::Keep | BallAction::Suspend => self.secondary_ball = Some(ball),
@@ -1259,6 +1235,27 @@ impl Game {
collided || scan.action == BallAction::Suspend
}
fn apply_bumper_rule(
&mut self,
object_id: u8,
auxiliary_fired: bool,
events: &mut Vec<Event>,
) {
if self.tilted || !BUMPERS.iter().any(|bumper| bumper.id == object_id) {
return;
}
self.record_countdowns[usize::from(object_id)] = 5;
events.push(Event::Bumper);
events.push(Event::Sound(2006));
let points = self
.player()
.bumper_value
.saturating_sub(if auxiliary_fired { 0 } else { 1_000 });
if points != 0 {
self.add_score(points, events);
}
}
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
if self.tilted {
return;
@@ -2692,6 +2689,28 @@ mod tests {
);
}
#[test]
fn bumper_sound_precedes_a_crossed_media_marker() {
let mut game = Game::new(1);
game.players[0].score = 139_000;
let mut events = Vec::new();
game.apply_bumper_rule(51, true, &mut events);
assert_eq!(game.record_countdown(51), 5);
assert_eq!(game.player().score, 140_000);
assert_eq!(
events,
[
Event::Bumper,
Event::Sound(2006),
Event::Media,
Event::ExtraBall,
Event::Sound(2007),
]
);
}
#[test]
fn wheel_reset_arms_effect_seven_multiball() {
let mut game = Game::new_with_seed(1, 7);