fix(physics): restore live flipper geometry

Replace each invented centerline capsule with the original pair of moving line
records and moving tip circle. Use the exact asymmetric raised coordinates read
from live objects 66-68 and 81-83, while preserving their recovered type-1 and
type-2 fixed-point response coefficients in normal object traversal.

Remove the last generic segment overlap solver. Edge-time ball transfer remains
separate because the original applies it after physics while moving the records.

Test Plan:
- `cargo test --all-targets` -- 43 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- live left and right key-edge record dumps matched the Rust coordinates
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:00:22 +02:00
parent c886a93788
commit e07a915277
4 changed files with 73 additions and 101 deletions
+64 -59
View File
@@ -1,5 +1,5 @@
use crate::{
geometry::{Segment, closest_point, segment_collision},
geometry::{Segment, closest_point},
original_physics::{
GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS,
collide_with_circle, collide_with_line,
@@ -12,10 +12,10 @@ 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(134.0, 376.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(179.0, 376.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] =
@@ -412,16 +412,13 @@ impl Game {
continue;
}
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
// The original moves these four records with the flipper bodies.
if matches!(wall.id, 66 | 68 | 81 | 83) {
continue;
}
let segment = self.live_wall_segment(wall.id, wall.segment);
let mut response = velocity;
if collide_with_line(
old_position,
&mut response,
wall.segment.start,
wall.segment.end,
segment.start,
segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
) {
@@ -437,15 +434,11 @@ impl Game {
.chain(BUMPERS.iter())
.find(|circle| circle.id == object_id);
if let Some(circle) = circle {
// Objects 67 and 82 are the movable flipper tips.
if matches!(circle.id, 67 | 82) {
continue;
}
let mut response = velocity;
if collide_with_circle(
old_position,
&mut response,
circle.center,
self.live_circle_center(circle.id, circle.center),
circle.contact_radius,
f64::from(circle.normal_rebound),
f64::from(circle.tangent_coupling),
@@ -485,25 +478,6 @@ impl Game {
}
}
if hit_wall.is_none() && hit_circle.is_none() {
let (left_flipper, right_flipper) = self.flipper_segments();
if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
FLIPPER_CONTACT_RADIUS,
left_flipper,
) {
self.last_collision_id = Some(66);
} else if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
FLIPPER_CONTACT_RADIUS,
right_flipper,
) {
self.last_collision_id = Some(81);
}
}
if let Some(index) = hit_circle
.and_then(|circle_id| BUMPERS.iter().position(|bumper| bumper.id == circle_id))
&& !self.tilted
@@ -803,21 +777,23 @@ impl Game {
self.finished = true;
}
fn flipper_segments(&self) -> (Segment, Segment) {
let left_tip = if self.flippers.left_raised {
LEFT_FLIPPER_RAISED_TIP
} else {
LEFT_FLIPPER_REST_TIP
fn live_wall_segment(&self, object_id: u8, resting: Segment) -> Segment {
let (start, end) = match object_id {
66 if self.flippers.left_raised => (vec2(98.0, 382.0), vec2(131.0, 369.0)),
68 if self.flippers.left_raised => (vec2(137.0, 384.0), vec2(116.0, 405.0)),
81 if self.flippers.right_raised => (vec2(197.0, 405.0), vec2(171.0, 378.0)),
83 if self.flippers.right_raised => (vec2(183.0, 368.0), vec2(217.0, 383.0)),
_ => return resting,
};
let right_tip = if self.flippers.right_raised {
RIGHT_FLIPPER_RAISED_TIP
} else {
RIGHT_FLIPPER_REST_TIP
};
(
flipper_segment(LEFT_FLIPPER_PIVOT, left_tip),
flipper_segment(RIGHT_FLIPPER_PIVOT, right_tip),
)
Segment::new(start, end, resting.bounce)
}
fn live_circle_center(&self, object_id: u8, resting: Vec2) -> Vec2 {
match object_id {
67 if self.flippers.left_raised => LEFT_FLIPPER_RAISED_TIP,
82 if self.flippers.right_raised => RIGHT_FLIPPER_RAISED_TIP,
_ => resting,
}
}
}
@@ -838,10 +814,6 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
}
fn flipper_segment(pivot: Vec2, tip: Vec2) -> Segment {
Segment::new(pivot, tip, 0.88)
}
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),
@@ -1098,14 +1070,22 @@ mod tests {
#[test]
fn flippers_use_the_recovered_binary_positions() {
let mut game = Game::new(1);
let (left, right) = game.flipper_segments();
let wall = |id| {
WALLS
.iter()
.find(|wall| wall.id == id)
.expect("flipper wall exists")
.segment
};
let left_leading = game.live_wall_segment(66, wall(66));
let right_leading = game.live_wall_segment(81, wall(81));
assert_eq!(
(left.start, left.end),
(LEFT_FLIPPER_PIVOT, LEFT_FLIPPER_REST_TIP)
(left_leading.start, left_leading.end),
(vec2(112.0, 386.0), vec2(141.0, 413.0))
);
assert_eq!(
(right.start, right.end),
(RIGHT_FLIPPER_PIVOT, RIGHT_FLIPPER_REST_TIP)
(right_leading.start, right_leading.end),
(vec2(216.0, 411.0), vec2(177.0, 427.0))
);
let events = game.update(
@@ -1117,7 +1097,6 @@ mod tests {
..Controls::default()
},
);
let (left, right) = game.flipper_segments();
assert_eq!(
events
.iter()
@@ -1125,8 +1104,34 @@ mod tests {
.count(),
2
);
assert_eq!(left.end, LEFT_FLIPPER_RAISED_TIP);
assert_eq!(right.end, RIGHT_FLIPPER_RAISED_TIP);
let left_leading = game.live_wall_segment(66, wall(66));
let left_trailing = game.live_wall_segment(68, wall(68));
let right_leading = game.live_wall_segment(81, wall(81));
let right_trailing = game.live_wall_segment(83, wall(83));
assert_eq!(
(left_leading.start, left_leading.end),
(vec2(98.0, 382.0), vec2(131.0, 369.0))
);
assert_eq!(
(left_trailing.start, left_trailing.end),
(vec2(137.0, 384.0), vec2(116.0, 405.0))
);
assert_eq!(
game.live_circle_center(67, LEFT_FLIPPER_REST_TIP),
vec2(133.0, 377.0)
);
assert_eq!(
(right_leading.start, right_leading.end),
(vec2(197.0, 405.0), vec2(171.0, 378.0))
);
assert_eq!(
(right_trailing.start, right_trailing.end),
(vec2(183.0, 368.0), vec2(217.0, 383.0))
);
assert_eq!(
game.live_circle_center(82, RIGHT_FLIPPER_REST_TIP),
vec2(181.0, 376.0)
);
}
#[test]