fix(scoring): restore marker comparison and hit sound order

The original score helper compares signed high words and unsigned low words,
which is a signed 32-bit comparison of the wrapped score bits. Rust used an
unsigned comparison and could award a media marker after the score crossed the
sign bit. Type-four trigger handling also queued WAVE 2004 after score mutation,
so it replaced WAVE 2007 when the same hit crossed an extra-ball threshold.

Use the binary's comparison semantics and dispatch the type-four hit sound
before both ordinary target and player-effect score mutations. Regression tests
cover both trigger paths, the exact monophonic event order, and a score with the
high bit set.

Test Plan:
- `cargo test --workspace --all-targets --all-features` -- 115 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:55:44 +02:00
parent 4a55150fc0
commit 50764f6151
3 changed files with 78 additions and 6 deletions
+73 -4
View File
@@ -34,6 +34,13 @@ const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
#[allow(clippy::cast_possible_wrap)]
const fn score_reaches_marker(score: u32, threshold: u32) -> bool {
// 1000:bc36 compares signed high words and, when equal, unsigned low
// words. That is exactly a signed 32-bit comparison of the bit patterns.
(score as i32) >= (threshold as i32)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Controls {
pub left_flipper: bool,
@@ -1736,6 +1743,8 @@ impl Game {
let entered = touched && !self.trigger_flags[effect_index];
self.trigger_flags[effect_index] = touched;
if entered {
events.push(Event::Target);
events.push(Event::Sound(2004));
self.add_score(EFFECT_SENSOR.score, events);
match self.target_effect {
1..=5 => {
@@ -1769,8 +1778,6 @@ impl Game {
}
self.target_effect = 0;
self.object_active[effect_index] = false;
events.push(Event::Target);
events.push(Event::Sound(2004));
self.randomize_trigger_velocity(ball);
}
}
@@ -1803,9 +1810,9 @@ impl Game {
if !entered {
continue;
}
self.add_score(sensor.score, events);
events.push(Event::Target);
events.push(Event::Sound(2004));
self.add_score(sensor.score, events);
self.record_countdowns[usize::from(sensor.id)] = if sensor.id <= 147 { 20 } else { 10 };
if (150..=152).contains(&sensor.id) {
self.top_targets[usize::from(sensor.id - 150)] = true;
@@ -2014,7 +2021,7 @@ impl Game {
let player = &mut self.players[self.current_player];
player.score = player.score.wrapping_add(points.wrapping_mul(multiplier));
let level = usize::from(player.media_level);
if level < THRESHOLDS.len() && player.score >= THRESHOLDS[level] {
if level < THRESHOLDS.len() && score_reaches_marker(player.score, THRESHOLDS[level]) {
player.media_level += 1;
player.extra_balls = player.extra_balls.wrapping_add(1);
events.push(Event::Media);
@@ -2621,6 +2628,68 @@ mod tests {
.collect::<Vec<_>>(),
[2007; 4]
);
let mut wrapped = Game::new(1);
wrapped.players[0].score = 0x8000_0000;
let mut wrapped_events = Vec::new();
wrapped.add_score(0, &mut wrapped_events);
assert_eq!(wrapped.player().media_level, 0);
assert_eq!(wrapped.player().extra_balls, 0);
assert!(wrapped_events.is_empty());
}
#[test]
fn type_four_hit_sound_precedes_a_crossed_media_marker() {
let mut target_game = Game::new_with_seed(1, 7);
target_game.players[0].score = 139_500;
let sensor = TARGET_SENSORS
.into_iter()
.find(|sensor| sensor.id == 150)
.expect("record 150 must be present");
let mut target_ball = target_game.ball;
target_ball.position = sensor.center;
let mut target_events = Vec::new();
target_game.check_target_sensors(
&mut target_ball,
MilliVec::from_position(sensor.center),
MilliVec::default(),
150..=152,
&mut target_events,
);
assert_eq!(
target_events,
[
Event::Target,
Event::Sound(2004),
Event::Media,
Event::ExtraBall,
Event::Sound(2007),
]
);
let mut effect_game = Game::new_with_seed(1, 7);
effect_game.players[0].score = 139_500;
effect_game.begin_effect_scenario(1);
let mut effect_ball = effect_game.ball;
effect_ball.position = EFFECT_SENSOR.center;
let mut effect_events = Vec::new();
effect_game.check_effect_sensor(
&mut effect_ball,
1,
MilliVec::from_position(EFFECT_SENSOR.center),
MilliVec::default(),
&mut effect_events,
);
assert_eq!(
effect_events,
[
Event::Target,
Event::Sound(2004),
Event::Media,
Event::ExtraBall,
Event::Sound(2007),
]
);
}
#[test]