diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index ef88215..846fc58 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -438,8 +438,6 @@ impl App { } } if game.ball.in_launcher { - let frame = [0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95] - .partition_point(|threshold| game.launcher_charge >= *threshold); draw_texture_ex( &self.assets.plunger, 314.0, @@ -447,7 +445,12 @@ impl App { WHITE, DrawTextureParams { dest_size: Some(vec2(21.0, 18.0)), - source: Some(Rect::new(frame as f32 * 21.0, 0.0, 20.0, 17.0)), + source: Some(Rect::new( + game.launcher_frame() as f32 * 21.0, + 0.0, + 20.0, + 17.0, + )), ..Default::default() }, ); diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 289a7ec..fd62fcf 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -22,7 +22,12 @@ const BALL_SEARCH_DELAY: f32 = 3.0; const BALL_SEARCH_SPEED: f32 = 24.0; const TILT_THRESHOLD: f32 = 1.15; const LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0); -const LAUNCHER_CHARGE_SECONDS: f32 = 0.55; +const LAUNCHER_FRAME_THRESHOLDS: [f32; 10] = + [0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95]; +// The original advances resource 901 through repeated Down-key events. A +// fixed duration keeps that deliberate hold consistent across modern systems +// whose keyboard-repeat delay and rate differ. +const LAUNCHER_CHARGE_SECONDS: f32 = 1.0; const LAUNCH_SPEED_MIN: f32 = 330.0; const LAUNCH_SPEED_RANGE: f32 = 100.0; @@ -159,6 +164,10 @@ impl Game { &self.players[self.current_player] } + pub fn launcher_frame(&self) -> usize { + LAUNCHER_FRAME_THRESHOLDS.partition_point(|threshold| self.launcher_charge >= *threshold) + } + fn fire_launcher(&mut self) { let launch_speed = LAUNCH_SPEED_MIN + self.launcher_charge * LAUNCH_SPEED_RANGE; self.ball.in_launcher = false; @@ -185,7 +194,12 @@ impl Game { if self.ball.in_launcher && !self.tilted { if controls.launch_down { let charge_step = frame_time.min(0.05) / LAUNCHER_CHARGE_SECONDS; - self.launcher_charge = (self.launcher_charge + charge_step).min(1.0); + let next_charge = self.launcher_charge + charge_step; + self.launcher_charge = if next_charge >= 1.0 - f32::EPSILON * 4.0 { + 1.0 + } else { + next_charge + }; self.launcher_was_down = true; } else if self.launcher_was_down { self.fire_launcher(); @@ -649,17 +663,38 @@ mod tests { let quick_speed = -quick.ball.velocity.y; let mut charged = Game::new(1); - launch_ball(&mut charged, 33); + launch_ball(&mut charged, 60); let charged_speed = -charged.ball.velocity.y; assert!(charged_speed > quick_speed + 80.0); } + #[test] + fn maximum_launch_requires_a_deliberate_hold() { + let mut game = Game::new(1); + let held = Controls { + launch_down: true, + ..Controls::default() + }; + + for _ in 0..30 { + game.update(1.0 / 60.0, 3, held); + } + assert!((0.45..0.55).contains(&game.launcher_charge)); + assert_eq!(game.launcher_frame(), 5); + + for _ in 0..30 { + game.update(1.0 / 60.0, 3, held); + } + assert!((game.launcher_charge - 1.0).abs() <= f32::EPSILON); + assert_eq!(game.launcher_frame(), 10); + } + #[test] fn charged_ball_clears_the_shooter_lane() { for detail in 1..=5 { let mut game = Game::new(1); - launch_ball(&mut game, 33); + launch_ball(&mut game, 60); let mut entered_table = false; let mut returned_to_launcher = false; let mut minimum_y = game.ball.position.y;