diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index e966d2f..269b0c7 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -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. diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 9023b5e..3aab607 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -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. | diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 754694e..b3b92f1 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -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)), diff --git a/tdkpin-rs/src/assets.rs b/tdkpin-rs/src/assets.rs index b998208..eed37a6 100644 --- a/tdkpin-rs/src/assets.rs +++ b/tdkpin-rs/src/assets.rs @@ -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, }, ); } diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 47e0358..8d25df9 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -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, pub panel_frame: Option, - pub nudge_shake: f32, pub launcher_charge: f32, pub finished: bool, pub last_collision_id: Option, @@ -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) -> bool { - self.nudge_shake = (self.nudge_shake - dt).max(0.0); + fn fixed_update(&mut self, events: &mut Vec) -> 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!( diff --git a/tdkpin-rs/src/original_physics.rs b/tdkpin-rs/src/original_physics.rs index 6c5d18d..93e56fd 100644 --- a/tdkpin-rs/src/original_physics.rs +++ b/tdkpin-rs/src/original_physics.rs @@ -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;