fix(physics): gate dynamic impulse on capture age

Match records 174/175 by transferring normal impulse only when the other slot capture age is nonpositive. A held slot now keeps its velocity and the moving ball resolves with the full normal velocity; successful transfer retains the original normal_velocity-1000 response.

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 19:32:23 +02:00
parent 6c879a3f91
commit 06d1010431
4 changed files with 47 additions and 13 deletions
+3
View File
@@ -749,6 +749,7 @@ impl Game {
ball.spin,
other_position,
MilliVec::from_velocity_per_second(secondary.velocity),
secondary.capture_age <= 0,
);
if predicted.x >= other_position.x.wrapping_sub(21_000)
&& predicted.x <= other_position.x.wrapping_add(21_000)
@@ -1140,6 +1141,7 @@ impl Game {
};
let primary_position = self.ball.position;
let primary_velocity = self.ball.velocity;
let primary_capture_age = self.ball.capture_age;
let old_position = MilliVec::from_position(ball.position);
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
velocity.y += GRAVITY_MILLI_PER_STEP;
@@ -1173,6 +1175,7 @@ impl Game {
ball.spin,
other_position,
MilliVec::from_velocity_per_second(primary_velocity),
primary_capture_age <= 0,
)
&& best_collision.is_none()
{
+40 -12
View File
@@ -549,6 +549,7 @@ pub fn ball_collision_response(
spin: Real48,
other_position: MilliVec,
other_velocity: MilliVec,
transfer_enabled: bool,
) -> Option<BallCollisionResponse> {
let mut surface_distance =
milli_distance(subtract(other_position, old_position)).wrapping_sub(17_000);
@@ -574,24 +575,32 @@ pub fn ball_collision_response(
return None;
}
surface_distance = surface_distance.wrapping_add(3_000);
let transfer_x = Real48::from_i32(collision_velocity)
.multiply(normal_y)
.divide(Real48::from_i32(length))
.round_i32();
let transfer_y = Real48::from_i32(collision_velocity)
.multiply(normal_x)
.divide(Real48::from_i32(length))
.round_i32();
let other_velocity = MilliVec {
x: other_velocity.x.wrapping_add(transfer_x),
y: other_velocity.y.wrapping_sub(transfer_y),
let other_velocity = if transfer_enabled {
let transfer_x = Real48::from_i32(collision_velocity)
.multiply(normal_y)
.divide(Real48::from_i32(length))
.round_i32();
let transfer_y = Real48::from_i32(collision_velocity)
.multiply(normal_x)
.divide(Real48::from_i32(length))
.round_i32();
MilliVec {
x: other_velocity.x.wrapping_add(transfer_x),
y: other_velocity.y.wrapping_sub(transfer_y),
}
} else {
other_velocity
};
let (moving_velocity, moving_spin, _) = apply_response(
velocity,
spin,
normal_x,
normal_y,
collision_velocity.wrapping_sub(1_000),
if transfer_enabled {
collision_velocity.wrapping_sub(1_000)
} else {
collision_velocity
},
CollisionMaterial::circle(0.9, 0.0, 0.0).real48(),
);
Some(BallCollisionResponse {
@@ -930,11 +939,30 @@ mod tests {
y: 100_000,
},
MilliVec::default(),
true,
)
.expect("moving ball enters the 17-pixel dynamic record");
assert_eq!(response.surface_distance, 4_000);
assert_eq!(response.other_velocity, MilliVec { x: 3_000, y: 0 });
assert_eq!(response.moving_velocity, MilliVec { x: -4_600, y: 0 });
assert_eq!(response.moving_spin, Real48::ZERO);
let held_other = ball_collision_response(
MilliVec {
x: 100_000,
y: 100_000,
},
MilliVec { x: 3_000, y: 0 },
Real48::ZERO,
MilliVec {
x: 118_000,
y: 100_000,
},
MilliVec::default(),
false,
)
.expect("the held slot still presents a dynamic collision record");
assert_eq!(held_other.other_velocity, MilliVec::default());
assert_eq!(held_other.moving_velocity, MilliVec { x: -2_700, y: 0 });
}
}