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
+8 -30
View File
@@ -191,23 +191,7 @@ static int32_t normal_velocity_checked(
velocity_x, velocity_y, normal_x, normal_y, normal_length);
}
static int32_t tangent_direction_unchecked(
int32_t velocity_x,
int32_t velocity_y,
BorlandReal48 normal_x,
BorlandReal48 normal_y,
int32_t normal_length)
{
return negate_wrap(borland_real48_round_to_i32(borland_real48_divide(
borland_real48_add(
borland_real48_multiply(
borland_i32_to_real48(velocity_x), normal_y),
borland_real48_multiply(
borland_i32_to_real48(velocity_y), normal_x)),
borland_i32_to_real48(normal_length))));
}
static int32_t spin_velocity_unchecked(
static int32_t tangent_velocity_unchecked(
int32_t velocity_x,
int32_t velocity_y,
BorlandReal48 normal_x,
@@ -943,16 +927,9 @@ static void apply_collision_response(
tdkpin_add_score_from_caller_frame(caller_bp, candidate->score);
}
int32_t length = candidate_normal_length(candidate);
/* Raw 1000:ef19..f16a keeps the contact-direction and spin projections
* separate. The swapped-component value selects the sign of the current
* tangential response; the standard dot product updates retained spin. */
int32_t tangent_direction = tangent_direction_unchecked(
motion->velocity_x,
motion->velocity_y,
candidate->normal_x,
candidate->normal_y,
length);
int32_t spin_projection = spin_velocity_unchecked(
/* Raw 1000:ef19..f16a uses the standard dot-product tangent both to
* oppose the current tangential motion and to update retained spin. */
int32_t tangent_velocity = tangent_velocity_unchecked(
motion->velocity_x,
motion->velocity_y,
candidate->normal_x,
@@ -961,7 +938,7 @@ static void apply_collision_response(
BorlandReal48 spin_delta = borland_real48_multiply(
borland_real48_multiply(
borland_i32_to_real48(spin_projection),
borland_i32_to_real48(tangent_velocity),
g_real48_point_zero_two),
candidate->response_tangent);
BorlandReal48 discarded = borland_real48_divide(
@@ -974,7 +951,8 @@ static void apply_collision_response(
BorlandReal48 tangent_response = borland_real48_multiply(
borland_i32_to_real48(absolute_wrap(candidate->normal_velocity)),
candidate->response_tangent);
if (tangent_direction < 0) {
tangent_response = negate_real48(tangent_response);
if (tangent_velocity < 0) {
tangent_response = negate_real48(tangent_response);
}
int32_t projection = borland_real48_round_to_i32(borland_real48_add(
@@ -982,7 +960,7 @@ static void apply_collision_response(
borland_real48_multiply(motion->spin, g_real48_point_four),
borland_real48_divide(
borland_real48_multiply(
borland_i32_to_real48(absolute_wrap(tangent_direction)),
borland_i32_to_real48(absolute_wrap(tangent_velocity)),
discarded),
borland_i32_to_real48(motion->speed))),
tangent_response));
@@ -518,6 +518,51 @@ static void test_live_relative_record_57_transition(void)
}
}
static void test_live_relative_record_72_transition(void)
{
prepare_fixture();
set_ball(238348, 356810, -3000, 0);
win16_write_u32(g_window, 0x5a, 15);
win16_write_u32(g_window, 0x56, 3800);
TdkpinCollisionRecord segment = base_record(2);
segment.bounds_min_x_milli = 209000;
segment.bounds_min_y_milli = 339000;
segment.bounds_max_x_milli = 266000;
segment.bounds_max_y_milli = 371000;
segment.point1_x_milli = 261000;
segment.point1_y_milli = 344000;
segment.point2_x_milli = 214000;
segment.point2_y_milli = 366000;
segment.response_normal =
(BorlandReal48){{0x80,0x00,0x00,0x00,0x00,0x00}};
segment.response_tangent =
(BorlandReal48){{0x7d,0xcd,0xcc,0xcc,0xcc,0x4c}};
segment.layer_mask = 2;
write_record(72, &segment);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d3)) == 235348);
assert((int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d7)) == 356825);
tdkpin_simulate_ball_and_dispatch_collision(0x0300);
assert((int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d3)) == 233253);
assert((int32_t)win16_read_u32(
win16_dgroup_pointer(0x07d7)) == 358494);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0baa)) == -2095);
assert((int32_t)win16_read_u32(
win16_far_add_offset(g_window, 0x0bae)) == 1669);
static const uint8_t expected_spin[6] = {
0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e};
for (uint16_t index = 0; index < 6; index++) {
assert(win16_read_u8(win16_far_add_offset(
g_window, (uint16_t)(0x0bce + index))) ==
expected_spin[index]);
}
}
int main(void)
{
test_free_integration_and_speed_clamp();
@@ -526,5 +571,6 @@ int main(void)
test_capture_boundary_and_claw();
test_out_of_bounds_and_zero_substeps_multiball();
test_live_relative_record_57_transition();
test_live_relative_record_72_transition();
return 0;
}