fix(physics): restore upward flipper transfer

Apply flipper movement after the physics substep, matching the original timer
order. Replace the weak generic tangent kick with the recovered swept-body
position transfer and pre-clamp velocity curve fitted from three live left-edge
probes, then mirror that local transform for the right flipper.

The x=125 probe now reaches `(138.182,355)` with raw velocity within two units
of `(2217,-5889)`. Keep downstroke interaction listed as pending rather than
claiming the upward fit is the complete raw routine.

Test Plan:
- `cargo test --all-targets` -- 43 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- three live left-flipper transfer probes matched within documented tolerance
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:05:02 +02:00
parent e07a915277
commit d813b5b9da
4 changed files with 39 additions and 21 deletions
+24 -17
View File
@@ -9,7 +9,6 @@ use crate::{
use macroquad::prelude::{Rect, Vec2, vec2};
const FLIPPER_CONTACT_RADIUS: f32 = 9.0;
const FLIPPER_EDGE_KICK: f32 = 118.0;
const LEFT_FLIPPER_PIVOT: Vec2 = Vec2::new(103.0, 397.0);
const LEFT_FLIPPER_REST_TIP: Vec2 = Vec2::new(134.0, 419.0);
const LEFT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(133.0, 377.0);
@@ -392,8 +391,6 @@ impl Game {
return;
}
self.apply_flipper_kicks();
if self.ball.in_launcher {
self.ball.position = LAUNCHER_POSITION;
self.ball.velocity = Vec2::ZERO;
@@ -498,6 +495,8 @@ impl Game {
return;
}
self.apply_flipper_kicks();
if self.ball.position.y > 470.0 {
// Only malformed/out-of-table states reach this guard; the real
// drain is collision object 2 at y=455.
@@ -645,20 +644,20 @@ impl Game {
return;
}
for (should_kick, pivot, rest_tip, raised_tip, clockwise) in [
for (should_kick, pivot, rest_tip, raised_tip, horizontal_sign) in [
(
pending[0],
LEFT_FLIPPER_PIVOT,
LEFT_FLIPPER_REST_TIP,
LEFT_FLIPPER_RAISED_TIP,
true,
1.0,
),
(
pending[1],
RIGHT_FLIPPER_PIVOT,
RIGHT_FLIPPER_REST_TIP,
RIGHT_FLIPPER_RAISED_TIP,
false,
-1.0,
),
] {
if !should_kick
@@ -666,13 +665,19 @@ impl Game {
{
continue;
}
let radial = (self.ball.position - pivot).normalize_or_zero();
let tangent = if clockwise {
vec2(radial.y, -radial.x)
} else {
vec2(-radial.y, radial.x)
};
self.ball.velocity += tangent * FLIPPER_EDGE_KICK;
let local_x = (self.ball.position.x - pivot.x) * horizontal_sign;
let local_y = self.ball.position.y - pivot.y;
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);
let velocity_scale = -0.026 * local_x.powi(2) + 3.614 * local_x + 101.327;
let displacement = vec2(
horizontal_displacement * horizontal_sign,
vertical_displacement,
);
self.ball.position += displacement;
let velocity_milli = MilliVec::from_millipixels(displacement * velocity_scale);
self.ball.velocity = velocity_milli.to_velocity_per_second();
}
}
@@ -1161,17 +1166,19 @@ mod tests {
}
#[test]
fn raising_flipper_applies_one_swept_tangential_kick() {
fn raising_flipper_matches_the_live_swept_transfer_probe() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = (LEFT_FLIPPER_PIVOT + LEFT_FLIPPER_REST_TIP) * 0.5;
game.ball.position = vec2(125.0, 390.0);
game.ball.velocity = Vec2::ZERO;
game.pending_flipper_kicks[0] = true;
game.apply_flipper_kicks();
let velocity_after_edge = game.ball.velocity;
assert!((velocity_after_edge.length() - FLIPPER_EDGE_KICK).abs() < 0.001);
assert!(velocity_after_edge.y < 0.0);
assert!(game.ball.position.distance(vec2(138.182, 355.0)) < 0.002);
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]);
game.apply_flipper_kicks();