refactor(physics): defer static collision responses

Split type-one and type-two detection from response resolution so the retained candidate can be applied to the motion state that exists after the complete record scan. Existing response functions remain compatibility wrappers, and a focused test proves late motion changes participate in resolution.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- git diff --check
This commit is contained in:
2026-08-23 18:28:02 +02:00
parent 742b8b7981
commit 975fb27494
+86 -28
View File
@@ -24,6 +24,15 @@ pub struct CollisionResponse {
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,
@@ -87,6 +96,25 @@ impl CollisionMaterial {
}
}
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,
@@ -293,14 +321,13 @@ fn apply_response(
}
/// Calculate the original type-2 response in the registered segment's basis.
pub fn line_collision_response(
pub fn line_collision_candidate(
old_position: MilliVec,
velocity: MilliVec,
line_start: Vec2,
line_end: Vec2,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
) -> Option<StaticCollisionCandidate> {
let start = MilliVec::from_position(line_start);
let end = MilliVec::from_position(line_end);
let predicted = old_position.add(velocity);
@@ -337,22 +364,27 @@ pub fn line_collision_response(
{
return None;
}
let (velocity, spin, auxiliary_fired) = apply_response(
velocity,
spin,
Some(StaticCollisionCandidate {
surface_distance: distance,
normal_x,
normal_y,
collision_velocity,
material.real48(),
);
Some(CollisionResponse {
surface_distance: distance,
velocity,
spin,
auxiliary_fired,
normal_velocity: collision_velocity,
material,
})
}
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,
@@ -377,14 +409,13 @@ pub fn collide_with_line(
}
/// Calculate the original type-1 circle response for a path entering it.
pub fn circle_collision_response(
pub fn circle_collision_candidate(
old_position: MilliVec,
velocity: MilliVec,
center: Vec2,
radius: f32,
material: CollisionMaterial,
spin: Real48,
) -> Option<CollisionResponse> {
) -> Option<StaticCollisionCandidate> {
let center = MilliVec::from_position(center);
let radius_milli = (radius * 1_000.0).round() as i32;
let mut surface_distance =
@@ -411,22 +442,27 @@ pub fn circle_collision_response(
return None;
}
surface_distance = surface_distance.wrapping_add(3_000);
let (velocity, spin, auxiliary_fired) = apply_response(
velocity,
spin,
Some(StaticCollisionCandidate {
surface_distance,
normal_x,
normal_y,
collision_velocity,
material.real48(),
);
Some(CollisionResponse {
surface_distance,
velocity,
spin,
auxiliary_fired,
normal_velocity: collision_velocity,
material,
})
}
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,
@@ -562,6 +598,28 @@ mod tests {
);
}
#[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: 1_000 });
assert_eq!(response.surface_distance, 2_000);
}
#[test]
fn auxiliary_kick_uses_the_recovered_negative_speed_threshold() {
let material = CollisionMaterial::line_with_kick(0.5, 0.1, -0.4, 0.4);