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:
2026-08-23 18:13:35 +02:00
parent 692e0087f9
commit 3d5a105c10
5 changed files with 247 additions and 22 deletions
+124 -7
View File
@@ -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);