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
+29 -16
View File
@@ -54,14 +54,27 @@ pub struct StaticCircle {
pub id: u8,
pub center: Vec2,
pub contact_radius: f32,
pub normal_rebound: f32,
pub tangent_coupling: f32,
pub normal_kick: f32,
}
impl StaticCircle {
const fn new(id: u8, center: Vec2, contact_radius: f32) -> Self {
let (normal_rebound, tangent_coupling, normal_kick) = match id {
54 | 56 | 58 | 65 | 69 | 71 | 73 => (0.50, 0.10, 0.0),
67 | 80 | 82 => (0.50, 0.20, 0.0),
167 | 172 => (0.60, 0.0, 0.0),
51..=53 => (0.80, 0.10, 0.40),
_ => (0.60, 0.10, 0.0),
};
Self {
id,
center,
contact_radius,
normal_rebound,
tangent_coupling,
normal_kick,
}
}
}
@@ -232,10 +245,7 @@ pub const BUMPERS: [StaticCircle; 3] = [
#[cfg(test)]
mod tests {
use super::*;
use crate::{
geometry::circle_collision,
original_physics::{MilliVec, collide_with_line},
};
use crate::original_physics::{MilliVec, collide_with_circle, collide_with_line};
#[test]
fn recovered_object_inventory_is_complete() {
@@ -302,28 +312,31 @@ mod tests {
}
#[test]
fn every_recovered_circle_separates_and_reflects_the_ball() {
fn every_recovered_circle_uses_its_fixed_point_response() {
for circle in PASSIVE_CIRCLES.iter().chain(BUMPERS.iter()) {
let mut position = circle.center + Vec2::new(circle.contact_radius - 0.5, 0.0);
let mut velocity = Vec2::new(-430.0, 0.0);
let hit = circle_collision(
&mut position,
let old_position = MilliVec::from_position(
circle.center + Vec2::new(circle.contact_radius + 1.0, 0.0),
);
let mut velocity = MilliVec { x: -2_000, y: 0 };
let hit = collide_with_circle(
old_position,
&mut velocity,
0.0,
circle.center,
circle.contact_radius,
0.0,
f64::from(circle.normal_rebound),
f64::from(circle.tangent_coupling),
f64::from(circle.normal_kick),
);
assert!(hit, "object {} missed an overlapping ball", circle.id);
assert!(hit, "object {} missed an entering path", circle.id);
assert!(
position.x >= circle.center.x + circle.contact_radius,
"object {} did not separate the ball",
velocity.x > 0,
"object {} did not reflect inward velocity",
circle.id
);
assert!(
velocity.x > 0.0,
"object {} did not reflect inward velocity",
old_position.add(velocity).x > old_position.x,
"object {} did not advance away from its surface",
circle.id
);
}