fix(physics): replace fitted flipper transfer

Remove the live-fitted upward and downward flipper polynomials. Port the
reconstructed 1000:7ed9 path with delta-specific pivots and record edges,
integer cross-product and radius gates, penetration, response-record gain,
wrapping velocity updates, and the final position delta.

Use resting records on press and the already-raised records on release, correct
the original delta direction, and apply each edge to both live ball slots. Keep
render-facing Vec2 projections, but adjust their float representation by ULPs so
every gameplay millipixel round-trips exactly instead of losing reconstructed
integer results.

Test Plan:
- `bash original/tools/test_reconstructed_c.sh` -- passed during raised-record reference capture
- `cargo test --all-targets` -- passed, 62 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 17:11:48 +02:00
parent c1d8e5755f
commit d72db79af6
7 changed files with 392 additions and 92 deletions
+31 -78
View File
@@ -1,6 +1,7 @@
use crate::{
borland_random::BorlandRandom,
geometry::{Segment, closest_point},
flipper_physics::{FlipperSide, moving_flipper_response},
geometry::Segment,
original_physics::{
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
STEP_SECONDS, circle_collision_response, line_collision_response, path_intersects_circle,
@@ -12,12 +13,7 @@ use crate::{
};
use macroquad::prelude::{Rect, Vec2, vec2};
const FLIPPER_CONTACT_RADIUS: f32 = 9.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);
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 LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0);
const LAUNCHER_PRESS_IMPULSE_MILLI: i32 = 750;
@@ -399,12 +395,12 @@ impl Game {
if self.flippers.left_raised != old_flippers.left_raised {
events.push(Event::FlipperMove);
events.push(Event::Sound(2021));
self.pending_flipper_edges[0] = if self.flippers.left_raised { 1 } else { -1 };
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);
events.push(Event::Sound(2021));
self.pending_flipper_edges[1] = if self.flippers.right_raised { 1 } else { -1 };
self.pending_flipper_edges[1] = if self.flippers.right_raised { -1 } else { 1 };
}
if self.ball.in_launcher && !self.tilted {
@@ -1092,47 +1088,17 @@ impl Game {
return;
}
for (edge, pivot, rest_tip, raised_tip, horizontal_sign) in [
(
pending[0],
LEFT_FLIPPER_PIVOT,
LEFT_FLIPPER_REST_TIP,
LEFT_FLIPPER_RAISED_TIP,
1.0,
),
(
pending[1],
RIGHT_FLIPPER_PIVOT,
RIGHT_FLIPPER_REST_TIP,
RIGHT_FLIPPER_RAISED_TIP,
-1.0,
),
for (delta, side) in [
(i32::from(pending[0]), FlipperSide::Left),
(i32::from(pending[1]), FlipperSide::Right),
] {
if edge == 0 || !point_in_flipper_sweep(self.ball.position, pivot, rest_tip, raised_tip)
{
if delta == 0 {
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;
apply_flipper_response_to_ball(&mut self.ball, delta, side);
if let Some(secondary) = &mut self.secondary_ball {
apply_flipper_response_to_ball(secondary, delta, side);
}
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();
}
}
@@ -1312,26 +1278,15 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
}
fn point_in_flipper_sweep(point: Vec2, pivot: Vec2, rest_tip: Vec2, raised_tip: Vec2) -> bool {
let boundary_distance_squared = [
Segment::new(pivot, rest_tip, 0.0),
Segment::new(pivot, raised_tip, 0.0),
Segment::new(rest_tip, raised_tip, 0.0),
]
.into_iter()
.map(|segment| point.distance_squared(closest_point(point, segment)))
.fold(f32::INFINITY, f32::min);
if boundary_distance_squared <= FLIPPER_CONTACT_RADIUS.powi(2) {
return true;
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
let position = MilliVec::from_position(ball.position);
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
if let Some(response) =
moving_flipper_response(position, velocity, delta, side, 0.5, MAXIMUM_SPEED_MILLI_PER_STEP)
{
ball.position = position.add(response.movement).to_position();
ball.velocity = response.velocity.to_velocity_per_second();
}
let cross = |a: Vec2, b: Vec2, p: Vec2| (b - a).perp_dot(p - a);
let signs = [
cross(pivot, rest_tip, point),
cross(rest_tip, raised_tip, point),
cross(raised_tip, pivot, point),
];
signs.iter().all(|value| *value >= 0.0) || signs.iter().all(|value| *value <= 0.0)
}
#[allow(clippy::too_many_lines)]
@@ -1801,7 +1756,7 @@ mod tests {
(vec2(137.0, 384.0), vec2(116.0, 405.0))
);
assert_eq!(
game.live_circle_center(67, LEFT_FLIPPER_REST_TIP),
game.live_circle_center(67, vec2(134.0, 419.0)),
vec2(133.0, 377.0)
);
assert_eq!(
@@ -1813,7 +1768,7 @@ mod tests {
(vec2(183.0, 368.0), vec2(217.0, 383.0))
);
assert_eq!(
game.live_circle_center(82, RIGHT_FLIPPER_REST_TIP),
game.live_circle_center(82, vec2(179.0, 419.0)),
vec2(181.0, 376.0)
);
}
@@ -1856,19 +1811,18 @@ mod tests {
}
#[test]
fn raising_flipper_matches_the_live_swept_transfer_probe() {
fn raising_flipper_uses_the_reconstructed_moving_response() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = vec2(125.0, 390.0);
game.ball.velocity = Vec2::ZERO;
game.pending_flipper_edges[0] = 1;
game.pending_flipper_edges[0] = -1;
game.apply_flipper_kicks();
let velocity_after_edge = game.ball.velocity;
assert!(game.ball.position.distance(vec2(138.182, 355.0)) < 0.002);
assert_eq!(game.ball.position, vec2(138.124, 355.0));
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!(raw_velocity, MilliVec { x: 2_209, y: -5_891 });
assert_eq!(game.pending_flipper_edges, [0, 0]);
game.apply_flipper_kicks();
@@ -1879,22 +1833,21 @@ mod tests {
}
#[test]
fn returning_flipper_matches_the_live_downstroke_probe() {
fn returning_flipper_uses_the_raised_record_geometry() {
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, Event::Sound(2021)]);
assert_eq!(game.pending_flipper_edges, [-1, 0]);
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.ball.position = vec2(110.0, 410.0);
game.ball.velocity = vec2(100.0, 200.0);
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!(raw_velocity, MilliVec { x: 765, y: 3_997 });
assert_eq!(game.ball.position, vec2(110.380, 411.984));
assert_eq!(game.pending_flipper_edges, [0, 0]);
}