fix(game): restore the recovered collision table

Replace the mirrored, partial wall transcription with all active static line
and circle objects from the Win16 registration table. Preserve the original
object IDs for drain and shooter-stop handling, and use bounded physics
substeps so fast balls cannot skip thin rails.

Test Plan:
- cargo fmt --all -- --check
- cargo test
- cargo clippy --all-targets --all-features -- -D warnings
- git diff --check
This commit is contained in:
2026-08-22 17:27:34 +02:00
parent 4bc370be12
commit f904977ae4
5 changed files with 342 additions and 180 deletions
+102 -171
View File
@@ -1,6 +1,8 @@
use crate::geometry::{Segment, circle_collision, segment_collision};
use crate::{
geometry::{Segment, circle_collision, segment_collision},
table::{BUMPERS, PASSIVE_CIRCLES, WALLS},
};
use macroquad::prelude::{Vec2, vec2};
use std::sync::LazyLock;
const BALL_RADIUS: f32 = 7.0;
const GRAVITY: f32 = 135.0;
@@ -228,59 +230,91 @@ impl Game {
return;
}
self.ball.velocity.y += GRAVITY * dt;
self.ball.velocity *= 1.0 - dt * 0.055;
self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED);
self.ball.position += self.ball.velocity * dt;
let travel = self.ball.velocity.length() * dt;
let maximum_step_travel = BALL_RADIUS * 0.45;
let mut substeps = 1_u8;
while f32::from(substeps) * maximum_step_travel < travel && substeps < 12 {
substeps += 1;
}
let substep = dt / f32::from(substeps);
for _ in 0..substeps {
self.ball.velocity.y += GRAVITY * substep;
self.ball.velocity *= 1.0 - substep * 0.055;
self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED);
self.ball.position += self.ball.velocity * substep;
for wall in table_walls() {
segment_collision(
let mut drained = false;
for wall in WALLS {
// Object 25 is the one-way shooter stop. It catches a returning
// ball, but must not reflect an upward launch from the line.
if wall.id == 25 && self.ball.velocity.y < 0.0 {
continue;
}
let hit = segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS,
wall.segment,
);
if hit && wall.id == 2 {
drained = true;
break;
}
}
if drained {
self.drain(events);
return;
}
for circle in PASSIVE_CIRCLES {
debug_assert_ne!(circle.id, 174, "the live ball is not a static obstacle");
circle_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS,
circle.center,
circle.radius,
0.0,
);
}
let left_flipper = flipper_segment(vec2(103.0, 397.0), self.left_flipper_angle);
let right_flipper = flipper_segment(vec2(210.0, 397.0), self.right_flipper_angle);
if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS,
*wall,
);
}
let left_flipper = flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle);
let right_flipper = flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle);
if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS + 2.0,
left_flipper,
) && self.left_flipper_angle > -2.55
{
self.ball.velocity += vec2(-20.0, -115.0);
}
if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS + 2.0,
right_flipper,
) && self.right_flipper_angle < -0.58
{
self.ball.velocity += vec2(20.0, -115.0);
}
for (index, center) in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)]
.into_iter()
.enumerate()
{
let hit = circle_collision(
BALL_RADIUS + 2.0,
left_flipper,
) && self.left_flipper_angle > -2.55
{
self.ball.velocity += vec2(-20.0, -115.0);
}
if segment_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS,
center,
14.0,
105.0,
);
if hit && !self.tilted && self.bumper_cooldown <= 0.0 {
self.add_score(self.player().bumper_value);
self.bonus = self.bonus.saturating_add(100);
self.bumper_cooldown = 0.08;
self.bumper_flash[index] = 0.16;
events.push(Event::Bumper);
BALL_RADIUS + 2.0,
right_flipper,
) && self.right_flipper_angle < -0.58
{
self.ball.velocity += vec2(20.0, -115.0);
}
for (index, bumper) in BUMPERS.into_iter().enumerate() {
let hit = circle_collision(
&mut self.ball.position,
&mut self.ball.velocity,
BALL_RADIUS,
bumper.center,
bumper.radius,
105.0,
);
if hit && !self.tilted && self.bumper_cooldown <= 0.0 {
self.add_score(self.player().bumper_value);
self.bonus = self.bonus.saturating_add(100);
self.bumper_cooldown = 0.08;
self.bumper_flash[index] = 0.16;
events.push(Event::Bumper);
}
}
}
@@ -487,8 +521,8 @@ impl Game {
pub fn flippers(&self) -> (Segment, Segment) {
(
flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle),
flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle),
flipper_segment(vec2(103.0, 397.0), self.left_flipper_angle),
flipper_segment(vec2(210.0, 397.0), self.right_flipper_angle),
)
}
}
@@ -498,117 +532,6 @@ fn flipper_segment(pivot: Vec2, angle: f32) -> Segment {
}
#[allow(clippy::too_many_lines)]
fn table_walls() -> &'static [Segment] {
static WALLS: LazyLock<Vec<Segment>> = LazyLock::new(|| {
vec![
Segment::new(Vec2::new(125.0, 21.0), Vec2::new(7.0, 61.0), 0.82),
Segment::new(Vec2::new(300.0, 3.0), Vec2::new(10.0, 3.0), 0.82),
Segment::new(Vec2::new(120.0, 6.0), Vec2::new(120.0, 26.0), 0.82),
Segment::new(Vec2::new(195.0, 26.0), Vec2::new(195.0, 6.0), 0.82),
Segment::new(Vec2::new(134.0, 33.0), Vec2::new(134.0, 16.0), 0.82),
Segment::new(Vec2::new(148.0, 11.0), Vec2::new(148.0, 33.0), 0.82),
Segment::new(Vec2::new(164.0, 12.0), Vec2::new(165.0, 33.0), 0.82),
Segment::new(Vec2::new(180.0, 11.0), Vec2::new(179.0, 33.0), 0.82),
Segment::new(Vec2::new(304.0, 64.0), Vec2::new(183.0, 21.0), 0.82),
Segment::new(Vec2::new(298.0, 417.0), Vec2::new(298.0, 313.0), 0.82),
Segment::new(Vec2::new(298.0, 336.0), Vec2::new(310.0, 336.0), 0.82),
Segment::new(Vec2::new(292.0, 374.0), Vec2::new(291.0, 342.0), 0.82),
Segment::new(Vec2::new(281.0, 378.0), Vec2::new(300.0, 369.0), 0.82),
Segment::new(Vec2::new(275.0, 343.0), Vec2::new(290.0, 382.0), 0.82),
Segment::new(Vec2::new(248.0, 364.0), Vec2::new(264.0, 339.0), 0.82),
Segment::new(Vec2::new(257.0, 387.0), Vec2::new(249.0, 364.0), 0.82),
Segment::new(Vec2::new(277.0, 429.0), Vec2::new(257.0, 387.0), 0.82),
Segment::new(Vec2::new(287.0, 446.0), Vec2::new(277.0, 427.0), 0.82),
Segment::new(Vec2::new(303.0, 430.0), Vec2::new(283.0, 436.0), 0.82),
Segment::new(Vec2::new(320.0, 410.0), Vec2::new(302.0, 430.0), 0.82),
Segment::new(Vec2::new(320.0, 21.0), Vec2::new(320.0, 410.0), 0.82),
Segment::new(Vec2::new(307.0, 437.0), Vec2::new(316.0, 430.0), 0.82),
Segment::new(Vec2::new(296.0, 442.0), Vec2::new(307.0, 437.0), 0.82),
Segment::new(Vec2::new(285.0, 443.0), Vec2::new(296.0, 442.0), 0.82),
// Preserve the recovered lower apron on either side of the center drain.
Segment::new(Vec2::new(112.0, 443.0), Vec2::new(141.0, 443.0), 0.82),
Segment::new(Vec2::new(173.0, 443.0), Vec2::new(285.0, 443.0), 0.82),
Segment::new(Vec2::new(99.0, 438.0), Vec2::new(121.0, 444.0), 0.82),
Segment::new(Vec2::new(78.0, 427.0), Vec2::new(100.0, 438.0), 0.82),
Segment::new(Vec2::new(66.0, 409.0), Vec2::new(79.0, 428.0), 0.82),
Segment::new(Vec2::new(57.0, 385.0), Vec2::new(68.0, 413.0), 0.82),
Segment::new(Vec2::new(59.0, 304.0), Vec2::new(59.0, 396.0), 0.82),
Segment::new(Vec2::new(67.0, 288.0), Vec2::new(58.0, 311.0), 0.82),
Segment::new(Vec2::new(41.0, 268.0), Vec2::new(64.0, 279.0), 0.82),
Segment::new(Vec2::new(22.0, 293.0), Vec2::new(31.0, 272.0), 0.82),
Segment::new(Vec2::new(22.0, 447.0), Vec2::new(22.0, 293.0), 0.82),
Segment::new(Vec2::new(11.0, 445.0), Vec2::new(23.0, 445.0), 0.82),
Segment::new(Vec2::new(13.0, 288.0), Vec2::new(13.0, 448.0), 0.82),
Segment::new(Vec2::new(32.0, 228.0), Vec2::new(12.0, 293.0), 0.82),
Segment::new(Vec2::new(10.0, 158.0), Vec2::new(33.0, 227.0), 0.82),
Segment::new(Vec2::new(12.0, 51.0), Vec2::new(12.0, 197.0), 0.82),
Segment::new(Vec2::new(37.0, 111.0), Vec2::new(37.0, 166.0), 0.82),
Segment::new(Vec2::new(110.0, 74.0), Vec2::new(37.0, 111.0), 0.82),
Segment::new(Vec2::new(19.0, 73.0), Vec2::new(92.0, 50.0), 0.82),
Segment::new(Vec2::new(19.0, 167.0), Vec2::new(19.0, 73.0), 0.82),
Segment::new(Vec2::new(141.0, 45.0), Vec2::new(112.0, 72.0), 0.82),
Segment::new(Vec2::new(97.0, 47.0), Vec2::new(130.0, 31.0), 0.82),
Segment::new(Vec2::new(0.0, 433.0), Vec2::new(7.0, 478.0), 0.82),
Segment::new(Vec2::new(292.0, 73.0), Vec2::new(292.0, 167.0), 0.82),
Segment::new(Vec2::new(221.0, 50.0), Vec2::new(292.0, 73.0), 0.82),
Segment::new(Vec2::new(275.0, 111.0), Vec2::new(205.0, 74.0), 0.82),
Segment::new(Vec2::new(275.0, 166.0), Vec2::new(275.0, 111.0), 0.82),
Segment::new(Vec2::new(177.0, 31.0), Vec2::new(216.0, 47.0), 0.82),
Segment::new(Vec2::new(201.0, 72.0), Vec2::new(172.0, 45.0), 0.82),
Segment::new(Vec2::new(40.0, 257.0), Vec2::new(73.0, 273.0), 0.82),
Segment::new(Vec2::new(73.0, 273.0), Vec2::new(67.0, 285.0), 0.82),
Segment::new(Vec2::new(34.0, 269.0), Vec2::new(40.0, 257.0), 0.82),
Segment::new(Vec2::new(292.0, 231.0), Vec2::new(292.0, 205.0), 0.82),
Segment::new(Vec2::new(292.0, 205.0), Vec2::new(302.0, 205.0), 0.82),
Segment::new(Vec2::new(302.0, 231.0), Vec2::new(292.0, 231.0), 0.82),
Segment::new(Vec2::new(292.0, 247.0), Vec2::new(292.0, 222.0), 0.82),
Segment::new(Vec2::new(292.0, 222.0), Vec2::new(302.0, 222.0), 0.82),
Segment::new(Vec2::new(302.0, 247.0), Vec2::new(292.0, 247.0), 0.82),
Segment::new(Vec2::new(292.0, 261.0), Vec2::new(292.0, 237.0), 0.82),
Segment::new(Vec2::new(292.0, 237.0), Vec2::new(302.0, 237.0), 0.82),
Segment::new(Vec2::new(302.0, 261.0), Vec2::new(292.0, 261.0), 0.82),
Segment::new(Vec2::new(292.0, 277.0), Vec2::new(292.0, 252.0), 0.82),
Segment::new(Vec2::new(292.0, 252.0), Vec2::new(302.0, 252.0), 0.82),
Segment::new(Vec2::new(302.0, 276.0), Vec2::new(292.0, 276.0), 0.82),
Segment::new(Vec2::new(292.0, 291.0), Vec2::new(292.0, 268.0), 0.82),
Segment::new(Vec2::new(292.0, 269.0), Vec2::new(302.0, 268.0), 0.82),
Segment::new(Vec2::new(302.0, 295.0), Vec2::new(292.0, 290.0), 0.82),
Segment::new(Vec2::new(148.0, 254.0), Vec2::new(203.0, 236.0), 0.82),
Segment::new(Vec2::new(215.0, 261.0), Vec2::new(158.0, 280.0), 0.82),
Segment::new(Vec2::new(187.0, 232.0), Vec2::new(209.0, 225.0), 0.82),
Segment::new(Vec2::new(209.0, 225.0), Vec2::new(214.0, 240.0), 0.82),
Segment::new(Vec2::new(191.0, 244.0), Vec2::new(187.0, 232.0), 0.82),
Segment::new(Vec2::new(169.0, 239.0), Vec2::new(198.0, 229.0), 0.82),
Segment::new(Vec2::new(198.0, 229.0), Vec2::new(202.0, 241.0), 0.82),
Segment::new(Vec2::new(173.0, 251.0), Vec2::new(169.0, 239.0), 0.82),
Segment::new(Vec2::new(153.0, 245.0), Vec2::new(182.0, 235.0), 0.82),
Segment::new(Vec2::new(182.0, 235.0), Vec2::new(186.0, 247.0), 0.82),
Segment::new(Vec2::new(157.0, 257.0), Vec2::new(153.0, 245.0), 0.82),
Segment::new(Vec2::new(139.0, 252.0), Vec2::new(163.0, 241.0), 0.82),
Segment::new(Vec2::new(163.0, 241.0), Vec2::new(167.0, 253.0), 0.82),
Segment::new(Vec2::new(144.0, 267.0), Vec2::new(139.0, 252.0), 0.82),
Segment::new(Vec2::new(84.0, 318.0), Vec2::new(105.0, 329.0), 0.82),
Segment::new(Vec2::new(105.0, 329.0), Vec2::new(101.0, 339.0), 0.82),
Segment::new(Vec2::new(80.0, 328.0), Vec2::new(84.0, 318.0), 0.82),
Segment::new(Vec2::new(87.0, 327.0), Vec2::new(111.0, 339.0), 0.82),
Segment::new(Vec2::new(77.0, 370.0), Vec2::new(77.0, 331.0), 0.82),
Segment::new(Vec2::new(87.0, 372.0), Vec2::new(77.0, 370.0), 0.82),
Segment::new(Vec2::new(110.0, 337.0), Vec2::new(87.0, 372.0), 0.82),
Segment::new(Vec2::new(184.0, 426.0), Vec2::new(172.0, 403.0), 0.82),
Segment::new(Vec2::new(193.0, 396.0), Vec2::new(205.0, 417.0), 0.82),
Segment::new(Vec2::new(213.0, 426.0), Vec2::new(201.0, 403.0), 0.82),
Segment::new(Vec2::new(222.0, 396.0), Vec2::new(234.0, 417.0), 0.82),
Segment::new(Vec2::new(242.0, 426.0), Vec2::new(230.0, 403.0), 0.82),
Segment::new(Vec2::new(251.0, 396.0), Vec2::new(263.0, 417.0), 0.82),
Segment::new(Vec2::new(291.0, 205.0), Vec2::new(298.0, 187.0), 0.82),
Segment::new(Vec2::new(298.0, 378.0), Vec2::new(298.0, 58.0), 0.82),
Segment::new(Vec2::new(298.0, 288.0), Vec2::new(298.0, 211.0), 0.82),
Segment::new(Vec2::new(298.0, 313.0), Vec2::new(291.0, 290.0), 0.82),
]
});
&WALLS
}
#[cfg(test)]
mod tests {
use super::*;
@@ -661,21 +584,29 @@ mod tests {
fn stalled_ball_search_restores_motion() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = vec2(220.0, 437.0);
game.ball.position = vec2(250.0, 300.0);
game.ball.velocity = Vec2::ZERO;
game.stalled_for = BALL_SEARCH_DELAY - 1.0 / 120.0;
let mut events = Vec::new();
for _ in 0..600 {
events.extend(game.update(1.0 / 120.0, 3, Controls::default()));
if events.contains(&Event::BallSearch) {
break;
}
}
let events = game.update(1.0 / 120.0, 3, Controls::default());
assert!(events.contains(&Event::BallSearch));
assert!(game.ball.velocity.y < 0.0);
}
#[test]
fn fast_ball_cannot_tunnel_through_the_top_rail() {
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, -MAX_SPEED);
game.fixed_update(1.0 / 30.0, &mut Vec::new());
assert!(game.ball.position.y >= 15.0 + BALL_RADIUS - 0.01);
assert!(game.ball.velocity.y > 0.0);
}
#[test]
fn tilt_latches_and_disables_flippers_and_scoring() {
let mut game = Game::new(1);