Files
tdkpin/tdkpin-rs/src/original_physics.rs
T
ddidderr c2f1443436
Build TDK Pinball / build (macos-latest) (push) Canceled after 0s
Build TDK Pinball / build (ubuntu-latest) (push) Canceled after 0s
Build TDK Pinball / build (windows-latest) (push) Canceled after 0s
fmt: just fmt (rust only)
2026-08-29 09:57:39 +02:00

1020 lines
30 KiB
Rust

//! Fixed-point primitives recovered from the original Win16 physics loop.
#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
use macroquad::prelude::{Vec2, vec2};
use crate::real48::Real48;
pub const GRAVITY_MILLI_PER_STEP: i32 = 15;
pub const MAXIMUM_SPEED_MILLI_PER_STEP: i32 = 3_800;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MilliVec {
pub x: i32,
pub y: i32,
}
#[derive(Clone, Copy, Debug)]
pub struct CollisionResponse {
#[allow(dead_code)]
pub surface_distance: i32,
pub velocity: MilliVec,
pub spin: Real48,
pub auxiliary_fired: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct StaticCollisionCandidate {
pub surface_distance: i32,
normal_x: Real48,
normal_y: Real48,
normal_velocity: i32,
material: CollisionMaterial,
}
#[derive(Clone, Copy, Debug)]
pub struct CollisionMaterial {
pub normal_rebound: f64,
pub tangent_coupling: f64,
pub response_auxiliary: 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,
response_auxiliary: 0.0,
normal_kick: 0.0,
}
}
pub const fn line_with_kick(
normal_rebound: f64,
tangent_coupling: f64,
response_auxiliary: f64,
normal_kick: f64,
) -> Self {
Self {
normal_rebound,
tangent_coupling,
response_auxiliary,
normal_kick,
}
}
pub const fn circle(normal_rebound: f64, tangent_coupling: f64, normal_kick: f64) -> Self {
Self {
normal_rebound,
tangent_coupling,
response_auxiliary: if normal_kick == 0.0 { 0.0 } else { -0.1 },
normal_kick,
}
}
fn real48(self) -> ResponseCoefficients {
ResponseCoefficients {
normal: coefficient(self.normal_rebound),
tangent: coefficient(self.tangent_coupling),
auxiliary: coefficient(self.response_auxiliary),
kick: coefficient(self.normal_kick),
}
}
}
impl StaticCollisionCandidate {
pub fn resolve(self, velocity: MilliVec, spin: Real48) -> CollisionResponse {
let (velocity, spin, auxiliary_fired) = apply_response(
velocity,
spin,
self.normal_x,
self.normal_y,
self.normal_velocity,
self.material.real48(),
);
CollisionResponse {
surface_distance: self.surface_distance,
velocity,
spin,
auxiliary_fired,
}
}
}
#[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 {
x: (position.x * 1_000.0).round() as i32,
y: (position.y * 1_000.0).round() as i32,
}
}
pub fn from_velocity_per_second(velocity: Vec2) -> Self {
Self {
x: (velocity.x * 10.0).round() as i32,
y: (velocity.y * 10.0).round() as i32,
}
}
pub fn to_position(self) -> Vec2 {
vec2(
scaled_f32_with_exact_roundtrip(self.x, 1_000.0),
scaled_f32_with_exact_roundtrip(self.y, 1_000.0),
)
}
pub fn to_velocity_per_second(self) -> Vec2 {
vec2(
scaled_f32_with_exact_roundtrip(self.x, 10.0),
scaled_f32_with_exact_roundtrip(self.y, 10.0),
)
}
pub const fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
pub fn clamp_speed(&mut self, maximum: i32) {
let speed = milli_distance(*self);
if speed <= maximum {
return;
}
// The original computes the excess fraction and subtracts the
// rounded component from each axis. Scaling directly by
// `maximum / speed` is mathematically equivalent, but can differ by
// one millipixel because each Real48 operation is rounded
// independently.
let excess = Real48::from_i32(speed.wrapping_sub(maximum)).divide(Real48::from_i32(speed));
self.x = self
.x
.wrapping_sub(Real48::from_i32(self.x).multiply(excess).round_i32());
self.y = self
.y
.wrapping_sub(Real48::from_i32(self.y).multiply(excess).round_i32());
}
}
fn scaled_f32_with_exact_roundtrip(value: i32, scale: f32) -> f32 {
let mut projected = value as f32 / scale;
for _ in 0..4 {
let recovered = (projected * scale).round() as i32;
if recovered == value {
return projected;
}
projected = if recovered < value {
projected.next_up()
} else {
projected.next_down()
};
}
debug_assert_eq!((projected * scale).round() as i32, value);
projected
}
const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
MilliVec {
x: left.x - right.x,
y: left.y - right.y,
}
}
fn coefficient(value: f64) -> Real48 {
let scaled = (value * 100.0).round() as i32;
let coefficient = match scaled.wrapping_abs() {
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}"),
};
if scaled < 0 {
coefficient.negate()
} else {
coefficient
}
}
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 tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
if length <= 0 {
return 0;
}
Real48::from_i32(velocity.x)
.multiply(normal_x)
.add(Real48::from_i32(velocity.y).multiply(normal_y))
.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, bool) {
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
// Raw 1000:ef19..f16a uses the standard dot-product tangent both to
// oppose the current tangential motion and to update retained spin.
let tangent_velocity = tangent_velocity(velocity, normal_x, normal_y, length);
let spin_delta = Real48::from_i32(tangent_velocity)
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
.multiply(coefficients.tangent);
let mut tangent_response = Real48::from_i32(collision_velocity.wrapping_abs())
.multiply(coefficients.tangent)
.negate();
if tangent_velocity < 0 {
tangent_response = tangent_response.negate();
}
let projection = spin
.multiply(Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]))
.add(tangent_response)
.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();
let auxiliary_fired = coefficients.auxiliary.compare(ZERO).is_lt()
&& Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
.multiply(coefficients.auxiliary)
.round_i32()
> collision_velocity;
if auxiliary_fired {
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,
auxiliary_fired,
)
}
/// Calculate the original type-2 response in the registered segment's basis.
#[cfg(test)]
pub fn line_collision_candidate(
old_position: MilliVec,
velocity: MilliVec,
line_start: Vec2,
line_end: Vec2,
material: CollisionMaterial,
) -> Option<StaticCollisionCandidate> {
line_collision_candidate_at(
old_position,
old_position.add(velocity),
velocity,
line_start,
line_end,
material,
)
}
pub fn line_collision_candidate_at(
old_position: MilliVec,
predicted_position: MilliVec,
velocity: MilliVec,
line_start: Vec2,
line_end: Vec2,
material: CollisionMaterial,
) -> Option<StaticCollisionCandidate> {
let start = MilliVec::from_position(line_start);
let end = MilliVec::from_position(line_end);
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_position) < -10
|| cross_at_endpoint(end, old_position, predicted_position) > 10
{
return None;
}
Some(StaticCollisionCandidate {
surface_distance: distance,
normal_x,
normal_y,
normal_velocity: collision_velocity,
material,
})
}
#[cfg(test)]
pub fn line_collision_response(
old_position: MilliVec,
velocity: MilliVec,
start: Vec2,
end: Vec2,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
line_collision_candidate(old_position, velocity, start, end, material)
.map(|candidate| candidate.resolve(velocity, spin))
}
#[cfg(test)]
pub fn collide_with_line(
old_position: MilliVec,
velocity: &mut MilliVec,
line_start: Vec2,
line_end: Vec2,
normal_rebound: f64,
tangent_coupling: f64,
) -> bool {
let Some(response) = line_collision_response(
old_position,
*velocity,
line_start,
line_end,
CollisionMaterial::line(normal_rebound, tangent_coupling),
Real48::ZERO,
) else {
return false;
};
*velocity = response.velocity;
true
}
/// Calculate the original type-1 circle response for a path entering it.
#[cfg(test)]
pub fn circle_collision_candidate(
old_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
material: CollisionMaterial,
) -> Option<StaticCollisionCandidate> {
circle_collision_candidate_at(
old_position,
old_position.add(velocity),
velocity,
center,
radius,
material,
)
}
pub fn circle_collision_candidate_at(
old_position: MilliVec,
predicted_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
material: CollisionMaterial,
) -> Option<StaticCollisionCandidate> {
let center = MilliVec::from_position(center);
let radius_milli = (radius * 1_000.0).round() as i32;
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 middle = MilliVec {
x: old_position.x.wrapping_add(predicted_position.x) / 2,
y: old_position.y.wrapping_add(predicted_position.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;
}
surface_distance = surface_distance.wrapping_add(3_000);
Some(StaticCollisionCandidate {
surface_distance,
normal_x,
normal_y,
normal_velocity: collision_velocity,
material,
})
}
/// Detect the non-capture boundary response of a type-3 record.
pub fn capture_collision_candidate(
old_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
material: CollisionMaterial,
) -> Option<StaticCollisionCandidate> {
let center = MilliVec::from_position(center);
let radius_milli = (radius * 1_000.0).round() as i32;
let delta = MilliVec {
x: center.x.wrapping_sub(old_position.x),
y: center.y.wrapping_sub(old_position.y).wrapping_sub(2_000),
};
let surface_distance = milli_distance(delta).wrapping_sub(radius_milli);
let normal_x = Real48::from_i32(center.y.wrapping_sub(old_position.y));
let normal_y = Real48::from_i32(old_position.x.wrapping_sub(center.x));
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
if length == 0 {
return None;
}
let normal_velocity = normal_velocity(velocity, normal_x, normal_y, length);
if normal_velocity >= 0 || normal_velocity.wrapping_abs() < surface_distance.wrapping_abs() {
return None;
}
Some(StaticCollisionCandidate {
surface_distance,
normal_x,
normal_y,
normal_velocity,
material,
})
}
#[cfg(test)]
pub fn circle_collision_response(
old_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
circle_collision_candidate(old_position, velocity, center, radius, material)
.map(|candidate| candidate.resolve(velocity, spin))
}
pub fn ball_collision_response(
old_position: MilliVec,
velocity: MilliVec,
spin: Real48,
other_position: MilliVec,
other_velocity: MilliVec,
transfer_enabled: bool,
) -> 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 other_velocity = if transfer_enabled {
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();
MilliVec {
x: other_velocity.x.wrapping_add(transfer_x),
y: other_velocity.y.wrapping_sub(transfer_y),
}
} else {
other_velocity
};
let (moving_velocity, moving_spin, _) = apply_response(
velocity,
spin,
normal_x,
normal_y,
if transfer_enabled {
collision_velocity.wrapping_sub(1_000)
} else {
collision_velocity
},
CollisionMaterial::circle(0.9, 0.0, 0.0).real48(),
);
Some(BallCollisionResponse {
moving_velocity,
moving_spin,
other_velocity,
surface_distance,
})
}
#[cfg(test)]
pub fn collide_with_circle(
old_position: MilliVec,
velocity: &mut MilliVec,
center: Vec2,
radius: f32,
normal_rebound: f64,
tangent_coupling: f64,
normal_kick: f64,
) -> bool {
let Some(response) = circle_collision_response(
old_position,
*velocity,
center,
radius,
CollisionMaterial::circle(normal_rebound, tangent_coupling, normal_kick),
Real48::ZERO,
) else {
return false;
};
*velocity = response.velocity;
true
}
/// Test a type-4 record against the original predicted ball position.
pub fn path_intersects_circle(
old_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
) -> bool {
let center = MilliVec::from_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);
i64::from(offset.x).pow(2) + i64::from(offset.y).pow(2) <= radius_squared
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn float_views_roundtrip_every_gameplay_velocity_millipixel() {
for value in -10_000..=10_000 {
let milli = MilliVec {
x: value,
y: -value,
};
assert_eq!(
MilliVec::from_velocity_per_second(milli.to_velocity_per_second()),
milli
);
}
}
#[test]
fn speed_clamp_matches_reconstructed_formula() {
let maximum = 3_800;
let mut mismatches = Vec::new();
for x in (-10_000..=10_000).step_by(37) {
for y in (-10_000..=10_000).step_by(41) {
let input = MilliVec { x, y };
let speed = milli_distance(input);
if speed <= maximum {
continue;
}
let excess = Real48::from_i32(speed - maximum).divide(Real48::from_i32(speed));
let expected = MilliVec {
x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()),
y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()),
};
let mut actual = input;
actual.clamp_speed(maximum);
if actual != expected {
mismatches.push((input, speed, actual, expected));
if mismatches.len() == 5 {
break;
}
}
}
if mismatches.len() == 5 {
break;
}
}
assert!(
mismatches.is_empty(),
"speed-clamp mismatches: {mismatches:?}"
);
}
#[test]
fn outer_shooter_wall_matches_the_live_tangent_response() {
let old = MilliVec {
x: 326_000,
y: 200_045,
};
let mut velocity = MilliVec { x: 3_000, y: 45 };
assert!(collide_with_line(
old,
&mut velocity,
vec2(328.0, 422.0),
vec2(328.0, 58.0),
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: -1_800, y: -255 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 324_200,
y: 199_790
}
);
}
#[test]
fn detected_candidate_resolves_against_the_later_motion_state() {
let old = MilliVec {
x: 326_000,
y: 200_000,
};
let detected_velocity = MilliVec { x: 3_000, y: 0 };
let candidate = line_collision_candidate(
old,
detected_velocity,
vec2(328.0, 422.0),
vec2(328.0, 58.0),
CollisionMaterial::line(0.6, 0.1),
)
.expect("the candidate must be retained during the record scan");
let response = candidate.resolve(MilliVec { x: 3_000, y: 1_000 }, Real48::ZERO);
assert_eq!(response.velocity, MilliVec { x: -1_800, y: 700 });
assert_eq!(response.surface_distance, 2_000);
}
#[test]
fn type_three_boundary_builds_the_recovered_radial_candidate() {
let candidate = capture_collision_candidate(
MilliVec {
x: 100_000,
y: 100_000,
},
MilliVec { x: 1_000, y: 0 },
vec2(103.0, 102.0),
3.8,
CollisionMaterial::line(0.6, 0.0),
)
.expect("the contacted type-three rim must retain a candidate");
assert_eq!(candidate.surface_distance, -800);
assert_ne!(
candidate
.resolve(MilliVec { x: 1_000, y: 0 }, Real48::ZERO)
.velocity,
MilliVec { x: 1_000, y: 0 }
);
}
#[test]
fn auxiliary_kick_uses_the_recovered_negative_speed_threshold() {
let material = CollisionMaterial::line_with_kick(0.5, 0.1, -0.4, 0.4);
let fast = line_collision_response(
MilliVec {
x: 326_000,
y: 200_000,
},
MilliVec { x: 3_000, y: 0 },
vec2(328.0, 422.0),
vec2(328.0, 58.0),
material,
Real48::ZERO,
)
.expect("the fast path must hit the vertical rail");
let slow = line_collision_response(
MilliVec {
x: 327_500,
y: 200_000,
},
MilliVec { x: 1_000, y: 0 },
vec2(328.0, 422.0),
vec2(328.0, 58.0),
material,
Real48::ZERO,
)
.expect("the slow path must hit the same vertical rail");
assert!(fast.auxiliary_fired);
assert!(!slow.auxiliary_fired);
assert_ne!(fast.velocity.x, slow.velocity.x);
}
#[test]
fn inner_shooter_wall_matches_the_live_tangent_response() {
let old = MilliVec {
x: 320_600,
y: 199_325,
};
let mut velocity = MilliVec { x: -1_800, y: -210 };
assert!(collide_with_line(
old,
&mut velocity,
vec2(320.0, 48.0),
vec2(320.0, 437.0),
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: 1_080, y: -30 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 321_680,
y: 199_295
}
);
}
#[test]
fn contact_at_the_old_position_does_not_block_launching_away() {
let old = MilliVec {
x: 325_000,
y: 413_000,
};
let mut velocity = MilliVec { x: 0, y: -3_000 };
assert!(!collide_with_line(
old,
&mut velocity,
vec2(315.0, 413.0),
vec2(332.0, 413.0),
0.1,
0.1,
));
}
#[test]
fn crossing_a_rail_from_its_back_side_is_allowed() {
let old = MilliVec {
x: 283_708,
y: 18_775,
};
let mut velocity = MilliVec { x: -1_672, y: -66 };
assert!(!collide_with_line(
old,
&mut velocity,
vec2(277.0, 31.0),
vec2(287.0, 12.0),
0.6,
0.1,
));
}
#[test]
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),
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),
CollisionMaterial::line(0.6, 0.1),
Real48::ZERO,
)
.expect("far rail should be crossed");
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_zero_spin_response() {
let old = MilliVec {
x: 183_000,
y: 70_090,
};
let mut velocity = MilliVec { x: 0, y: -940 };
assert!(collide_with_circle(
old,
&mut velocity,
vec2(183.0, 59.0),
11.0,
0.6,
0.1,
0.0,
));
assert_eq!(velocity, MilliVec { x: 94, y: 564 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 183_094,
y: 70_654
}
);
}
#[test]
fn bumper_circle_matches_the_live_zero_spin_response() {
let old = MilliVec {
x: 165_000,
y: 172_015,
};
let mut velocity = MilliVec { x: 0, y: -1_970 };
assert!(collide_with_circle(
old,
&mut velocity,
vec2(165.0, 148.0),
23.0,
0.8,
0.1,
0.4,
));
assert_eq!(velocity, MilliVec { x: 197, y: 3_096 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 165_197,
y: 175_111
}
);
}
#[test]
fn retained_spin_adds_to_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: 188, 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(),
true,
)
.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);
let held_other = 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(),
false,
)
.expect("the held slot still presents a dynamic collision record");
assert_eq!(held_other.other_velocity, MilliVec::default());
assert_eq!(held_other.moving_velocity, MilliVec { x: -2_700, y: 0 });
}
}