fix(physics): apply predicted record broadphase

Use unresolved type-one/type-two candidates in the production scan and gate detection on the original predicted-position bounds with the registered five-pixel ball margin. This restores the 1000:9b69 broadphase, including overshoot rejection, while keeping late response resolution ready for ordered type-three/type-four integration.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- git diff --check
This commit is contained in:
2026-08-23 18:29:53 +02:00
parent 975fb27494
commit 95b6994735
4 changed files with 60 additions and 21 deletions
+54 -20
View File
@@ -4,8 +4,9 @@ use crate::{
geometry::Segment,
original_physics::{
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP,
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS, ball_collision_response,
circle_collision_response, line_collision_response, milli_distance, path_intersects_circle,
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS, StaticCollisionCandidate,
ball_collision_response, circle_collision_candidate, line_collision_candidate,
milli_distance, path_intersects_circle,
},
real48::Real48,
table::{
@@ -672,7 +673,10 @@ impl Game {
self.apply_magnetic_fields(old_position, &mut velocity);
let movement_velocity = velocity;
let mut position = old_position.add(velocity);
let mut best_collision = self.find_static_collision(old_position, velocity, self.ball.spin);
let best_static = self.find_static_collision_candidate(old_position, velocity);
let mut best_collision = best_static.map(|(id, is_wall, candidate)| {
(id, is_wall, candidate.resolve(velocity, self.ball.spin))
});
let mut transferred_secondary_velocity = None;
if let Some(secondary) = self.secondary_ball {
let response = ball_collision_response(
@@ -774,14 +778,14 @@ impl Game {
collided
}
fn find_static_collision(
fn find_static_collision_candidate(
&self,
old_position: MilliVec,
velocity: MilliVec,
spin: Real48,
) -> Option<(u8, bool, CollisionResponse)> {
) -> Option<(u8, bool, StaticCollisionCandidate)> {
let mut best = None;
let layer = if old_position.y < 250_000 { 0x01 } else { 0x02 };
let predicted = old_position.add(velocity);
for object_id in 1..=175 {
if !self.object_active[usize::from(object_id)]
|| self.claw.active && (12..=20).contains(&object_id)
@@ -793,6 +797,15 @@ impl Game {
continue;
}
let segment = self.live_wall_segment(wall.id, wall.segment);
let start = MilliVec::from_position(segment.start);
let end = MilliVec::from_position(segment.end);
if predicted.x < start.x.min(end.x).wrapping_sub(5_000)
|| predicted.x > start.x.max(end.x).wrapping_add(5_000)
|| predicted.y < start.y.min(end.y).wrapping_sub(5_000)
|| predicted.y > start.y.max(end.y).wrapping_add(5_000)
{
continue;
}
let material = if self.tilted {
CollisionMaterial::line(
f64::from(wall.normal_rebound),
@@ -806,17 +819,16 @@ impl Game {
f64::from(wall.response_kick),
)
};
if let Some(response) = line_collision_response(
if let Some(candidate) = line_collision_candidate(
old_position,
velocity,
segment.start,
segment.end,
material,
spin,
) && best.is_none_or(|(_, _, closest): (u8, bool, CollisionResponse)| {
response.surface_distance <= closest.surface_distance
) && best.is_none_or(|(_, _, closest): (u8, bool, StaticCollisionCandidate)| {
candidate.surface_distance <= closest.surface_distance
}) {
best = Some((wall.id, true, response));
best = Some((wall.id, true, candidate));
}
}
let circle = PASSIVE_CIRCLES
@@ -825,7 +837,18 @@ impl Game {
.find(|circle| circle.id == object_id);
if let Some(circle) = circle
&& circle.layer_mask & layer != 0
&& let Some(response) = circle_collision_response(
&& {
let center = MilliVec::from_position(
self.live_circle_center(circle.id, circle.center),
);
let margin =
MilliVec::from_position(vec2(circle.contact_radius + 5.0, 0.0)).x;
predicted.x >= center.x.wrapping_sub(margin)
&& predicted.x <= center.x.wrapping_add(margin)
&& predicted.y >= center.y.wrapping_sub(margin)
&& predicted.y <= center.y.wrapping_add(margin)
}
&& let Some(candidate) = circle_collision_candidate(
old_position,
velocity,
self.live_circle_center(circle.id, circle.center),
@@ -839,13 +862,12 @@ impl Game {
f64::from(circle.normal_kick)
},
),
spin,
)
&& best.is_none_or(|(_, _, closest)| {
response.surface_distance <= closest.surface_distance
candidate.surface_distance <= closest.surface_distance
})
{
best = Some((circle.id, false, response));
best = Some((circle.id, false, candidate));
}
}
best
@@ -869,7 +891,10 @@ impl Game {
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
self.apply_magnetic_fields(old_position, &mut velocity);
let movement_velocity = velocity;
let mut best_collision = self.find_static_collision(old_position, velocity, ball.spin);
let best_static = self.find_static_collision_candidate(old_position, velocity);
let mut best_collision = best_static.map(|(id, is_wall, candidate)| {
(id, is_wall, candidate.resolve(velocity, ball.spin))
});
let mut transferred_primary_velocity = None;
if primary_slot_active
@@ -2382,28 +2407,37 @@ mod tests {
let velocity = MilliVec { x: 0, y: -15_000 };
assert_eq!(
game.find_static_collision(
game.find_static_collision_candidate(
MilliVec {
x: 25_000,
y: 249_000,
},
velocity,
Real48::ZERO,
)
.map(|collision| collision.0),
Some(48)
);
assert!(
game.find_static_collision(
game.find_static_collision_candidate(
MilliVec {
x: 25_000,
y: 250_000,
},
velocity,
Real48::ZERO,
)
.is_none()
);
assert!(
game.find_static_collision_candidate(
MilliVec {
x: 25_000,
y: 249_000,
},
MilliVec { x: 0, y: -40_000 },
)
.is_none(),
"the original predicted-position broadphase rejects an overshoot"
);
}
#[test]