fix(input): restore original nudge and tilt behavior

Replace the float nudge direction, cooldown, and decaying meter with the
scan-code-specific Win16 behavior. Left Shift and keypad 3 now use the original
random directional impulse, while Space uses the reconstructed random horizontal
and vertical formulas with scalar 15 and wrapping millipixel velocities.

Track the original 16-bit tilt counter: each nudge adds 25, compares against
30 + Random(10), and the selected detail timer decrements a nonzero counter once
per callback. Allow the same input while a ball waits in the launcher, retain
bonus state when tilt latches, clear flippers immediately, and keep Right Shift
only as a documented modern alias for keypad 3.

Test Plan:
- `cargo test --all-targets` -- passed, 57 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/README.md tdkpin-rs/RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 16:55:02 +02:00
parent c25287186a
commit aae4671586
5 changed files with 140 additions and 65 deletions
+5 -13
View File
@@ -1,6 +1,6 @@
use crate::{
assets::Assets,
game::{Controls, Event, Game},
game::{Controls, Event, Game, Nudge},
persistence::{HighScore, Language, Persistence, SavedData, insert_high_score},
table::BUMPERS,
};
@@ -174,21 +174,13 @@ impl App {
|| is_key_down(KeyCode::D)
|| is_key_down(KeyCode::Right);
let nudge = if is_key_pressed(KeyCode::LeftShift) {
-1.0
Nudge::Left
} else if is_key_pressed(KeyCode::RightShift) || is_key_pressed(KeyCode::Kp3) {
1.0
Nudge::Right
} else if is_key_pressed(KeyCode::Space) {
if self
.game
.as_ref()
.is_some_and(|game| game.ball.position.x < WIDTH / 4.0)
{
1.0
} else {
-1.0
}
Nudge::Center
} else {
0.0
Nudge::None
};
let controls = Controls {
left_flipper,
+129 -50
View File
@@ -19,7 +19,6 @@ const LEFT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(133.0, 377.0);
const RIGHT_FLIPPER_PIVOT: Vec2 = Vec2::new(210.0, 397.0);
const RIGHT_FLIPPER_REST_TIP: Vec2 = Vec2::new(179.0, 419.0);
const RIGHT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(181.0, 376.0);
const TILT_THRESHOLD: f32 = 1.15;
const LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0);
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];
@@ -41,7 +40,16 @@ pub struct Controls {
pub left_flipper: bool,
pub right_flipper: bool,
pub launch_down: bool,
pub nudge: f32,
pub nudge: Nudge,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Nudge {
#[default]
None,
Left,
Right,
Center,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -229,8 +237,8 @@ pub struct Game {
pub last_collision_id: Option<u8>,
accumulator: f32,
bumper_cooldown: f32,
nudge_cooldown: f32,
nudge_meter: f32,
tilt_counter: u16,
tilt_counter_accumulator: f32,
launcher_was_down: bool,
launcher_hold_seconds: f32,
launcher_next_repeat: f32,
@@ -270,8 +278,8 @@ impl Game {
last_collision_id: None,
accumulator: 0.0,
bumper_cooldown: 0.0,
nudge_cooldown: 0.0,
nudge_meter: 0.0,
tilt_counter: 0,
tilt_counter_accumulator: 0.0,
launcher_was_down: false,
launcher_hold_seconds: 0.0,
launcher_next_repeat: LAUNCHER_REPEAT_DELAY_SECONDS,
@@ -339,6 +347,7 @@ impl Game {
}
self.last_collision_id = None;
self.claw_frame_seconds = DETAIL_TIMER_SECONDS[usize::from(detail.clamp(1, 5) - 1)];
self.advance_tilt_counter(frame_time.min(0.05));
let mut events = Vec::new();
let old_flippers = self.flippers;
self.flippers.left_raised = controls.left_flipper && !self.tilted;
@@ -376,24 +385,8 @@ impl Game {
events.push(Event::Sound(2002));
}
}
if !self.tilted
&& !self.ball.in_launcher
&& controls.nudge.abs() > 0.1
&& self.nudge_cooldown <= 0.0
{
self.ball.velocity.x += controls.nudge * 55.0;
self.nudge_meter += controls.nudge.abs() * 0.34;
self.nudge_cooldown = 0.22;
self.nudge_shake = 0.18;
events.push(Event::Sound(2019));
if self.nudge_meter >= TILT_THRESHOLD {
self.tilted = true;
self.bonus = 0;
events.push(Event::Tilt);
events.push(Event::Sound(2020));
} else {
events.push(Event::Nudge);
}
if !self.tilted && controls.nudge != Nudge::None {
self.apply_nudge(controls.nudge, &mut events);
}
self.accumulator = (self.accumulator + frame_time.min(0.05)).min(0.1);
@@ -405,19 +398,79 @@ impl Game {
events
}
fn advance_tilt_counter(&mut self, elapsed: f32) {
self.tilt_counter_accumulator += elapsed;
while self.tilt_counter_accumulator >= self.claw_frame_seconds {
self.tilt_counter_accumulator -= self.claw_frame_seconds;
if self.tilt_counter != 0 {
self.tilt_counter -= 1;
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn apply_nudge(&mut self, nudge: Nudge, events: &mut Vec<Event>) {
const SCALAR: i32 = 15;
events.push(Event::Sound(2019));
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
match nudge {
Nudge::Left | Nudge::Right => {
let amount = (50 - i32::from(self.random.below(20))) * SCALAR;
let signed = if nudge == Nudge::Left { -amount } else { amount };
velocity.x = velocity.x.wrapping_add(signed);
}
Nudge::Center => {
let horizontal =
(f64::from(SCALAR) * (self.random.unit_interval() * 20.0 - 10.0)).round()
as i32;
let vertical = -(60 - i32::from(self.random.below(20))) * SCALAR;
velocity.x = velocity.x.wrapping_add(horizontal);
velocity.y = velocity.y.wrapping_add(vertical);
}
Nudge::None => return,
}
self.ball.velocity = velocity.to_velocity_per_second();
if let Some(secondary) = &mut self.secondary_ball {
let mut slot_velocity = MilliVec::from_velocity_per_second(secondary.velocity);
match nudge {
Nudge::Left | Nudge::Right => {
let amount = if nudge == Nudge::Left { -600 } else { 600 };
slot_velocity.x = slot_velocity.x.wrapping_add(amount);
}
Nudge::Center => {
let horizontal = (i32::from(self.random.below(21)) - 10) * SCALAR;
let vertical = -(i32::from(self.random.below(100)) + 50) * SCALAR;
slot_velocity.x = slot_velocity.x.wrapping_add(horizontal);
slot_velocity.y = slot_velocity.y.wrapping_add(vertical);
}
Nudge::None => {}
}
secondary.velocity = slot_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 {
self.tilted = true;
self.flippers = Flippers::default();
self.pending_flipper_edges.fill(0);
events.push(Event::Tilt);
events.push(Event::Sound(2020));
} else {
events.push(Event::Nudge);
}
}
#[allow(clippy::too_many_lines)]
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
self.nudge_cooldown = (self.nudge_cooldown - dt).max(0.0);
self.wheel_animation = (self.wheel_animation - dt).max(0.0);
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
for flash in &mut self.bumper_flash {
*flash = (*flash - dt).max(0.0);
}
if !self.tilted {
self.nudge_meter = (self.nudge_meter - dt * 0.34).max(0.0);
}
self.update_claw(dt, events);
if self.claw.ball_suspended {
self.pending_flipper_edges.fill(0);
@@ -1109,7 +1162,8 @@ impl Game {
self.top_targets.fill(false);
self.wheel_holes.fill(false);
self.tilted = false;
self.nudge_meter = 0.0;
self.tilt_counter = 0;
self.tilt_counter_accumulator = 0.0;
self.bumper_flash.fill(0.0);
self.wheel_animation = 0.0;
self.claw = Claw::default();
@@ -1515,22 +1569,21 @@ mod tests {
#[test]
fn tilt_latches_and_disables_flippers_and_scoring() {
let mut game = Game::new(1);
let mut game = Game::new_with_seed(1, 7);
assert!(launch_ball(&mut game, 1).contains(&Event::Launch));
game.bonus = 4_000;
for _ in 0..4 {
game.nudge_cooldown = 0.0;
for _ in 0..2 {
game.update(
1.0 / 60.0,
0.0,
3,
Controls {
nudge: 1.0,
nudge: Nudge::Right,
..Controls::default()
},
);
}
assert!(game.tilted);
assert_eq!(game.bonus, 0);
assert_eq!(game.bonus, 4_000);
game.flippers.left_raised = true;
game.flippers.right_raised = true;
@@ -1713,10 +1766,10 @@ mod tests {
#[test]
fn nudge_and_tilt_emit_the_original_ordered_sound_sequence() {
let mut game = Game::new(1);
let mut game = Game::new_with_seed(1, 7);
game.ball.in_launcher = false;
let controls = Controls {
nudge: 1.0,
nudge: Nudge::Right,
..Controls::default()
};
@@ -1724,15 +1777,43 @@ mod tests {
game.update(0.0, 3, controls),
[Event::Sound(2019), Event::Nudge]
);
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { x: 690, y: 0 }
);
assert_eq!(game.tilt_counter, 25);
game.nudge_cooldown = 0.0;
game.nudge_meter = TILT_THRESHOLD - 0.01;
game.tilt_counter = 15;
assert_eq!(
game.update(0.0, 3, controls),
[Event::Sound(2019), Event::Tilt, Event::Sound(2020)]
);
}
#[test]
fn center_nudge_and_tilt_decay_match_the_detail_timer() {
let mut game = Game::new_with_seed(1, 7);
let events = game.update(
0.0,
3,
Controls {
nudge: Nudge::Center,
..Controls::default()
},
);
assert_eq!(events, [Event::Sound(2019), Event::Nudge]);
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { x: -84, y: -660 }
);
assert_eq!(game.tilt_counter, 25);
game.update(0.029, 3, Controls::default());
assert_eq!(game.tilt_counter, 25);
game.update(0.001_1, 3, Controls::default());
assert_eq!(game.tilt_counter, 24);
}
#[test]
fn claw_sprite_rects_follow_the_recovered_four_row_sheet() {
let source = |frame, bank| {
@@ -1997,24 +2078,22 @@ mod tests {
}
#[test]
fn waiting_ball_cannot_be_tilted() {
let mut game = Game::new(1);
fn waiting_ball_uses_the_same_nudge_tilt_counter() {
let mut game = Game::new_with_seed(1, 7);
for _ in 0..12 {
game.nudge_cooldown = 0.0;
for attempt in 0..2 {
let events = game.update(
1.0 / 60.0,
0.0,
3,
Controls {
nudge: 1.0,
nudge: Nudge::Right,
..Controls::default()
},
);
assert!(!events.contains(&Event::Nudge));
assert!(!events.contains(&Event::Tilt));
assert_eq!(events.contains(&Event::Tilt), attempt == 1);
}
assert!(!game.tilted);
assert!(launch_ball(&mut game, 1).contains(&Event::Launch));
assert!(game.tilted);
assert!(game.ball.in_launcher);
}
}