fix(flippers): match original moving-hit geometry
The clone's moving-flipper gate used the cross-product operands in the opposite order, mirroring and narrowing the hit wedge. Right-flipper release also used record 81's first endpoint even though the original reads its second endpoint. Correct both geometry paths, retain the recovered swept tip bounds, and add live-binary boundary vectors plus a dense transition regression. Test Plan: - `cargo test --workspace --all-targets --all-features` -- passed (134 tests) - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `cargo build --profile production` -- passed - `LSAN_OPTIONS=detect_leaks=0 ASAN_OPTIONS=detect_leaks=0 bash original/tools/test_reconstructed_c.sh` -- passed - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -158,10 +158,10 @@ static FlipperCollisionGeometry load_flipper_geometry(
|
|||||||
geometry.unused_positive_point_x = record_i32(
|
geometry.unused_positive_point_x = record_i32(
|
||||||
81, offsetof(TdkpinCollisionRecord, point1_x_milli));
|
81, offsetof(TdkpinCollisionRecord, point1_x_milli));
|
||||||
geometry.positive_edge_x = subtract_wrap_i32(
|
geometry.positive_edge_x = subtract_wrap_i32(
|
||||||
record_i32(81, offsetof(TdkpinCollisionRecord, point1_x_milli)),
|
record_i32(81, offsetof(TdkpinCollisionRecord, point2_x_milli)),
|
||||||
20000);
|
20000);
|
||||||
geometry.positive_edge_y = record_i32(
|
geometry.positive_edge_y = record_i32(
|
||||||
81, offsetof(TdkpinCollisionRecord, point1_y_milli));
|
81, offsetof(TdkpinCollisionRecord, point2_y_milli));
|
||||||
geometry.response_record = delta == 1 ? 81 : 83;
|
geometry.response_record = delta == 1 ? 81 : 83;
|
||||||
} else {
|
} else {
|
||||||
geometry = (FlipperCollisionGeometry){
|
geometry = (FlipperCollisionGeometry){
|
||||||
@@ -202,8 +202,8 @@ static int32_t cross_product_for_edge(
|
|||||||
int32_t edge_from_pivot_x = divide_by_thousand(
|
int32_t edge_from_pivot_x = divide_by_thousand(
|
||||||
subtract_wrap_i32(edge_x, pivot_x));
|
subtract_wrap_i32(edge_x, pivot_x));
|
||||||
return subtract_wrap_i32(
|
return subtract_wrap_i32(
|
||||||
borland_multiply_i32(edge_from_ball_y, edge_from_pivot_x),
|
borland_multiply_i32(edge_from_ball_x, edge_from_pivot_y),
|
||||||
borland_multiply_i32(edge_from_ball_x, edge_from_pivot_y));
|
borland_multiply_i32(edge_from_ball_y, edge_from_pivot_x));
|
||||||
}
|
}
|
||||||
|
|
||||||
static int32_t radial_distance_milli(
|
static int32_t radial_distance_milli(
|
||||||
|
|||||||
@@ -185,11 +185,25 @@ static void test_right_positive_contact(void)
|
|||||||
win16_far_add_offset(g_receiver, 0x0baa)) == -737);
|
win16_far_add_offset(g_receiver, 0x0baa)) == -737);
|
||||||
assert((int32_t)win16_read_u32(
|
assert((int32_t)win16_read_u32(
|
||||||
win16_far_add_offset(g_receiver, 0x0bae)) == 2263);
|
win16_far_add_offset(g_receiver, 0x0bae)) == 2263);
|
||||||
assert(g_move_x == -8549 && g_move_y == 26250);
|
assert(g_move_x == -5578 && g_move_y == 17127);
|
||||||
assert(g_sound_count == 1 && g_sounds[0] == 21);
|
assert(g_sound_count == 1 && g_sounds[0] == 21);
|
||||||
assert(g_move_calls == 1);
|
assert(g_move_calls == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_live_binary_cross_product_order(void)
|
||||||
|
{
|
||||||
|
prepare_fixture();
|
||||||
|
win16_write_u32(g_receiver, 0x56, 3800);
|
||||||
|
set_ball(100000, 370000, 1000, 2000);
|
||||||
|
tdkpin_move_flipper_collision_geometry(0x0300, -1, 1, 0);
|
||||||
|
assert((int32_t)win16_read_u32(
|
||||||
|
win16_far_add_offset(g_receiver, 0x0baa)) == -189);
|
||||||
|
assert((int32_t)win16_read_u32(
|
||||||
|
win16_far_add_offset(g_receiver, 0x0bae)) == 960);
|
||||||
|
assert(g_move_x == 0 && g_move_y == 0);
|
||||||
|
assert(g_move_calls == 1);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_left_positive_contact(void)
|
static void test_left_positive_contact(void)
|
||||||
{
|
{
|
||||||
prepare_fixture();
|
prepare_fixture();
|
||||||
@@ -230,8 +244,12 @@ static void test_vertical_edge_fallbacks(void)
|
|||||||
prepare_fixture();
|
prepare_fixture();
|
||||||
write_i32(
|
write_i32(
|
||||||
81,
|
81,
|
||||||
offsetof(TdkpinCollisionRecord, point1_x_milli),
|
offsetof(TdkpinCollisionRecord, point2_x_milli),
|
||||||
240000);
|
240000);
|
||||||
|
write_i32(
|
||||||
|
81,
|
||||||
|
offsetof(TdkpinCollisionRecord, point2_y_milli),
|
||||||
|
411000);
|
||||||
set_ball(197000, 455000, -1000, 2000);
|
set_ball(197000, 455000, -1000, 2000);
|
||||||
tdkpin_move_flipper_collision_geometry(0x0300, 1, 2, 0);
|
tdkpin_move_flipper_collision_geometry(0x0300, 1, 2, 0);
|
||||||
assert(g_move_calls == 1 && g_move_y == 1000);
|
assert(g_move_calls == 1 && g_move_y == 1000);
|
||||||
@@ -270,6 +288,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
test_left_negative_contact();
|
test_left_negative_contact();
|
||||||
test_right_positive_contact();
|
test_right_positive_contact();
|
||||||
|
test_live_binary_cross_product_order();
|
||||||
test_left_positive_contact();
|
test_left_positive_contact();
|
||||||
test_right_negative_contact();
|
test_right_negative_contact();
|
||||||
test_vertical_edge_fallbacks();
|
test_vertical_edge_fallbacks();
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ impl Geometry {
|
|||||||
x: 152_000,
|
x: 152_000,
|
||||||
y: 413_000,
|
y: 413_000,
|
||||||
},
|
},
|
||||||
|
// The binary reads record 81 point 1 but uses point 2 for this
|
||||||
|
// edge. On release that point is already in its raised state.
|
||||||
positive_edge: MilliVec {
|
positive_edge: MilliVec {
|
||||||
x: if delta == 1 { 177_000 } else { 196_000 },
|
x: if delta == 1 { 151_000 } else { 157_000 },
|
||||||
y: if delta == 1 { 405_000 } else { 411_000 },
|
y: if delta == 1 { 378_000 } else { 427_000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -71,8 +73,8 @@ impl Geometry {
|
|||||||
y: 427_000,
|
y: 427_000,
|
||||||
},
|
},
|
||||||
FlipperSide::Right => MilliVec {
|
FlipperSide::Right => MilliVec {
|
||||||
x: 196_000,
|
x: 157_000,
|
||||||
y: 411_000,
|
y: 427_000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
geometry
|
geometry
|
||||||
@@ -84,9 +86,11 @@ fn cross_for_edge(edge: MilliVec, pivot: MilliVec, ball: MilliVec) -> i32 {
|
|||||||
let edge_from_pivot_y = edge.y.wrapping_sub(pivot.y) / 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_ball_y = edge.y.wrapping_sub(ball.y) / 1_000;
|
||||||
let edge_from_pivot_x = edge.x.wrapping_sub(pivot.x) / 1_000;
|
let edge_from_pivot_x = edge.x.wrapping_sub(pivot.x) / 1_000;
|
||||||
edge_from_ball_y
|
// 1000:8208 computes C*D first, then computes A*B and subtracts C*D.
|
||||||
.wrapping_mul(edge_from_pivot_x)
|
// Reversing these operands mirrors the complete moving-hit wedge.
|
||||||
.wrapping_sub(edge_from_ball_x.wrapping_mul(edge_from_pivot_y))
|
edge_from_ball_x
|
||||||
|
.wrapping_mul(edge_from_pivot_y)
|
||||||
|
.wrapping_sub(edge_from_ball_y.wrapping_mul(edge_from_pivot_x))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collision_distance(ball: MilliVec, pivot: MilliVec) -> i32 {
|
fn collision_distance(ball: MilliVec, pivot: MilliVec) -> i32 {
|
||||||
@@ -279,6 +283,61 @@ fn response_with_geometry(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn hash_u32(mut hash: u64, value: u32) -> u64 {
|
||||||
|
for byte in value.to_le_bytes() {
|
||||||
|
hash ^= u64::from(byte);
|
||||||
|
hash = hash.wrapping_mul(1_099_511_628_211);
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn i32_bits(value: i32) -> u32 {
|
||||||
|
u32::from_ne_bytes(value.to_ne_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dense_transition_digest(side: FlipperSide, delta: i32) -> (u32, u64) {
|
||||||
|
let velocities = [
|
||||||
|
MilliVec { x: 0, y: 2_000 },
|
||||||
|
MilliVec { x: 1_000, y: 2_000 },
|
||||||
|
MilliVec { x: -1_000, y: 2_000 },
|
||||||
|
MilliVec { x: 2_700, y: -2_700 },
|
||||||
|
];
|
||||||
|
let mut hash = 14_695_981_039_346_656_037_u64;
|
||||||
|
let mut hits = 0_u32;
|
||||||
|
for x in (40_000..=280_000).step_by(1_000) {
|
||||||
|
for y in (320_000..=470_000).step_by(1_000) {
|
||||||
|
for (velocity_index, input_velocity) in velocities.into_iter().enumerate() {
|
||||||
|
let response = moving_flipper_response(
|
||||||
|
MilliVec { x, y },
|
||||||
|
input_velocity,
|
||||||
|
delta,
|
||||||
|
side,
|
||||||
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
||||||
|
3_800,
|
||||||
|
);
|
||||||
|
let (hit, velocity, movement) = response.map_or(
|
||||||
|
(0_u32, input_velocity, MilliVec::default()),
|
||||||
|
|response| (1, response.velocity, response.movement),
|
||||||
|
);
|
||||||
|
hits += hit;
|
||||||
|
for value in [
|
||||||
|
i32_bits(x),
|
||||||
|
i32_bits(y),
|
||||||
|
u32::try_from(velocity_index).expect("four velocities fit u32"),
|
||||||
|
hit,
|
||||||
|
i32_bits(velocity.x),
|
||||||
|
i32_bits(velocity.y),
|
||||||
|
i32_bits(movement.x),
|
||||||
|
i32_bits(movement.y),
|
||||||
|
] {
|
||||||
|
hash = hash_u32(hash, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(hits, hash)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn four_direction_vectors_match_the_reconstructed_c_harness() {
|
fn four_direction_vectors_match_the_reconstructed_c_harness() {
|
||||||
let cases = [
|
let cases = [
|
||||||
@@ -298,7 +357,7 @@ mod tests {
|
|||||||
FlipperSide::Right,
|
FlipperSide::Right,
|
||||||
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
|
||||||
MilliVec { x: -737, y: 2_263 },
|
MilliVec { x: -737, y: 2_263 },
|
||||||
MilliVec { x: -8_549, y: 26_250 },
|
MilliVec { x: -5_578, y: 17_127 },
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
MilliVec { x: 104_000, y: 421_000 },
|
MilliVec { x: 104_000, y: 421_000 },
|
||||||
@@ -334,4 +393,54 @@ mod tests {
|
|||||||
assert_eq!(result.movement, expected_movement);
|
assert_eq!(result.movement, expected_movement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boundary_vectors_match_the_live_original_binary() {
|
||||||
|
for (ball, velocity, delta, side, expected_velocity, expected_movement) in [
|
||||||
|
(
|
||||||
|
MilliVec { x: 100_000, y: 370_000 },
|
||||||
|
MilliVec { x: 1_000, y: 2_000 },
|
||||||
|
-1,
|
||||||
|
FlipperSide::Left,
|
||||||
|
MilliVec { x: -189, y: 960 },
|
||||||
|
MilliVec::default(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MilliVec { x: 209_000, y: 419_000 },
|
||||||
|
MilliVec { x: 1_000, y: 2_000 },
|
||||||
|
1,
|
||||||
|
FlipperSide::Right,
|
||||||
|
MilliVec { x: 2_133, y: 3_133 },
|
||||||
|
MilliVec { x: 5_743, y: 8_435 },
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let response = moving_flipper_response(
|
||||||
|
ball,
|
||||||
|
velocity,
|
||||||
|
delta,
|
||||||
|
side,
|
||||||
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
||||||
|
3_800,
|
||||||
|
)
|
||||||
|
.expect("the live original hits this flipper boundary");
|
||||||
|
assert_eq!(response.velocity, expected_velocity);
|
||||||
|
assert_eq!(response.movement, expected_movement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dense_transition_matrix_matches_the_reconstructed_c_oracle() {
|
||||||
|
for (side, delta, expected_hits, expected_hash) in [
|
||||||
|
(FlipperSide::Left, -1, 9_704, 0x280f_c478_572e_ccc5),
|
||||||
|
(FlipperSide::Left, 1, 10_256, 0x65f5_a7da_3013_c0f5),
|
||||||
|
(FlipperSide::Right, -1, 9_704, 0x875b_7dd6_1574_7fac),
|
||||||
|
(FlipperSide::Right, 1, 9_512, 0x4713_d619_16d8_0fe1),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
dense_transition_digest(side, delta),
|
||||||
|
(expected_hits, expected_hash),
|
||||||
|
"moving-flipper matrix mismatch for {side:?} delta {delta}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+329
-38
@@ -277,6 +277,13 @@ fn trigger_broadphase_contains(predicted: MilliVec, center: Vec2, radius: f32) -
|
|||||||
&& predicted.y <= center.y.wrapping_add(margin)
|
&& predicted.y <= center.y.wrapping_add(margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn movement_from_prediction(old_position: MilliVec, predicted: MilliVec) -> MilliVec {
|
||||||
|
MilliVec {
|
||||||
|
x: predicted.x.wrapping_sub(old_position.x),
|
||||||
|
y: predicted.y.wrapping_sub(old_position.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct RuleState {
|
struct RuleState {
|
||||||
wheel_holes: [bool; 5],
|
wheel_holes: [bool; 5],
|
||||||
@@ -777,6 +784,7 @@ impl Game {
|
|||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
|
let speed_before_collision = milli_distance(velocity);
|
||||||
if predicted_outside_board(old_position.add(velocity)) {
|
if predicted_outside_board(old_position.add(velocity)) {
|
||||||
self.drain(events);
|
self.drain(events);
|
||||||
return true;
|
return true;
|
||||||
@@ -831,7 +839,6 @@ impl Game {
|
|||||||
velocity = response.velocity;
|
velocity = response.velocity;
|
||||||
ball.spin = response.spin;
|
ball.spin = response.spin;
|
||||||
auxiliary_fired = response.auxiliary_fired;
|
auxiliary_fired = response.auxiliary_fired;
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
self.last_collision_id = Some(object_id);
|
self.last_collision_id = Some(object_id);
|
||||||
if object_id == 175
|
if object_id == 175
|
||||||
&& let (Some(secondary), Some(transferred)) =
|
&& let (Some(secondary), Some(transferred)) =
|
||||||
@@ -847,6 +854,13 @@ impl Game {
|
|||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
if hit_wall == Some(25) && speed_before_collision >= 1_000 {
|
||||||
|
velocity.x = 0;
|
||||||
|
}
|
||||||
|
if hit_wall == Some(25) && speed_before_collision < 1_000 {
|
||||||
|
velocity.x = 0;
|
||||||
|
velocity.y = 0;
|
||||||
|
}
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
ball.position = old_position.add(velocity).to_position();
|
ball.position = old_position.add(velocity).to_position();
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
@@ -865,9 +879,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if wall_id == 25
|
if wall_id == 25 && speed_before_collision < 1_000 {
|
||||||
&& i64::from(velocity.x).pow(2) + i64::from(velocity.y).pow(2) < 1_000_i64.pow(2)
|
|
||||||
{
|
|
||||||
self.ball = Ball::default();
|
self.ball = Ball::default();
|
||||||
self.launcher_charge = 0.0;
|
self.launcher_charge = 0.0;
|
||||||
self.launcher_was_down = false;
|
self.launcher_was_down = false;
|
||||||
@@ -994,30 +1006,20 @@ impl Game {
|
|||||||
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
||||||
if old_position.y < 250_000 {
|
if old_position.y < 250_000 {
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
let predicted_velocity = MilliVec {
|
if self.type4_broadphase_contains(predicted, 140..=147) {
|
||||||
x: predicted.x.wrapping_sub(old_position.x),
|
|
||||||
y: predicted.y.wrapping_sub(old_position.y),
|
|
||||||
};
|
|
||||||
if !self.tilted
|
|
||||||
&& (TARGET_SENSORS.into_iter().any(|sensor| {
|
|
||||||
self.object_active[usize::from(sensor.id)]
|
|
||||||
&& trigger_broadphase_contains(predicted, sensor.center, sensor.radius)
|
|
||||||
}) || self.object_active[usize::from(EFFECT_SENSOR.id)]
|
|
||||||
&& trigger_broadphase_contains(
|
|
||||||
predicted,
|
|
||||||
EFFECT_SENSOR.center,
|
|
||||||
EFFECT_SENSOR.radius,
|
|
||||||
))
|
|
||||||
{
|
|
||||||
self.special_hole_gate = SpecialHoleGate::Enabled;
|
self.special_hole_gate = SpecialHoleGate::Enabled;
|
||||||
}
|
}
|
||||||
self.check_target_sensors(
|
let target_triggered = self.check_target_sensors(
|
||||||
ball,
|
ball,
|
||||||
old_position,
|
old_position,
|
||||||
predicted_velocity,
|
movement_from_prediction(old_position, predicted),
|
||||||
140..=147,
|
140..=147,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
if target_triggered {
|
||||||
|
predicted = old_position.add(*velocity);
|
||||||
|
}
|
||||||
if let Some(completed_action) = self.check_special_hole(
|
if let Some(completed_action) = self.check_special_hole(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
ball_number,
|
||||||
@@ -1029,21 +1031,29 @@ impl Game {
|
|||||||
) {
|
) {
|
||||||
action = completed_action;
|
action = completed_action;
|
||||||
}
|
}
|
||||||
self.check_effect_sensor(
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
let effect_triggered = self.check_effect_sensor(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
ball_number,
|
||||||
old_position,
|
old_position,
|
||||||
predicted_velocity,
|
movement_from_prediction(old_position, predicted),
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
self.check_target_sensors(
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
if effect_triggered {
|
||||||
|
predicted = old_position.add(*velocity);
|
||||||
|
}
|
||||||
|
let target_triggered = self.check_target_sensors(
|
||||||
ball,
|
ball,
|
||||||
old_position,
|
old_position,
|
||||||
predicted_velocity,
|
movement_from_prediction(old_position, predicted),
|
||||||
150..=152,
|
150..=152,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
if target_triggered {
|
||||||
|
predicted = old_position.add(*velocity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.apply_magnetic_record(153, old_position, velocity) {
|
if self.apply_magnetic_record(153, old_position, velocity) {
|
||||||
@@ -1129,15 +1139,15 @@ impl Game {
|
|||||||
if let Some(circle) = circle
|
if let Some(circle) = circle
|
||||||
&& circle.layer_mask & layer != 0
|
&& circle.layer_mask & layer != 0
|
||||||
&& {
|
&& {
|
||||||
let center = MilliVec::from_position(
|
let (minimum, maximum) = self.live_circle_bounds(
|
||||||
|
circle.id,
|
||||||
self.live_circle_center(circle.id, circle.center),
|
self.live_circle_center(circle.id, circle.center),
|
||||||
|
circle.contact_radius,
|
||||||
);
|
);
|
||||||
let margin =
|
predicted.x >= minimum.x
|
||||||
MilliVec::from_position(vec2(circle.contact_radius + 5.0, 0.0)).x;
|
&& predicted.x <= maximum.x
|
||||||
predicted.x >= center.x.wrapping_sub(margin)
|
&& predicted.y >= minimum.y
|
||||||
&& predicted.x <= center.x.wrapping_add(margin)
|
&& predicted.y <= maximum.y
|
||||||
&& predicted.y >= center.y.wrapping_sub(margin)
|
|
||||||
&& predicted.y <= center.y.wrapping_add(margin)
|
|
||||||
}
|
}
|
||||||
&& let Some(candidate) = circle_collision_candidate_at(
|
&& let Some(candidate) = circle_collision_candidate_at(
|
||||||
old_position,
|
old_position,
|
||||||
@@ -1163,6 +1173,19 @@ impl Game {
|
|||||||
best
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type4_broadphase_contains(
|
||||||
|
&self,
|
||||||
|
predicted: MilliVec,
|
||||||
|
record_ids: std::ops::RangeInclusive<u8>,
|
||||||
|
) -> bool {
|
||||||
|
!self.tilted
|
||||||
|
&& TARGET_SENSORS.into_iter().any(|sensor| {
|
||||||
|
record_ids.contains(&sensor.id)
|
||||||
|
&& self.object_active[usize::from(sensor.id)]
|
||||||
|
&& trigger_broadphase_contains(predicted, sensor.center, sensor.radius)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn find_static_collision_candidate(
|
fn find_static_collision_candidate(
|
||||||
&self,
|
&self,
|
||||||
@@ -1197,6 +1220,7 @@ impl Game {
|
|||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
|
let speed_before_collision = milli_distance(velocity);
|
||||||
if predicted_outside_board(old_position.add(velocity)) {
|
if predicted_outside_board(old_position.add(velocity)) {
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
events.push(Event::Drain);
|
events.push(Event::Drain);
|
||||||
@@ -1252,7 +1276,6 @@ impl Game {
|
|||||||
velocity = response.velocity;
|
velocity = response.velocity;
|
||||||
ball.spin = response.spin;
|
ball.spin = response.spin;
|
||||||
auxiliary_fired = response.auxiliary_fired;
|
auxiliary_fired = response.auxiliary_fired;
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
hit = Some((object_id, is_wall));
|
hit = Some((object_id, is_wall));
|
||||||
if object_id == 174
|
if object_id == 174
|
||||||
&& let Some(transferred) = transferred_primary_velocity
|
&& let Some(transferred) = transferred_primary_velocity
|
||||||
@@ -1260,6 +1283,13 @@ impl Game {
|
|||||||
self.ball.velocity = transferred.to_velocity_per_second();
|
self.ball.velocity = transferred.to_velocity_per_second();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if hit == Some((25, true)) && speed_before_collision >= 1_000 {
|
||||||
|
velocity.x = 0;
|
||||||
|
}
|
||||||
|
if hit == Some((25, true)) && speed_before_collision < 1_000 {
|
||||||
|
velocity.x = 0;
|
||||||
|
velocity.y = 0;
|
||||||
|
}
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
ball.position = old_position.add(velocity).to_position();
|
ball.position = old_position.add(velocity).to_position();
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
@@ -1269,6 +1299,10 @@ impl Game {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if let Some((object_id, true)) = hit {
|
if let Some((object_id, true)) = hit {
|
||||||
|
if object_id == 25 && speed_before_collision < 1_000 {
|
||||||
|
self.score_mode = ScoreMode::Normal;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
self.apply_wall_rule(object_id, events);
|
self.apply_wall_rule(object_id, events);
|
||||||
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
|
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
|
||||||
events.push(Event::Sound(2019));
|
events.push(Event::Sound(2019));
|
||||||
@@ -1796,9 +1830,9 @@ impl Game {
|
|||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) {
|
) -> bool {
|
||||||
if self.tilted {
|
if self.tilted {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
let effect_index = usize::from(EFFECT_SENSOR.id);
|
let effect_index = usize::from(EFFECT_SENSOR.id);
|
||||||
if self.object_active[effect_index] {
|
if self.object_active[effect_index] {
|
||||||
@@ -1827,6 +1861,7 @@ impl Game {
|
|||||||
self.players[self.current_player].secondary_score = 0;
|
self.players[self.current_player].secondary_score = 0;
|
||||||
}
|
}
|
||||||
7 if ball_number != 2
|
7 if ball_number != 2
|
||||||
|
&& self.secondary_ball.is_none()
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.multiball_state,
|
self.multiball_state,
|
||||||
MultiballState::Ready | MultiballState::Active
|
MultiballState::Ready | MultiballState::Active
|
||||||
@@ -1847,8 +1882,10 @@ impl Game {
|
|||||||
self.target_effect = 0;
|
self.target_effect = 0;
|
||||||
self.object_active[effect_index] = false;
|
self.object_active[effect_index] = false;
|
||||||
self.randomize_trigger_velocity(ball);
|
self.randomize_trigger_velocity(ball);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_target_sensors(
|
fn check_target_sensors(
|
||||||
@@ -1858,10 +1895,11 @@ impl Game {
|
|||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
record_ids: std::ops::RangeInclusive<u8>,
|
record_ids: std::ops::RangeInclusive<u8>,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) {
|
) -> bool {
|
||||||
if self.tilted {
|
if self.tilted {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
let mut triggered = false;
|
||||||
for sensor in TARGET_SENSORS {
|
for sensor in TARGET_SENSORS {
|
||||||
if !record_ids.contains(&sensor.id) {
|
if !record_ids.contains(&sensor.id) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1888,7 +1926,9 @@ impl Game {
|
|||||||
self.object_active[field_id] = true;
|
self.object_active[field_id] = true;
|
||||||
}
|
}
|
||||||
self.randomize_trigger_velocity(ball);
|
self.randomize_trigger_velocity(ball);
|
||||||
|
triggered = true;
|
||||||
}
|
}
|
||||||
|
triggered
|
||||||
}
|
}
|
||||||
|
|
||||||
fn randomize_trigger_velocity(&mut self, ball: &mut Ball) {
|
fn randomize_trigger_velocity(&mut self, ball: &mut Ball) {
|
||||||
@@ -1909,6 +1949,8 @@ impl Game {
|
|||||||
{
|
{
|
||||||
self.panel_frame = Some(0);
|
self.panel_frame = Some(0);
|
||||||
}
|
}
|
||||||
|
self.tilted = false;
|
||||||
|
self.tilt_counter = 0;
|
||||||
self.ball = Ball::default();
|
self.ball = Ball::default();
|
||||||
self.launcher_charge = 0.0;
|
self.launcher_charge = 0.0;
|
||||||
self.launcher_was_down = false;
|
self.launcher_was_down = false;
|
||||||
@@ -2230,6 +2272,52 @@ impl Game {
|
|||||||
_ => resting,
|
_ => resting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn live_circle_bounds(
|
||||||
|
&self,
|
||||||
|
object_id: u8,
|
||||||
|
center: Vec2,
|
||||||
|
contact_radius: f32,
|
||||||
|
) -> (MilliVec, MilliVec) {
|
||||||
|
// 1000:8b0d moves the tip records through an asymmetric swept box;
|
||||||
|
// their collision broadphase is not center +/- (radius + 5).
|
||||||
|
if object_id == 67 && self.flippers.left_raised {
|
||||||
|
return (
|
||||||
|
MilliVec {
|
||||||
|
x: 118_000,
|
||||||
|
y: 362_000,
|
||||||
|
},
|
||||||
|
MilliVec {
|
||||||
|
x: 150_000,
|
||||||
|
y: 476_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if object_id == 82 && self.flippers.right_raised {
|
||||||
|
return (
|
||||||
|
MilliVec {
|
||||||
|
x: 167_000,
|
||||||
|
y: 362_000,
|
||||||
|
},
|
||||||
|
MilliVec {
|
||||||
|
x: 191_000,
|
||||||
|
y: 476_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let center = MilliVec::from_position(center);
|
||||||
|
let margin = MilliVec::from_position(vec2(contact_radius + 5.0, 0.0)).x;
|
||||||
|
(
|
||||||
|
MilliVec {
|
||||||
|
x: center.x.wrapping_sub(margin),
|
||||||
|
y: center.y.wrapping_sub(margin),
|
||||||
|
},
|
||||||
|
MilliVec {
|
||||||
|
x: center.x.wrapping_add(margin),
|
||||||
|
y: center.y.wrapping_add(margin),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
||||||
@@ -2360,6 +2448,47 @@ mod tests {
|
|||||||
assert!(game.ball.in_launcher);
|
assert!(game.ball.in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_static_record_reaches_the_production_scanner() {
|
||||||
|
let mut missed_walls = Vec::new();
|
||||||
|
for wall in WALLS {
|
||||||
|
let direction = wall.segment.end - wall.segment.start;
|
||||||
|
let normal = vec2(-direction.y, direction.x).normalize();
|
||||||
|
let midpoint = (wall.segment.start + wall.segment.end) * 0.5;
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.object_active[usize::from(wall.id)] = true;
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = midpoint - normal;
|
||||||
|
game.ball.velocity = MilliVec::from_velocity_per_second(normal * 200.0)
|
||||||
|
.to_velocity_per_second();
|
||||||
|
game.fixed_update(&mut Vec::new());
|
||||||
|
if game.last_collision_id != Some(wall.id) {
|
||||||
|
missed_walls.push(wall.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut missed_circles = Vec::new();
|
||||||
|
for circle in PASSIVE_CIRCLES.iter().chain(BUMPERS.iter()) {
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.object_active[usize::from(circle.id)] = true;
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = circle.center + vec2(circle.contact_radius + 1.0, 0.0);
|
||||||
|
game.ball.velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
||||||
|
game.fixed_update(&mut Vec::new());
|
||||||
|
if game.last_collision_id != Some(circle.id) {
|
||||||
|
missed_circles.push(circle.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(missed_walls.is_empty(), "missed production wall records: {missed_walls:?}");
|
||||||
|
assert!(
|
||||||
|
missed_circles.is_empty(),
|
||||||
|
"missed production circle records: {missed_circles:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detail_timer_batches_the_original_number_of_substeps() {
|
fn detail_timer_batches_the_original_number_of_substeps() {
|
||||||
for (detail, interval, substeps) in [
|
for (detail, interval, substeps) in [
|
||||||
@@ -2565,7 +2694,7 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.ball.in_launcher = false;
|
||||||
game.ball.position = Vec2::new(325.0, 410.0);
|
game.ball.position = Vec2::new(325.0, 410.0);
|
||||||
game.ball.velocity = Vec2::new(0.0, 180.0);
|
game.ball.velocity = Vec2::new(0.0, 80.0);
|
||||||
|
|
||||||
for _ in 0..8 {
|
for _ in 0..8 {
|
||||||
game.update(1.0 / 120.0, 5, Controls::default());
|
game.update(1.0 / 120.0, 5, Controls::default());
|
||||||
@@ -2579,6 +2708,36 @@ mod tests {
|
|||||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_shooter_stop_contact_bounces_without_rearming() {
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.object_active[25] = true;
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = Vec2::new(320.0, 412.0);
|
||||||
|
game.ball.velocity = MilliVec { x: 500, y: 1_000 }.to_velocity_per_second();
|
||||||
|
|
||||||
|
assert!(game.fixed_update(&mut Vec::new()));
|
||||||
|
|
||||||
|
assert!(!game.ball.in_launcher);
|
||||||
|
assert!(game.ball.velocity.y < 0.0);
|
||||||
|
assert_eq!(MilliVec::from_velocity_per_second(game.ball.velocity).x, 0);
|
||||||
|
assert_eq!(MilliVec::from_position(game.ball.position).x, 320_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ball_reset_clears_tilt_state_before_the_next_launch() {
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.tilted = true;
|
||||||
|
game.tilt_counter = 39;
|
||||||
|
|
||||||
|
game.reset_ball_to_launcher();
|
||||||
|
|
||||||
|
assert!(!game.tilted);
|
||||||
|
assert_eq!(game.tilt_counter, 0);
|
||||||
|
assert!(game.ball.in_launcher);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ninth_diamond_and_followup_banks_match_original_awards() {
|
fn ninth_diamond_and_followup_banks_match_original_awards() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
@@ -2866,6 +3025,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_four_motion_response_refreshes_the_remaining_prediction() {
|
||||||
|
let sensor = TARGET_SENSORS
|
||||||
|
.into_iter()
|
||||||
|
.find(|sensor| sensor.id == 150)
|
||||||
|
.expect("record 150 must be present");
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.object_active[usize::from(sensor.id)] = true;
|
||||||
|
let old_position = MilliVec::from_position(sensor.center - vec2(1.0, 0.0));
|
||||||
|
let mut ball = Ball {
|
||||||
|
position: old_position.to_position(),
|
||||||
|
velocity: MilliVec { x: 100, y: 0 }.to_velocity_per_second(),
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
};
|
||||||
|
let initial_velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
let mut velocity = initial_velocity;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
let (_, _, predicted) = game.scan_ordered_records_for_ball(
|
||||||
|
&mut ball,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
old_position,
|
||||||
|
&mut velocity,
|
||||||
|
&mut events,
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated_velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
assert_ne!(updated_velocity, initial_velocity);
|
||||||
|
assert_eq!(predicted, old_position.add(updated_velocity));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bumper_sound_precedes_a_crossed_media_marker() {
|
fn bumper_sound_precedes_a_crossed_media_marker() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
@@ -3126,6 +3320,41 @@ mod tests {
|
|||||||
assert!(!game.effect_target_active());
|
assert!(!game.effect_target_active());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rearmed_effect_seven_does_not_replace_an_active_secondary_ball() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.multiball_state = MultiballState::Active;
|
||||||
|
game.target_effect = 7;
|
||||||
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
|
let secondary = Ball {
|
||||||
|
position: vec2(200.0, 200.0),
|
||||||
|
velocity: vec2(-12.0, 34.0),
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
};
|
||||||
|
game.secondary_ball = Some(secondary);
|
||||||
|
let mut primary = game.ball;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects_for_ball(
|
||||||
|
&mut primary,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.action,
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.secondary_ball.map(|ball| ball.position), Some(secondary.position));
|
||||||
|
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||||
|
assert_eq!(game.target_effect, 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiball_capture_age_is_per_slot_and_removes_only_ball_two() {
|
fn multiball_capture_age_is_per_slot_and_removes_only_ball_two() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
@@ -3628,6 +3857,68 @@ mod tests {
|
|||||||
game.live_circle_center(82, vec2(179.0, 419.0)),
|
game.live_circle_center(82, vec2(179.0, 419.0)),
|
||||||
vec2(181.0, 376.0)
|
vec2(181.0, 376.0)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.live_circle_bounds(67, vec2(133.0, 377.0), 9.0),
|
||||||
|
(
|
||||||
|
MilliVec {
|
||||||
|
x: 118_000,
|
||||||
|
y: 362_000,
|
||||||
|
},
|
||||||
|
MilliVec {
|
||||||
|
x: 150_000,
|
||||||
|
y: 476_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.live_circle_bounds(82, vec2(181.0, 376.0), 9.0),
|
||||||
|
(
|
||||||
|
MilliVec {
|
||||||
|
x: 167_000,
|
||||||
|
y: 362_000,
|
||||||
|
},
|
||||||
|
MilliVec {
|
||||||
|
x: 191_000,
|
||||||
|
y: 476_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raised_flipper_tips_use_their_asymmetric_swept_bounds() {
|
||||||
|
for (object_id, center) in [(67_u8, vec2(133.0, 377.0)), (82, vec2(181.0, 376.0))]
|
||||||
|
{
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.object_active[usize::from(object_id)] = true;
|
||||||
|
if object_id == 67 {
|
||||||
|
game.flippers.left_raised = true;
|
||||||
|
} else {
|
||||||
|
game.flippers.right_raised = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_position = MilliVec::from_position(vec2(center.x, center.y + 12.0));
|
||||||
|
let velocity = MilliVec { x: 0, y: -3_800 };
|
||||||
|
// The scanner carries prediction separately from mutable motion;
|
||||||
|
// keep this point outside the old radius box while the motion
|
||||||
|
// still approaches the tip.
|
||||||
|
let predicted = old_position.add(MilliVec { x: 0, y: 4_000 });
|
||||||
|
let generic_max_y = MilliVec::from_position(vec2(center.x, center.y + 14.0)).y;
|
||||||
|
assert!(predicted.y > generic_max_y);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.find_static_collision_candidate_in_range(
|
||||||
|
old_position,
|
||||||
|
predicted,
|
||||||
|
velocity,
|
||||||
|
object_id..=object_id,
|
||||||
|
)
|
||||||
|
.map(|candidate| candidate.0),
|
||||||
|
Some(object_id),
|
||||||
|
"the original swept tip bounds must admit the path for record {object_id}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -168,9 +168,19 @@ impl MilliVec {
|
|||||||
if speed <= maximum {
|
if speed <= maximum {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let scale = Real48::from_i32(maximum).divide(Real48::from_i32(speed));
|
// The original computes the excess fraction and subtracts the
|
||||||
self.x = Real48::from_i32(self.x).multiply(scale).round_i32();
|
// rounded component from each axis. Scaling directly by
|
||||||
self.y = Real48::from_i32(self.y).multiply(scale).round_i32();
|
// `maximum / speed` is mathematically equivalent, but can differ by
|
||||||
|
// one millipixel because each Real48 operation is rounded
|
||||||
|
// independently.
|
||||||
|
let excess = Real48::from_i32(speed.wrapping_sub(maximum))
|
||||||
|
.divide(Real48::from_i32(speed));
|
||||||
|
self.x = self
|
||||||
|
.x
|
||||||
|
.wrapping_sub(Real48::from_i32(self.x).multiply(excess).round_i32());
|
||||||
|
self.y = self
|
||||||
|
.y
|
||||||
|
.wrapping_sub(Real48::from_i32(self.y).multiply(excess).round_i32());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,6 +686,39 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn speed_clamp_matches_reconstructed_formula() {
|
||||||
|
let maximum = 3_800;
|
||||||
|
let mut mismatches = Vec::new();
|
||||||
|
for x in (-10_000..=10_000).step_by(37) {
|
||||||
|
for y in (-10_000..=10_000).step_by(41) {
|
||||||
|
let input = MilliVec { x, y };
|
||||||
|
let speed = milli_distance(input);
|
||||||
|
if speed <= maximum {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let excess = Real48::from_i32(speed - maximum)
|
||||||
|
.divide(Real48::from_i32(speed));
|
||||||
|
let expected = MilliVec {
|
||||||
|
x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()),
|
||||||
|
y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()),
|
||||||
|
};
|
||||||
|
let mut actual = input;
|
||||||
|
actual.clamp_speed(maximum);
|
||||||
|
if actual != expected {
|
||||||
|
mismatches.push((input, speed, actual, expected));
|
||||||
|
if mismatches.len() == 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mismatches.len() == 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(mismatches.is_empty(), "speed-clamp mismatches: {mismatches:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn outer_shooter_wall_matches_the_live_tangent_response() {
|
fn outer_shooter_wall_matches_the_live_tangent_response() {
|
||||||
let old = MilliVec {
|
let old = MilliVec {
|
||||||
|
|||||||
Reference in New Issue
Block a user