fix(physics): restore the flipper downstroke

Represent press and release as distinct deferred flipper edges. Keep upward
position/velocity transfer unchanged, and apply the position-preserving
return-stroke velocity recovered from a live ball at `(125,390)`, mirrored for
the right flipper.

The downstroke now assigns raw `(3519,6302)` before the next physics clamp,
matching the original instead of moving geometry silently.

Test Plan:
- `cargo test --all-targets` -- 48 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- live downstroke transfer test -- passed within one raw unit
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:36:24 +02:00
parent 7d28818e23
commit b86810ebed
3 changed files with 35 additions and 17 deletions
+30 -14
View File
@@ -219,7 +219,7 @@ pub struct Game {
launcher_velocity_milli: i32,
player_entry: PlayerEntry,
claw_rng_state: u32,
pending_flipper_kicks: [bool; 2],
pending_flipper_edges: [i8; 2],
trigger_contacts: [bool; 176],
object_active: [bool; 176],
}
@@ -257,7 +257,7 @@ impl Game {
launcher_velocity_milli: 0,
player_entry: PlayerEntry::Open,
claw_rng_state: seed.max(1),
pending_flipper_kicks: [false; 2],
pending_flipper_edges: [0; 2],
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
}
@@ -323,11 +323,11 @@ impl Game {
self.flippers.right_raised = controls.right_flipper && !self.tilted;
if self.flippers.left_raised != old_flippers.left_raised {
events.push(Event::FlipperMove);
self.pending_flipper_kicks[0] = self.flippers.left_raised;
self.pending_flipper_edges[0] = if self.flippers.left_raised { 1 } else { -1 };
}
if self.flippers.right_raised != old_flippers.right_raised {
events.push(Event::FlipperMove);
self.pending_flipper_kicks[1] = self.flippers.right_raised;
self.pending_flipper_edges[1] = if self.flippers.right_raised { 1 } else { -1 };
}
if self.ball.in_launcher && !self.tilted {
@@ -393,7 +393,7 @@ impl Game {
self.update_claw(dt, events);
if self.claw.ball_suspended {
self.pending_flipper_kicks.fill(false);
self.pending_flipper_edges.fill(0);
return;
}
@@ -720,13 +720,13 @@ impl Game {
}
fn apply_flipper_kicks(&mut self) {
let pending = self.pending_flipper_kicks;
self.pending_flipper_kicks.fill(false);
let pending = self.pending_flipper_edges;
self.pending_flipper_edges.fill(0);
if self.ball.in_launcher || self.tilted {
return;
}
for (should_kick, pivot, rest_tip, raised_tip, horizontal_sign) in [
for (edge, pivot, rest_tip, raised_tip, horizontal_sign) in [
(
pending[0],
LEFT_FLIPPER_PIVOT,
@@ -742,13 +742,20 @@ impl Game {
-1.0,
),
] {
if !should_kick
|| !point_in_flipper_sweep(self.ball.position, pivot, rest_tip, raised_tip)
if edge == 0 || !point_in_flipper_sweep(self.ball.position, pivot, rest_tip, raised_tip)
{
continue;
}
let local_x = (self.ball.position.x - pivot.x) * horizontal_sign;
let local_y = self.ball.position.y - pivot.y;
if edge < 0 {
let velocity_milli = MilliVec::from_millipixels(vec2(
horizontal_sign * local_x * 159.95,
local_x * 286.45,
));
self.ball.velocity = velocity_milli.to_velocity_per_second();
continue;
}
let horizontal_displacement =
0.002_12 * local_x.powi(2) - 0.125_68 * local_x + 14.921 - (local_y + 7.0);
let vertical_displacement = -(local_x + 13.0);
@@ -1271,7 +1278,7 @@ mod tests {
game.ball.in_launcher = false;
game.ball.position = vec2(125.0, 390.0);
game.ball.velocity = Vec2::ZERO;
game.pending_flipper_kicks[0] = true;
game.pending_flipper_edges[0] = 1;
game.apply_flipper_kicks();
let velocity_after_edge = game.ball.velocity;
@@ -1279,7 +1286,7 @@ mod tests {
let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge);
assert!((raw_velocity.x - 2_217).abs() <= 2);
assert!((raw_velocity.y - -5_889).abs() <= 2);
assert_eq!(game.pending_flipper_kicks, [false, false]);
assert_eq!(game.pending_flipper_edges, [0, 0]);
game.apply_flipper_kicks();
assert_eq!(
@@ -1289,14 +1296,23 @@ mod tests {
}
#[test]
fn returning_flipper_moves_and_sounds_without_queueing_a_kick() {
fn returning_flipper_matches_the_live_downstroke_probe() {
let mut game = Game::new(1);
game.flippers.left_raised = true;
let events = game.update(0.0, 3, Controls::default());
assert_eq!(events, [Event::FlipperMove]);
assert_eq!(game.pending_flipper_kicks, [false, false]);
assert_eq!(game.pending_flipper_edges, [-1, 0]);
game.ball.in_launcher = false;
game.ball.position = vec2(125.0, 390.0);
game.ball.velocity = Vec2::ZERO;
game.apply_flipper_kicks();
let raw_velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
assert!((raw_velocity.x - 3_519).abs() <= 1);
assert!((raw_velocity.y - 6_302).abs() <= 1);
assert_eq!(game.ball.position, vec2(125.0, 390.0));
assert_eq!(game.pending_flipper_edges, [0, 0]);
}
#[test]