fix(physics): port the Borland Real48 collision core
Add a bit-exact six-byte Real48 implementation for integer conversion, rounding, comparison, add/subtract, multiply/divide, and Newton square root. Use the normalized Borland random-register result and route speed clamps, segment/circle detection, moving flippers, captures, triggers, and magnetic fields through the recovered arithmetic instead of host floating formulas. Preserve Real48 spin per ball and feed it through the original tangent/spin response, separating zero-spin C fixtures from retained-spin Wine traces. Replace path-progress selection with surface-distance ordering and implement dynamic records 174/175, including impulse transfer to the other slot, normal_velocity-1000 response, and the second post-collision speed clamp. Test Plan: - `cargo test --all-targets` -- passed, 69 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:
+145
-88
@@ -3,9 +3,11 @@ use crate::{
|
||||
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,
|
||||
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP,
|
||||
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS, ball_collision_response,
|
||||
circle_collision_response, line_collision_response, milli_distance, path_intersects_circle,
|
||||
},
|
||||
real48::Real48,
|
||||
table::{
|
||||
BUMPERS, EFFECT_SENSOR, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS,
|
||||
WHEEL_RESET_SENSOR,
|
||||
@@ -184,6 +186,7 @@ pub struct Ball {
|
||||
pub position: Vec2,
|
||||
pub velocity: Vec2,
|
||||
pub in_launcher: bool,
|
||||
spin: Real48,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -244,6 +247,7 @@ impl Default for Ball {
|
||||
position: LAUNCHER_POSITION,
|
||||
velocity: Vec2::ZERO,
|
||||
in_launcher: true,
|
||||
spin: Real48::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,9 +479,14 @@ impl Game {
|
||||
velocity.x = velocity.x.wrapping_add(signed);
|
||||
}
|
||||
Nudge::Center => {
|
||||
let horizontal =
|
||||
(f64::from(SCALAR) * (self.random.unit_interval() * 20.0 - 10.0)).round()
|
||||
as i32;
|
||||
let horizontal = Real48::from_i32(SCALAR)
|
||||
.multiply(
|
||||
self.random
|
||||
.real48()
|
||||
.multiply(Real48::from_i32(20))
|
||||
.subtract(Real48::from_i32(10)),
|
||||
)
|
||||
.round_i32();
|
||||
let vertical = -(60 - i32::from(self.random.below(20))) * SCALAR;
|
||||
velocity.x = velocity.x.wrapping_add(horizontal);
|
||||
velocity.y = velocity.y.wrapping_add(vertical);
|
||||
@@ -545,52 +554,45 @@ impl Game {
|
||||
self.apply_magnetic_fields(old_position, &mut velocity);
|
||||
let movement_velocity = velocity;
|
||||
let mut position = old_position.add(velocity);
|
||||
let mut best_collision: Option<(u8, bool, CollisionResponse)> = None;
|
||||
for object_id in 1..=175 {
|
||||
if !self.object_active[usize::from(object_id)] {
|
||||
continue;
|
||||
}
|
||||
if self.claw.active && (12..=20).contains(&object_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
|
||||
let segment = self.live_wall_segment(wall.id, wall.segment);
|
||||
if let Some(response) = line_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
segment.start,
|
||||
segment.end,
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
) && best_collision
|
||||
.is_none_or(|(_, _, closest)| response.progress < closest.progress)
|
||||
{
|
||||
best_collision = Some((wall.id, true, response));
|
||||
}
|
||||
}
|
||||
let circle = PASSIVE_CIRCLES
|
||||
.iter()
|
||||
.chain(BUMPERS.iter())
|
||||
.find(|circle| circle.id == object_id);
|
||||
if let Some(circle) = circle
|
||||
&& let Some(response) = circle_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
self.live_circle_center(circle.id, circle.center),
|
||||
circle.contact_radius,
|
||||
f64::from(circle.normal_rebound),
|
||||
f64::from(circle.tangent_coupling),
|
||||
f64::from(circle.normal_kick),
|
||||
)
|
||||
&& best_collision.is_none_or(|(_, _, closest)| response.progress < closest.progress)
|
||||
let mut best_collision = self.find_static_collision(old_position, velocity, self.ball.spin);
|
||||
let mut transferred_secondary_velocity = None;
|
||||
if let Some(secondary) = self.secondary_ball {
|
||||
let response = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
self.ball.spin,
|
||||
MilliVec::from_position(secondary.position),
|
||||
MilliVec::from_velocity_per_second(secondary.velocity),
|
||||
);
|
||||
if let Some(response) = response
|
||||
&& best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
best_collision = Some((circle.id, false, response));
|
||||
transferred_secondary_velocity = Some(response.other_velocity);
|
||||
best_collision = Some((
|
||||
175,
|
||||
false,
|
||||
CollisionResponse {
|
||||
surface_distance: response.surface_distance,
|
||||
velocity: response.moving_velocity,
|
||||
spin: response.moving_spin,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
self.ball.spin = response.spin;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
position = old_position.add(velocity);
|
||||
self.last_collision_id = Some(object_id);
|
||||
if object_id == 175
|
||||
&& let (Some(secondary), Some(transferred)) =
|
||||
(&mut self.secondary_ball, transferred_secondary_velocity)
|
||||
{
|
||||
secondary.velocity = transferred.to_velocity_per_second();
|
||||
}
|
||||
if is_wall {
|
||||
(Some(object_id), None)
|
||||
} else {
|
||||
@@ -644,19 +646,17 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) {
|
||||
let Some(mut ball) = self.secondary_ball.take() else {
|
||||
return;
|
||||
};
|
||||
let old_position = MilliVec::from_position(ball.position);
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
self.apply_magnetic_fields(old_position, &mut velocity);
|
||||
let mut best_collision: Option<(u8, bool, CollisionResponse)> = None;
|
||||
|
||||
fn find_static_collision(
|
||||
&self,
|
||||
old_position: MilliVec,
|
||||
velocity: MilliVec,
|
||||
spin: Real48,
|
||||
) -> Option<(u8, bool, CollisionResponse)> {
|
||||
let mut best = None;
|
||||
for object_id in 1..=175 {
|
||||
if !self.object_active[usize::from(object_id)] {
|
||||
if !self.object_active[usize::from(object_id)]
|
||||
|| self.claw.active && (12..=20).contains(&object_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
|
||||
@@ -666,12 +666,15 @@ impl Game {
|
||||
velocity,
|
||||
segment.start,
|
||||
segment.end,
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
) && best_collision
|
||||
.is_none_or(|(_, _, closest)| response.progress < closest.progress)
|
||||
{
|
||||
best_collision = Some((wall.id, true, response));
|
||||
CollisionMaterial::line(
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
),
|
||||
spin,
|
||||
) && best.is_none_or(|(_, _, closest): (u8, bool, CollisionResponse)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
}) {
|
||||
best = Some((wall.id, true, response));
|
||||
}
|
||||
}
|
||||
let circle = PASSIVE_CIRCLES
|
||||
@@ -684,32 +687,67 @@ impl Game {
|
||||
velocity,
|
||||
self.live_circle_center(circle.id, circle.center),
|
||||
circle.contact_radius,
|
||||
f64::from(circle.normal_rebound),
|
||||
f64::from(circle.tangent_coupling),
|
||||
f64::from(circle.normal_kick),
|
||||
CollisionMaterial::circle(
|
||||
f64::from(circle.normal_rebound),
|
||||
f64::from(circle.tangent_coupling),
|
||||
f64::from(circle.normal_kick),
|
||||
),
|
||||
spin,
|
||||
)
|
||||
&& best_collision.is_none_or(|(_, _, closest)| response.progress < closest.progress)
|
||||
&& best.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
best_collision = Some((circle.id, false, response));
|
||||
best = Some((circle.id, false, response));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
if let Some(response) = circle_collision_response(
|
||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) {
|
||||
let Some(mut ball) = self.secondary_ball.take() else {
|
||||
return;
|
||||
};
|
||||
let old_position = MilliVec::from_position(ball.position);
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
self.apply_magnetic_fields(old_position, &mut velocity);
|
||||
let mut best_collision = self.find_static_collision(old_position, velocity, ball.spin);
|
||||
|
||||
let mut transferred_primary_velocity = None;
|
||||
if let Some(response) = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
self.ball.position,
|
||||
17.0,
|
||||
0.9,
|
||||
0.0,
|
||||
0.0,
|
||||
) && best_collision.is_none_or(|(_, _, closest)| response.progress < closest.progress)
|
||||
ball.spin,
|
||||
MilliVec::from_position(self.ball.position),
|
||||
MilliVec::from_velocity_per_second(self.ball.velocity),
|
||||
) && best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
best_collision = Some((174, false, response));
|
||||
transferred_primary_velocity = Some(response.other_velocity);
|
||||
best_collision = Some((
|
||||
174,
|
||||
false,
|
||||
CollisionResponse {
|
||||
surface_distance: response.surface_distance,
|
||||
velocity: response.moving_velocity,
|
||||
spin: response.moving_spin,
|
||||
},
|
||||
));
|
||||
}
|
||||
let mut hit = None;
|
||||
if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
ball.spin = response.spin;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
hit = Some((object_id, is_wall));
|
||||
if object_id == 174
|
||||
&& let Some(transferred) = transferred_primary_velocity
|
||||
{
|
||||
self.ball.velocity = transferred.to_velocity_per_second();
|
||||
}
|
||||
}
|
||||
ball.position = old_position.add(velocity).to_position();
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
@@ -879,7 +917,7 @@ impl Game {
|
||||
.y
|
||||
.wrapping_sub(current_position.y)
|
||||
.wrapping_sub(2_000);
|
||||
let surface_distance = (f64::from(dx).hypot(f64::from(dy))).round() as i32 - radius;
|
||||
let surface_distance = milli_distance(MilliVec { x: dx, y: dy }) - radius;
|
||||
let contact_index = usize::from(record_id);
|
||||
let contact = self.record_contacts[contact_index];
|
||||
let ball_count = if self.secondary_ball.is_some() { 2 } else { 1 };
|
||||
@@ -905,8 +943,9 @@ impl Game {
|
||||
self.capture_age.wrapping_add(5)
|
||||
};
|
||||
} else {
|
||||
velocity.x = (f64::from(velocity.x) * 0.9).round() as i32;
|
||||
velocity.y = (f64::from(velocity.y) * 0.9).round() as i32;
|
||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(damping).round_i32();
|
||||
velocity.y = Real48::from_i32(velocity.y).multiply(damping).round_i32();
|
||||
velocity.x = if center.x > current_position.x {
|
||||
velocity.x.wrapping_add(150)
|
||||
} else {
|
||||
@@ -1036,6 +1075,7 @@ impl Game {
|
||||
position: LAUNCHER_POSITION,
|
||||
velocity: vec2(0.0, -300.0),
|
||||
in_launcher: false,
|
||||
spin: Real48::ZERO,
|
||||
});
|
||||
self.multiball_state = MultiballState::Unavailable;
|
||||
}
|
||||
@@ -1087,13 +1127,14 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn randomize_trigger_velocity(&mut self) {
|
||||
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
|
||||
let x_factor = 1.03 - self.random.unit_interval() * 0.08;
|
||||
let y_factor = 1.03 - self.random.unit_interval() * 0.08;
|
||||
velocity.x = (f64::from(velocity.x) * x_factor).round() as i32;
|
||||
velocity.y = (f64::from(velocity.y) * y_factor).round() as i32;
|
||||
let offset = Real48::from_bytes([0x81, 0x71, 0x3d, 0x0a, 0xd7, 0x03]);
|
||||
let span = Real48::from_bytes([0x7d, 0x71, 0x3d, 0x0a, 0xd7, 0x23]);
|
||||
let x_factor = offset.subtract(self.random.real48().multiply(span));
|
||||
let y_factor = offset.subtract(self.random.real48().multiply(span));
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(x_factor).round_i32();
|
||||
velocity.y = Real48::from_i32(velocity.y).multiply(y_factor).round_i32();
|
||||
self.ball.velocity = velocity.to_velocity_per_second();
|
||||
}
|
||||
|
||||
@@ -1106,7 +1147,6 @@ impl Game {
|
||||
self.launcher_velocity_milli = 0;
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn apply_magnetic_fields(&mut self, old_position: MilliVec, velocity: &mut MilliVec) {
|
||||
for (object_id, min_x, min_y, max_x, max_y) in [
|
||||
(6, 143_000, 421_000, 169_000, 452_000),
|
||||
@@ -1128,9 +1168,17 @@ impl Game {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
velocity.x = (f64::from(velocity.x) * 0.9).round() as i32;
|
||||
let vertical_factor = 1.0 - self.random.unit_interval() * 0.3;
|
||||
velocity.y = -(f64::from(MAXIMUM_SPEED_MILLI_PER_STEP) * vertical_factor).round() as i32;
|
||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(damping).round_i32();
|
||||
let vertical_factor = Real48::from_i32(1).subtract(
|
||||
self.random
|
||||
.real48()
|
||||
.multiply(Real48::from_bytes([0x7f, 0x9a, 0x99, 0x99, 0x99, 0x19])),
|
||||
);
|
||||
velocity.y = Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
.multiply(vertical_factor)
|
||||
.round_i32()
|
||||
.wrapping_neg();
|
||||
if old_position.add(*velocity).y < min_y {
|
||||
self.object_active[object_id] = false;
|
||||
}
|
||||
@@ -1182,6 +1230,7 @@ impl Game {
|
||||
self.claw.ball_suspended = true;
|
||||
self.claw.frame_accumulator = 0.0;
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
self.ball.spin = Real48::ZERO;
|
||||
events.push(Event::ClawCapture);
|
||||
events.push(Event::Sound(2015));
|
||||
}
|
||||
@@ -1197,6 +1246,7 @@ impl Game {
|
||||
self.claw.ball_suspended = true;
|
||||
self.claw.frame_accumulator = 0.0;
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
self.ball.spin = Real48::ZERO;
|
||||
events.push(Event::ClawCapture);
|
||||
}
|
||||
|
||||
@@ -1367,7 +1417,14 @@ 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)
|
||||
moving_flipper_response(
|
||||
position,
|
||||
velocity,
|
||||
delta,
|
||||
side,
|
||||
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
||||
MAXIMUM_SPEED_MILLI_PER_STEP,
|
||||
)
|
||||
{
|
||||
ball.position = position.add(response.movement).to_position();
|
||||
ball.velocity = response.velocity.to_velocity_per_second();
|
||||
|
||||
Reference in New Issue
Block a user