fix(physics): resolve the earliest substep contact

Return exact path progress with each fixed-point line and circle candidate.
Evaluate the complete recovered physical object set and apply the nearest
contact, matching the selection logic in the original collision dispatcher
instead of stopping at the lowest matching object id.

This prevents dense assemblies from choosing a later wall or circle merely
because its record appears earlier in the table.

Test Plan:
- `cargo test --all-targets` -- 45 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- collision-progress ordering test -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:15:45 +02:00
parent bf5b855f44
commit 20e751de80
4 changed files with 174 additions and 89 deletions
+28 -24
View File
@@ -1,8 +1,8 @@
use crate::{
geometry::{Segment, closest_point},
original_physics::{
GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS,
collide_with_circle, collide_with_line, path_intersects_circle,
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
STEP_SECONDS, circle_collision_response, line_collision_response, path_intersects_circle,
},
table::{BUMPERS, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS},
};
@@ -405,53 +405,57 @@ impl Game {
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
let movement_velocity = velocity;
let mut position = old_position.add(velocity);
let mut hit_wall = None;
let mut hit_circle = None;
let mut best_collision: Option<(u8, bool, CollisionResponse)> = None;
for object_id in 1..=175 {
if self.claw.active && (12..=20).contains(&object_id) {
continue;
}
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
let segment = self.live_wall_segment(wall.id, wall.segment);
let mut response = velocity;
if collide_with_line(
if let Some(response) = line_collision_response(
old_position,
&mut response,
velocity,
segment.start,
segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
) {
velocity = response;
position = old_position.add(velocity);
hit_wall = Some(wall.id);
self.last_collision_id = Some(wall.id);
break;
) && best_collision
.is_none_or(|(_, _, closest)| response.progress < closest.progress)
{
best_collision = Some((wall.id, true, response));
}
}
let circle = PASSIVE_CIRCLES
.iter()
.chain(BUMPERS.iter())
.find(|circle| circle.id == object_id);
if let Some(circle) = circle {
let mut response = velocity;
if collide_with_circle(
if let Some(circle) = circle
&& let Some(response) = circle_collision_response(
old_position,
&mut response,
velocity,
self.live_circle_center(circle.id, circle.center),
circle.contact_radius,
f64::from(circle.normal_rebound),
f64::from(circle.tangent_coupling),
f64::from(circle.normal_kick),
) {
velocity = response;
position = old_position.add(velocity);
hit_circle = Some(circle.id);
self.last_collision_id = Some(circle.id);
break;
}
)
&& best_collision.is_none_or(|(_, _, closest)| response.progress < closest.progress)
{
best_collision = Some((circle.id, false, response));
}
}
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
velocity = response.velocity;
position = old_position.add(velocity);
self.last_collision_id = Some(object_id);
if is_wall {
(Some(object_id), None)
} else {
(None, Some(object_id))
}
} else {
(None, None)
};
self.ball.position = position.to_position();
self.ball.velocity = velocity.to_velocity_per_second();