From 7da0f4d171101ff2cb5dd74fa7ff91947e9349a9 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 22 Aug 2026 16:54:12 +0200 Subject: [PATCH] fix(game): launch immediately and enforce tilt Starting from the attract screen previously created an idle ball and required a second Down press to launch it. Launch the first ball as part of the start action while retaining the normal launch action for later balls. Replace the decaying visual-only tilt value with a latched game state. A tilt now forfeits the current bonus, suppresses scoring and further nudges, returns both flippers to rest, and remains active until the ball drains. Keep the warning on the playfield and use the recovered tilt sound instead of treating the state as a temporary decorative overlay. Test Plan: - `cargo fmt -- --check` -- passed - `cargo test --all-targets` -- passed, including tilt latch, scoring, flipper, drain-reset, and launch coverage - `cargo clippy --all-targets -- -D warnings` -- passed - `git diff --check` -- passed --- tdkpin-rs/src/app.rs | 12 +++-- tdkpin-rs/src/game.rs | 123 +++++++++++++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 23 deletions(-) diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 3cf0928..e161057 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -135,9 +135,14 @@ impl App { self.assets.play(2012, self.saved.settings.sounds); } if is_key_pressed(KeyCode::Down) || is_key_pressed(KeyCode::Enter) { - self.game = Some(Game::new(self.player_count)); + let mut game = Game::new(self.player_count); + let launched = game.launch(); + self.game = Some(game); self.screen = Screen::Playing; self.assets.play(2008, self.saved.settings.sounds); + if launched { + self.assets.play(2016, self.saved.settings.sounds); + } } } @@ -196,6 +201,7 @@ impl App { Event::Media => 2022, Event::ExtraBall => 2019, Event::Nudge | Event::BallSearch => 2021, + Event::Tilt => 2015, Event::Drain => 2013, }; self.assets.play(id, self.saved.settings.sounds); @@ -481,8 +487,8 @@ impl App { ); self.draw_displays(game); - if game.tilt > 1.15 { - draw_centered("TILT", 263.0, 34, RED); + if game.tilted { + draw_centered_at("TILT", 160.0, 263.0, 30, RED); } draw_text("F1 HELP F2 SETUP", 345.0, 448.0, 11.0, BLACK); } diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 847a841..e662553 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -7,6 +7,7 @@ const GRAVITY: f32 = 135.0; const MAX_SPEED: f32 = 430.0; const BALL_SEARCH_DELAY: f32 = 3.0; const BALL_SEARCH_SPEED: f32 = 24.0; +const TILT_THRESHOLD: f32 = 1.15; #[derive(Clone, Copy, Debug, Default)] pub struct Controls { @@ -28,6 +29,7 @@ pub enum Event { Media, ExtraBall, Nudge, + Tilt, BallSearch, Drain, } @@ -83,7 +85,7 @@ pub struct Game { pub top_targets: [bool; 3], pub lock_lights: u8, pub magnets: f32, - pub tilt: f32, + pub tilted: bool, pub left_flipper_angle: f32, pub right_flipper_angle: f32, pub finished: bool, @@ -91,6 +93,7 @@ pub struct Game { target_cooldown: f32, bumper_cooldown: f32, nudge_cooldown: f32, + nudge_meter: f32, stalled_for: f32, } @@ -106,7 +109,7 @@ impl Game { top_targets: [false; 3], lock_lights: 0, magnets: 0.0, - tilt: 0.0, + tilted: false, left_flipper_angle: -2.72, right_flipper_angle: -0.42, finished: false, @@ -114,6 +117,7 @@ impl Game { target_cooldown: 0.0, bumper_cooldown: 0.0, nudge_cooldown: 0.0, + nudge_meter: 0.0, stalled_for: 0.0, } } @@ -122,13 +126,31 @@ impl Game { &self.players[self.current_player] } + pub fn launch(&mut self) -> bool { + if !self.ball.in_launcher || self.tilted { + return false; + } + self.ball.in_launcher = false; + self.ball.velocity = vec2(12.0, -330.0); + self.stalled_for = 0.0; + true + } + pub fn update(&mut self, frame_time: f32, detail: u8, controls: Controls) -> Vec { if self.finished { return Vec::new(); } let mut events = Vec::new(); - let desired_left = if controls.left_flipper { -2.18 } else { -2.72 }; - let desired_right = if controls.right_flipper { -0.96 } else { -0.42 }; + let desired_left = if controls.left_flipper && !self.tilted { + -2.18 + } else { + -2.72 + }; + let desired_right = if controls.right_flipper && !self.tilted { + -0.96 + } else { + -0.42 + }; let flipper_rate = 14.0 * frame_time; let old_left = self.left_flipper_angle; let old_right = self.right_flipper_angle; @@ -136,23 +158,27 @@ impl Game { (desired_left - self.left_flipper_angle).clamp(-flipper_rate, flipper_rate); self.right_flipper_angle += (desired_right - self.right_flipper_angle).clamp(-flipper_rate, flipper_rate); - if (controls.left_flipper && old_left < -2.68) - || (controls.right_flipper && old_right > -0.46) + if !self.tilted + && ((controls.left_flipper && old_left < -2.68) + || (controls.right_flipper && old_right > -0.46)) { events.push(Event::Flipper); } - if controls.launch_pressed && self.ball.in_launcher { - self.ball.in_launcher = false; - self.ball.velocity = vec2(12.0, -330.0); - self.stalled_for = 0.0; + if controls.launch_pressed && self.launch() { events.push(Event::Launch); } - if controls.nudge.abs() > 0.1 && self.nudge_cooldown <= 0.0 { + if !self.tilted && controls.nudge.abs() > 0.1 && self.nudge_cooldown <= 0.0 { self.ball.velocity.x += controls.nudge * 55.0; - self.tilt += controls.nudge.abs() * 0.34; + self.nudge_meter += controls.nudge.abs() * 0.34; self.nudge_cooldown = 0.22; - events.push(Event::Nudge); + if self.nudge_meter >= TILT_THRESHOLD { + self.tilted = true; + self.bonus = 0; + events.push(Event::Tilt); + } else { + events.push(Event::Nudge); + } } self.accumulator = (self.accumulator + frame_time.min(0.05)).min(0.1); @@ -171,7 +197,9 @@ impl Game { self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0); self.nudge_cooldown = (self.nudge_cooldown - dt).max(0.0); self.magnets = (self.magnets - dt).max(0.0); - self.tilt = (self.tilt - dt * 0.08).max(0.0); + if !self.tilted { + self.nudge_meter = (self.nudge_meter - dt * 0.34).max(0.0); + } if self.ball.in_launcher { self.ball.position = vec2(157.0, 426.0); @@ -216,15 +244,15 @@ impl Game { } for center in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)] { - if circle_collision( + let hit = circle_collision( &mut self.ball.position, &mut self.ball.velocity, BALL_RADIUS, center, 14.0, 105.0, - ) && self.bumper_cooldown <= 0.0 - { + ); + if hit && !self.tilted && self.bumper_cooldown <= 0.0 { self.add_score(self.player().bumper_value); self.bonus = self.bonus.saturating_add(100); self.bumper_cooldown = 0.08; @@ -232,8 +260,10 @@ impl Game { } } - self.check_targets(events); - self.check_media(events); + if !self.tilted { + self.check_targets(events); + self.check_media(events); + } if self.ball.position.y > 454.0 { if self.magnets > 0.0 && (self.ball.position.x < 145.0 || self.ball.position.x > 175.0) @@ -408,7 +438,8 @@ impl Game { self.wheel_holes.fill(false); self.lock_lights = 0; self.magnets = 0.0; - self.tilt = 0.0; + self.tilted = false; + self.nudge_meter = 0.0; self.stalled_for = 0.0; let mut next = (self.current_player + 1) % self.players.len(); @@ -613,4 +644,56 @@ mod tests { assert!(events.contains(&Event::BallSearch)); assert!(game.ball.velocity.y < 0.0); } + + #[test] + fn tilt_latches_and_disables_flippers_and_scoring() { + let mut game = Game::new(1); + game.bonus = 4_000; + for _ in 0..4 { + game.nudge_cooldown = 0.0; + game.update( + 1.0 / 60.0, + 3, + Controls { + nudge: 1.0, + ..Controls::default() + }, + ); + } + assert!(game.tilted); + assert_eq!(game.bonus, 0); + + game.left_flipper_angle = -2.18; + game.right_flipper_angle = -0.96; + let score = game.player().score; + game.ball.in_launcher = false; + game.ball.position = vec2(201.0, 285.0); + game.ball.velocity = Vec2::ZERO; + game.update( + 1.0 / 30.0, + 5, + Controls { + left_flipper: true, + right_flipper: true, + ..Controls::default() + }, + ); + + assert!(game.left_flipper_angle < -2.18); + assert!(game.right_flipper_angle > -0.96); + assert_eq!(game.player().score, score); + } + + #[test] + fn drain_clears_latched_tilt() { + let mut game = Game::new(1); + game.tilted = true; + game.ball.in_launcher = false; + game.ball.position = vec2(157.0, 455.0); + + game.update(1.0 / 60.0, 3, Controls::default()); + + assert!(!game.tilted); + assert!(game.ball.in_launcher); + } }