fix(physics): restore fixed-point circle responses

Replace the overlap-and-separate circle approximation with the original swept
ball-center entry test and radial/tangent fixed-point response. Preserve every
type-1 record's Real48 rebound and tangent coefficients, plus the bumper normal
kick derived from the 3800-unit speed scalar.

Unify line and circle traversal in original object-id order and remove the old
floating passive-circle and bumper impulses. Add exact tests for live object 155
and bumper 51 transitions, and keep moving flippers as the explicit remaining
collision approximation.

Test Plan:
- `cargo test --all-targets` -- 44 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- 20-second deterministic launch traversed bumpers and drained without rescue
- live object 155 and bumper 51 fixed-point transitions matched exactly
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 20:52:58 +02:00
parent 5c2a80cfa5
commit c886a93788
6 changed files with 226 additions and 116 deletions
+115
View File
@@ -136,6 +136,67 @@ pub fn collide_with_line(
true
}
/// Apply the original type-1 circle response to a path entering the circle.
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 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 {
return false;
}
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 {
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);
if !(0.0 < progress && progress <= 1.0) {
return false;
}
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 false;
}
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 false;
}
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;
velocity.x = (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32;
velocity.y = (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32;
true
}
#[cfg(test)]
mod tests {
use super::*;
@@ -227,4 +288,58 @@ mod tests {
0.1,
));
}
#[test]
fn ordinary_circle_matches_the_live_object_155_probe() {
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_object_51_probe() {
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
}
);
}
}