fix(game): restore original scoring and sound dispatch

The rewrite assigned one WAV resource to each logical event, which could not
represent the original call-site behavior. It played reserved WAV 2022 for
media awards, omitted multi-sound bank sequences, and used the wrong cues for
captures, bumpers, targets, nudges, and tilt.

Emit explicit sound-resource events at the reconstructed C control-flow sites
so logical rendering events no longer imply audio. Move media threshold
advancement into score addition, retain one threshold transition per call, and
use wrapping 32-bit score arithmetic. Restore the ninth-diamond 24,464-point
completion, the subsequent double-score activation, and later 100,000-point
secondary-score awards.

Test Plan:
- `cargo test --all-targets` -- passed, 53 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check CHANGELOG.md RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 16:48:41 +02:00
parent 8b99e9607c
commit 9de3dcc755
5 changed files with 194 additions and 100 deletions
+3 -2
View File
@@ -209,8 +209,9 @@ impl App {
}
fn play_event(&self, event: Event) {
self.assets
.play(event.sound_resource(), self.saved.settings.sounds);
if let Some(resource) = event.sound_resource() {
self.assets.play(resource, self.saved.settings.sounds);
}
}
fn finish_game(&mut self) {
+180 -93
View File
@@ -58,25 +58,27 @@ pub enum Event {
Nudge,
Tilt,
Drain,
Sound(u16),
}
impl Event {
/// Original Win16 WAVE resource selected by this gameplay transition.
pub const fn sound_resource(self) -> u16 {
/// Explicit original Win16 WAVE resource selected at this control-flow site.
pub const fn sound_resource(self) -> Option<u16> {
match self {
Self::FlipperMove => 2021,
Self::Launch => 2002,
Self::Bumper => 2004,
Self::Target => 2006,
Self::Wheel => 2011,
Self::ClawCapture => 2015,
Self::ClawRelease => 2016,
Self::Lock => 2017,
Self::Media => 2022,
Self::ExtraBall => 2007,
Self::Nudge => 2019,
Self::Tilt => 2020,
Self::Drain => 2008,
Self::Sound(resource) => Some(resource),
Self::FlipperMove
| Self::Launch
| Self::Bumper
| Self::Target
| Self::Wheel
| Self::ClawCapture
| Self::ClawRelease
| Self::Lock
| Self::Media
| Self::ExtraBall
| Self::Nudge
| Self::Tilt
| Self::Drain => None,
}
}
}
@@ -143,10 +145,12 @@ impl Claw {
#[derive(Clone, Debug)]
pub struct Player {
pub score: u32,
pub secondary_score: u32,
pub balls: u8,
pub extra_balls: u8,
pub bumper_value: u32,
pub diamond_segments: u8,
pub double_score: bool,
pub media_level: u8,
}
@@ -154,10 +158,12 @@ impl Default for Player {
fn default() -> Self {
Self {
score: 0,
secondary_score: 0,
balls: 3,
extra_balls: 0,
bumper_value: 1_000,
diamond_segments: 0,
double_score: false,
media_level: 0,
}
}
@@ -341,10 +347,12 @@ impl Game {
self.flippers.right_raised = controls.right_flipper && !self.tilted;
if self.flippers.left_raised != old_flippers.left_raised {
events.push(Event::FlipperMove);
events.push(Event::Sound(2021));
self.pending_flipper_edges[0] = if self.flippers.left_raised { 1 } else { -1 };
}
if self.flippers.right_raised != old_flippers.right_raised {
events.push(Event::FlipperMove);
events.push(Event::Sound(2021));
self.pending_flipper_edges[1] = if self.flippers.right_raised { 1 } else { -1 };
}
@@ -367,6 +375,7 @@ impl Game {
} else if self.launcher_was_down {
self.fire_launcher();
events.push(Event::Launch);
events.push(Event::Sound(2002));
}
}
if !self.tilted
@@ -378,10 +387,12 @@ impl Game {
self.nudge_meter += controls.nudge.abs() * 0.34;
self.nudge_cooldown = 0.22;
self.nudge_shake = 0.18;
events.push(Event::Sound(2019));
if self.nudge_meter >= TILT_THRESHOLD {
self.tilted = true;
self.bonus = 0;
events.push(Event::Tilt);
events.push(Event::Sound(2020));
} else {
events.push(Event::Nudge);
}
@@ -506,11 +517,12 @@ impl Game {
&& !self.tilted
&& self.bumper_cooldown <= 0.0
{
self.add_score(self.player().bumper_value);
self.add_score(self.player().bumper_value, events);
self.bonus = self.bonus.saturating_add(100);
self.bumper_cooldown = 0.08;
self.bumper_flash[index] = 0.16;
events.push(Event::Bumper);
events.push(Event::Sound(2006));
}
self.check_sensor_objects(old_position, movement_velocity, events);
@@ -518,13 +530,6 @@ impl Game {
return;
}
if !self.tilted {
self.check_media(events);
}
if self.claw.ball_suspended {
return;
}
self.apply_flipper_kicks();
if self.ball.position.y > 470.0 {
@@ -614,11 +619,12 @@ impl Game {
&& !self.tilted
&& self.bumper_cooldown <= 0.0
{
self.add_score(self.player().bumper_value);
self.add_score(self.player().bumper_value, events);
self.bonus = self.bonus.saturating_add(100);
self.bumper_cooldown = 0.08;
self.bumper_flash[index] = 0.16;
events.push(Event::Bumper);
events.push(Event::Sound(2006));
}
self.secondary_ball = Some(ball);
}
@@ -629,7 +635,7 @@ impl Game {
.find(|wall| wall.id == object_id)
.expect("a wall collision id must reference a recovered wall");
if !self.tilted && wall.score != 0 {
self.add_score(wall.score);
self.add_score(wall.score, events);
events.push(if object_id == 121 {
Event::Wheel
} else {
@@ -645,40 +651,64 @@ impl Game {
self.object_active[group + 2] = false;
}
}
if wall.flags & 0x0008 != 0
&& [90, 93, 96, 99, 102]
if wall.flags & 0x0008 != 0 {
events.push(Event::Sound(2012));
if [90, 93, 96, 99, 102]
.into_iter()
.all(|id| !self.object_active[id])
{
let player = &mut self.players[self.current_player];
let completion_bonus = if player.bumper_value >= 6_000 {
50_000
} else {
player.bumper_value += 1_000;
0
};
self.add_score(completion_bonus);
for active in &mut self.object_active[90..=104] {
*active = true;
{
let player = &mut self.players[self.current_player];
let completion_bonus = if player.bumper_value >= 6_000 {
50_000
} else {
player.bumper_value += 1_000;
0
};
if completion_bonus != 0 {
self.add_score(completion_bonus, events);
}
events.push(Event::Sound(2017));
for active in &mut self.object_active[90..=104] {
*active = true;
}
}
}
if wall.flags & 0x0010 != 0
&& [109, 112, 115, 118]
if wall.flags & 0x0010 != 0 {
events.push(Event::Sound(2012));
if [109, 112, 115, 118]
.into_iter()
.all(|id| !self.object_active[id])
{
let player = &mut self.players[self.current_player];
player.diamond_segments = (player.diamond_segments + 1).min(9);
let value = u32::from(player.diamond_segments) * 10_000;
self.add_score(value);
for active in &mut self.object_active[109..=120] {
*active = true;
{
let segments = self.player().diamond_segments;
if segments < 9 {
self.players[self.current_player].diamond_segments += 1;
let award = if segments == 8 {
24_464
} else {
u32::from(segments + 1) * 10_000
};
self.add_score(award, events);
events.push(Event::Sound(2017));
} else {
let player = &mut self.players[self.current_player];
if player.double_score {
player.secondary_score = player.secondary_score.wrapping_add(100_000);
} else {
player.double_score = true;
}
events.push(Event::Sound(2007));
}
for active in &mut self.object_active[109..=120] {
*active = true;
}
}
}
if wall.flags & 0x0400 != 0 {
self.wheel_animation = 0.65;
events.push(Event::Sound(2011));
}
if wall.flags & 0x0004 != 0 {
events.push(Event::Sound(2012));
if self.multiball_state == MultiballState::Ready {
self.target_effect = 7;
} else {
@@ -695,20 +725,6 @@ impl Game {
}
}
fn check_media(&mut self, events: &mut Vec<Event>) {
const THRESHOLDS: [u32; 4] = [140_000, 650_000, 1_300_000, 4_000_000];
let score = self.player().score;
let level =
u8::try_from(THRESHOLDS.partition_point(|threshold| score >= *threshold)).unwrap_or(4);
if level > self.player().media_level {
let player = &mut self.players[self.current_player];
player.media_level = level;
player.extra_balls = player.extra_balls.saturating_add(1);
events.push(Event::Media);
events.push(Event::ExtraBall);
}
}
fn check_sensor_objects(
&mut self,
old_position: MilliVec,
@@ -770,11 +786,12 @@ impl Game {
.bonus
.saturating_add(10_000_u32 << u32::try_from(filled_before).unwrap_or_default());
if self.wheel_holes.iter().all(|filled| *filled) {
self.add_score(self.bonus);
self.add_score(self.bonus, events);
self.bonus = 0;
}
self.reset_ball_to_launcher();
events.push(Event::Lock);
events.push(Event::Sound(2015));
return true;
}
false
@@ -807,6 +824,7 @@ impl Game {
self.multiball_state = MultiballState::Ready;
self.reset_ball_to_launcher();
events.push(Event::Wheel);
events.push(Event::Sound(2015));
return true;
}
false
@@ -835,7 +853,7 @@ impl Game {
EFFECT_SENSOR.radius,
);
if entered && !self.tilted {
self.add_score(EFFECT_SENSOR.score);
self.add_score(EFFECT_SENSOR.score, events);
match self.target_effect {
1 => self.bonus = self.bonus.saturating_add(10_000),
2 => self.bonus = self.bonus.saturating_add(20_000),
@@ -843,7 +861,7 @@ impl Game {
4 => self.bonus = self.bonus.saturating_add(100_000),
5 => self.bonus = self.bonus.saturating_add(200_000),
6 => {
self.add_score(self.bonus);
self.add_score(self.bonus, events);
self.bonus = 0;
}
7 => {
@@ -859,6 +877,7 @@ impl Game {
self.target_effect = 0;
self.object_active[effect_index] = false;
events.push(Event::Target);
events.push(Event::Sound(2004));
}
}
}
@@ -888,8 +907,9 @@ impl Game {
if !entered || self.tilted {
continue;
}
self.add_score(sensor.score);
self.add_score(sensor.score, events);
events.push(Event::Target);
events.push(Event::Sound(2004));
if (150..=152).contains(&sensor.id) {
self.top_targets[usize::from(sensor.id - 150)] = true;
let field_id = [153, 6, 154][usize::from(sensor.id - 150)];
@@ -1021,6 +1041,7 @@ impl Game {
self.claw.frame_accumulator = 0.0;
self.ball.velocity = Vec2::ZERO;
events.push(Event::ClawCapture);
events.push(Event::Sound(2015));
}
fn update_claw(&mut self, dt: f32, events: &mut Vec<Event>) {
@@ -1057,18 +1078,26 @@ impl Game {
(self.ball.position, self.ball.velocity) = claw_release(release_frame);
self.claw.ball_suspended = false;
events.push(Event::ClawRelease);
events.push(Event::Sound(2016));
}
fn add_score(&mut self, points: u32) {
let multiplier = if self.player().diamond_segments == 9 {
fn add_score(&mut self, points: u32, events: &mut Vec<Event>) {
const THRESHOLDS: [u32; 4] = [140_000, 650_000, 1_300_000, 4_000_000];
let multiplier = if self.player().double_score {
2
} else {
1
};
let player = &mut self.players[self.current_player];
player.score = player
.score
.saturating_add(points.saturating_mul(multiplier));
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] {
player.media_level += 1;
player.extra_balls = player.extra_balls.wrapping_add(1);
events.push(Event::Media);
events.push(Event::ExtraBall);
events.push(Event::Sound(2007));
}
}
fn drain(&mut self, events: &mut Vec<Event>) {
@@ -1085,6 +1114,7 @@ impl Game {
player.balls = player.balls.saturating_sub(1);
}
events.push(Event::Drain);
events.push(Event::Sound(2008));
self.bonus = 0;
self.top_targets.fill(false);
self.wheel_holes.fill(false);
@@ -1331,11 +1361,30 @@ mod tests {
}
#[test]
fn completed_diamond_doubles_score() {
fn ninth_diamond_and_followup_banks_match_original_awards() {
let mut game = Game::new(1);
game.players[0].diamond_segments = 9;
game.add_score(1_000);
assert_eq!(game.players[0].score, 2_000);
game.players[0].diamond_segments = 8;
let mut events = Vec::new();
for object_id in [109, 112, 115, 118] {
game.apply_wall_rule(object_id, &mut events);
}
assert_eq!(game.player().diamond_segments, 9);
assert_eq!(game.player().score, 4 * 1_500 + 24_464);
assert!(!game.player().double_score);
for object_id in [109, 112, 115, 118] {
game.apply_wall_rule(object_id, &mut events);
}
assert!(game.player().double_score);
let score = game.player().score;
game.add_score(1_000, &mut events);
assert_eq!(game.player().score, score + 2_000);
for object_id in [109, 112, 115, 118] {
game.apply_wall_rule(object_id, &mut events);
}
assert_eq!(game.player().secondary_score, 100_000);
}
#[test]
@@ -1389,11 +1438,9 @@ mod tests {
.enumerate()
{
game.players[0].score = threshold - 1;
game.check_media(&mut events);
assert_eq!(usize::from(game.player().media_level), expected_level);
game.players[0].score = threshold;
game.check_media(&mut events);
game.add_score(1, &mut events);
assert_eq!(usize::from(game.player().media_level), expected_level + 1);
}
assert_eq!(game.player().extra_balls, 4);
@@ -1404,6 +1451,13 @@ mod tests {
.count(),
4
);
assert_eq!(
events
.iter()
.filter_map(|event| event.sound_resource())
.collect::<Vec<_>>(),
[2007; 4]
);
}
#[test]
@@ -1588,7 +1642,12 @@ mod tests {
let press_events = game.update(1.0 / 60.0, 3, raised);
assert_eq!(
press_events,
[Event::FlipperMove, Event::FlipperMove],
[
Event::FlipperMove,
Event::Sound(2021),
Event::FlipperMove,
Event::Sound(2021),
],
"each original move_flipper call starts WAVE 2021"
);
assert!(game.update(1.0 / 60.0, 3, raised).is_empty());
@@ -1596,10 +1655,16 @@ mod tests {
let release_events = game.update(1.0 / 60.0, 3, Controls::default());
assert_eq!(
release_events,
[Event::FlipperMove, Event::FlipperMove],
[
Event::FlipperMove,
Event::Sound(2021),
Event::FlipperMove,
Event::Sound(2021),
],
"the original also plays the sound while returning"
);
assert_eq!(Event::FlipperMove.sound_resource(), 2021);
assert_eq!(Event::FlipperMove.sound_resource(), None);
assert_eq!(Event::Sound(2021).sound_resource(), Some(2021));
}
#[test]
@@ -1632,7 +1697,7 @@ mod tests {
let events = game.update(0.0, 3, Controls::default());
assert_eq!(events, [Event::FlipperMove]);
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
assert_eq!(game.pending_flipper_edges, [-1, 0]);
game.ball.in_launcher = false;
game.ball.position = vec2(125.0, 390.0);
@@ -1647,13 +1712,35 @@ mod tests {
#[test]
fn recovered_control_sounds_use_the_original_resource_numbers() {
assert_eq!(Event::Launch.sound_resource(), 2002);
assert_eq!(Event::Wheel.sound_resource(), 2011);
assert_eq!(Event::Nudge.sound_resource(), 2019);
assert_eq!(Event::Tilt.sound_resource(), 2020);
assert_eq!(Event::Drain.sound_resource(), 2008);
assert_eq!(Event::ClawCapture.sound_resource(), 2015);
assert_eq!(Event::ClawRelease.sound_resource(), 2016);
for resource in [
2002, 2006, 2007, 2008, 2011, 2012, 2015, 2016, 2019, 2020, 2021,
] {
assert_eq!(Event::Sound(resource).sound_resource(), Some(resource));
}
assert_eq!(Event::Launch.sound_resource(), None);
assert_eq!(Event::Media.sound_resource(), None);
}
#[test]
fn nudge_and_tilt_emit_the_original_ordered_sound_sequence() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
let controls = Controls {
nudge: 1.0,
..Controls::default()
};
assert_eq!(
game.update(0.0, 3, controls),
[Event::Sound(2019), Event::Nudge]
);
game.nudge_cooldown = 0.0;
game.nudge_meter = TILT_THRESHOLD - 0.01;
assert_eq!(
game.update(0.0, 3, controls),
[Event::Sound(2019), Event::Tilt, Event::Sound(2020)]
);
}
#[test]
@@ -1706,7 +1793,7 @@ mod tests {
let mut events = Vec::new();
game.begin_claw_capture(6, &mut events);
assert_eq!(events, [Event::ClawCapture]);
assert_eq!(events, [Event::ClawCapture, Event::Sound(2015)]);
assert_eq!(game.claw.frame, 10);
assert_eq!(game.claw.bank, ClawSpriteBank::Closing);
assert!(game.claw.ball_suspended);
@@ -1723,7 +1810,7 @@ mod tests {
}
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
assert_eq!(events.last(), Some(&Event::ClawRelease));
assert!(events.ends_with(&[Event::ClawRelease, Event::Sound(2016)]));
assert_eq!(game.claw.frame, 6);
assert_eq!(game.claw.bank, ClawSpriteBank::Opening);
assert!(!game.claw.ball_suspended);
@@ -1777,7 +1864,7 @@ mod tests {
&mut events,
);
assert!(game.claw.active);
assert_eq!(events, [Event::ClawCapture]);
assert_eq!(events, [Event::ClawCapture, Event::Sound(2015)]);
assert!(CLAW_TERMINAL_FRAMES.contains(&game.claw.target_frame));
}
@@ -1794,7 +1881,7 @@ mod tests {
&mut events,
);
assert_eq!(game.player().score, 500);
assert_eq!(events, [Event::Target]);
assert_eq!(events, [Event::Target, Event::Sound(2004)]);
assert!(game.object_active[153]);
assert!(!game.object_active[6]);
assert!(!game.object_active[154]);
@@ -1871,7 +1958,7 @@ mod tests {
&mut events,
);
assert!(game.wheel_holes.iter().all(|filled| !*filled));
assert_eq!(events.last(), Some(&Event::Wheel));
assert!(events.ends_with(&[Event::Wheel, Event::Sound(2015)]));
}
#[test]
+2 -2
View File
@@ -423,8 +423,8 @@ mod tests {
let mut simulation = Simulation::new(Scenario::Flippers, 1);
simulation.advance_to(46);
assert_eq!(simulation.trace[16].events, ["FlipperMove"]);
assert_eq!(simulation.trace[46].events, ["FlipperMove"]);
assert_eq!(simulation.trace[16].events, ["FlipperMove", "Sound(2021)"]);
assert_eq!(simulation.trace[46].events, ["FlipperMove", "Sound(2021)"]);
assert!(!simulation.game.flippers.left_raised);
}