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:
2026-08-23 20:33:20 +02:00
parent f79652bc9f
commit 23195cb9d1
9 changed files with 124 additions and 68 deletions
+40
View File
@@ -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);