diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 9c610a9..f2c561f 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -9,6 +9,7 @@ const FLIPPER_CONTACT_RADIUS: f32 = 9.0; // geometry. A small contact epsilon preserves thin-line hits without adding // the rendered ball radius to every recovered boundary. const TABLE_LINE_RADIUS: f32 = 1.0; +const SHOOTER_EXIT_GATE_ID: u8 = 22; const GRAVITY: f32 = 135.0; const MAX_SPEED: f32 = 430.0; const BALL_SEARCH_DELAY: f32 = 3.0; @@ -263,6 +264,18 @@ impl Game { let mut drained = false; for wall in WALLS { + // The upper shooter exit is a one-way gate. After the outer + // curve turns the launched ball down and left, object 22 must + // let it enter the playfield. A ball approaching from below + // still collides with the gate. + if wall.id == SHOOTER_EXIT_GATE_ID + && self.ball.velocity.x < 0.0 + && self.ball.velocity.y > 0.0 + && self.ball.position.x < 290.0 + && self.ball.position.y < 35.0 + { + continue; + } // Object 25 is the one-way shooter stop. It catches a returning // ball, but must not reflect an upward launch from the line. if wall.id == 25 && self.ball.velocity.y < 0.0 { @@ -623,25 +636,32 @@ mod tests { #[test] fn charged_ball_clears_the_shooter_lane() { - let mut game = Game::new(1); - launch_ball(&mut game, 33); - let mut entered_table = false; - let mut minimum_y = game.ball.position.y; + for detail in 1..=5 { + let mut game = Game::new(1); + launch_ball(&mut game, 33); + let mut entered_table = false; + let mut returned_to_launcher = false; + let mut minimum_y = game.ball.position.y; - for _ in 0..240 { - game.update(1.0 / 120.0, 5, Controls::default()); - minimum_y = minimum_y.min(game.ball.position.y); - if game.ball.position.x < 298.0 { - entered_table = true; - break; + for _ in 0..240 { + game.update(1.0 / 120.0, detail, Controls::default()); + minimum_y = minimum_y.min(game.ball.position.y); + if game.ball.position.x < 280.0 && game.ball.position.y > 30.0 { + entered_table = true; + } + returned_to_launcher |= game.ball.in_launcher; } - } - assert!( - entered_table, - "the shooter curve should guide the ball onto the table; position={:?}, velocity={:?}, minimum_y={minimum_y}", - game.ball.position, game.ball.velocity - ); + assert!( + entered_table, + "detail {detail}: the shooter curve should guide the ball onto the table; position={:?}, velocity={:?}, minimum_y={minimum_y}", + game.ball.position, game.ball.velocity + ); + assert!( + !returned_to_launcher, + "detail {detail}: a launched ball must not rebound into the launcher" + ); + } } #[test] diff --git a/tdkpin-rs/src/table.rs b/tdkpin-rs/src/table.rs index 216b8f1..9428983 100644 --- a/tdkpin-rs/src/table.rs +++ b/tdkpin-rs/src/table.rs @@ -12,7 +12,10 @@ use crate::geometry::Segment; use macroquad::prelude::Vec2; -const WALL_BOUNCE: f32 = 0.82; +// The ordinary rail registrations in `FUN_1000_34ab` use the recovered 0.1 +// rebound coefficient. The previous 0.82 value made the shooter curve behave +// like a rubber bumper and sent the ball back down the launch lane. +const WALL_BOUNCE: f32 = 0.10; #[derive(Clone, Copy, Debug)] pub struct TableSegment {