fix(physics): restore relative slingshot response

A full comparison against the 175-record ledger found stale guessed coordinates
in both relative slingshot chains. Circles 56/58/71/73 and lines 57/59/72/74
were displaced by up to 70 pixels. The corrected record-57 geometry then
exposed a second issue: readable C and Rust had collapsed two independent
binary response projections into one.

Transcribe the accumulated initializer coordinates exactly. Use the
swapped-component projection only to sign the current tangential response
`abs(normal_velocity) * response_tangent`, and use the standard dot product for
`spin_delta * 0.02 * response_tangent`. Correct the readable C evidence and
Rust together, extend live tracing with the six-byte spin field, and seal the
stopped-Wine transition from `(75530,354765)/(3000,15)` to
`(77369,356597)/(1839,1832)` with Real48 spin `-5.28` in both harnesses.

Test Plan:
- stopped Wine 11.15 record-57 injection/trace -- matched position, velocity, and spin
- `cargo test --workspace --all-targets --all-features` -- 123 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
- `python3 -m py_compile original/tools/trace_original_state.py original/tools/inject_original_state.py` -- passed
- `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:28:29 +02:00
parent 9b56247c32
commit f79652bc9f
11 changed files with 222 additions and 49 deletions
+40
View File
@@ -3234,6 +3234,46 @@ mod tests {
assert!(game.ball.velocity.y > 0.0);
}
#[test]
fn relative_record_57_matches_live_two_substep_transition() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = vec2(72.530, 354.750);
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: 75_530,
y: 354_765,
}
);
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(57));
assert_eq!(
game.ball.spin.bytes(),
[0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]
);
assert_eq!(
MilliVec::from_position(game.ball.position),
MilliVec {
x: 77_369,
y: 356_597,
}
);
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { x: 1_839, y: 1_832 }
);
}
#[test]
fn retained_collision_candidate_clears_the_slot_capture_age() {
let mut game = Game::new(1);