fix(physics): preserve record layer and kick fields
Carry the recovered +0x49 layer mask through every static line/circle and select the original upper/lower scan layer from the previous Y coordinate. Restore negative auxiliary thresholds and kick coefficients for bumpers and records 55, 74, and 107, including weak-hit scoring, rail sound, and tilt suppression. Test Plan: - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - rumdl check CHANGELOG.md RECONSTRUCTION.md README.md - git diff --check
This commit is contained in:
+124
-7
@@ -695,14 +695,17 @@ impl Game {
|
||||
surface_distance: response.surface_distance,
|
||||
velocity: response.moving_velocity,
|
||||
spin: response.moving_spin,
|
||||
auxiliary_fired: false,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
let collided = best_collision.is_some();
|
||||
let mut auxiliary_fired = false;
|
||||
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
self.ball.spin = response.spin;
|
||||
auxiliary_fired = response.auxiliary_fired;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
position = old_position.add(velocity);
|
||||
self.last_collision_id = Some(object_id);
|
||||
@@ -737,13 +740,22 @@ impl Game {
|
||||
return true;
|
||||
}
|
||||
self.apply_wall_rule(wall_id, events);
|
||||
if auxiliary_fired && matches!(wall_id, 55 | 74 | 107) {
|
||||
events.push(Event::Sound(2019));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = hit_circle
|
||||
.and_then(|circle_id| BUMPERS.iter().position(|bumper| bumper.id == circle_id))
|
||||
&& !self.tilted
|
||||
{
|
||||
self.add_score(self.player().bumper_value, events);
|
||||
let points = self
|
||||
.player()
|
||||
.bumper_value
|
||||
.saturating_sub(if auxiliary_fired { 0 } else { 1_000 });
|
||||
if points != 0 {
|
||||
self.add_score(points, events);
|
||||
}
|
||||
self.record_countdowns[usize::from(BUMPERS[index].id)] = 5;
|
||||
events.push(Event::Bumper);
|
||||
events.push(Event::Sound(2006));
|
||||
@@ -769,6 +781,7 @@ impl Game {
|
||||
spin: Real48,
|
||||
) -> Option<(u8, bool, CollisionResponse)> {
|
||||
let mut best = None;
|
||||
let layer = if old_position.y < 250_000 { 0x01 } else { 0x02 };
|
||||
for object_id in 1..=175 {
|
||||
if !self.object_active[usize::from(object_id)]
|
||||
|| self.claw.active && (12..=20).contains(&object_id)
|
||||
@@ -776,16 +789,29 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
if let Some(wall) = WALLS.iter().find(|wall| wall.id == object_id) {
|
||||
if wall.layer_mask & layer == 0 {
|
||||
continue;
|
||||
}
|
||||
let segment = self.live_wall_segment(wall.id, wall.segment);
|
||||
let material = if self.tilted {
|
||||
CollisionMaterial::line(
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
)
|
||||
} else {
|
||||
CollisionMaterial::line_with_kick(
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
f64::from(wall.response_auxiliary),
|
||||
f64::from(wall.response_kick),
|
||||
)
|
||||
};
|
||||
if let Some(response) = line_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
segment.start,
|
||||
segment.end,
|
||||
CollisionMaterial::line(
|
||||
f64::from(wall.normal_rebound),
|
||||
f64::from(wall.tangent_coupling),
|
||||
),
|
||||
material,
|
||||
spin,
|
||||
) && best.is_none_or(|(_, _, closest): (u8, bool, CollisionResponse)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
@@ -798,6 +824,7 @@ impl Game {
|
||||
.chain(BUMPERS.iter())
|
||||
.find(|circle| circle.id == object_id);
|
||||
if let Some(circle) = circle
|
||||
&& circle.layer_mask & layer != 0
|
||||
&& let Some(response) = circle_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
@@ -806,7 +833,11 @@ impl Game {
|
||||
CollisionMaterial::circle(
|
||||
f64::from(circle.normal_rebound),
|
||||
f64::from(circle.tangent_coupling),
|
||||
f64::from(circle.normal_kick),
|
||||
if self.tilted {
|
||||
0.0
|
||||
} else {
|
||||
f64::from(circle.normal_kick)
|
||||
},
|
||||
),
|
||||
spin,
|
||||
)
|
||||
@@ -861,14 +892,17 @@ impl Game {
|
||||
surface_distance: response.surface_distance,
|
||||
velocity: response.moving_velocity,
|
||||
spin: response.moving_spin,
|
||||
auxiliary_fired: false,
|
||||
},
|
||||
));
|
||||
}
|
||||
let mut hit = None;
|
||||
let collided = best_collision.is_some();
|
||||
let mut auxiliary_fired = false;
|
||||
if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
ball.spin = response.spin;
|
||||
auxiliary_fired = response.auxiliary_fired;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
hit = Some((object_id, is_wall));
|
||||
if object_id == 174
|
||||
@@ -885,11 +919,20 @@ impl Game {
|
||||
}
|
||||
if let Some((object_id, true)) = hit {
|
||||
self.apply_wall_rule(object_id, events);
|
||||
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
|
||||
events.push(Event::Sound(2019));
|
||||
}
|
||||
} else if let Some((object_id, false)) = hit
|
||||
&& let Some(index) = BUMPERS.iter().position(|bumper| bumper.id == object_id)
|
||||
&& !self.tilted
|
||||
{
|
||||
self.add_score(self.player().bumper_value, events);
|
||||
let points = self
|
||||
.player()
|
||||
.bumper_value
|
||||
.saturating_sub(if auxiliary_fired { 0 } else { 1_000 });
|
||||
if points != 0 {
|
||||
self.add_score(points, events);
|
||||
}
|
||||
self.record_countdowns[usize::from(BUMPERS[index].id)] = 5;
|
||||
events.push(Event::Bumper);
|
||||
events.push(Event::Sound(2006));
|
||||
@@ -1044,6 +1087,9 @@ impl Game {
|
||||
movement_velocity: MilliVec,
|
||||
events: &mut Vec<Event>,
|
||||
) -> BallAction {
|
||||
if old_position.y >= 250_000 {
|
||||
return BallAction::Keep;
|
||||
}
|
||||
if !self.claw.active {
|
||||
match self.capture_record_step(
|
||||
ball,
|
||||
@@ -2329,6 +2375,77 @@ mod tests {
|
||||
assert!(game.ball.velocity.y > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_scan_selects_the_layer_from_the_previous_y_position() {
|
||||
let mut game = Game::new(1);
|
||||
game.object_active.fill(false);
|
||||
game.object_active[48] = true;
|
||||
let velocity = MilliVec { x: 0, y: -15_000 };
|
||||
|
||||
assert_eq!(
|
||||
game.find_static_collision(
|
||||
MilliVec {
|
||||
x: 25_000,
|
||||
y: 249_000,
|
||||
},
|
||||
velocity,
|
||||
Real48::ZERO,
|
||||
)
|
||||
.map(|collision| collision.0),
|
||||
Some(48)
|
||||
);
|
||||
assert!(
|
||||
game.find_static_collision(
|
||||
MilliVec {
|
||||
x: 25_000,
|
||||
y: 250_000,
|
||||
},
|
||||
velocity,
|
||||
Real48::ZERO,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bumper_base_score_and_kick_require_the_recovered_speed_threshold() {
|
||||
let mut weak = Game::new(1);
|
||||
weak.object_active.fill(false);
|
||||
weak.object_active[51] = true;
|
||||
weak.players[0].bumper_value = 2_000;
|
||||
weak.ball.in_launcher = false;
|
||||
weak.ball.position = vec2(165.0, 171.1);
|
||||
weak.ball.velocity = vec2(0.0, -20.0);
|
||||
let mut weak_events = Vec::new();
|
||||
assert!(weak.fixed_update(STEP_SECONDS, &mut weak_events));
|
||||
|
||||
let mut fast = Game::new(1);
|
||||
fast.object_active.fill(false);
|
||||
fast.object_active[51] = true;
|
||||
fast.players[0].bumper_value = 2_000;
|
||||
fast.ball.in_launcher = false;
|
||||
fast.ball.position = vec2(165.0, 172.015);
|
||||
fast.ball.velocity = vec2(0.0, -198.5);
|
||||
let mut tilted = fast.clone();
|
||||
tilted.tilted = true;
|
||||
let mut fast_events = Vec::new();
|
||||
assert!(fast.fixed_update(STEP_SECONDS, &mut fast_events));
|
||||
let mut tilted_events = Vec::new();
|
||||
assert!(tilted.fixed_update(STEP_SECONDS, &mut tilted_events));
|
||||
|
||||
assert_eq!(weak.player().score, 1_000);
|
||||
assert_eq!(fast.player().score, 2_000);
|
||||
assert!(weak_events.contains(&Event::Sound(2006)));
|
||||
assert!(fast_events.contains(&Event::Sound(2006)));
|
||||
assert_eq!(weak.record_countdown(51), 5);
|
||||
assert_eq!(fast.record_countdown(51), 5);
|
||||
assert!(fast.ball.velocity.y > weak.ball.velocity.y);
|
||||
assert_eq!(tilted.player().score, 0);
|
||||
assert_eq!(tilted.record_countdown(51), 0);
|
||||
assert!(!tilted_events.contains(&Event::Sound(2006)));
|
||||
assert!(fast.ball.velocity.y > tilted.ball.velocity.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilt_latches_and_disables_flippers_and_scoring() {
|
||||
let mut game = Game::new_with_seed(1, 7);
|
||||
|
||||
@@ -21,12 +21,14 @@ pub struct CollisionResponse {
|
||||
pub surface_distance: i32,
|
||||
pub velocity: MilliVec,
|
||||
pub spin: Real48,
|
||||
pub auxiliary_fired: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CollisionMaterial {
|
||||
pub normal_rebound: f64,
|
||||
pub tangent_coupling: f64,
|
||||
pub response_auxiliary: f64,
|
||||
pub normal_kick: f64,
|
||||
}
|
||||
|
||||
@@ -43,10 +45,25 @@ impl CollisionMaterial {
|
||||
Self {
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
response_auxiliary: 0.0,
|
||||
normal_kick: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn line_with_kick(
|
||||
normal_rebound: f64,
|
||||
tangent_coupling: f64,
|
||||
response_auxiliary: f64,
|
||||
normal_kick: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
response_auxiliary,
|
||||
normal_kick,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn circle(
|
||||
normal_rebound: f64,
|
||||
tangent_coupling: f64,
|
||||
@@ -55,6 +72,7 @@ impl CollisionMaterial {
|
||||
Self {
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
response_auxiliary: if normal_kick == 0.0 { 0.0 } else { -0.1 },
|
||||
normal_kick,
|
||||
}
|
||||
}
|
||||
@@ -63,11 +81,7 @@ impl CollisionMaterial {
|
||||
ResponseCoefficients {
|
||||
normal: coefficient(self.normal_rebound),
|
||||
tangent: coefficient(self.tangent_coupling),
|
||||
auxiliary: if self.normal_kick == 0.0 {
|
||||
ZERO
|
||||
} else {
|
||||
Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc])
|
||||
},
|
||||
auxiliary: coefficient(self.response_auxiliary),
|
||||
kick: coefficient(self.normal_kick),
|
||||
}
|
||||
}
|
||||
@@ -157,7 +171,8 @@ const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
|
||||
}
|
||||
|
||||
fn coefficient(value: f64) -> Real48 {
|
||||
match (value * 100.0).round() as i32 {
|
||||
let scaled = (value * 100.0).round() as i32;
|
||||
let coefficient = match scaled.wrapping_abs() {
|
||||
0 => ZERO,
|
||||
5 => Real48::from_bytes([0x7c, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
|
||||
10 => Real48::from_bytes([0x7d, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
|
||||
@@ -169,6 +184,11 @@ fn coefficient(value: f64) -> Real48 {
|
||||
80 => Real48::from_bytes([0x80, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]),
|
||||
90 => Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]),
|
||||
other => panic!("unsupported binary Real48 coefficient {other}"),
|
||||
};
|
||||
if scaled < 0 {
|
||||
coefficient.negate()
|
||||
} else {
|
||||
coefficient
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +237,7 @@ fn apply_response(
|
||||
normal_y: Real48,
|
||||
collision_velocity: i32,
|
||||
coefficients: ResponseCoefficients,
|
||||
) -> (MilliVec, Real48) {
|
||||
) -> (MilliVec, Real48, bool) {
|
||||
let length = milli_distance(MilliVec {
|
||||
x: normal_x.round_i32(),
|
||||
y: normal_y.round_i32(),
|
||||
@@ -240,12 +260,12 @@ fn apply_response(
|
||||
let mut impulse = Real48::from_i32(collision_velocity)
|
||||
.multiply(ONE.add(coefficients.normal))
|
||||
.round_i32();
|
||||
if coefficients.auxiliary.compare(ZERO).is_lt()
|
||||
let auxiliary_fired = coefficients.auxiliary.compare(ZERO).is_lt()
|
||||
&& Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
.multiply(coefficients.auxiliary)
|
||||
.round_i32()
|
||||
> collision_velocity
|
||||
{
|
||||
> collision_velocity;
|
||||
if auxiliary_fired {
|
||||
impulse = impulse.wrapping_sub(
|
||||
Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
.multiply(coefficients.kick)
|
||||
@@ -268,6 +288,7 @@ fn apply_response(
|
||||
y: velocity.y.wrapping_add(delta_y),
|
||||
},
|
||||
spin,
|
||||
auxiliary_fired,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -316,7 +337,7 @@ pub fn line_collision_response(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (velocity, spin) = apply_response(
|
||||
let (velocity, spin, auxiliary_fired) = apply_response(
|
||||
velocity,
|
||||
spin,
|
||||
normal_x,
|
||||
@@ -328,6 +349,7 @@ pub fn line_collision_response(
|
||||
surface_distance: distance,
|
||||
velocity,
|
||||
spin,
|
||||
auxiliary_fired,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -389,7 +411,7 @@ pub fn circle_collision_response(
|
||||
return None;
|
||||
}
|
||||
surface_distance = surface_distance.wrapping_add(3_000);
|
||||
let (velocity, spin) = apply_response(
|
||||
let (velocity, spin, auxiliary_fired) = apply_response(
|
||||
velocity,
|
||||
spin,
|
||||
normal_x,
|
||||
@@ -401,6 +423,7 @@ pub fn circle_collision_response(
|
||||
surface_distance,
|
||||
velocity,
|
||||
spin,
|
||||
auxiliary_fired,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -447,7 +470,7 @@ pub fn ball_collision_response(
|
||||
x: other_velocity.x.wrapping_add(transfer_x),
|
||||
y: other_velocity.y.wrapping_sub(transfer_y),
|
||||
};
|
||||
let (moving_velocity, moving_spin) = apply_response(
|
||||
let (moving_velocity, moving_spin, _) = apply_response(
|
||||
velocity,
|
||||
spin,
|
||||
normal_x,
|
||||
@@ -539,6 +562,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_kick_uses_the_recovered_negative_speed_threshold() {
|
||||
let material = CollisionMaterial::line_with_kick(0.5, 0.1, -0.4, 0.4);
|
||||
let fast = line_collision_response(
|
||||
MilliVec {
|
||||
x: 326_000,
|
||||
y: 200_000,
|
||||
},
|
||||
MilliVec { x: 3_000, y: 0 },
|
||||
vec2(328.0, 422.0),
|
||||
vec2(328.0, 58.0),
|
||||
material,
|
||||
Real48::ZERO,
|
||||
)
|
||||
.expect("the fast path must hit the vertical rail");
|
||||
let slow = line_collision_response(
|
||||
MilliVec {
|
||||
x: 327_500,
|
||||
y: 200_000,
|
||||
},
|
||||
MilliVec { x: 1_000, y: 0 },
|
||||
vec2(328.0, 422.0),
|
||||
vec2(328.0, 58.0),
|
||||
material,
|
||||
Real48::ZERO,
|
||||
)
|
||||
.expect("the slow path must hit the same vertical rail");
|
||||
|
||||
assert!(fast.auxiliary_fired);
|
||||
assert!(!slow.auxiliary_fired);
|
||||
assert_ne!(fast.velocity.x, slow.velocity.x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_shooter_wall_matches_the_zero_spin_c_response() {
|
||||
let old = MilliVec {
|
||||
|
||||
@@ -18,9 +18,12 @@ pub struct TableSegment {
|
||||
pub segment: Segment,
|
||||
pub normal_rebound: f32,
|
||||
pub tangent_coupling: f32,
|
||||
pub response_auxiliary: f32,
|
||||
pub response_kick: f32,
|
||||
pub score: u32,
|
||||
pub flags: u16,
|
||||
pub contact_group: u8,
|
||||
pub layer_mask: u8,
|
||||
}
|
||||
|
||||
impl TableSegment {
|
||||
@@ -69,14 +72,22 @@ impl TableSegment {
|
||||
118..=120 => 118,
|
||||
_ => 0,
|
||||
};
|
||||
let (response_auxiliary, response_kick) = match id {
|
||||
55 | 74 => (-0.40, 0.40),
|
||||
107 => (-0.40, 0.30),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
Self {
|
||||
id,
|
||||
segment: Segment::new(start, end, normal_rebound),
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
response_auxiliary,
|
||||
response_kick,
|
||||
score,
|
||||
flags,
|
||||
contact_group,
|
||||
layer_mask: layer_mask_for_record(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +100,7 @@ pub struct StaticCircle {
|
||||
pub normal_rebound: f32,
|
||||
pub tangent_coupling: f32,
|
||||
pub normal_kick: f32,
|
||||
pub layer_mask: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -139,10 +151,19 @@ impl StaticCircle {
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
normal_kick,
|
||||
layer_mask: layer_mask_for_record(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn layer_mask_for_record(id: u8) -> u8 {
|
||||
match id {
|
||||
11 | 24 | 49 | 84..=120 | 167..=175 => 0x03,
|
||||
1..=10 | 25 | 50 | 54..=83 | 153 | 154 => 0x02,
|
||||
_ => 0x01,
|
||||
}
|
||||
}
|
||||
|
||||
/// Active type-2 objects after later registrations overwrite objects 169 and
|
||||
/// 170. Object 2 is the drain sensor and object 25 is the shooter stop; callers
|
||||
/// apply their special behavior after detecting contact.
|
||||
@@ -347,6 +368,32 @@ mod tests {
|
||||
assert_eq!(BUMPERS.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_layer_masks_cover_upper_lower_and_shared_records() {
|
||||
assert_eq!(layer_mask_for_record(48), 0x01);
|
||||
assert_eq!(layer_mask_for_record(50), 0x02);
|
||||
assert_eq!(layer_mask_for_record(49), 0x03);
|
||||
assert_eq!(layer_mask_for_record(89), 0x03);
|
||||
assert_eq!(layer_mask_for_record(140), 0x01);
|
||||
assert_eq!(layer_mask_for_record(153), 0x02);
|
||||
assert_eq!(layer_mask_for_record(174), 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_auxiliary_kick_records_keep_both_coefficients() {
|
||||
for (id, expected_kick) in [(55, 0.40_f32), (74, 0.40), (107, 0.30)] {
|
||||
let wall = WALLS
|
||||
.iter()
|
||||
.find(|wall| wall.id == id)
|
||||
.expect("the kicked line record must be present");
|
||||
assert_eq!(wall.response_auxiliary.to_bits(), (-0.40_f32).to_bits());
|
||||
assert_eq!(wall.response_kick.to_bits(), expected_kick.to_bits());
|
||||
}
|
||||
for bumper in BUMPERS {
|
||||
assert_eq!(bumper.normal_kick.to_bits(), 0.40_f32.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_landmarks_match_the_visible_table() {
|
||||
let left_flipper_post = PASSIVE_CIRCLES
|
||||
|
||||
Reference in New Issue
Block a user