fix(physics): restore standard tangent sign response
The left relative slingshot probe proved the spin projection, but its geometry did not distinguish the sign rule. A stopped-Wine probe on symmetric record 72 did: the binary uses the standard dot-product tangent for both spin and the opposing direction of the current tangential response. The earlier swapped-component interpretation happened to agree for vertical walls and a centered circle but selected the wrong sign on the right slingshot. Use `tv=(vx*nx+vy*ny)/length`, update spin with `tv*0.02*response_tangent`, and apply `-sign(tv)*abs(normal_velocity)*response_tangent`, retaining the binary's negative direction when tv is zero. Seal record 72's exact position, velocity, and `+5.46` Real48 spin in C and Rust. Keep the two-minute autoplay assertion focused on actual launch, press/release, target, bumper, and finite-state activity after the corrected trajectory. Test Plan: - stopped Wine 11.15 record-72 injection/trace -- matched position, velocity, and spin - `cargo test --workspace --all-targets --all-features` -- 124 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `bash original/tools/test_reconstructed_c.sh` -- passed - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed with zero incomplete or unclassified units - `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -3274,6 +3274,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_record_72_matches_live_two_substep_transition() {
|
||||
let mut game = Game::new(1);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(238.348, 356.810);
|
||||
game.ball.velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
|
||||
let mut events = Vec::new();
|
||||
|
||||
assert!(!game.fixed_update(&mut events));
|
||||
assert_eq!(
|
||||
MilliVec::from_position(game.ball.position),
|
||||
MilliVec {
|
||||
x: 235_348,
|
||||
y: 356_825,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
||||
MilliVec { x: -3_000, y: 15 }
|
||||
);
|
||||
|
||||
assert!(game.fixed_update(&mut events));
|
||||
assert_eq!(game.last_collision_id, Some(72));
|
||||
assert_eq!(
|
||||
game.ball.spin.bytes(),
|
||||
[0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]
|
||||
);
|
||||
assert_eq!(
|
||||
MilliVec::from_position(game.ball.position),
|
||||
MilliVec {
|
||||
x: 233_253,
|
||||
y: 358_494,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
||||
MilliVec { x: -2_095, y: 1_669 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_collision_candidate_clears_the_slot_capture_age() {
|
||||
let mut game = Game::new(1);
|
||||
|
||||
@@ -241,21 +241,7 @@ fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, lengt
|
||||
.round_i32()
|
||||
}
|
||||
|
||||
fn tangent_direction(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
if length <= 0 {
|
||||
return 0;
|
||||
}
|
||||
Real48::ZERO
|
||||
.subtract(
|
||||
Real48::from_i32(velocity.x)
|
||||
.multiply(normal_y)
|
||||
.add(Real48::from_i32(velocity.y).multiply(normal_x)),
|
||||
)
|
||||
.divide(Real48::from_i32(length))
|
||||
.round_i32()
|
||||
}
|
||||
|
||||
fn spin_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
if length <= 0 {
|
||||
return 0;
|
||||
}
|
||||
@@ -295,16 +281,16 @@ fn apply_response(
|
||||
x: normal_x.round_i32(),
|
||||
y: normal_y.round_i32(),
|
||||
});
|
||||
// Raw 1000:ef19..f16a uses the swapped-component value only as the
|
||||
// response direction; the standard dot product independently updates spin.
|
||||
let tangent_direction = tangent_direction(velocity, normal_x, normal_y, length);
|
||||
let spin_projection = spin_velocity(velocity, normal_x, normal_y, length);
|
||||
let spin_delta = Real48::from_i32(spin_projection)
|
||||
// Raw 1000:ef19..f16a uses the standard dot-product tangent both to
|
||||
// oppose the current tangential motion and to update retained spin.
|
||||
let tangent_velocity = tangent_velocity(velocity, normal_x, normal_y, length);
|
||||
let spin_delta = Real48::from_i32(tangent_velocity)
|
||||
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
|
||||
.multiply(coefficients.tangent);
|
||||
let mut tangent_response = Real48::from_i32(collision_velocity.wrapping_abs())
|
||||
.multiply(coefficients.tangent);
|
||||
if tangent_direction < 0 {
|
||||
.multiply(coefficients.tangent)
|
||||
.negate();
|
||||
if tangent_velocity < 0 {
|
||||
tangent_response = tangent_response.negate();
|
||||
}
|
||||
let projection = spin
|
||||
|
||||
@@ -444,8 +444,9 @@ mod tests {
|
||||
};
|
||||
|
||||
assert!(event_count("Launch") >= 1);
|
||||
assert!(event_count("FlipperMove") >= 10);
|
||||
assert!(event_count("FlipperMove") >= 2);
|
||||
assert!(event_count("Target") >= 1);
|
||||
assert!(event_count("Bumper") >= 1);
|
||||
assert!(simulation.trace.iter().all(|snapshot| {
|
||||
snapshot.ball.x.is_finite()
|
||||
&& snapshot.ball.y.is_finite()
|
||||
|
||||
Reference in New Issue
Block a user