fix(timing): batch state on original detail callbacks
Replace the host-frame 10 ms publication loop with the original timer matrix: 50/40/30/20/10 ms callbacks execute exactly 5/4/3/2/1 internal physics substeps. Ball state, claw progression, and flipper geometry are now visible only at the selected callback boundary rather than leaking intermediate substeps. Separate flipper input flags from flipper position. Key changes are sampled by the timer, then produce one delta edge, WAVE 2021, collision response, and rendered position update after physics, matching the Win16 callback order. Clear both input and position state on turn reset and keep tilt decay on the same timer cadence. Test Plan: - `cargo test --all-targets` -- passed, 70 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
+90
-38
@@ -29,6 +29,7 @@ const CLAW_TRIGGER_RADIUS: f32 = 19.0;
|
||||
// deterministic on modern machines.
|
||||
const CLAW_FRAME_SECONDS: f32 = 0.030;
|
||||
const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
|
||||
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
||||
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
||||
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
||||
|
||||
@@ -262,6 +263,7 @@ pub struct Game {
|
||||
pub top_targets: [bool; 3],
|
||||
pub tilted: bool,
|
||||
pub flippers: Flippers,
|
||||
flipper_inputs: Flippers,
|
||||
pub claw: Claw,
|
||||
pub bumper_flash: [f32; 3],
|
||||
pub wheel_animation: f32,
|
||||
@@ -304,6 +306,7 @@ impl Game {
|
||||
top_targets: [false; 3],
|
||||
tilted: false,
|
||||
flippers: Flippers::default(),
|
||||
flipper_inputs: Flippers::default(),
|
||||
claw: Claw::default(),
|
||||
bumper_flash: [0.0; 3],
|
||||
wheel_animation: 0.0,
|
||||
@@ -404,19 +407,8 @@ impl Game {
|
||||
self.claw_frame_seconds = DETAIL_TIMER_SECONDS[usize::from(detail.clamp(1, 5) - 1)];
|
||||
self.advance_tilt_counter(frame_time.min(0.05));
|
||||
let mut events = Vec::new();
|
||||
let old_flippers = self.flippers;
|
||||
self.flippers.left_raised = controls.left_flipper && !self.tilted;
|
||||
self.flippers.right_raised = controls.right_flipper && !self.tilted;
|
||||
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 };
|
||||
}
|
||||
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.flipper_inputs.left_raised = controls.left_flipper && !self.tilted;
|
||||
self.flipper_inputs.right_raised = controls.right_flipper && !self.tilted;
|
||||
|
||||
if self.ball.in_launcher && !self.tilted {
|
||||
if controls.launch_down {
|
||||
@@ -449,14 +441,53 @@ impl Game {
|
||||
}
|
||||
|
||||
self.accumulator = (self.accumulator + frame_time.min(0.05)).min(0.1);
|
||||
while self.accumulator >= STEP_SECONDS {
|
||||
self.fixed_update(STEP_SECONDS, &mut events);
|
||||
self.advance_secondary_ball(&mut events);
|
||||
self.accumulator -= STEP_SECONDS;
|
||||
let detail_index = usize::from(detail.clamp(1, 5) - 1);
|
||||
let timer_interval = DETAIL_TIMER_SECONDS[detail_index];
|
||||
while self.accumulator >= timer_interval {
|
||||
self.timer_tick(timer_interval, DETAIL_SUBSTEPS[detail_index], &mut events);
|
||||
self.accumulator -= timer_interval;
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
|
||||
self.update_claw(elapsed, events);
|
||||
if !self.claw.ball_suspended {
|
||||
for _ in 0..substeps {
|
||||
self.fixed_update(STEP_SECONDS, events);
|
||||
self.advance_secondary_ball(events);
|
||||
if self.finished || self.claw.ball_suspended {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_flipper_edges[0] = match (
|
||||
self.flipper_inputs.left_raised,
|
||||
self.flippers.left_raised,
|
||||
) {
|
||||
(true, false) => -1,
|
||||
(false, true) => 1,
|
||||
_ => 0,
|
||||
};
|
||||
self.pending_flipper_edges[1] = match (
|
||||
self.flipper_inputs.right_raised,
|
||||
self.flippers.right_raised,
|
||||
) {
|
||||
(true, false) => -1,
|
||||
(false, true) => 1,
|
||||
_ => 0,
|
||||
};
|
||||
self.flippers = self.flipper_inputs;
|
||||
for edge in self.pending_flipper_edges {
|
||||
if edge != 0 {
|
||||
events.push(Event::FlipperMove);
|
||||
events.push(Event::Sound(2021));
|
||||
}
|
||||
}
|
||||
self.apply_flipper_kicks();
|
||||
}
|
||||
|
||||
fn advance_tilt_counter(&mut self, elapsed: f32) {
|
||||
self.tilt_counter_accumulator += elapsed;
|
||||
while self.tilt_counter_accumulator >= self.claw_frame_seconds {
|
||||
@@ -518,7 +549,7 @@ impl Game {
|
||||
let threshold = self.random.below(10) + 30;
|
||||
if threshold < self.tilt_counter {
|
||||
self.tilted = true;
|
||||
self.flippers = Flippers::default();
|
||||
self.flipper_inputs = Flippers::default();
|
||||
self.pending_flipper_edges.fill(0);
|
||||
events.push(Event::Tilt);
|
||||
events.push(Event::Sound(2020));
|
||||
@@ -535,12 +566,6 @@ impl Game {
|
||||
for flash in &mut self.bumper_flash {
|
||||
*flash = (*flash - dt).max(0.0);
|
||||
}
|
||||
self.update_claw(dt, events);
|
||||
if self.claw.ball_suspended {
|
||||
self.pending_flipper_edges.fill(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ball.in_launcher {
|
||||
self.ball.position = LAUNCHER_POSITION;
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
@@ -637,8 +662,6 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
|
||||
self.apply_flipper_kicks();
|
||||
|
||||
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.
|
||||
@@ -1332,6 +1355,9 @@ impl Game {
|
||||
self.bumper_flash.fill(0.0);
|
||||
self.wheel_animation = 0.0;
|
||||
self.claw = Claw::default();
|
||||
self.flipper_inputs = Flippers::default();
|
||||
self.flippers = Flippers::default();
|
||||
self.pending_flipper_edges.fill(0);
|
||||
self.capture_age = 0;
|
||||
self.secondary_ball = None;
|
||||
self.nudge_shake = 0.0;
|
||||
@@ -1497,6 +1523,33 @@ mod tests {
|
||||
assert!(game.ball.in_launcher);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detail_timer_batches_the_original_number_of_substeps() {
|
||||
for (detail, interval, substeps) in [
|
||||
(1, 0.050, 5_i32),
|
||||
(2, 0.040, 4),
|
||||
(3, 0.030, 3),
|
||||
(4, 0.020, 2),
|
||||
(5, 0.010, 1),
|
||||
] {
|
||||
let mut game = Game::new(1);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(200.0, 250.0);
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.object_active.fill(false);
|
||||
|
||||
game.update(interval - 0.001, detail, Controls::default());
|
||||
assert_eq!(game.ball.position, vec2(200.0, 250.0));
|
||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
||||
|
||||
game.update(0.001_1, detail, Controls::default());
|
||||
let velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
||||
let position = MilliVec::from_position(game.ball.position);
|
||||
assert_eq!(velocity.y, 15 * substeps);
|
||||
assert_eq!(position.y, 250_000 + 15 * substeps * (substeps + 1) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_charges_while_held_and_fires_on_release() {
|
||||
let mut game = Game::new(1);
|
||||
@@ -1890,7 +1943,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let events = game.update(
|
||||
1.0 / 60.0,
|
||||
0.030,
|
||||
3,
|
||||
Controls {
|
||||
left_flipper: true,
|
||||
@@ -1944,7 +1997,8 @@ mod tests {
|
||||
..Controls::default()
|
||||
};
|
||||
|
||||
let press_events = game.update(1.0 / 60.0, 3, raised);
|
||||
assert!(game.update(0.029, 3, raised).is_empty());
|
||||
let press_events = game.update(0.001_1, 3, raised);
|
||||
assert_eq!(
|
||||
press_events,
|
||||
[
|
||||
@@ -1955,9 +2009,9 @@ mod tests {
|
||||
],
|
||||
"each original move_flipper call starts WAVE 2021"
|
||||
);
|
||||
assert!(game.update(1.0 / 60.0, 3, raised).is_empty());
|
||||
assert!(game.update(0.030, 3, raised).is_empty());
|
||||
|
||||
let release_events = game.update(1.0 / 60.0, 3, Controls::default());
|
||||
let release_events = game.update(0.030, 3, Controls::default());
|
||||
assert_eq!(
|
||||
release_events,
|
||||
[
|
||||
@@ -1998,15 +2052,13 @@ mod tests {
|
||||
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]);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(110.0, 410.0);
|
||||
game.ball.velocity = vec2(100.0, 200.0);
|
||||
game.apply_flipper_kicks();
|
||||
let mut events = Vec::new();
|
||||
game.timer_tick(0.030, 0, &mut events);
|
||||
|
||||
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
|
||||
let raw_velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
||||
assert_eq!(raw_velocity, MilliVec { x: 765, y: 3_997 });
|
||||
assert_eq!(game.ball.position, vec2(110.380, 411.984));
|
||||
@@ -2130,7 +2182,7 @@ mod tests {
|
||||
assert!(game.claw.ball_suspended);
|
||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
||||
|
||||
game.fixed_update(CLAW_FRAME_SECONDS / 3.0, &mut events);
|
||||
game.timer_tick(CLAW_FRAME_SECONDS / 3.0, 0, &mut events);
|
||||
assert_eq!(game.ball.position, held_position);
|
||||
assert_eq!(game.claw.frame, 10);
|
||||
|
||||
@@ -2397,7 +2449,7 @@ mod tests {
|
||||
game.ball.position = vec2(157.0, 454.0);
|
||||
game.ball.velocity = vec2(0.0, 200.0);
|
||||
|
||||
game.update(1.0 / 60.0, 3, Controls::default());
|
||||
game.update(0.030, 3, Controls::default());
|
||||
|
||||
assert!(!game.tilted);
|
||||
assert!(game.ball.in_launcher);
|
||||
|
||||
Reference in New Issue
Block a user