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:
+399
-165
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user