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:
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user