fix(physics): restore fixed-point rail movement

Replace the guessed variable-rate floating integration with the original 10 ms
millipixel step, 15-unit gravity, and 3800-unit speed limit. Restore every
registered type-2 Real48 response pair, one-sided start-to-end orientation, and
the original normal/tangent velocity transform validated against live shooter
wall traces.

Restore repeat-driven plunger charging and its release-time randomized clamp.
The shooter route now follows table orientation instead of coordinate-specific
gate exceptions. Remove the synthetic ball-search impulse so future stuck balls
remain visible reconstruction failures, and include collision ids in deterministic
traces for differential work on circles and triggers.

Test Plan:
- `cargo test --all-targets` -- 42 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- charged launcher scenario cleared the shooter lane -- passed
- all recovered rails accepted front-side and rejected back-side probes
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 20:42:47 +02:00
parent c50ed39a35
commit 7caf460022
8 changed files with 502 additions and 260 deletions
+54 -26
View File
@@ -12,22 +12,39 @@
use crate::geometry::Segment;
use macroquad::prelude::Vec2;
// The ordinary rail registrations in `FUN_1000_34ab` use the recovered 0.1
// rebound coefficient. The previous 0.82 value made the shooter curve behave
// like a rubber bumper and sent the ball back down the launch lane.
const WALL_BOUNCE: f32 = 0.10;
#[derive(Clone, Copy, Debug)]
pub struct TableSegment {
pub id: u8,
pub segment: Segment,
pub normal_rebound: f32,
pub tangent_coupling: f32,
}
impl TableSegment {
const fn new(id: u8, start: Vec2, end: Vec2) -> Self {
// These are the first two Borland Real48 fields in each original
// 0x53-byte collision record. They control normal rebound and tangent
// coupling in the type-2 response basis.
let (normal_rebound, tangent_coupling) = match id {
3 | 9 | 25 => (0.10, 0.10),
91 | 92 | 94 | 95 | 97 | 98 | 100 | 101 | 103 | 104 => (0.40, 0.0),
62 => (0.40, 0.05),
15 | 16 | 45 => (0.40, 0.10),
78 => (0.50, 0.05),
55 | 57 | 59 | 61 | 70 | 72 | 74 | 76 => (0.50, 0.10),
66 | 68 | 81 | 83 => (0.50, 0.20),
84 | 85 | 86 | 87 | 88 | 90 | 93 | 96 | 99 | 102 | 109 | 110 | 111 | 112 | 113
| 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 168 | 170 | 171 | 173 => {
(0.60, 0.0)
}
27 | 28 | 29 | 30 | 31 | 32 | 105 => (0.60, 0.20),
_ => (0.60, 0.10),
};
Self {
id,
segment: Segment::new(start, end, WALL_BOUNCE),
segment: Segment::new(start, end, normal_rebound),
normal_rebound,
tangent_coupling,
}
}
}
@@ -215,7 +232,10 @@ pub const BUMPERS: [StaticCircle; 3] = [
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{circle_collision, segment_collision};
use crate::{
geometry::circle_collision,
original_physics::{MilliVec, collide_with_line},
};
#[test]
fn recovered_object_inventory_is_complete() {
@@ -241,7 +261,7 @@ mod tests {
}
#[test]
fn every_recovered_wall_has_a_working_contact_surface_on_both_sides() {
fn every_recovered_wall_has_its_registered_one_sided_contact() {
for wall in WALLS {
let line = wall.segment.end - wall.segment.start;
assert!(
@@ -251,25 +271,33 @@ mod tests {
);
let normal = Vec2::new(-line.y, line.x).normalize();
let midpoint = (wall.segment.start + wall.segment.end) * 0.5;
let mut front_velocity = MilliVec::from_velocity_per_second(normal * 200.0);
assert!(
collide_with_line(
MilliVec::from_position(midpoint - normal),
&mut front_velocity,
wall.segment.start,
wall.segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
),
"object {} missed its registered front side",
wall.id
);
for side in [-1.0, 1.0] {
let outward = normal * side;
let mut position = midpoint + outward * 0.5;
let mut velocity = -outward * 430.0;
let hit = segment_collision(&mut position, &mut velocity, 1.0, wall.segment);
assert!(hit, "object {} missed its midpoint contact", wall.id);
assert!(
velocity.dot(outward) >= 0.0,
"object {} did not reject an inward trajectory",
wall.id
);
assert!(
position.distance(midpoint) >= 0.99,
"object {} did not separate the ball from its surface",
wall.id
);
}
let mut back_velocity = MilliVec::from_velocity_per_second(-normal * 200.0);
assert!(
!collide_with_line(
MilliVec::from_position(midpoint + normal),
&mut back_velocity,
wall.segment.start,
wall.segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
),
"object {} accepted contact from its back side",
wall.id
);
}
}