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
+231
View File
@@ -0,0 +1,231 @@
//! Collision primitives recovered from the original Win16 object table.
//!
//! `FUN_1000_34ab` registers objects 1 through 175 through
//! `FUN_1008_0138`. The latter reveals the coordinate conversion: ordinary
//! objects use `(param_34, param_32 - 20)` and `(param_30, param_28 - 20)`;
//! relative objects accumulate those values from the preceding endpoint.
//! The old port used `478 - x`, which mirrored the complete table vertically.
use crate::geometry::Segment;
use macroquad::prelude::Vec2;
const WALL_BOUNCE: f32 = 0.82;
#[derive(Clone, Copy, Debug)]
pub struct TableSegment {
pub id: u8,
pub segment: Segment,
}
impl TableSegment {
const fn new(id: u8, start: Vec2, end: Vec2) -> Self {
Self {
id,
segment: Segment::new(start, end, WALL_BOUNCE),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct StaticCircle {
pub id: u8,
pub center: Vec2,
pub radius: f32,
}
impl StaticCircle {
const fn new(id: u8, center: Vec2, radius: f32) -> Self {
Self { id, center, radius }
}
}
/// 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.
pub const WALLS: &[TableSegment] = &[
TableSegment::new(1, Vec2::new(7.0, 397.0), Vec2::new(125.0, 437.0)),
TableSegment::new(2, Vec2::new(10.0, 455.0), Vec2::new(300.0, 455.0)),
TableSegment::new(3, Vec2::new(134.0, 442.0), Vec2::new(134.0, 425.0)),
TableSegment::new(5, Vec2::new(148.0, 425.0), Vec2::new(148.0, 447.0)),
TableSegment::new(7, Vec2::new(165.0, 425.0), Vec2::new(164.0, 446.0)),
TableSegment::new(9, Vec2::new(179.0, 425.0), Vec2::new(180.0, 447.0)),
TableSegment::new(10, Vec2::new(183.0, 437.0), Vec2::new(304.0, 394.0)),
TableSegment::new(11, Vec2::new(298.0, 145.0), Vec2::new(298.0, 41.0)),
TableSegment::new(12, Vec2::new(310.0, 122.0), Vec2::new(298.0, 122.0)),
TableSegment::new(14, Vec2::new(291.0, 116.0), Vec2::new(292.0, 84.0)),
TableSegment::new(15, Vec2::new(300.0, 89.0), Vec2::new(281.0, 80.0)),
TableSegment::new(16, Vec2::new(290.0, 76.0), Vec2::new(275.0, 115.0)),
TableSegment::new(18, Vec2::new(264.0, 119.0), Vec2::new(248.0, 94.0)),
TableSegment::new(19, Vec2::new(249.0, 94.0), Vec2::new(257.0, 71.0)),
TableSegment::new(20, Vec2::new(257.0, 71.0), Vec2::new(277.0, 29.0)),
TableSegment::new(21, Vec2::new(277.0, 31.0), Vec2::new(287.0, 12.0)),
TableSegment::new(22, Vec2::new(283.0, 22.0), Vec2::new(303.0, 28.0)),
TableSegment::new(23, Vec2::new(302.0, 28.0), Vec2::new(320.0, 48.0)),
TableSegment::new(24, Vec2::new(320.0, 48.0), Vec2::new(320.0, 437.0)),
TableSegment::new(25, Vec2::new(315.0, 413.0), Vec2::new(332.0, 413.0)),
TableSegment::new(27, Vec2::new(328.0, 58.0), Vec2::new(326.0, 47.0)),
TableSegment::new(28, Vec2::new(326.0, 47.0), Vec2::new(322.0, 37.0)),
TableSegment::new(29, Vec2::new(322.0, 37.0), Vec2::new(316.0, 28.0)),
TableSegment::new(30, Vec2::new(316.0, 28.0), Vec2::new(307.0, 21.0)),
TableSegment::new(31, Vec2::new(307.0, 21.0), Vec2::new(296.0, 16.0)),
TableSegment::new(32, Vec2::new(296.0, 16.0), Vec2::new(285.0, 15.0)),
TableSegment::new(33, Vec2::new(285.0, 15.0), Vec2::new(112.0, 15.0)),
TableSegment::new(34, Vec2::new(121.0, 14.0), Vec2::new(99.0, 20.0)),
TableSegment::new(35, Vec2::new(100.0, 20.0), Vec2::new(78.0, 31.0)),
TableSegment::new(36, Vec2::new(79.0, 30.0), Vec2::new(66.0, 49.0)),
TableSegment::new(37, Vec2::new(68.0, 45.0), Vec2::new(57.0, 73.0)),
TableSegment::new(38, Vec2::new(59.0, 62.0), Vec2::new(59.0, 154.0)),
TableSegment::new(39, Vec2::new(58.0, 147.0), Vec2::new(67.0, 170.0)),
TableSegment::new(41, Vec2::new(64.0, 179.0), Vec2::new(41.0, 190.0)),
TableSegment::new(43, Vec2::new(31.0, 186.0), Vec2::new(22.0, 165.0)),
TableSegment::new(44, Vec2::new(22.0, 165.0), Vec2::new(22.0, 11.0)),
TableSegment::new(45, Vec2::new(23.0, 13.0), Vec2::new(11.0, 13.0)),
TableSegment::new(46, Vec2::new(13.0, 10.0), Vec2::new(13.0, 170.0)),
TableSegment::new(47, Vec2::new(12.0, 165.0), Vec2::new(32.0, 230.0)),
TableSegment::new(49, Vec2::new(33.0, 231.0), Vec2::new(10.0, 300.0)),
TableSegment::new(50, Vec2::new(12.0, 261.0), Vec2::new(12.0, 407.0)),
TableSegment::new(55, Vec2::new(59.0, 287.0), Vec2::new(107.0, 359.0)),
TableSegment::new(57, Vec2::new(49.0, 294.0), Vec2::new(2.0, 268.0)),
TableSegment::new(59, Vec2::new(44.0, 287.0), Vec2::new(44.0, 242.0)),
TableSegment::new(61, Vec2::new(37.0, 292.0), Vec2::new(37.0, 347.0)),
TableSegment::new(62, Vec2::new(37.0, 347.0), Vec2::new(110.0, 384.0)),
TableSegment::new(63, Vec2::new(92.0, 408.0), Vec2::new(19.0, 385.0)),
TableSegment::new(64, Vec2::new(19.0, 385.0), Vec2::new(19.0, 291.0)),
TableSegment::new(66, Vec2::new(112.0, 386.0), Vec2::new(141.0, 413.0)),
TableSegment::new(68, Vec2::new(130.0, 427.0), Vec2::new(97.0, 411.0)),
TableSegment::new(70, Vec2::new(267.0, 292.0), Vec2::new(267.0, 337.0)),
TableSegment::new(72, Vec2::new(261.0, 299.0), Vec2::new(214.0, 321.0)),
TableSegment::new(74, Vec2::new(252.0, 289.0), Vec2::new(302.0, 218.0)),
TableSegment::new(76, Vec2::new(292.0, 291.0), Vec2::new(292.0, 385.0)),
TableSegment::new(77, Vec2::new(292.0, 385.0), Vec2::new(221.0, 408.0)),
TableSegment::new(78, Vec2::new(205.0, 384.0), Vec2::new(275.0, 347.0)),
TableSegment::new(79, Vec2::new(275.0, 347.0), Vec2::new(275.0, 292.0)),
TableSegment::new(81, Vec2::new(216.0, 411.0), Vec2::new(177.0, 427.0)),
TableSegment::new(83, Vec2::new(172.0, 413.0), Vec2::new(201.0, 386.0)),
TableSegment::new(84, Vec2::new(73.0, 185.0), Vec2::new(40.0, 201.0)),
TableSegment::new(85, Vec2::new(67.0, 173.0), Vec2::new(73.0, 185.0)),
TableSegment::new(86, Vec2::new(40.0, 201.0), Vec2::new(34.0, 189.0)),
TableSegment::new(90, Vec2::new(292.0, 253.0), Vec2::new(292.0, 227.0)),
TableSegment::new(91, Vec2::new(302.0, 253.0), Vec2::new(292.0, 253.0)),
TableSegment::new(92, Vec2::new(292.0, 227.0), Vec2::new(302.0, 227.0)),
TableSegment::new(93, Vec2::new(292.0, 236.0), Vec2::new(292.0, 211.0)),
TableSegment::new(94, Vec2::new(302.0, 236.0), Vec2::new(292.0, 236.0)),
TableSegment::new(95, Vec2::new(292.0, 211.0), Vec2::new(302.0, 211.0)),
TableSegment::new(96, Vec2::new(292.0, 221.0), Vec2::new(292.0, 197.0)),
TableSegment::new(97, Vec2::new(302.0, 221.0), Vec2::new(292.0, 221.0)),
TableSegment::new(98, Vec2::new(292.0, 197.0), Vec2::new(302.0, 197.0)),
TableSegment::new(99, Vec2::new(292.0, 206.0), Vec2::new(292.0, 181.0)),
TableSegment::new(100, Vec2::new(302.0, 206.0), Vec2::new(292.0, 206.0)),
TableSegment::new(101, Vec2::new(292.0, 182.0), Vec2::new(302.0, 182.0)),
TableSegment::new(102, Vec2::new(292.0, 190.0), Vec2::new(292.0, 167.0)),
TableSegment::new(103, Vec2::new(302.0, 190.0), Vec2::new(292.0, 189.0)),
TableSegment::new(104, Vec2::new(292.0, 168.0), Vec2::new(302.0, 163.0)),
TableSegment::new(105, Vec2::new(203.0, 222.0), Vec2::new(148.0, 204.0)),
TableSegment::new(107, Vec2::new(158.0, 178.0), Vec2::new(215.0, 197.0)),
TableSegment::new(109, Vec2::new(209.0, 233.0), Vec2::new(187.0, 226.0)),
TableSegment::new(110, Vec2::new(214.0, 218.0), Vec2::new(209.0, 233.0)),
TableSegment::new(111, Vec2::new(187.0, 226.0), Vec2::new(191.0, 214.0)),
TableSegment::new(112, Vec2::new(198.0, 229.0), Vec2::new(169.0, 219.0)),
TableSegment::new(113, Vec2::new(202.0, 217.0), Vec2::new(198.0, 229.0)),
TableSegment::new(114, Vec2::new(169.0, 219.0), Vec2::new(173.0, 207.0)),
TableSegment::new(115, Vec2::new(182.0, 223.0), Vec2::new(153.0, 213.0)),
TableSegment::new(116, Vec2::new(186.0, 211.0), Vec2::new(182.0, 223.0)),
TableSegment::new(117, Vec2::new(153.0, 213.0), Vec2::new(157.0, 201.0)),
TableSegment::new(118, Vec2::new(163.0, 217.0), Vec2::new(139.0, 206.0)),
TableSegment::new(119, Vec2::new(167.0, 205.0), Vec2::new(163.0, 217.0)),
TableSegment::new(120, Vec2::new(139.0, 206.0), Vec2::new(144.0, 191.0)),
TableSegment::new(121, Vec2::new(105.0, 129.0), Vec2::new(84.0, 140.0)),
TableSegment::new(122, Vec2::new(101.0, 119.0), Vec2::new(105.0, 129.0)),
TableSegment::new(123, Vec2::new(84.0, 140.0), Vec2::new(80.0, 130.0)),
TableSegment::new(124, Vec2::new(111.0, 119.0), Vec2::new(87.0, 131.0)),
TableSegment::new(126, Vec2::new(77.0, 127.0), Vec2::new(77.0, 88.0)),
TableSegment::new(127, Vec2::new(77.0, 88.0), Vec2::new(87.0, 86.0)),
TableSegment::new(128, Vec2::new(87.0, 86.0), Vec2::new(110.0, 121.0)),
TableSegment::new(156, Vec2::new(172.0, 55.0), Vec2::new(184.0, 32.0)),
TableSegment::new(158, Vec2::new(205.0, 41.0), Vec2::new(193.0, 62.0)),
TableSegment::new(160, Vec2::new(201.0, 55.0), Vec2::new(213.0, 32.0)),
TableSegment::new(162, Vec2::new(234.0, 41.0), Vec2::new(222.0, 62.0)),
TableSegment::new(164, Vec2::new(230.0, 55.0), Vec2::new(242.0, 32.0)),
TableSegment::new(166, Vec2::new(263.0, 41.0), Vec2::new(251.0, 62.0)),
TableSegment::new(168, Vec2::new(298.0, 271.0), Vec2::new(291.0, 253.0)),
TableSegment::new(169, Vec2::new(328.0, 422.0), Vec2::new(328.0, 58.0)),
TableSegment::new(170, Vec2::new(298.0, 400.0), Vec2::new(298.0, 80.0)),
TableSegment::new(171, Vec2::new(298.0, 247.0), Vec2::new(298.0, 170.0)),
TableSegment::new(173, Vec2::new(291.0, 168.0), Vec2::new(298.0, 145.0)),
];
/// Active type-1 solid circles, excluding the three scored bumpers and object
/// 174, whose position is overwritten every frame for the live ball.
pub const PASSIVE_CIRCLES: &[StaticCircle] = &[
StaticCircle::new(4, Vec2::new(141.0, 426.0), 8.0),
StaticCircle::new(8, Vec2::new(172.0, 426.0), 8.0),
StaticCircle::new(13, Vec2::new(298.0, 116.0), 7.0),
StaticCircle::new(17, Vec2::new(269.0, 114.0), 7.0),
StaticCircle::new(26, Vec2::new(257.0, 95.0), 7.0),
StaticCircle::new(40, Vec2::new(60.0, 173.0), 8.0),
StaticCircle::new(42, Vec2::new(38.0, 183.0), 8.0),
StaticCircle::new(48, Vec2::new(25.0, 231.0), 8.0),
StaticCircle::new(54, Vec2::new(53.0, 292.0), 8.0),
StaticCircle::new(56, Vec2::new(50.0, 287.0), 8.0),
StaticCircle::new(58, Vec2::new(52.0, 287.0), 8.0),
StaticCircle::new(60, Vec2::new(29.0, 294.0), 10.0),
StaticCircle::new(65, Vec2::new(103.0, 397.0), 15.0),
StaticCircle::new(67, Vec2::new(134.0, 419.0), 9.0),
StaticCircle::new(69, Vec2::new(260.0, 292.0), 8.0),
StaticCircle::new(71, Vec2::new(260.0, 291.0), 8.0),
StaticCircle::new(73, Vec2::new(261.0, 291.0), 8.0),
StaticCircle::new(75, Vec2::new(284.0, 292.0), 9.0),
StaticCircle::new(80, Vec2::new(210.0, 397.0), 15.0),
StaticCircle::new(82, Vec2::new(179.0, 419.0), 9.0),
StaticCircle::new(106, Vec2::new(156.0, 192.0), 14.0),
StaticCircle::new(108, Vec2::new(210.0, 210.0), 14.0),
StaticCircle::new(125, Vec2::new(84.0, 126.0), 8.0),
StaticCircle::new(134, Vec2::new(124.0, 76.0), 20.0),
StaticCircle::new(135, Vec2::new(100.0, 58.0), 17.0),
StaticCircle::new(136, Vec2::new(133.0, 48.0), 17.0),
StaticCircle::new(137, Vec2::new(154.0, 76.0), 17.0),
StaticCircle::new(138, Vec2::new(133.0, 105.0), 17.0),
StaticCircle::new(139, Vec2::new(100.0, 94.0), 17.0),
StaticCircle::new(155, Vec2::new(183.0, 59.0), 11.0),
StaticCircle::new(157, Vec2::new(195.0, 36.0), 11.0),
StaticCircle::new(159, Vec2::new(212.0, 59.0), 11.0),
StaticCircle::new(161, Vec2::new(224.0, 36.0), 11.0),
StaticCircle::new(163, Vec2::new(241.0, 59.0), 11.0),
StaticCircle::new(165, Vec2::new(253.0, 36.0), 11.0),
StaticCircle::new(167, Vec2::new(299.0, 248.0), 9.0),
StaticCircle::new(172, Vec2::new(299.0, 171.0), 9.0),
];
pub const BUMPERS: [StaticCircle; 3] = [
StaticCircle::new(51, Vec2::new(165.0, 148.0), 23.0),
StaticCircle::new(52, Vec2::new(206.0, 110.0), 23.0),
StaticCircle::new(53, Vec2::new(219.0, 165.0), 23.0),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recovered_object_inventory_is_complete() {
assert_eq!(WALLS.len(), 109);
assert_eq!(PASSIVE_CIRCLES.len(), 37);
assert_eq!(BUMPERS.len(), 3);
}
#[test]
fn recovered_landmarks_match_the_visible_table() {
let left_flipper_post = PASSIVE_CIRCLES
.iter()
.find(|circle| circle.id == 65)
.expect("object 65 should be the left flipper post");
assert_eq!(left_flipper_post.center, Vec2::new(103.0, 397.0));
let shooter_wall = WALLS
.iter()
.find(|wall| wall.id == 169)
.expect("object 169 should be the outside shooter wall");
assert_eq!(shooter_wall.segment.start, Vec2::new(328.0, 422.0));
assert_eq!(shooter_wall.segment.end, Vec2::new(328.0, 58.0));
}
}