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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+103
-20
@@ -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<Event> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user