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
+230
View File
@@ -0,0 +1,230 @@
//! Fixed-point primitives recovered from the original Win16 physics loop.
#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
use macroquad::prelude::{Vec2, vec2};
pub const STEP_SECONDS: f32 = 0.010;
pub const GRAVITY_MILLI_PER_STEP: i32 = 15;
pub const MAXIMUM_SPEED_MILLI_PER_STEP: i32 = 3_800;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MilliVec {
pub x: i32,
pub y: i32,
}
impl MilliVec {
pub fn from_position(position: Vec2) -> Self {
Self {
x: (position.x * 1_000.0).round() as i32,
y: (position.y * 1_000.0).round() as i32,
}
}
pub fn from_velocity_per_second(velocity: Vec2) -> Self {
Self {
x: (velocity.x * 10.0).round() as i32,
y: (velocity.y * 10.0).round() as i32,
}
}
pub fn to_position(self) -> Vec2 {
vec2(self.x as f32 / 1_000.0, self.y as f32 / 1_000.0)
}
pub fn to_velocity_per_second(self) -> Vec2 {
vec2(self.x as f32 / 10.0, self.y as f32 / 10.0)
}
pub const fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
pub fn clamp_speed(&mut self, maximum: i32) {
let speed_squared = i64::from(self.x).pow(2) + i64::from(self.y).pow(2);
if speed_squared <= i64::from(maximum).pow(2) {
return;
}
let scale = f64::from(maximum) / (speed_squared as f64).sqrt();
self.x = (f64::from(self.x) * scale).round() as i32;
self.y = (f64::from(self.y) * scale).round() as i32;
}
}
const fn cross(left: MilliVec, right: MilliVec) -> i64 {
left.x as i64 * right.y as i64 - left.y as i64 * right.x as i64
}
const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
MilliVec {
x: left.x - right.x,
y: left.y - right.y,
}
}
/// Reports whether the ball-center path crosses the registered line segment.
/// A contact at the old position is excluded, matching the shooter-stop path:
/// the waiting ball starts on object 25 and must be able to launch away from it.
fn paths_intersect(
old_position: MilliVec,
velocity: MilliVec,
line_start: MilliVec,
line_end: MilliVec,
) -> bool {
let line = subtract(line_end, line_start);
let from_ball = subtract(line_start, old_position);
let denominator = cross(velocity, line);
if denominator == 0 {
return false;
}
let path_numerator = cross(from_ball, line);
let line_numerator = cross(from_ball, velocity);
if denominator > 0 {
path_numerator > 0
&& path_numerator <= denominator
&& line_numerator >= 0
&& line_numerator <= denominator
} else {
path_numerator < 0
&& path_numerator >= denominator
&& line_numerator <= 0
&& line_numerator >= denominator
}
}
/// Apply the original type-2 response in the registered segment's basis.
pub fn collide_with_line(
old_position: MilliVec,
velocity: &mut MilliVec,
line_start: Vec2,
line_end: Vec2,
normal_rebound: f64,
tangent_coupling: f64,
) -> bool {
let start = MilliVec::from_position(line_start);
let end = MilliVec::from_position(line_end);
if !paths_intersect(old_position, *velocity, start, end) {
return false;
}
let line_x = f64::from(end.x - start.x);
let line_y = f64::from(end.y - start.y);
let length = line_x.hypot(line_y);
if length == 0.0 {
return false;
}
let tangent_x = line_x / length;
let tangent_y = line_y / length;
let normal_x = -tangent_y;
let normal_y = tangent_x;
let incoming_x = f64::from(velocity.x);
let incoming_y = f64::from(velocity.y);
let normal_speed = incoming_x * normal_x + incoming_y * normal_y;
if normal_speed <= 0.0 {
return false;
}
let tangent_speed = incoming_x * tangent_x + incoming_y * tangent_y;
let outgoing_normal = -normal_rebound * normal_speed;
let outgoing_tangent = tangent_speed + tangent_coupling * normal_speed;
velocity.x = (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32;
velocity.y = (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32;
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outer_shooter_wall_matches_the_live_original_probe() {
let old = MilliVec {
x: 326_000,
y: 200_045,
};
let mut velocity = MilliVec { x: 3_000, y: 45 };
assert!(collide_with_line(
old,
&mut velocity,
vec2(328.0, 422.0),
vec2(328.0, 58.0),
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: -1_800, y: -255 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 324_200,
y: 199_790
}
);
}
#[test]
fn inner_shooter_wall_matches_the_live_original_probe() {
let old = MilliVec {
x: 320_600,
y: 199_325,
};
let mut velocity = MilliVec { x: -1_800, y: -210 };
assert!(collide_with_line(
old,
&mut velocity,
vec2(320.0, 48.0),
vec2(320.0, 437.0),
0.6,
0.1,
));
assert_eq!(velocity, MilliVec { x: 1_080, y: -30 });
assert_eq!(
old.add(velocity),
MilliVec {
x: 321_680,
y: 199_295
}
);
}
#[test]
fn contact_at_the_old_position_does_not_block_launching_away() {
let old = MilliVec {
x: 325_000,
y: 413_000,
};
let mut velocity = MilliVec { x: 0, y: -3_000 };
assert!(!collide_with_line(
old,
&mut velocity,
vec2(315.0, 413.0),
vec2(332.0, 413.0),
0.1,
0.1,
));
}
#[test]
fn crossing_a_rail_from_its_back_side_is_allowed() {
let old = MilliVec {
x: 283_708,
y: 18_775,
};
let mut velocity = MilliVec { x: -1_672, y: -66 };
assert!(!collide_with_line(
old,
&mut velocity,
vec2(277.0, 31.0),
vec2(287.0, 12.0),
0.6,
0.1,
));
}
}