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:
@@ -131,3 +131,11 @@ speed `vt`, ordinary object 155 produced `vn' = -0.6 * vn` and
|
|||||||
`vt' = vt - 0.1 * vn`. Bumper 51 additionally added `0.4 * 3800` along `n`.
|
`vt' = vt - 0.1 * vn`. Bumper 51 additionally added `0.4 * 3800` along `n`.
|
||||||
The exact captured transitions were `(0,-940) -> (94,564)` and
|
The exact captured transitions were `(0,-940) -> (94,564)` and
|
||||||
`(0,-1970) -> (197,3096)` millipixels per substep.
|
`(0,-1970) -> (197,3096)` millipixels per substep.
|
||||||
|
|
||||||
|
Live key edges recovered all moving flipper geometry. The raised left records
|
||||||
|
are line 66 `(98,382)->(131,369)`, circle 67 `(133,377)`, and line 68
|
||||||
|
`(137,384)->(116,405)`. The raised right records are line 81
|
||||||
|
`(197,405)->(171,378)`, circle 82 `(181,376)`, and line 83
|
||||||
|
`(183,368)->(217,383)`. Resting records retain their registration-table
|
||||||
|
coordinates. Edge-time position and velocity transfer remains a separate
|
||||||
|
arithmetic restoration task.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ implementation.
|
|||||||
| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, loading, help, ball, wheel, robot, plunger, media, and diamond frames. |
|
| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, loading, help, ball, wheel, robot, plunger, media, and diamond frames. |
|
||||||
| Audio | Exact samples | All 16 mono PCM WAV resources are embedded unchanged. Their trigger roles were recovered from resource use and gameplay context. |
|
| Audio | Exact samples | All 16 mono PCM WAV resources are embedded unchanged. Their trigger roles were recovered from resource use and gameplay context. |
|
||||||
| Help and languages | Exact | Original resource images 1001-1005 are displayed directly. |
|
| Help and languages | Exact | Original resource images 1001-1005 are displayed directly. |
|
||||||
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Object 174 is omitted because the original overwrites it with the live ball every frame. Moving flippers remain pending exact integration. |
|
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Each flipper now uses its exact two line records plus moving tip circle in both binary positions; the edge-time ball impulse remains pending. Object 174 is omitted because the original overwrites it with the live ball every frame. |
|
||||||
| Ball launcher | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. The port reproduces the initial `-375` millipixel Down event, 650 ms repeat delay, 40 ms repeats, release impulse, and randomized clamp below the original `-3800` maximum. This replaces the former guessed 330-430 px/s shot. |
|
| Ball launcher | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. The port reproduces the initial `-375` millipixel Down event, 650 ms repeat delay, 40 ms repeats, release impulse, and randomized clamp below the original `-3800` maximum. This replaces the former guessed 330-430 px/s shot. |
|
||||||
| Physics arithmetic | Partly recovered | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, `3800` speed bound, point-path type-2 intersection, one-sided line response, and swept type-1 circle response. Live probes cover ordinary rails, ordinary circles, and a kicked bumper. Type-3/4 triggers, closest-contact selection, persistent contact bookkeeping, and flipper impulses remain pending. |
|
| Physics arithmetic | Partly recovered | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, `3800` speed bound, point-path type-2 intersection, one-sided line response, and swept type-1 circle response. Live probes cover ordinary rails, ordinary circles, and a kicked bumper. Type-3/4 triggers, closest-contact selection, persistent contact bookkeeping, and flipper impulses remain pending. |
|
||||||
| Rules | Partly recovered | Player count, controls, wheel holes, magnetic saves, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. The claw state machine and release table are readable and terminal 18 has live differential evidence; terminals 1, 6, and 7 still need equivalent live coverage. |
|
| Rules | Partly recovered | Player count, controls, wheel holes, magnetic saves, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. The claw state machine and release table are readable and terminal 18 has live differential evidence; terminals 1, 6, and 7 still need equivalent live coverage. |
|
||||||
|
|||||||
+64
-59
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
geometry::{Segment, closest_point, segment_collision},
|
geometry::{Segment, closest_point},
|
||||||
original_physics::{
|
original_physics::{
|
||||||
GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS,
|
GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS,
|
||||||
collide_with_circle, collide_with_line,
|
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 FLIPPER_EDGE_KICK: f32 = 118.0;
|
||||||
const LEFT_FLIPPER_PIVOT: Vec2 = Vec2::new(103.0, 397.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_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_PIVOT: Vec2 = Vec2::new(210.0, 397.0);
|
||||||
const RIGHT_FLIPPER_REST_TIP: Vec2 = Vec2::new(179.0, 419.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 TILT_THRESHOLD: f32 = 1.15;
|
||||||
const LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0);
|
const LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0);
|
||||||
const LAUNCHER_FRAME_THRESHOLDS: [f32; 10] =
|
const LAUNCHER_FRAME_THRESHOLDS: [f32; 10] =
|
||||||
@@ -412,16 +412,13 @@ impl Game {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
|
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
|
||||||
// The original moves these four records with the flipper bodies.
|
let segment = self.live_wall_segment(wall.id, wall.segment);
|
||||||
if matches!(wall.id, 66 | 68 | 81 | 83) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut response = velocity;
|
let mut response = velocity;
|
||||||
if collide_with_line(
|
if collide_with_line(
|
||||||
old_position,
|
old_position,
|
||||||
&mut response,
|
&mut response,
|
||||||
wall.segment.start,
|
segment.start,
|
||||||
wall.segment.end,
|
segment.end,
|
||||||
f64::from(wall.normal_rebound),
|
f64::from(wall.normal_rebound),
|
||||||
f64::from(wall.tangent_coupling),
|
f64::from(wall.tangent_coupling),
|
||||||
) {
|
) {
|
||||||
@@ -437,15 +434,11 @@ impl Game {
|
|||||||
.chain(BUMPERS.iter())
|
.chain(BUMPERS.iter())
|
||||||
.find(|circle| circle.id == object_id);
|
.find(|circle| circle.id == object_id);
|
||||||
if let Some(circle) = circle {
|
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;
|
let mut response = velocity;
|
||||||
if collide_with_circle(
|
if collide_with_circle(
|
||||||
old_position,
|
old_position,
|
||||||
&mut response,
|
&mut response,
|
||||||
circle.center,
|
self.live_circle_center(circle.id, circle.center),
|
||||||
circle.contact_radius,
|
circle.contact_radius,
|
||||||
f64::from(circle.normal_rebound),
|
f64::from(circle.normal_rebound),
|
||||||
f64::from(circle.tangent_coupling),
|
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
|
if let Some(index) = hit_circle
|
||||||
.and_then(|circle_id| BUMPERS.iter().position(|bumper| bumper.id == circle_id))
|
.and_then(|circle_id| BUMPERS.iter().position(|bumper| bumper.id == circle_id))
|
||||||
&& !self.tilted
|
&& !self.tilted
|
||||||
@@ -803,21 +777,23 @@ impl Game {
|
|||||||
self.finished = true;
|
self.finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flipper_segments(&self) -> (Segment, Segment) {
|
fn live_wall_segment(&self, object_id: u8, resting: Segment) -> Segment {
|
||||||
let left_tip = if self.flippers.left_raised {
|
let (start, end) = match object_id {
|
||||||
LEFT_FLIPPER_RAISED_TIP
|
66 if self.flippers.left_raised => (vec2(98.0, 382.0), vec2(131.0, 369.0)),
|
||||||
} else {
|
68 if self.flippers.left_raised => (vec2(137.0, 384.0), vec2(116.0, 405.0)),
|
||||||
LEFT_FLIPPER_REST_TIP
|
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 {
|
Segment::new(start, end, resting.bounce)
|
||||||
RIGHT_FLIPPER_RAISED_TIP
|
}
|
||||||
} else {
|
|
||||||
RIGHT_FLIPPER_REST_TIP
|
fn live_circle_center(&self, object_id: u8, resting: Vec2) -> Vec2 {
|
||||||
};
|
match object_id {
|
||||||
(
|
67 if self.flippers.left_raised => LEFT_FLIPPER_RAISED_TIP,
|
||||||
flipper_segment(LEFT_FLIPPER_PIVOT, left_tip),
|
82 if self.flippers.right_raised => RIGHT_FLIPPER_RAISED_TIP,
|
||||||
flipper_segment(RIGHT_FLIPPER_PIVOT, right_tip),
|
_ => resting,
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,10 +814,6 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
|||||||
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
(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 {
|
fn point_in_flipper_sweep(point: Vec2, pivot: Vec2, rest_tip: Vec2, raised_tip: Vec2) -> bool {
|
||||||
let boundary_distance_squared = [
|
let boundary_distance_squared = [
|
||||||
Segment::new(pivot, rest_tip, 0.0),
|
Segment::new(pivot, rest_tip, 0.0),
|
||||||
@@ -1098,14 +1070,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn flippers_use_the_recovered_binary_positions() {
|
fn flippers_use_the_recovered_binary_positions() {
|
||||||
let mut game = Game::new(1);
|
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!(
|
assert_eq!(
|
||||||
(left.start, left.end),
|
(left_leading.start, left_leading.end),
|
||||||
(LEFT_FLIPPER_PIVOT, LEFT_FLIPPER_REST_TIP)
|
(vec2(112.0, 386.0), vec2(141.0, 413.0))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(right.start, right.end),
|
(right_leading.start, right_leading.end),
|
||||||
(RIGHT_FLIPPER_PIVOT, RIGHT_FLIPPER_REST_TIP)
|
(vec2(216.0, 411.0), vec2(177.0, 427.0))
|
||||||
);
|
);
|
||||||
|
|
||||||
let events = game.update(
|
let events = game.update(
|
||||||
@@ -1117,7 +1097,6 @@ mod tests {
|
|||||||
..Controls::default()
|
..Controls::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let (left, right) = game.flipper_segments();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
events
|
events
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1125,8 +1104,34 @@ mod tests {
|
|||||||
.count(),
|
.count(),
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
assert_eq!(left.end, LEFT_FLIPPER_RAISED_TIP);
|
let left_leading = game.live_wall_segment(66, wall(66));
|
||||||
assert_eq!(right.end, RIGHT_FLIPPER_RAISED_TIP);
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -23,33 +23,6 @@ pub fn closest_point(point: Vec2, segment: Segment) -> Vec2 {
|
|||||||
segment.start + line * t
|
segment.start + line * t
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn segment_collision(
|
|
||||||
position: &mut Vec2,
|
|
||||||
velocity: &mut Vec2,
|
|
||||||
radius: f32,
|
|
||||||
segment: Segment,
|
|
||||||
) -> bool {
|
|
||||||
let point = closest_point(*position, segment);
|
|
||||||
let offset = *position - point;
|
|
||||||
let distance_squared = offset.length_squared();
|
|
||||||
if distance_squared >= radius * radius {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let normal = if distance_squared > 0.000_001 {
|
|
||||||
offset / distance_squared.sqrt()
|
|
||||||
} else {
|
|
||||||
let line = segment.end - segment.start;
|
|
||||||
Vec2::new(-line.y, line.x).normalize_or_zero()
|
|
||||||
};
|
|
||||||
*position = point + normal * radius;
|
|
||||||
let inward_speed = velocity.dot(normal);
|
|
||||||
if inward_speed < 0.0 {
|
|
||||||
*velocity -= normal * inward_speed * (1.0 + segment.bounce);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -62,18 +35,4 @@ mod tests {
|
|||||||
Vec2::new(10.0, 0.0)
|
Vec2::new(10.0, 0.0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collision_reflects_velocity() {
|
|
||||||
let mut position = Vec2::new(5.0, 1.0);
|
|
||||||
let mut velocity = Vec2::new(0.0, -10.0);
|
|
||||||
let hit = segment_collision(
|
|
||||||
&mut position,
|
|
||||||
&mut velocity,
|
|
||||||
2.0,
|
|
||||||
Segment::new(Vec2::ZERO, Vec2::new(10.0, 0.0), 1.0),
|
|
||||||
);
|
|
||||||
assert!(hit);
|
|
||||||
assert!(velocity.y > 9.9);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user