fix(physics): replace fitted flipper transfer
Remove the live-fitted upward and downward flipper polynomials. Port the reconstructed 1000:7ed9 path with delta-specific pivots and record edges, integer cross-product and radius gates, penetration, response-record gain, wrapping velocity updates, and the final position delta. Use resting records on press and the already-raised records on release, correct the original delta direction, and apply each edge to both live ball slots. Keep render-facing Vec2 projections, but adjust their float representation by ULPs so every gameplay millipixel round-trips exactly instead of losing reconstructed integer results. Test Plan: - `bash original/tools/test_reconstructed_c.sh` -- passed during raised-record reference capture - `cargo test --all-targets` -- passed, 62 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
//! Moving-flipper collision response reconstructed from `1000:7ed9`.
|
||||
|
||||
use crate::original_physics::MilliVec;
|
||||
|
||||
const SEARCH_RADIUS: i32 = 54_000;
|
||||
const RESPONSE_RADIUS: f64 = 44_000.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FlipperSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct FlipperResponse {
|
||||
pub velocity: MilliVec,
|
||||
pub movement: MilliVec,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Geometry {
|
||||
pivot: MilliVec,
|
||||
negative_edge: MilliVec,
|
||||
positive_edge: MilliVec,
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
fn for_side(side: FlipperSide, delta: i32) -> Self {
|
||||
match side {
|
||||
FlipperSide::Left => Self {
|
||||
pivot: MilliVec {
|
||||
x: 93_000,
|
||||
y: 397_000_i32.wrapping_add(delta.wrapping_mul(15_000)),
|
||||
},
|
||||
negative_edge: MilliVec {
|
||||
x: 161_000,
|
||||
y: 413_000,
|
||||
},
|
||||
positive_edge: MilliVec {
|
||||
x: if delta == 1 { 157_000 } else { 150_000 },
|
||||
y: if delta == 1 { 384_000 } else { 427_000 },
|
||||
},
|
||||
},
|
||||
FlipperSide::Right => Self {
|
||||
pivot: MilliVec {
|
||||
x: 220_000,
|
||||
y: 397_000_i32.wrapping_add(delta.wrapping_mul(15_000)),
|
||||
},
|
||||
negative_edge: MilliVec {
|
||||
x: 152_000,
|
||||
y: 413_000,
|
||||
},
|
||||
positive_edge: MilliVec {
|
||||
x: if delta == 1 { 177_000 } else { 196_000 },
|
||||
y: if delta == 1 { 405_000 } else { 411_000 },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn isolated_c_fixture(side: FlipperSide, delta: i32) -> Self {
|
||||
let mut geometry = Self::for_side(side, delta);
|
||||
geometry.positive_edge = match side {
|
||||
FlipperSide::Left => MilliVec {
|
||||
x: 150_000,
|
||||
y: 427_000,
|
||||
},
|
||||
FlipperSide::Right => MilliVec {
|
||||
x: 196_000,
|
||||
y: 411_000,
|
||||
},
|
||||
};
|
||||
geometry
|
||||
}
|
||||
}
|
||||
|
||||
fn cross_for_edge(edge: MilliVec, pivot: MilliVec, ball: MilliVec) -> i32 {
|
||||
let edge_from_ball_x = edge.x.wrapping_sub(ball.x) / 1_000;
|
||||
let edge_from_pivot_y = edge.y.wrapping_sub(pivot.y) / 1_000;
|
||||
let edge_from_ball_y = edge.y.wrapping_sub(ball.y) / 1_000;
|
||||
let edge_from_pivot_x = edge.x.wrapping_sub(pivot.x) / 1_000;
|
||||
edge_from_ball_y
|
||||
.wrapping_mul(edge_from_pivot_x)
|
||||
.wrapping_sub(edge_from_ball_x.wrapping_mul(edge_from_pivot_y))
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn rounded(value: f64) -> i32 {
|
||||
value.round() as i32
|
||||
}
|
||||
|
||||
fn collision_distance(ball: MilliVec, pivot: MilliVec) -> i32 {
|
||||
rounded(f64::from(ball.x.wrapping_sub(pivot.x)).hypot(f64::from(
|
||||
ball.y.wrapping_sub(pivot.y),
|
||||
)))
|
||||
}
|
||||
|
||||
fn contains(geometry: Geometry, side: FlipperSide, delta: i32, ball: MilliVec) -> Option<i32> {
|
||||
let (first_edge, second_edge) = if delta == -1 {
|
||||
(
|
||||
MilliVec {
|
||||
x: geometry.negative_edge.x,
|
||||
y: geometry.negative_edge.y.wrapping_sub(43_000),
|
||||
},
|
||||
MilliVec {
|
||||
x: geometry.negative_edge.x,
|
||||
y: geometry.negative_edge.y.wrapping_add(12_000),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
MilliVec {
|
||||
x: geometry.positive_edge.x,
|
||||
y: geometry.positive_edge.y.wrapping_sub(12_000),
|
||||
},
|
||||
MilliVec {
|
||||
x: geometry.positive_edge.x,
|
||||
y: geometry.positive_edge.y.wrapping_add(43_000),
|
||||
},
|
||||
)
|
||||
};
|
||||
if ball.x.wrapping_sub(geometry.pivot.x).wrapping_abs() >= SEARCH_RADIUS
|
||||
|| ball.y.wrapping_sub(geometry.pivot.y).wrapping_abs() >= SEARCH_RADIUS
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let distance = collision_distance(ball, geometry.pivot);
|
||||
if distance > SEARCH_RADIUS {
|
||||
return None;
|
||||
}
|
||||
let first = cross_for_edge(first_edge, geometry.pivot, ball);
|
||||
let second = cross_for_edge(second_edge, geometry.pivot, ball);
|
||||
let inside = match side {
|
||||
FlipperSide::Left => first > -1_000 && second < 1_000 && ball.x > geometry.pivot.x,
|
||||
FlipperSide::Right => first < 1_000 && second > -1_000 && ball.x < geometry.pivot.x,
|
||||
};
|
||||
inside.then_some(distance)
|
||||
}
|
||||
|
||||
fn penetration(geometry: Geometry, delta: i32, ball: MilliVec) -> i32 {
|
||||
let collision_y = if delta == -1 {
|
||||
let edge_dx = geometry
|
||||
.negative_edge
|
||||
.x
|
||||
.wrapping_sub(geometry.pivot.x)
|
||||
.wrapping_abs();
|
||||
if edge_dx > 0 {
|
||||
geometry
|
||||
.pivot
|
||||
.y
|
||||
.wrapping_sub(ball.x.wrapping_sub(geometry.pivot.x).wrapping_abs())
|
||||
.wrapping_add(5_000)
|
||||
} else {
|
||||
geometry.negative_edge.y.wrapping_sub(43_000)
|
||||
}
|
||||
} else {
|
||||
let edge_dx = geometry.positive_edge.x.wrapping_sub(geometry.pivot.x);
|
||||
if edge_dx.wrapping_abs() > 0 {
|
||||
let numerator = ball.x.wrapping_sub(geometry.pivot.x).wrapping_mul(
|
||||
geometry
|
||||
.positive_edge
|
||||
.y
|
||||
.wrapping_add(43_000)
|
||||
.wrapping_sub(geometry.pivot.y),
|
||||
);
|
||||
geometry
|
||||
.pivot
|
||||
.y
|
||||
.wrapping_sub(rounded(f64::from(numerator) / f64::from(edge_dx)))
|
||||
} else {
|
||||
geometry.positive_edge.y.wrapping_add(43_000)
|
||||
}
|
||||
};
|
||||
let value = collision_y.wrapping_sub(ball.y);
|
||||
if value < 0 {
|
||||
value.wrapping_mul(delta).wrapping_neg()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn moving_flipper_response(
|
||||
ball: MilliVec,
|
||||
velocity: MilliVec,
|
||||
delta: i32,
|
||||
side: FlipperSide,
|
||||
response_normal: f64,
|
||||
maximum_speed: i32,
|
||||
) -> Option<FlipperResponse> {
|
||||
let geometry = Geometry::for_side(side, delta);
|
||||
response_with_geometry(
|
||||
ball,
|
||||
velocity,
|
||||
delta,
|
||||
side,
|
||||
response_normal,
|
||||
maximum_speed,
|
||||
geometry,
|
||||
)
|
||||
}
|
||||
|
||||
fn response_with_geometry(
|
||||
ball: MilliVec,
|
||||
velocity: MilliVec,
|
||||
delta: i32,
|
||||
side: FlipperSide,
|
||||
response_normal: f64,
|
||||
maximum_speed: i32,
|
||||
geometry: Geometry,
|
||||
) -> Option<FlipperResponse> {
|
||||
let distance = contains(geometry, side, delta, ball)?;
|
||||
let penetration = penetration(geometry, delta, ball);
|
||||
let mut normal_x = ball.x.wrapping_sub(geometry.pivot.x).wrapping_mul(delta);
|
||||
let mut normal_y = ball
|
||||
.y
|
||||
.wrapping_sub(geometry.pivot.y)
|
||||
.wrapping_add(4_000)
|
||||
.wrapping_mul(delta);
|
||||
if side == FlipperSide::Right {
|
||||
normal_x = normal_x.wrapping_neg();
|
||||
normal_y = normal_y.wrapping_neg();
|
||||
}
|
||||
|
||||
let tangent_projection = rounded(
|
||||
(f64::from(velocity.x) * f64::from(normal_y)
|
||||
- f64::from(velocity.y) * f64::from(normal_x))
|
||||
/ RESPONSE_RADIUS,
|
||||
);
|
||||
let gain = (f64::from(distance) / RESPONSE_RADIUS).sqrt() * 2.0 + 0.4;
|
||||
let tangent_projection = rounded(f64::from(tangent_projection) * (1.0 + response_normal))
|
||||
.wrapping_add(rounded(f64::from(maximum_speed) * gain));
|
||||
let delta_velocity_x = rounded(
|
||||
-f64::from(tangent_projection) * f64::from(normal_y) / RESPONSE_RADIUS,
|
||||
);
|
||||
let delta_velocity_y = rounded(
|
||||
f64::from(tangent_projection) * f64::from(normal_x) / RESPONSE_RADIUS,
|
||||
);
|
||||
let velocity = MilliVec {
|
||||
x: velocity.x.wrapping_add(delta_velocity_x),
|
||||
y: velocity.y.wrapping_add(delta_velocity_y),
|
||||
};
|
||||
if velocity.y == 0 {
|
||||
return None;
|
||||
}
|
||||
let movement_x = rounded(
|
||||
f64::from(penetration.wrapping_mul(velocity.x)) / f64::from(velocity.y),
|
||||
);
|
||||
Some(FlipperResponse {
|
||||
velocity,
|
||||
movement: MilliVec {
|
||||
x: movement_x,
|
||||
y: penetration,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn four_direction_vectors_match_the_reconstructed_c_harness() {
|
||||
let cases = [
|
||||
(
|
||||
MilliVec { x: 104_000, y: 384_000 },
|
||||
MilliVec { x: 1_000, y: 2_000 },
|
||||
-1,
|
||||
FlipperSide::Left,
|
||||
0.5,
|
||||
MilliVec { x: 1_266, y: 1_511 },
|
||||
MilliVec { x: -6_703, y: -8_000 },
|
||||
),
|
||||
(
|
||||
MilliVec { x: 209_000, y: 419_000 },
|
||||
MilliVec { x: -1_000, y: 2_000 },
|
||||
1,
|
||||
FlipperSide::Right,
|
||||
0.75,
|
||||
MilliVec { x: -737, y: 2_263 },
|
||||
MilliVec { x: -8_549, y: 26_250 },
|
||||
),
|
||||
(
|
||||
MilliVec { x: 104_000, y: 421_000 },
|
||||
MilliVec { x: 1_000, y: 2_000 },
|
||||
1,
|
||||
FlipperSide::Left,
|
||||
0.25,
|
||||
MilliVec { x: 622, y: 2_320 },
|
||||
MilliVec { x: 5_414, y: 20_193 },
|
||||
),
|
||||
(
|
||||
MilliVec { x: 209_000, y: 385_000 },
|
||||
MilliVec { x: -1_000, y: 2_000 },
|
||||
-1,
|
||||
FlipperSide::Right,
|
||||
0.0,
|
||||
MilliVec { x: -1_280, y: 1_560 },
|
||||
MilliVec { x: 7_385, y: -9_000 },
|
||||
),
|
||||
];
|
||||
for (ball, velocity, delta, side, response, expected_velocity, expected_movement) in cases {
|
||||
let result = response_with_geometry(
|
||||
ball,
|
||||
velocity,
|
||||
delta,
|
||||
side,
|
||||
response,
|
||||
1_000,
|
||||
Geometry::isolated_c_fixture(side, delta),
|
||||
)
|
||||
.expect("C fixture must contact the moving flipper");
|
||||
assert_eq!(result.velocity, expected_velocity);
|
||||
assert_eq!(result.movement, expected_movement);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-78
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
borland_random::BorlandRandom,
|
||||
geometry::{Segment, closest_point},
|
||||
flipper_physics::{FlipperSide, moving_flipper_response},
|
||||
geometry::Segment,
|
||||
original_physics::{
|
||||
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
|
||||
STEP_SECONDS, circle_collision_response, line_collision_response, path_intersects_circle,
|
||||
@@ -12,12 +13,7 @@ use crate::{
|
||||
};
|
||||
use macroquad::prelude::{Rect, Vec2, vec2};
|
||||
|
||||
const FLIPPER_CONTACT_RADIUS: f32 = 9.0;
|
||||
const LEFT_FLIPPER_PIVOT: Vec2 = Vec2::new(103.0, 397.0);
|
||||
const LEFT_FLIPPER_REST_TIP: Vec2 = Vec2::new(134.0, 419.0);
|
||||
const LEFT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(133.0, 377.0);
|
||||
const RIGHT_FLIPPER_PIVOT: Vec2 = Vec2::new(210.0, 397.0);
|
||||
const RIGHT_FLIPPER_REST_TIP: Vec2 = Vec2::new(179.0, 419.0);
|
||||
const RIGHT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(181.0, 376.0);
|
||||
const LAUNCHER_POSITION: Vec2 = Vec2::new(325.0, 413.0);
|
||||
const LAUNCHER_PRESS_IMPULSE_MILLI: i32 = 750;
|
||||
@@ -399,12 +395,12 @@ impl Game {
|
||||
if self.flippers.left_raised != old_flippers.left_raised {
|
||||
events.push(Event::FlipperMove);
|
||||
events.push(Event::Sound(2021));
|
||||
self.pending_flipper_edges[0] = if self.flippers.left_raised { 1 } else { -1 };
|
||||
self.pending_flipper_edges[0] = if self.flippers.left_raised { -1 } else { 1 };
|
||||
}
|
||||
if self.flippers.right_raised != old_flippers.right_raised {
|
||||
events.push(Event::FlipperMove);
|
||||
events.push(Event::Sound(2021));
|
||||
self.pending_flipper_edges[1] = if self.flippers.right_raised { 1 } else { -1 };
|
||||
self.pending_flipper_edges[1] = if self.flippers.right_raised { -1 } else { 1 };
|
||||
}
|
||||
|
||||
if self.ball.in_launcher && !self.tilted {
|
||||
@@ -1092,47 +1088,17 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
|
||||
for (edge, pivot, rest_tip, raised_tip, horizontal_sign) in [
|
||||
(
|
||||
pending[0],
|
||||
LEFT_FLIPPER_PIVOT,
|
||||
LEFT_FLIPPER_REST_TIP,
|
||||
LEFT_FLIPPER_RAISED_TIP,
|
||||
1.0,
|
||||
),
|
||||
(
|
||||
pending[1],
|
||||
RIGHT_FLIPPER_PIVOT,
|
||||
RIGHT_FLIPPER_REST_TIP,
|
||||
RIGHT_FLIPPER_RAISED_TIP,
|
||||
-1.0,
|
||||
),
|
||||
for (delta, side) in [
|
||||
(i32::from(pending[0]), FlipperSide::Left),
|
||||
(i32::from(pending[1]), FlipperSide::Right),
|
||||
] {
|
||||
if edge == 0 || !point_in_flipper_sweep(self.ball.position, pivot, rest_tip, raised_tip)
|
||||
{
|
||||
if delta == 0 {
|
||||
continue;
|
||||
}
|
||||
let local_x = (self.ball.position.x - pivot.x) * horizontal_sign;
|
||||
let local_y = self.ball.position.y - pivot.y;
|
||||
if edge < 0 {
|
||||
let velocity_milli = MilliVec::from_millipixels(vec2(
|
||||
horizontal_sign * local_x * 159.95,
|
||||
local_x * 286.45,
|
||||
));
|
||||
self.ball.velocity = velocity_milli.to_velocity_per_second();
|
||||
continue;
|
||||
apply_flipper_response_to_ball(&mut self.ball, delta, side);
|
||||
if let Some(secondary) = &mut self.secondary_ball {
|
||||
apply_flipper_response_to_ball(secondary, delta, side);
|
||||
}
|
||||
let horizontal_displacement =
|
||||
0.002_12 * local_x.powi(2) - 0.125_68 * local_x + 14.921 - (local_y + 7.0);
|
||||
let vertical_displacement = -(local_x + 13.0);
|
||||
let velocity_scale = -0.026 * local_x.powi(2) + 3.614 * local_x + 101.327;
|
||||
let displacement = vec2(
|
||||
horizontal_displacement * horizontal_sign,
|
||||
vertical_displacement,
|
||||
);
|
||||
self.ball.position += displacement;
|
||||
let velocity_milli = MilliVec::from_millipixels(displacement * velocity_scale);
|
||||
self.ball.velocity = velocity_milli.to_velocity_per_second();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1312,26 +1278,15 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
||||
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
||||
}
|
||||
|
||||
fn point_in_flipper_sweep(point: Vec2, pivot: Vec2, rest_tip: Vec2, raised_tip: Vec2) -> bool {
|
||||
let boundary_distance_squared = [
|
||||
Segment::new(pivot, rest_tip, 0.0),
|
||||
Segment::new(pivot, raised_tip, 0.0),
|
||||
Segment::new(rest_tip, raised_tip, 0.0),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|segment| point.distance_squared(closest_point(point, segment)))
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
if boundary_distance_squared <= FLIPPER_CONTACT_RADIUS.powi(2) {
|
||||
return true;
|
||||
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
|
||||
let position = MilliVec::from_position(ball.position);
|
||||
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
if let Some(response) =
|
||||
moving_flipper_response(position, velocity, delta, side, 0.5, MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
{
|
||||
ball.position = position.add(response.movement).to_position();
|
||||
ball.velocity = response.velocity.to_velocity_per_second();
|
||||
}
|
||||
|
||||
let cross = |a: Vec2, b: Vec2, p: Vec2| (b - a).perp_dot(p - a);
|
||||
let signs = [
|
||||
cross(pivot, rest_tip, point),
|
||||
cross(rest_tip, raised_tip, point),
|
||||
cross(raised_tip, pivot, point),
|
||||
];
|
||||
signs.iter().all(|value| *value >= 0.0) || signs.iter().all(|value| *value <= 0.0)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -1801,7 +1756,7 @@ mod tests {
|
||||
(vec2(137.0, 384.0), vec2(116.0, 405.0))
|
||||
);
|
||||
assert_eq!(
|
||||
game.live_circle_center(67, LEFT_FLIPPER_REST_TIP),
|
||||
game.live_circle_center(67, vec2(134.0, 419.0)),
|
||||
vec2(133.0, 377.0)
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1813,7 +1768,7 @@ mod tests {
|
||||
(vec2(183.0, 368.0), vec2(217.0, 383.0))
|
||||
);
|
||||
assert_eq!(
|
||||
game.live_circle_center(82, RIGHT_FLIPPER_REST_TIP),
|
||||
game.live_circle_center(82, vec2(179.0, 419.0)),
|
||||
vec2(181.0, 376.0)
|
||||
);
|
||||
}
|
||||
@@ -1856,19 +1811,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raising_flipper_matches_the_live_swept_transfer_probe() {
|
||||
fn raising_flipper_uses_the_reconstructed_moving_response() {
|
||||
let mut game = Game::new(1);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(125.0, 390.0);
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.pending_flipper_edges[0] = 1;
|
||||
game.pending_flipper_edges[0] = -1;
|
||||
|
||||
game.apply_flipper_kicks();
|
||||
let velocity_after_edge = game.ball.velocity;
|
||||
assert!(game.ball.position.distance(vec2(138.182, 355.0)) < 0.002);
|
||||
assert_eq!(game.ball.position, vec2(138.124, 355.0));
|
||||
let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge);
|
||||
assert!((raw_velocity.x - 2_217).abs() <= 2);
|
||||
assert!((raw_velocity.y - -5_889).abs() <= 2);
|
||||
assert_eq!(raw_velocity, MilliVec { x: 2_209, y: -5_891 });
|
||||
assert_eq!(game.pending_flipper_edges, [0, 0]);
|
||||
|
||||
game.apply_flipper_kicks();
|
||||
@@ -1879,22 +1833,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_flipper_matches_the_live_downstroke_probe() {
|
||||
fn returning_flipper_uses_the_raised_record_geometry() {
|
||||
let mut game = Game::new(1);
|
||||
game.flippers.left_raised = true;
|
||||
|
||||
let events = game.update(0.0, 3, Controls::default());
|
||||
|
||||
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
|
||||
assert_eq!(game.pending_flipper_edges, [-1, 0]);
|
||||
assert_eq!(game.pending_flipper_edges, [1, 0]);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(125.0, 390.0);
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.ball.position = vec2(110.0, 410.0);
|
||||
game.ball.velocity = vec2(100.0, 200.0);
|
||||
game.apply_flipper_kicks();
|
||||
let raw_velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
||||
assert!((raw_velocity.x - 3_519).abs() <= 1);
|
||||
assert!((raw_velocity.y - 6_302).abs() <= 1);
|
||||
assert_eq!(game.ball.position, vec2(125.0, 390.0));
|
||||
assert_eq!(raw_velocity, MilliVec { x: 765, y: 3_997 });
|
||||
assert_eq!(game.ball.position, vec2(110.380, 411.984));
|
||||
assert_eq!(game.pending_flipper_edges, [0, 0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ impl Segment {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn closest_point(point: Vec2, segment: Segment) -> Vec2 {
|
||||
let line = segment.end - segment.start;
|
||||
let length_squared = line.length_squared();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod assets;
|
||||
mod borland_random;
|
||||
mod flipper_physics;
|
||||
mod game;
|
||||
mod geometry;
|
||||
mod original_physics;
|
||||
|
||||
@@ -35,19 +35,18 @@ impl MilliVec {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_millipixels(value: Vec2) -> Self {
|
||||
Self {
|
||||
x: value.x.round() as i32,
|
||||
y: value.y.round() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_position(self) -> Vec2 {
|
||||
vec2(self.x as f32 / 1_000.0, self.y as f32 / 1_000.0)
|
||||
vec2(
|
||||
scaled_f32_with_exact_roundtrip(self.x, 1_000.0),
|
||||
scaled_f32_with_exact_roundtrip(self.y, 1_000.0),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_velocity_per_second(self) -> Vec2 {
|
||||
vec2(self.x as f32 / 10.0, self.y as f32 / 10.0)
|
||||
vec2(
|
||||
scaled_f32_with_exact_roundtrip(self.x, 10.0),
|
||||
scaled_f32_with_exact_roundtrip(self.y, 10.0),
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn add(self, other: Self) -> Self {
|
||||
@@ -68,6 +67,23 @@ impl MilliVec {
|
||||
}
|
||||
}
|
||||
|
||||
fn scaled_f32_with_exact_roundtrip(value: i32, scale: f32) -> f32 {
|
||||
let mut projected = value as f32 / scale;
|
||||
for _ in 0..4 {
|
||||
let recovered = (projected * scale).round() as i32;
|
||||
if recovered == value {
|
||||
return projected;
|
||||
}
|
||||
projected = if recovered < value {
|
||||
projected.next_up()
|
||||
} else {
|
||||
projected.next_down()
|
||||
};
|
||||
}
|
||||
debug_assert_eq!((projected * scale).round() as i32, value);
|
||||
projected
|
||||
}
|
||||
|
||||
const fn cross(left: MilliVec, right: MilliVec) -> i64 {
|
||||
left.x as i64 * right.y as i64 - left.y as i64 * right.x as i64
|
||||
}
|
||||
@@ -308,6 +324,14 @@ pub fn path_intersects_circle(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn float_views_roundtrip_every_gameplay_velocity_millipixel() {
|
||||
for value in -10_000..=10_000 {
|
||||
let milli = MilliVec { x: value, y: -value };
|
||||
assert_eq!(MilliVec::from_velocity_per_second(milli.to_velocity_per_second()), milli);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_shooter_wall_matches_the_live_original_probe() {
|
||||
let old = MilliVec {
|
||||
|
||||
Reference in New Issue
Block a user