fix(physics): preserve record layer and kick fields

Carry the recovered +0x49 layer mask through every static line/circle and select the original upper/lower scan layer from the previous Y coordinate. Restore negative auxiliary thresholds and kick coefficients for bumpers and records 55, 74, and 107, including weak-hit scoring, rail sound, and tilt suppression.

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:13:35 +02:00
parent 692e0087f9
commit 3d5a105c10
5 changed files with 247 additions and 22 deletions
+69 -13
View File
@@ -21,12 +21,14 @@ pub struct CollisionResponse {
pub surface_distance: i32,
pub velocity: MilliVec,
pub spin: Real48,
pub auxiliary_fired: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct CollisionMaterial {
pub normal_rebound: f64,
pub tangent_coupling: f64,
pub response_auxiliary: f64,
pub normal_kick: f64,
}
@@ -43,10 +45,25 @@ impl CollisionMaterial {
Self {
normal_rebound,
tangent_coupling,
response_auxiliary: 0.0,
normal_kick: 0.0,
}
}
pub const fn line_with_kick(
normal_rebound: f64,
tangent_coupling: f64,
response_auxiliary: f64,
normal_kick: f64,
) -> Self {
Self {
normal_rebound,
tangent_coupling,
response_auxiliary,
normal_kick,
}
}
pub const fn circle(
normal_rebound: f64,
tangent_coupling: f64,
@@ -55,6 +72,7 @@ impl CollisionMaterial {
Self {
normal_rebound,
tangent_coupling,
response_auxiliary: if normal_kick == 0.0 { 0.0 } else { -0.1 },
normal_kick,
}
}
@@ -63,11 +81,7 @@ impl CollisionMaterial {
ResponseCoefficients {
normal: coefficient(self.normal_rebound),
tangent: coefficient(self.tangent_coupling),
auxiliary: if self.normal_kick == 0.0 {
ZERO
} else {
Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc])
},
auxiliary: coefficient(self.response_auxiliary),
kick: coefficient(self.normal_kick),
}
}
@@ -157,7 +171,8 @@ const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
}
fn coefficient(value: f64) -> Real48 {
match (value * 100.0).round() as i32 {
let scaled = (value * 100.0).round() as i32;
let coefficient = match scaled.wrapping_abs() {
0 => ZERO,
5 => Real48::from_bytes([0x7c, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
10 => Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
@@ -169,6 +184,11 @@ fn coefficient(value: f64) -> Real48 {
80 => Real48::from_bytes([0x80, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
90 => Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]),
other => panic!("unsupported binary Real48 coefficient {other}"),
};
if scaled < 0 {
coefficient.negate()
} else {
coefficient
}
}
@@ -217,7 +237,7 @@ fn apply_response(
normal_y: Real48,
collision_velocity: i32,
coefficients: ResponseCoefficients,
) -> (MilliVec, Real48) {
) -> (MilliVec, Real48, bool) {
let length = milli_distance(MilliVec {
x: normal_x.round_i32(),
y: normal_y.round_i32(),
@@ -240,12 +260,12 @@ fn apply_response(
let mut impulse = Real48::from_i32(collision_velocity)
.multiply(ONE.add(coefficients.normal))
.round_i32();
if coefficients.auxiliary.compare(ZERO).is_lt()
let auxiliary_fired = coefficients.auxiliary.compare(ZERO).is_lt()
&& Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
.multiply(coefficients.auxiliary)
.round_i32()
> collision_velocity
{
> collision_velocity;
if auxiliary_fired {
impulse = impulse.wrapping_sub(
Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
.multiply(coefficients.kick)
@@ -268,6 +288,7 @@ fn apply_response(
y: velocity.y.wrapping_add(delta_y),
},
spin,
auxiliary_fired,
)
}
@@ -316,7 +337,7 @@ pub fn line_collision_response(
{
return None;
}
let (velocity, spin) = apply_response(
let (velocity, spin, auxiliary_fired) = apply_response(
velocity,
spin,
normal_x,
@@ -328,6 +349,7 @@ pub fn line_collision_response(
surface_distance: distance,
velocity,
spin,
auxiliary_fired,
})
}
@@ -389,7 +411,7 @@ pub fn circle_collision_response(
return None;
}
surface_distance = surface_distance.wrapping_add(3_000);
let (velocity, spin) = apply_response(
let (velocity, spin, auxiliary_fired) = apply_response(
velocity,
spin,
normal_x,
@@ -401,6 +423,7 @@ pub fn circle_collision_response(
surface_distance,
velocity,
spin,
auxiliary_fired,
})
}
@@ -447,7 +470,7 @@ pub fn ball_collision_response(
x: other_velocity.x.wrapping_add(transfer_x),
y: other_velocity.y.wrapping_sub(transfer_y),
};
let (moving_velocity, moving_spin) = apply_response(
let (moving_velocity, moving_spin, _) = apply_response(
velocity,
spin,
normal_x,
@@ -539,6 +562,39 @@ mod tests {
);
}
#[test]
fn auxiliary_kick_uses_the_recovered_negative_speed_threshold() {
let material = CollisionMaterial::line_with_kick(0.5, 0.1, -0.4, 0.4);
let fast = line_collision_response(
MilliVec {
x: 326_000,
y: 200_000,
},
MilliVec { x: 3_000, y: 0 },
vec2(328.0, 422.0),
vec2(328.0, 58.0),
material,
Real48::ZERO,
)
.expect("the fast path must hit the vertical rail");
let slow = line_collision_response(
MilliVec {
x: 327_500,
y: 200_000,
},
MilliVec { x: 1_000, y: 0 },
vec2(328.0, 422.0),
vec2(328.0, 58.0),
material,
Real48::ZERO,
)
.expect("the slow path must hit the same vertical rail");
assert!(fast.auxiliary_fired);
assert!(!slow.auxiliary_fired);
assert_ne!(fast.velocity.x, slow.velocity.x);
}
#[test]
fn inner_shooter_wall_matches_the_zero_spin_c_response() {
let old = MilliVec {