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:
@@ -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);
|
||||
|
||||
@@ -241,7 +241,7 @@ fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, lengt
|
||||
.round_i32()
|
||||
}
|
||||
|
||||
fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
fn tangent_direction(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
if length <= 0 {
|
||||
return 0;
|
||||
}
|
||||
@@ -255,6 +255,17 @@ fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, leng
|
||||
.round_i32()
|
||||
}
|
||||
|
||||
fn spin_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
|
||||
if length <= 0 {
|
||||
return 0;
|
||||
}
|
||||
Real48::from_i32(velocity.x)
|
||||
.multiply(normal_x)
|
||||
.add(Real48::from_i32(velocity.y).multiply(normal_y))
|
||||
.divide(Real48::from_i32(length))
|
||||
.round_i32()
|
||||
}
|
||||
|
||||
fn cross_at_endpoint(
|
||||
point: MilliVec,
|
||||
current: MilliVec,
|
||||
@@ -284,17 +295,21 @@ fn apply_response(
|
||||
x: normal_x.round_i32(),
|
||||
y: normal_y.round_i32(),
|
||||
});
|
||||
let initial_projection = tangent_velocity(velocity, normal_x, normal_y, length);
|
||||
let mut spin_delta = Real48::from_i32(initial_projection)
|
||||
// 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)
|
||||
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
|
||||
.multiply(coefficients.tangent)
|
||||
.multiply(Real48::from_i32(collision_velocity.wrapping_abs()));
|
||||
if initial_projection < 0 {
|
||||
spin_delta = spin_delta.negate();
|
||||
.multiply(coefficients.tangent);
|
||||
let mut tangent_response = Real48::from_i32(collision_velocity.wrapping_abs())
|
||||
.multiply(coefficients.tangent);
|
||||
if tangent_direction < 0 {
|
||||
tangent_response = tangent_response.negate();
|
||||
}
|
||||
let projection = spin
|
||||
.multiply(Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]))
|
||||
.add(Real48::from_i32(initial_projection).multiply(coefficients.tangent))
|
||||
.add(tangent_response)
|
||||
.round_i32();
|
||||
let spin = spin
|
||||
.multiply(Real48::from_bytes([0x80, 0x9a, 0x99, 0x99, 0x99, 0x19]))
|
||||
|
||||
+44
-12
@@ -66,10 +66,10 @@ impl TableSegment {
|
||||
96..=98 => 96,
|
||||
99..=101 => 99,
|
||||
102..=104 => 102,
|
||||
109..=111 => 109,
|
||||
112..=114 => 112,
|
||||
115..=117 => 115,
|
||||
118..=120 => 118,
|
||||
109 => 109,
|
||||
112 => 112,
|
||||
115 => 115,
|
||||
118 => 118,
|
||||
_ => 0,
|
||||
};
|
||||
let (response_auxiliary, response_kick) = match id {
|
||||
@@ -210,8 +210,8 @@ pub const WALLS: &[TableSegment] = &[
|
||||
TableSegment::new(49, Vec2::new(33.0, 231.0), Vec2::new(10.0, 300.0)),
|
||||
TableSegment::new(50, Vec2::new(12.0, 261.0), Vec2::new(12.0, 407.0)),
|
||||
TableSegment::new(55, Vec2::new(59.0, 287.0), Vec2::new(107.0, 359.0)),
|
||||
TableSegment::new(57, Vec2::new(49.0, 294.0), Vec2::new(2.0, 268.0)),
|
||||
TableSegment::new(59, Vec2::new(44.0, 287.0), Vec2::new(44.0, 242.0)),
|
||||
TableSegment::new(57, Vec2::new(97.0, 366.0), Vec2::new(50.0, 340.0)),
|
||||
TableSegment::new(59, Vec2::new(45.0, 333.0), Vec2::new(45.0, 288.0)),
|
||||
TableSegment::new(61, Vec2::new(37.0, 292.0), Vec2::new(37.0, 347.0)),
|
||||
TableSegment::new(62, Vec2::new(37.0, 347.0), Vec2::new(110.0, 384.0)),
|
||||
TableSegment::new(63, Vec2::new(92.0, 408.0), Vec2::new(19.0, 385.0)),
|
||||
@@ -219,8 +219,8 @@ pub const WALLS: &[TableSegment] = &[
|
||||
TableSegment::new(66, Vec2::new(112.0, 386.0), Vec2::new(141.0, 413.0)),
|
||||
TableSegment::new(68, Vec2::new(130.0, 427.0), Vec2::new(97.0, 411.0)),
|
||||
TableSegment::new(70, Vec2::new(267.0, 292.0), Vec2::new(267.0, 337.0)),
|
||||
TableSegment::new(72, Vec2::new(261.0, 299.0), Vec2::new(214.0, 321.0)),
|
||||
TableSegment::new(74, Vec2::new(252.0, 289.0), Vec2::new(302.0, 218.0)),
|
||||
TableSegment::new(72, Vec2::new(261.0, 344.0), Vec2::new(214.0, 366.0)),
|
||||
TableSegment::new(74, Vec2::new(205.0, 356.0), Vec2::new(255.0, 285.0)),
|
||||
TableSegment::new(76, Vec2::new(292.0, 291.0), Vec2::new(292.0, 385.0)),
|
||||
TableSegment::new(77, Vec2::new(292.0, 385.0), Vec2::new(221.0, 408.0)),
|
||||
TableSegment::new(78, Vec2::new(205.0, 384.0), Vec2::new(275.0, 347.0)),
|
||||
@@ -291,14 +291,14 @@ pub const PASSIVE_CIRCLES: &[StaticCircle] = &[
|
||||
StaticCircle::new(42, Vec2::new(38.0, 183.0), 8.0),
|
||||
StaticCircle::new(48, Vec2::new(25.0, 231.0), 8.0),
|
||||
StaticCircle::new(54, Vec2::new(53.0, 292.0), 8.0),
|
||||
StaticCircle::new(56, Vec2::new(50.0, 287.0), 8.0),
|
||||
StaticCircle::new(58, Vec2::new(52.0, 287.0), 8.0),
|
||||
StaticCircle::new(56, Vec2::new(98.0, 359.0), 8.0),
|
||||
StaticCircle::new(58, Vec2::new(53.0, 333.0), 8.0),
|
||||
StaticCircle::new(60, Vec2::new(29.0, 294.0), 10.0),
|
||||
StaticCircle::new(65, Vec2::new(103.0, 397.0), 15.0),
|
||||
StaticCircle::new(67, Vec2::new(134.0, 419.0), 9.0),
|
||||
StaticCircle::new(69, Vec2::new(260.0, 292.0), 8.0),
|
||||
StaticCircle::new(71, Vec2::new(260.0, 291.0), 8.0),
|
||||
StaticCircle::new(73, Vec2::new(261.0, 291.0), 8.0),
|
||||
StaticCircle::new(71, Vec2::new(260.0, 336.0), 8.0),
|
||||
StaticCircle::new(73, Vec2::new(214.0, 358.0), 8.0),
|
||||
StaticCircle::new(75, Vec2::new(284.0, 292.0), 9.0),
|
||||
StaticCircle::new(80, Vec2::new(210.0, 397.0), 15.0),
|
||||
StaticCircle::new(82, Vec2::new(179.0, 419.0), 9.0),
|
||||
@@ -411,6 +411,38 @@ mod tests {
|
||||
assert_eq!(shooter_wall.segment.end, Vec2::new(328.0, 58.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_slingshot_chains_match_the_initialized_record_ledger() {
|
||||
for (id, expected_start, expected_end) in [
|
||||
(55, Vec2::new(59.0, 287.0), Vec2::new(107.0, 359.0)),
|
||||
(57, Vec2::new(97.0, 366.0), Vec2::new(50.0, 340.0)),
|
||||
(59, Vec2::new(45.0, 333.0), Vec2::new(45.0, 288.0)),
|
||||
(70, Vec2::new(267.0, 292.0), Vec2::new(267.0, 337.0)),
|
||||
(72, Vec2::new(261.0, 344.0), Vec2::new(214.0, 366.0)),
|
||||
(74, Vec2::new(205.0, 356.0), Vec2::new(255.0, 285.0)),
|
||||
] {
|
||||
let wall = WALLS
|
||||
.iter()
|
||||
.find(|wall| wall.id == id)
|
||||
.expect("relative line record must be present");
|
||||
assert_eq!((wall.segment.start, wall.segment.end), (expected_start, expected_end));
|
||||
}
|
||||
for (id, expected_center) in [
|
||||
(54, Vec2::new(53.0, 292.0)),
|
||||
(56, Vec2::new(98.0, 359.0)),
|
||||
(58, Vec2::new(53.0, 333.0)),
|
||||
(69, Vec2::new(260.0, 292.0)),
|
||||
(71, Vec2::new(260.0, 336.0)),
|
||||
(73, Vec2::new(214.0, 358.0)),
|
||||
] {
|
||||
let circle = PASSIVE_CIRCLES
|
||||
.iter()
|
||||
.find(|circle| circle.id == id)
|
||||
.expect("relative circle record must be present");
|
||||
assert_eq!(circle.center, expected_center);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_recovered_wall_has_its_registered_one_sided_contact() {
|
||||
for wall in WALLS {
|
||||
|
||||
Reference in New Issue
Block a user