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:
2026-08-23 17:36:03 +02:00
parent 98f21b31c0
commit 1f0085902c
9 changed files with 1004 additions and 297 deletions
+399 -165
View File
@@ -4,6 +4,8 @@
use macroquad::prelude::{Vec2, vec2};
use crate::real48::Real48;
pub const STEP_SECONDS: f32 = 0.010;
pub const GRAVITY_MILLI_PER_STEP: i32 = 15;
pub const MAXIMUM_SPEED_MILLI_PER_STEP: i32 = 3_800;
@@ -16,10 +18,73 @@ pub struct MilliVec {
#[derive(Clone, Copy, Debug)]
pub struct CollisionResponse {
pub progress: f64,
pub surface_distance: i32,
pub velocity: MilliVec,
pub spin: Real48,
}
#[derive(Clone, Copy, Debug)]
pub struct CollisionMaterial {
pub normal_rebound: f64,
pub tangent_coupling: f64,
pub normal_kick: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BallCollisionResponse {
pub moving_velocity: MilliVec,
pub moving_spin: Real48,
pub other_velocity: MilliVec,
pub surface_distance: i32,
}
impl CollisionMaterial {
pub const fn line(normal_rebound: f64, tangent_coupling: f64) -> Self {
Self {
normal_rebound,
tangent_coupling,
normal_kick: 0.0,
}
}
pub const fn circle(
normal_rebound: f64,
tangent_coupling: f64,
normal_kick: f64,
) -> Self {
Self {
normal_rebound,
tangent_coupling,
normal_kick,
}
}
fn real48(self) -> ResponseCoefficients {
ResponseCoefficients {
normal: coefficient(self.normal_rebound),
tangent: coefficient(self.tangent_coupling),
auxiliary: if self.normal_kick == 0.0 {
ZERO
} else {
Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc])
},
kick: coefficient(self.normal_kick),
}
}
}
#[derive(Clone, Copy)]
struct ResponseCoefficients {
normal: Real48,
tangent: Real48,
auxiliary: Real48,
kick: Real48,
}
const ZERO: Real48 = Real48::ZERO;
const ONE: Real48 = Real48::from_bytes([0x81, 0, 0, 0, 0, 0]);
const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]);
impl MilliVec {
pub fn from_position(position: Vec2) -> Self {
Self {
@@ -57,13 +122,13 @@ impl MilliVec {
}
pub fn clamp_speed(&mut self, maximum: i32) {
let speed_squared = i64::from(self.x).pow(2) + i64::from(self.y).pow(2);
if speed_squared <= i64::from(maximum).pow(2) {
let speed = milli_distance(*self);
if speed <= maximum {
return;
}
let scale = f64::from(maximum) / (speed_squared as f64).sqrt();
self.x = (f64::from(self.x) * scale).round() as i32;
self.y = (f64::from(self.y) * scale).round() as i32;
let scale = Real48::from_i32(maximum).divide(Real48::from_i32(speed));
self.x = Real48::from_i32(self.x).multiply(scale).round_i32();
self.y = Real48::from_i32(self.y).multiply(scale).round_i32();
}
}
@@ -84,10 +149,6 @@ fn scaled_f32_with_exact_roundtrip(value: i32, scale: f32) -> f32 {
projected
}
const fn cross(left: MilliVec, right: MilliVec) -> i64 {
left.x as i64 * right.y as i64 - left.y as i64 * right.x as i64
}
const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
MilliVec {
x: left.x - right.x,
@@ -95,35 +156,119 @@ const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
}
}
/// Reports whether the ball-center path crosses the registered line segment.
/// A contact at the old position is excluded, matching the shooter-stop path:
/// the waiting ball starts on object 25 and must be able to launch away from it.
fn path_intersection_progress(
old_position: MilliVec,
velocity: MilliVec,
line_start: MilliVec,
line_end: MilliVec,
) -> Option<f64> {
let line = subtract(line_end, line_start);
let from_ball = subtract(line_start, old_position);
let denominator = cross(velocity, line);
if denominator == 0 {
return None;
fn coefficient(value: f64) -> Real48 {
match (value * 100.0).round() as i32 {
0 => ZERO,
5 => Real48::from_bytes([0x7c, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
10 => Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
20 => Real48::from_bytes([0x7e, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
30 => Real48::from_bytes([0x7f, 0x9a, 0x99, 0x99, 0x99, 0x19]),
40 => Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
50 => Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
60 => Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19]),
80 => Real48::from_bytes([0x80, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
90 => Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]),
other => panic!("unsupported binary Real48 coefficient {other}"),
}
let path_numerator = cross(from_ball, line);
let line_numerator = cross(from_ball, velocity);
let intersects = if denominator > 0 {
path_numerator > 0
&& path_numerator <= denominator
&& line_numerator >= 0
&& line_numerator <= denominator
} else {
path_numerator < 0
&& path_numerator >= denominator
&& line_numerator <= 0
&& line_numerator >= denominator
};
intersects.then(|| path_numerator as f64 / denominator as f64)
}
pub fn milli_distance(delta: MilliVec) -> i32 {
let x = Real48::from_i32(delta.x).divide(THOUSAND);
let y = Real48::from_i32(delta.y).divide(THOUSAND);
x.square()
.add(y.square())
.sqrt()
.multiply(THOUSAND)
.round_i32()
}
fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
if length <= 0 {
return 0;
}
Real48::from_i32(velocity.x)
.multiply(normal_y)
.subtract(Real48::from_i32(velocity.y).multiply(normal_x))
.divide(Real48::from_i32(length))
.round_i32()
}
fn cross_at_endpoint(
point: MilliVec,
current: MilliVec,
predicted: MilliVec,
) -> i32 {
predicted
.x
.wrapping_sub(point.x)
.wrapping_mul(point.y.wrapping_sub(current.y))
.wrapping_sub(
predicted
.y
.wrapping_sub(point.y)
.wrapping_mul(point.x.wrapping_sub(current.x)),
)
}
fn apply_response(
velocity: MilliVec,
spin: Real48,
normal_x: Real48,
normal_y: Real48,
collision_velocity: i32,
coefficients: ResponseCoefficients,
) -> (MilliVec, Real48) {
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
let initial_projection = normal_velocity(velocity, normal_x, normal_y, length);
let mut spin_delta = Real48::from_i32(initial_projection)
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
.multiply(coefficients.tangent)
.multiply(Real48::from_i32(collision_velocity.wrapping_abs()));
if initial_projection < 0 {
spin_delta = spin_delta.negate();
}
let projection = spin
.multiply(Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]))
.subtract(coefficients.tangent)
.round_i32();
let spin = spin
.multiply(Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19]))
.add(spin_delta);
let mut impulse = Real48::from_i32(collision_velocity)
.multiply(ONE.add(coefficients.normal))
.round_i32();
if coefficients.auxiliary.compare(ZERO).is_lt()
&& Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
.multiply(coefficients.auxiliary)
.round_i32()
> collision_velocity
{
impulse = impulse.wrapping_sub(
Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
.multiply(coefficients.kick)
.round_i32(),
);
}
let delta_x = Real48::from_i32(projection)
.multiply(normal_x)
.subtract(Real48::from_i32(impulse).multiply(normal_y))
.divide(Real48::from_i32(length))
.round_i32();
let delta_y = Real48::from_i32(impulse)
.multiply(normal_x)
.add(Real48::from_i32(projection).multiply(normal_y))
.divide(Real48::from_i32(length))
.round_i32();
(
MilliVec {
x: velocity.x.wrapping_add(delta_x),
y: velocity.y.wrapping_add(delta_y),
},
spin,
)
}
/// Calculate the original type-2 response in the registered segment's basis.
@@ -132,39 +277,57 @@ pub fn line_collision_response(
velocity: MilliVec,
line_start: Vec2,
line_end: Vec2,
normal_rebound: f64,
tangent_coupling: f64,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
let start = MilliVec::from_position(line_start);
let end = MilliVec::from_position(line_end);
let progress = path_intersection_progress(old_position, velocity, start, end)?;
let line_x = f64::from(end.x - start.x);
let line_y = f64::from(end.y - start.y);
let length = line_x.hypot(line_y);
if length == 0.0 {
let predicted = old_position.add(velocity);
let segment = subtract(end, start);
let normal_x = Real48::from_i32(segment.x);
let normal_y = Real48::from_i32(segment.y);
let length = milli_distance(segment);
let length_units = Real48::from_i32(length).divide(THOUSAND);
let horizontal_units = normal_x.divide(THOUSAND);
let vertical_units = normal_y.divide(THOUSAND);
let distance = Real48::from_i32(old_position.x.wrapping_sub(start.x))
.multiply(vertical_units)
.subtract(
Real48::from_i32(old_position.y.wrapping_sub(start.y)).multiply(horizontal_units),
)
.divide(length_units);
let distance = if distance.compare(ZERO).is_lt() {
ZERO.subtract(distance)
} else {
distance
}
.round_i32();
let collision_velocity = Real48::from_i32(velocity.x)
.multiply(vertical_units)
.subtract(Real48::from_i32(velocity.y).multiply(horizontal_units))
.divide(length_units)
.round_i32();
let speed = milli_distance(velocity);
if distance > speed
|| collision_velocity > 10
|| distance.wrapping_sub(10) > collision_velocity.wrapping_abs()
|| cross_at_endpoint(start, old_position, predicted) < -10
|| cross_at_endpoint(end, old_position, predicted) > 10
{
return None;
}
let tangent_x = line_x / length;
let tangent_y = line_y / length;
let normal_x = -tangent_y;
let normal_y = tangent_x;
let incoming_x = f64::from(velocity.x);
let incoming_y = f64::from(velocity.y);
let normal_speed = incoming_x * normal_x + incoming_y * normal_y;
if normal_speed <= 0.0 {
return None;
}
let tangent_speed = incoming_x * tangent_x + incoming_y * tangent_y;
let outgoing_normal = -normal_rebound * normal_speed;
let outgoing_tangent = tangent_speed + tangent_coupling * normal_speed;
let (velocity, spin) = apply_response(
velocity,
spin,
normal_x,
normal_y,
collision_velocity,
material.real48(),
);
Some(CollisionResponse {
progress,
velocity: MilliVec {
x: (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32,
y: (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32,
},
surface_distance: distance,
velocity,
spin,
})
}
@@ -182,8 +345,8 @@ pub fn collide_with_line(
*velocity,
line_start,
line_end,
normal_rebound,
tangent_coupling,
CollisionMaterial::line(normal_rebound, tangent_coupling),
Real48::ZERO,
) else {
return false;
};
@@ -197,62 +360,106 @@ pub fn circle_collision_response(
velocity: MilliVec,
center: Vec2,
radius: f32,
normal_rebound: f64,
tangent_coupling: f64,
normal_kick: f64,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
let center = MilliVec::from_position(center);
let radius_milli = (radius * 1_000.0).round() as i32;
let from_center = subtract(old_position, center);
let radius_squared = i64::from(radius_milli).pow(2);
let old_distance_squared = i64::from(from_center.x).pow(2) + i64::from(from_center.y).pow(2);
if old_distance_squared <= radius_squared {
let mut surface_distance =
milli_distance(subtract(center, old_position)).wrapping_sub(radius_milli);
let speed = milli_distance(velocity);
if surface_distance > speed {
return None;
}
let vx = f64::from(velocity.x);
let vy = f64::from(velocity.y);
let offset_x = f64::from(from_center.x);
let offset_y = f64::from(from_center.y);
let quadratic_a = vx * vx + vy * vy;
if quadratic_a == 0.0 {
let predicted = old_position.add(velocity);
let middle = MilliVec {
x: old_position.x.wrapping_add(predicted.x) / 2,
y: old_position.y.wrapping_add(predicted.y) / 2,
};
let normal_x = Real48::from_i32(center.y.wrapping_sub(middle.y));
let normal_y = Real48::from_i32(middle.x.wrapping_sub(center.x));
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
let collision_velocity = normal_velocity(velocity, normal_x, normal_y, length);
if collision_velocity >= 0
|| surface_distance.wrapping_abs() > collision_velocity.wrapping_abs()
{
return None;
}
let quadratic_b = 2.0 * (offset_x * vx + offset_y * vy);
let quadratic_c = old_distance_squared as f64 - f64::from(radius_milli).powi(2);
let discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c;
if discriminant < 0.0 {
return None;
}
let progress = (-quadratic_b - discriminant.sqrt()) / (2.0 * quadratic_a);
if !(0.0 < progress && progress <= 1.0) {
return None;
}
let hit_x = offset_x + vx * progress;
let hit_y = offset_y + vy * progress;
let hit_length = hit_x.hypot(hit_y);
if hit_length == 0.0 {
return None;
}
let normal_x = hit_x / hit_length;
let normal_y = hit_y / hit_length;
let tangent_x = normal_y;
let tangent_y = -normal_x;
let normal_speed = vx * normal_x + vy * normal_y;
if normal_speed >= 0.0 {
return None;
}
let tangent_speed = vx * tangent_x + vy * tangent_y;
let outgoing_normal =
-normal_rebound * normal_speed + normal_kick * f64::from(MAXIMUM_SPEED_MILLI_PER_STEP);
let outgoing_tangent = tangent_speed - tangent_coupling * normal_speed;
surface_distance = surface_distance.wrapping_add(3_000);
let (velocity, spin) = apply_response(
velocity,
spin,
normal_x,
normal_y,
collision_velocity,
material.real48(),
);
Some(CollisionResponse {
progress,
velocity: MilliVec {
x: (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32,
y: (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32,
},
surface_distance,
velocity,
spin,
})
}
pub fn ball_collision_response(
old_position: MilliVec,
velocity: MilliVec,
spin: Real48,
other_position: MilliVec,
other_velocity: MilliVec,
) -> Option<BallCollisionResponse> {
let mut surface_distance =
milli_distance(subtract(other_position, old_position)).wrapping_sub(17_000);
let speed = milli_distance(velocity);
if surface_distance > speed {
return None;
}
let predicted = old_position.add(velocity);
let middle = MilliVec {
x: old_position.x.wrapping_add(predicted.x) / 2,
y: old_position.y.wrapping_add(predicted.y) / 2,
};
let normal_x = Real48::from_i32(other_position.y.wrapping_sub(middle.y));
let normal_y = Real48::from_i32(middle.x.wrapping_sub(other_position.x));
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
let collision_velocity = normal_velocity(velocity, normal_x, normal_y, length);
if collision_velocity >= 0
|| surface_distance.wrapping_abs() > collision_velocity.wrapping_abs()
{
return None;
}
surface_distance = surface_distance.wrapping_add(3_000);
let transfer_x = Real48::from_i32(collision_velocity)
.multiply(normal_y)
.divide(Real48::from_i32(length))
.round_i32();
let transfer_y = Real48::from_i32(collision_velocity)
.multiply(normal_x)
.divide(Real48::from_i32(length))
.round_i32();
let other_velocity = MilliVec {
x: other_velocity.x.wrapping_add(transfer_x),
y: other_velocity.y.wrapping_sub(transfer_y),
};
let (moving_velocity, moving_spin) = apply_response(
velocity,
spin,
normal_x,
normal_y,
collision_velocity.wrapping_sub(1_000),
CollisionMaterial::circle(0.9, 0.0, 0.0).real48(),
);
Some(BallCollisionResponse {
moving_velocity,
moving_spin,
other_velocity,
surface_distance,
})
}
@@ -271,9 +478,8 @@ pub fn collide_with_circle(
*velocity,
center,
radius,
normal_rebound,
tangent_coupling,
normal_kick,
CollisionMaterial::circle(normal_rebound, tangent_coupling, normal_kick),
Real48::ZERO,
) else {
return false;
};
@@ -281,7 +487,7 @@ pub fn collide_with_circle(
true
}
/// Test a non-physical circle record against the complete ball-center path.
/// Test a type-4 record against the original predicted ball position.
pub fn path_intersects_circle(
old_position: MilliVec,
velocity: MilliVec,
@@ -289,35 +495,10 @@ pub fn path_intersects_circle(
radius: f32,
) -> bool {
let center = MilliVec::from_position(center);
let offset = subtract(old_position, center);
let offset = subtract(old_position.add(velocity), center);
let radius_milli = (radius * 1_000.0).round() as i32;
let radius_squared = i64::from(radius_milli).pow(2);
let old_distance_squared = i64::from(offset.x).pow(2) + i64::from(offset.y).pow(2);
if old_distance_squared <= radius_squared {
return true;
}
let next = offset.add(velocity);
let next_distance_squared = i64::from(next.x).pow(2) + i64::from(next.y).pow(2);
if next_distance_squared <= radius_squared {
return true;
}
let vx = f64::from(velocity.x);
let vy = f64::from(velocity.y);
let offset_x = f64::from(offset.x);
let offset_y = f64::from(offset.y);
let quadratic_a = vx * vx + vy * vy;
if quadratic_a == 0.0 {
return false;
}
let quadratic_b = 2.0 * (offset_x * vx + offset_y * vy);
let quadratic_c = old_distance_squared as f64 - f64::from(radius_milli).powi(2);
let discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c;
if discriminant < 0.0 {
return false;
}
let progress = (-quadratic_b - discriminant.sqrt()) / (2.0 * quadratic_a);
0.0 < progress && progress <= 1.0
i64::from(offset.x).pow(2) + i64::from(offset.y).pow(2) <= radius_squared
}
#[cfg(test)]
@@ -333,7 +514,7 @@ mod tests {
}
#[test]
fn outer_shooter_wall_matches_the_live_original_probe() {
fn outer_shooter_wall_matches_the_zero_spin_c_response() {
let old = MilliVec {
x: 326_000,
y: 200_045,
@@ -348,18 +529,18 @@ mod tests {
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: -1_800, y: -255 });
assert_eq!(velocity, MilliVec { x: -1_800, y: 45 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 324_200,
y: 199_790
y: 200_090
}
);
}
#[test]
fn inner_shooter_wall_matches_the_live_original_probe() {
fn inner_shooter_wall_matches_the_zero_spin_c_response() {
let old = MilliVec {
x: 320_600,
y: 199_325,
@@ -374,12 +555,12 @@ mod tests {
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: 1_080, y: -30 });
assert_eq!(velocity, MilliVec { x: 1_080, y: -210 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 321_680,
y: 199_295
y: 199_115
}
);
}
@@ -421,22 +602,36 @@ mod tests {
}
#[test]
fn collision_progress_orders_contacts_along_the_substep() {
fn surface_distance_orders_candidate_contacts() {
let old = MilliVec::default();
let velocity = MilliVec { x: 10_000, y: 0 };
let near =
line_collision_response(old, velocity, vec2(2.0, 1.0), vec2(2.0, -1.0), 0.6, 0.1)
line_collision_response(
old,
velocity,
vec2(2.0, 1.0),
vec2(2.0, -1.0),
CollisionMaterial::line(0.6, 0.1),
Real48::ZERO,
)
.expect("near rail should be crossed");
let far = line_collision_response(old, velocity, vec2(8.0, 1.0), vec2(8.0, -1.0), 0.6, 0.1)
.expect("far rail should be crossed");
let far = line_collision_response(
old,
velocity,
vec2(8.0, 1.0),
vec2(8.0, -1.0),
CollisionMaterial::line(0.6, 0.1),
Real48::ZERO,
)
.expect("far rail should be crossed");
assert!((near.progress - 0.2).abs() < f64::EPSILON);
assert!((far.progress - 0.8).abs() < f64::EPSILON);
assert!(near.progress < far.progress);
assert_eq!(near.surface_distance, 2_000);
assert_eq!(far.surface_distance, 8_000);
assert!(near.surface_distance < far.surface_distance);
}
#[test]
fn ordinary_circle_matches_the_live_object_155_probe() {
fn ordinary_circle_matches_the_zero_spin_c_response() {
let old = MilliVec {
x: 183_000,
y: 70_090,
@@ -452,18 +647,18 @@ mod tests {
0.1,
0.0,
));
assert_eq!(velocity, MilliVec { x: 94, y: 564 });
assert_eq!(velocity, MilliVec { x: 0, y: 564 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 183_094,
x: 183_000,
y: 70_654
}
);
}
#[test]
fn bumper_circle_matches_the_live_object_51_probe() {
fn bumper_circle_matches_the_zero_spin_c_response() {
let old = MilliVec {
x: 165_000,
y: 172_015,
@@ -479,13 +674,52 @@ mod tests {
0.1,
0.4,
));
assert_eq!(velocity, MilliVec { x: 197, y: 3_096 });
assert_eq!(velocity, MilliVec { x: 0, y: 3_096 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 165_197,
x: 165_000,
y: 175_111
}
);
}
#[test]
fn retained_spin_reproduces_the_live_circle_tangent() {
let response = circle_collision_response(
MilliVec {
x: 183_000,
y: 70_090,
},
MilliVec { x: 0, y: -940 },
vec2(183.0, 59.0),
11.0,
CollisionMaterial::circle(0.6, 0.1, 0.0),
Real48::from_i32(-235),
)
.expect("live probe enters object 155");
assert_eq!(response.velocity, MilliVec { x: 94, y: 564 });
}
#[test]
fn dynamic_ball_record_transfers_impulse_to_the_other_slot() {
let response = ball_collision_response(
MilliVec {
x: 100_000,
y: 100_000,
},
MilliVec { x: 3_000, y: 0 },
Real48::ZERO,
MilliVec {
x: 118_000,
y: 100_000,
},
MilliVec::default(),
)
.expect("moving ball enters the 17-pixel dynamic record");
assert_eq!(response.surface_distance, 4_000);
assert_eq!(response.other_velocity, MilliVec { x: 3_000, y: 0 });
assert_eq!(response.moving_velocity, MilliVec { x: -4_600, y: 0 });
assert_eq!(response.moving_spin, Real48::ZERO);
}
}