fix(av): restore original nudge and sound presentation

Remove the invented whole-frame nudge shake so nudges affect only recovered ball state, sound, and tilt behavior. Match monophonic SndPlaySound semantics by stopping the current WAV before each new resource and using full host mixer volume instead of an arbitrary 72 percent attenuation.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- git diff --check
This commit is contained in:
2026-08-23 19:23:24 +02:00
parent c84df73857
commit f4e6a5a372
6 changed files with 24 additions and 35 deletions
+5
View File
@@ -10,6 +10,11 @@ and this project adheres to
### Fixed
- Remove the invented 180 ms whole-frame nudge shake; original nudges affect
ball-slot velocities, sound, and Tilt state without moving the viewport.
- Match `SndPlaySound(SND_ASYNC|SND_NODEFAULT)` playback: stop the previous WAV
before every new resource and use full host mixer volume instead of an
invented 72 percent attenuation/overlap.
- Load and draw all DAT400-407 player/effect sprites at `(55,79)`, preserving
the original saved 14x14 wheel overlap at `(84,77)`; selected bonuses,
collect-bonus, and release-ball states are now visible.
+1 -1
View File
@@ -20,7 +20,7 @@ implementation.
| Subsystem | Rust status | Evidence and boundary |
| --- | --- | --- |
| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. DAT400-407 expose the current per-player bonus/collect/release effect at `(55,79)` while preserving the saved 14x14 wheel overlap. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. |
| Audio | Exact samples and recovered dispatch | All 16 mono PCM WAV resources are embedded unchanged. Playback is emitted at the reconstructed call sites, including multi-sound bank completions and nudge-then-tilt ordering. WAV 2022 is loaded by the original generic resource loop but has no playback call and is therefore never played by Rust. |
| Audio | Exact samples and recovered dispatch | All 16 mono PCM WAV resources are embedded unchanged. Playback is emitted at the reconstructed call sites, including multi-sound bank completions and nudge-then-tilt ordering. Like Win16 `SndPlaySound(SND_ASYNC|SND_NODEFAULT)`, each new resource stops the previous one and plays at the host mixer level without an application-side attenuation. WAV 2022 is loaded by the original generic resource loop but has no playback call and is therefore never played by Rust. |
| Help and languages | Exact | Original resource images 1001-1005 are displayed directly. |
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Every static record retains its `+0x49` layer mask; the scanner selects layer 1 below the old Y value 250,000 and layer 2 at or above it, while mask 3 records remain shared. Before detection, the raw `1000:9b69` predicted position must lie in the record bounds derived with the registered five-pixel ball margin. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Detection retains unresolved normal/material candidates so later scan-time motion changes can participate in the selected response. Raw stack slot `SS:...d8a2` makes the first detected record ID win; later nearer candidates are stored but never applied, and both C/Rust have two-candidate regression coverage for this quirk. Each flipper uses its exact two line records plus moving tip circle in both positions. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. |
| Ball launcher and nudge input | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Each Down keydown subtracts `15*50 = 750` millipixels, release subtracts another `15*100 = 1500`, and the result follows the recovered randomized `-3800` lower and `-2280` weak upper clamp branches. The ten decoration frames use the same strict 750-millipixel thresholds. Left Shift and keypad 3 apply their directional `(50-Random(20))*15` impulses; Space uses the recovered Real48 horizontal factor and `(60-Random(20))*15` vertical impulse. With two balls, the key path still consumes those active-window draws, then applies its separately recovered fixed/shared random slot impulse to both saved velocities. Each nudge adds 25 to the wrapping 16-bit tilt counter, compares it with `30+Random(10)`, and the detail timer decrements a nonzero counter once per callback. |
+2 -13
View File
@@ -1192,21 +1192,10 @@ impl App {
let height = HEIGHT * scale;
let x = (screen_width() - width) * 0.5;
let y = (screen_height() - height) * 0.5;
let shake = self.game.as_ref().map_or(Vec2::ZERO, |game| {
if game.nudge_shake <= 0.0 {
return Vec2::ZERO;
}
let strength = (game.nudge_shake / 0.18).min(1.0) * 2.5 * scale;
let phase = game.nudge_shake * 950.0;
vec2(
phase.sin() * strength,
(phase * 0.73).cos() * strength * 0.45,
)
});
draw_texture_ex(
&self.render_target.texture,
x + shake.x,
y + shake.y,
x,
y,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(width, height)),
+2 -1
View File
@@ -186,11 +186,12 @@ impl Assets {
return;
}
if let Some((_, sound)) = self.sounds.iter().find(|(sound_id, _)| *sound_id == id) {
self.stop_all_sounds();
play_sound(
sound,
PlaySoundParams {
looped: false,
volume: 0.72,
volume: 1.0,
},
);
}
+14 -19
View File
@@ -4,7 +4,7 @@ use crate::{
geometry::Segment,
original_physics::{
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP,
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS, StaticCollisionCandidate,
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, StaticCollisionCandidate,
ball_collision_response, capture_collision_candidate, circle_collision_candidate_at,
line_collision_candidate_at, milli_distance, path_intersects_circle,
},
@@ -301,7 +301,6 @@ pub struct Game {
pub claw: Claw,
pub target_rotation_state: Option<u8>,
pub panel_frame: Option<u16>,
pub nudge_shake: f32,
pub launcher_charge: f32,
pub finished: bool,
pub last_collision_id: Option<u8>,
@@ -343,7 +342,6 @@ impl Game {
claw: Claw::default(),
target_rotation_state: None,
panel_frame: None,
nudge_shake: 0.0,
launcher_charge: 0.0,
finished: false,
last_collision_id: None,
@@ -499,7 +497,7 @@ impl Game {
self.score_mode = ScoreMode::Multiball;
}
for _ in 0..substeps {
if self.fixed_update(STEP_SECONDS, events)
if self.fixed_update(events)
|| self.finished
|| self.claw.ball_suspended
{
@@ -677,7 +675,6 @@ impl Game {
self.ball.velocity = velocity.to_velocity_per_second();
}
self.nudge_shake = 0.18;
self.tilt_counter = self.tilt_counter.wrapping_add(25);
let threshold = self.random.below(10) + 30;
if threshold < self.tilt_counter {
@@ -692,8 +689,7 @@ impl Game {
}
#[allow(clippy::too_many_lines)]
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) -> bool {
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
fn fixed_update(&mut self, events: &mut Vec<Event>) -> bool {
if self.ball.in_launcher {
self.ball.position = LAUNCHER_POSITION;
self.ball.velocity = Vec2::ZERO;
@@ -1963,7 +1959,6 @@ impl Game {
self.pending_flipper_edges.fill(0);
self.secondary_ball = None;
self.score_mode = ScoreMode::Normal;
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
self.launcher_was_down = false;
self.launcher_hold_seconds = 0.0;
@@ -2190,7 +2185,7 @@ mod tests {
let mut expected_events = Vec::new();
let mut stopped = false;
for _ in 0..5 {
if expected.fixed_update(STEP_SECONDS, &mut expected_events) {
if expected.fixed_update(&mut expected_events) {
stopped = true;
break;
}
@@ -2757,7 +2752,7 @@ mod tests {
game.ball.velocity = vec2(0.0, -380.0);
for _ in 0..5 {
game.fixed_update(STEP_SECONDS, &mut Vec::new());
game.fixed_update(&mut Vec::new());
}
assert!(game.ball.position.y >= 15.0);
@@ -2773,7 +2768,7 @@ mod tests {
game.ball.capture_age = 42;
for _ in 0..5 {
if game.fixed_update(STEP_SECONDS, &mut Vec::new()) {
if game.fixed_update(&mut Vec::new()) {
break;
}
}
@@ -2867,7 +2862,7 @@ mod tests {
weak.ball.position = vec2(165.0, 171.1);
weak.ball.velocity = vec2(0.0, -20.0);
let mut weak_events = Vec::new();
assert!(weak.fixed_update(STEP_SECONDS, &mut weak_events));
assert!(weak.fixed_update(&mut weak_events));
let mut fast = Game::new(1);
fast.object_active.fill(false);
@@ -2879,9 +2874,9 @@ mod tests {
let mut tilted = fast.clone();
tilted.tilted = true;
let mut fast_events = Vec::new();
assert!(fast.fixed_update(STEP_SECONDS, &mut fast_events));
assert!(fast.fixed_update(&mut fast_events));
let mut tilted_events = Vec::new();
assert!(tilted.fixed_update(STEP_SECONDS, &mut tilted_events));
assert!(tilted.fixed_update(&mut tilted_events));
assert_eq!(weak.player().score, 1_000);
assert_eq!(fast.player().score, 2_000);
@@ -3471,7 +3466,7 @@ mod tests {
let mut expected_velocity = expected_response.velocity;
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
assert!(game.fixed_update(STEP_SECONDS, &mut Vec::new()));
assert!(game.fixed_update(&mut Vec::new()));
assert_eq!(game.last_collision_id, Some(5));
assert_eq!(
@@ -3601,7 +3596,7 @@ mod tests {
.round_i32()
.wrapping_sub(150);
assert!(!game.fixed_update(STEP_SECONDS, &mut Vec::new()));
assert!(!game.fixed_update(&mut Vec::new()));
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
@@ -3625,7 +3620,7 @@ mod tests {
game.ball.velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
let old_position = MilliVec::from_position(game.ball.position);
assert!(game.fixed_update(STEP_SECONDS, &mut Vec::new()));
assert!(game.fixed_update(&mut Vec::new()));
let velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
assert!(velocity.x > 0, "the type-three rim must reflect the ball");
@@ -3647,7 +3642,7 @@ mod tests {
game.ball.position = sensor.center + vec2(23.0, 0.0);
game.ball.velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
game.fixed_update(STEP_SECONDS, &mut Vec::new());
game.fixed_update(&mut Vec::new());
assert_eq!(game.record_contacts[usize::from(sensor.id)], 1);
}
@@ -3671,7 +3666,7 @@ mod tests {
expected_game.randomize_trigger_velocity(&mut expected_ball);
let expected_velocity = MilliVec::from_velocity_per_second(expected_ball.velocity);
game.fixed_update(STEP_SECONDS, &mut Vec::new());
game.fixed_update(&mut Vec::new());
assert_eq!(game.player().score, 500);
assert_eq!(
-1
View File
@@ -6,7 +6,6 @@ use macroquad::prelude::{Vec2, vec2};
use crate::real48::Real48;
pub const STEP_SECONDS: f32 = 0.010;
pub const GRAVITY_MILLI_PER_STEP: i32 = 15;
pub const MAXIMUM_SPEED_MILLI_PER_STEP: i32 = 3_800;