fix(physics): restore slot-major timer batches
Freeze the active ball count at each detail callback, integrate every substep for slot one before slot two, and stop a slot after its first collision or lifecycle action. Effect-seven spawns are now deferred until the next callback, while a surviving slot two retains its original ball number for the remainder of a callback after slot one exits. 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:
+150
-31
@@ -216,6 +216,7 @@ enum CaptureStep {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum BallAction {
|
||||
Keep,
|
||||
Suspend,
|
||||
Reset,
|
||||
Remove,
|
||||
}
|
||||
@@ -467,13 +468,36 @@ impl Game {
|
||||
self.update_target_rotation(events);
|
||||
let panel_active = self.update_panel_completion(events);
|
||||
if !self.claw.ball_suspended && !panel_active {
|
||||
let had_secondary_ball = self.secondary_ball.is_some();
|
||||
for _ in 0..substeps {
|
||||
self.fixed_update(STEP_SECONDS, events);
|
||||
self.advance_secondary_ball(events);
|
||||
if self.finished || self.claw.ball_suspended {
|
||||
if self.fixed_update(STEP_SECONDS, events)
|
||||
|| self.finished
|
||||
|| self.claw.ball_suspended
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if had_secondary_ball && !self.finished && !self.claw.ball_suspended {
|
||||
if self.secondary_ball.is_some() {
|
||||
for _ in 0..substeps {
|
||||
if self.advance_secondary_ball(events) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.secondary_ball = Some(self.ball);
|
||||
for _ in 0..substeps {
|
||||
if self.advance_secondary_ball_slot(events, false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(survivor) = self.secondary_ball.take() {
|
||||
self.ball = survivor;
|
||||
} else {
|
||||
self.drain(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_flipper_edges[0] = match (
|
||||
@@ -633,12 +657,12 @@ impl Game {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
|
||||
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) -> bool {
|
||||
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
|
||||
if self.ball.in_launcher {
|
||||
self.ball.position = LAUNCHER_POSITION;
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
let old_position = MilliVec::from_position(self.ball.position);
|
||||
@@ -675,6 +699,7 @@ impl Game {
|
||||
));
|
||||
}
|
||||
}
|
||||
let collided = best_collision.is_some();
|
||||
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
self.ball.spin = response.spin;
|
||||
@@ -701,7 +726,7 @@ impl Game {
|
||||
if let Some(wall_id) = hit_wall {
|
||||
if wall_id == 2 {
|
||||
self.drain(events);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if wall_id == 25
|
||||
&& i64::from(velocity.x).pow(2) + i64::from(velocity.y).pow(2) < 1_000_i64.pow(2)
|
||||
@@ -709,7 +734,7 @@ impl Game {
|
||||
self.ball = Ball::default();
|
||||
self.launcher_charge = 0.0;
|
||||
self.launcher_was_down = false;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
self.apply_wall_rule(wall_id, events);
|
||||
}
|
||||
@@ -724,16 +749,17 @@ impl Game {
|
||||
events.push(Event::Sound(2006));
|
||||
}
|
||||
|
||||
self.check_sensor_objects(old_position, movement_velocity, events);
|
||||
if self.claw.ball_suspended {
|
||||
return;
|
||||
if self.check_sensor_objects(old_position, movement_velocity, events) != BallAction::Keep {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.ball.position.y > 470.0 {
|
||||
// Only malformed/out-of-table states reach this guard; the real
|
||||
// drain is collision object 2 at y=455.
|
||||
self.drain(events);
|
||||
return true;
|
||||
}
|
||||
collided
|
||||
}
|
||||
|
||||
fn find_static_collision(
|
||||
@@ -794,9 +820,17 @@ impl Game {
|
||||
best
|
||||
}
|
||||
|
||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) {
|
||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) -> bool {
|
||||
self.advance_secondary_ball_slot(events, true)
|
||||
}
|
||||
|
||||
fn advance_secondary_ball_slot(
|
||||
&mut self,
|
||||
events: &mut Vec<Event>,
|
||||
primary_slot_active: bool,
|
||||
) -> bool {
|
||||
let Some(mut ball) = self.secondary_ball.take() else {
|
||||
return;
|
||||
return true;
|
||||
};
|
||||
let old_position = MilliVec::from_position(ball.position);
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
@@ -807,15 +841,17 @@ impl Game {
|
||||
let mut best_collision = self.find_static_collision(old_position, velocity, ball.spin);
|
||||
|
||||
let mut transferred_primary_velocity = None;
|
||||
if let Some(response) = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
ball.spin,
|
||||
MilliVec::from_position(self.ball.position),
|
||||
MilliVec::from_velocity_per_second(self.ball.velocity),
|
||||
) && best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
if primary_slot_active
|
||||
&& let Some(response) = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
ball.spin,
|
||||
MilliVec::from_position(self.ball.position),
|
||||
MilliVec::from_velocity_per_second(self.ball.velocity),
|
||||
)
|
||||
&& best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
transferred_primary_velocity = Some(response.other_velocity);
|
||||
best_collision = Some((
|
||||
@@ -829,6 +865,7 @@ impl Game {
|
||||
));
|
||||
}
|
||||
let mut hit = None;
|
||||
let collided = best_collision.is_some();
|
||||
if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
ball.spin = response.spin;
|
||||
@@ -844,7 +881,7 @@ impl Game {
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
if hit == Some((2, true)) || ball.position.y > 470.0 {
|
||||
events.push(Event::Drain);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if let Some((object_id, true)) = hit {
|
||||
self.apply_wall_rule(object_id, events);
|
||||
@@ -857,17 +894,19 @@ impl Game {
|
||||
events.push(Event::Bumper);
|
||||
events.push(Event::Sound(2006));
|
||||
}
|
||||
if self.check_sensor_objects_for_ball(
|
||||
let action = self.check_sensor_objects_for_ball(
|
||||
&mut ball,
|
||||
2,
|
||||
2,
|
||||
old_position,
|
||||
movement_velocity,
|
||||
events,
|
||||
) == BallAction::Keep
|
||||
{
|
||||
self.secondary_ball = Some(ball);
|
||||
);
|
||||
match action {
|
||||
BallAction::Keep | BallAction::Suspend => self.secondary_ball = Some(ball),
|
||||
BallAction::Reset | BallAction::Remove => return true,
|
||||
}
|
||||
collided || action == BallAction::Suspend
|
||||
}
|
||||
|
||||
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
|
||||
@@ -972,18 +1011,19 @@ impl Game {
|
||||
old_position: MilliVec,
|
||||
movement_velocity: MilliVec,
|
||||
events: &mut Vec<Event>,
|
||||
) {
|
||||
) -> BallAction {
|
||||
let ball_count = if self.secondary_ball.is_some() { 2 } else { 1 };
|
||||
let mut ball = self.ball;
|
||||
match self.check_sensor_objects_for_ball(
|
||||
let action = self.check_sensor_objects_for_ball(
|
||||
&mut ball,
|
||||
1,
|
||||
ball_count,
|
||||
old_position,
|
||||
movement_velocity,
|
||||
events,
|
||||
) {
|
||||
BallAction::Keep => self.ball = ball,
|
||||
);
|
||||
match action {
|
||||
BallAction::Keep | BallAction::Suspend => self.ball = ball,
|
||||
BallAction::Reset => self.reset_ball_to_launcher(),
|
||||
BallAction::Remove => {
|
||||
self.ball = self
|
||||
@@ -992,6 +1032,7 @@ impl Game {
|
||||
.expect("a multiball capture must leave the other slot active");
|
||||
}
|
||||
}
|
||||
action
|
||||
}
|
||||
|
||||
fn check_sensor_objects_for_ball(
|
||||
@@ -1019,7 +1060,7 @@ impl Game {
|
||||
if ball_count == 1 {
|
||||
let terminal_frame = self.next_claw_terminal_frame();
|
||||
self.begin_claw_capture_after_hold(ball, terminal_frame, events);
|
||||
return BallAction::Keep;
|
||||
return BallAction::Suspend;
|
||||
}
|
||||
return BallAction::Remove;
|
||||
}
|
||||
@@ -1741,6 +1782,84 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detail_batch_stops_after_the_first_collision_like_integrate_one_ball() {
|
||||
let mut game = Game::new(1);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(270.0, 30.0);
|
||||
game.ball.velocity = vec2(0.0, -380.0);
|
||||
let mut expected = game.clone();
|
||||
let mut expected_events = Vec::new();
|
||||
let mut stopped = false;
|
||||
for _ in 0..5 {
|
||||
if expected.fixed_update(STEP_SECONDS, &mut expected_events) {
|
||||
stopped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(stopped, "the recovered top rail must end this detail batch");
|
||||
|
||||
let mut events = Vec::new();
|
||||
game.timer_tick(0.050, 5, &mut events);
|
||||
|
||||
assert_eq!(game.ball.position, expected.ball.position);
|
||||
assert_eq!(game.ball.velocity, expected.ball.velocity);
|
||||
assert_eq!(events, expected_events);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_seven_spawn_waits_until_the_next_timer_callback() {
|
||||
let mut game = Game::new_with_seed(1, 7);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = EFFECT_SENSOR.center;
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.object_active.fill(false);
|
||||
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||
game.multiball_state = MultiballState::Ready;
|
||||
game.target_effect = 7;
|
||||
|
||||
game.timer_tick(0.050, 5, &mut Vec::new());
|
||||
|
||||
let spawned = game
|
||||
.secondary_ball
|
||||
.expect("the effect-seven request must create slot two after simulation");
|
||||
assert_eq!(spawned.position, vec2(17.0, 23.0));
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(spawned.velocity),
|
||||
MilliVec { x: 0, y: 3_040 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn survivor_keeps_ball_two_identity_for_the_rest_of_the_callback() {
|
||||
let lock = LOCK_HOLES[0];
|
||||
let mut game = Game::new_with_seed(1, 7);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = lock.center;
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.ball.capture_age = 300;
|
||||
game.record_contacts[usize::from(lock.id)] = 1;
|
||||
game.secondary_ball = Some(Ball {
|
||||
position: EFFECT_SENSOR.center,
|
||||
velocity: Vec2::ZERO,
|
||||
in_launcher: false,
|
||||
spin: Real48::ZERO,
|
||||
capture_age: 0,
|
||||
});
|
||||
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||
game.multiball_state = MultiballState::Ready;
|
||||
game.target_effect = 7;
|
||||
|
||||
game.timer_tick(0.050, 5, &mut Vec::new());
|
||||
|
||||
assert!(game.secondary_ball.is_none());
|
||||
assert_eq!(game.target_effect, 0);
|
||||
assert!(!game.effect_target_active());
|
||||
assert_eq!(game.multiball_state, MultiballState::Ready);
|
||||
assert!(game.wheel_holes[0]);
|
||||
assert!(game.ball.position.y > EFFECT_SENSOR.center.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_charges_while_held_and_fires_on_release() {
|
||||
let mut game = Game::new(1);
|
||||
|
||||
Reference in New Issue
Block a user