Files
tdkpin/tdkpin-rs/src/table.rs
T
ddidderr c2f1443436
Build TDK Pinball / build (macos-latest) (push) Canceled after 0s
Build TDK Pinball / build (ubuntu-latest) (push) Canceled after 0s
Build TDK Pinball / build (windows-latest) (push) Canceled after 0s
fmt: just fmt (rust only)
2026-08-29 09:57:39 +02:00

522 lines
23 KiB
Rust

//! 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.
//! Circle sizes are already center-contact extents: `FUN_1008_0138` expands
//! each object's bounds by `param_26`. They must not be enlarged again by the
//! radius of the rendered ball.
use macroquad::prelude::Vec2;
use crate::geometry::Segment;
#[derive(Clone, Copy, Debug)]
pub struct TableSegment {
pub id: u8,
pub segment: Segment,
pub normal_rebound: f32,
pub tangent_coupling: f32,
pub response_auxiliary: f32,
pub response_kick: f32,
pub score: u32,
pub flags: u16,
pub contact_group: u8,
pub layer_mask: u8,
}
impl TableSegment {
const fn new(id: u8, start: Vec2, end: Vec2) -> Self {
// These are the first two Borland Real48 fields in each original
// 0x53-byte collision record. They control normal rebound and tangent
// coupling in the type-2 response basis.
let (normal_rebound, tangent_coupling) = match id {
3 | 9 | 25 => (0.10, 0.10),
91 | 92 | 94 | 95 | 97 | 98 | 100 | 101 | 103 | 104 => (0.40, 0.0),
62 => (0.40, 0.05),
15 | 16 | 45 => (0.40, 0.10),
78 => (0.50, 0.05),
55 | 57 | 59 | 61 | 70 | 72 | 74 | 76 => (0.50, 0.10),
66 | 68 | 81 | 83 => (0.50, 0.20),
84 | 85 | 86 | 87 | 88 | 90 | 93 | 96 | 99 | 102 | 109 | 110 | 111 | 112 | 113
| 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 168 | 170 | 171 | 173 => {
(0.60, 0.0)
}
27 | 28 | 29 | 30 | 31 | 32 | 105 => (0.60, 0.20),
_ => (0.60, 0.10),
};
let score = match id {
12 => 5_000,
45 => 100_000,
84 | 90..=99 | 102 | 109 | 112 | 115 | 118 => 1_500,
121 => 2_000,
_ => 0,
};
let flags = match id {
84 => 0x0004,
90..=104 => 0x208a,
109 | 112 | 115 | 118 => 0x2092,
121 => 0x0400,
_ => 0,
};
let contact_group = match id {
90..=92 => 90,
93..=95 => 93,
96..=98 => 96,
99..=101 => 99,
102..=104 => 102,
109 => 109,
112 => 112,
115 => 115,
118 => 118,
_ => 0,
};
let (response_auxiliary, response_kick) = match id {
55 | 74 => (-0.40, 0.40),
107 => (-0.40, 0.30),
_ => (0.0, 0.0),
};
Self {
id,
segment: Segment::new(start, end, normal_rebound),
normal_rebound,
tangent_coupling,
response_auxiliary,
response_kick,
score,
flags,
contact_group,
layer_mask: layer_mask_for_record(id),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct StaticCircle {
pub id: u8,
pub center: Vec2,
pub contact_radius: f32,
pub normal_rebound: f32,
pub tangent_coupling: f32,
pub normal_kick: f32,
pub layer_mask: u8,
}
#[derive(Clone, Copy, Debug)]
pub struct TargetSensor {
pub id: u8,
pub center: Vec2,
pub radius: f32,
pub score: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct MechanismSensor {
pub id: u8,
pub center: Vec2,
pub radius: f32,
}
impl MechanismSensor {
const fn new(id: u8, center: Vec2, radius: f32) -> Self {
Self { id, center, radius }
}
}
impl TargetSensor {
const fn new(id: u8, center: Vec2, radius: f32, score: u32) -> Self {
Self {
id,
center,
radius,
score,
}
}
}
impl StaticCircle {
const fn new(id: u8, center: Vec2, contact_radius: f32) -> Self {
let (normal_rebound, tangent_coupling, normal_kick) = match id {
54 | 56 | 58 | 65 | 69 | 71 | 73 => (0.50, 0.10, 0.0),
67 | 80 | 82 => (0.50, 0.20, 0.0),
167 | 172 => (0.60, 0.0, 0.0),
51..=53 => (0.80, 0.10, 0.40),
_ => (0.60, 0.10, 0.0),
};
Self {
id,
center,
contact_radius,
normal_rebound,
tangent_coupling,
normal_kick,
layer_mask: layer_mask_for_record(id),
}
}
}
const fn layer_mask_for_record(id: u8) -> u8 {
match id {
11 | 24 | 49 | 84..=120 | 167..=175 => 0x03,
1..=10 | 25 | 50 | 54..=83 | 153 | 154 => 0x02,
_ => 0x01,
}
}
/// 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(97.0, 366.0), Vec2::new(50.0, 340.0)),
TableSegment::new(59, Vec2::new(45.0, 333.0), Vec2::new(45.0, 288.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, 344.0), Vec2::new(214.0, 366.0)),
TableSegment::new(74, Vec2::new(205.0, 356.0), Vec2::new(255.0, 285.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(98.0, 359.0), 8.0),
StaticCircle::new(58, Vec2::new(53.0, 333.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, 336.0), 8.0),
StaticCircle::new(73, Vec2::new(214.0, 358.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),
];
/// Initially active type-4 circle records. Objects 149, 153, and 154 are
/// mechanism-controlled and begin disabled in the original table.
pub const TARGET_SENSORS: [TargetSensor; 11] = [
TargetSensor::new(140, Vec2::new(24.0, 182.0), 12.0, 1_000),
TargetSensor::new(141, Vec2::new(18.0, 162.0), 12.0, 1_000),
TargetSensor::new(142, Vec2::new(17.0, 144.0), 12.0, 1_500),
TargetSensor::new(143, Vec2::new(17.0, 125.0), 12.0, 1_500),
TargetSensor::new(144, Vec2::new(17.0, 106.0), 12.0, 2_000),
TargetSensor::new(145, Vec2::new(17.0, 87.0), 12.0, 2_000),
TargetSensor::new(146, Vec2::new(17.0, 67.0), 12.0, 2_500),
TargetSensor::new(147, Vec2::new(17.0, 48.0), 12.0, 3_000),
TargetSensor::new(150, Vec2::new(205.0, 47.0), 11.0, 500),
TargetSensor::new(151, Vec2::new(233.0, 46.0), 11.0, 500),
TargetSensor::new(152, Vec2::new(264.0, 46.0), 11.0, 500),
];
pub const LOCK_HOLES: [MechanismSensor; 5] = [
MechanismSensor::new(129, Vec2::new(96.0, 76.0), 19.0),
MechanismSensor::new(130, Vec2::new(115.0, 49.0), 19.0),
MechanismSensor::new(131, Vec2::new(147.0, 60.0), 19.0),
MechanismSensor::new(132, Vec2::new(147.0, 92.0), 19.0),
MechanismSensor::new(133, Vec2::new(115.0, 103.0), 19.0),
];
/// Record 148 arms the special/effect-seven path without clearing lock holes.
pub const SPECIAL_HOLE_SENSOR: MechanismSensor =
MechanismSensor::new(148, Vec2::new(17.0, 23.0), 19.0);
pub const EFFECT_SENSOR: TargetSensor = TargetSensor::new(149, Vec2::new(68.0, 65.0), 16.0, 500);
#[cfg(test)]
mod tests {
use super::*;
use crate::original_physics::{MilliVec, collide_with_circle, collide_with_line};
#[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_layer_masks_cover_upper_lower_and_shared_records() {
assert_eq!(layer_mask_for_record(48), 0x01);
assert_eq!(layer_mask_for_record(50), 0x02);
assert_eq!(layer_mask_for_record(49), 0x03);
assert_eq!(layer_mask_for_record(89), 0x03);
assert_eq!(layer_mask_for_record(140), 0x01);
assert_eq!(layer_mask_for_record(153), 0x02);
assert_eq!(layer_mask_for_record(174), 0x03);
}
#[test]
fn recovered_auxiliary_kick_records_keep_both_coefficients() {
for (id, expected_kick) in [(55, 0.40_f32), (74, 0.40), (107, 0.30)] {
let wall = WALLS
.iter()
.find(|wall| wall.id == id)
.expect("the kicked line record must be present");
assert_eq!(wall.response_auxiliary.to_bits(), (-0.40_f32).to_bits());
assert_eq!(wall.response_kick.to_bits(), expected_kick.to_bits());
}
for bumper in BUMPERS {
assert_eq!(bumper.normal_kick.to_bits(), 0.40_f32.to_bits());
}
}
#[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));
}
#[test]
fn relative_slingshot_chains_match_the_initialized_record_ledger() {
for (id, expected_start, expected_end) in [
(55, Vec2::new(59.0, 287.0), Vec2::new(107.0, 359.0)),
(57, Vec2::new(97.0, 366.0), Vec2::new(50.0, 340.0)),
(59, Vec2::new(45.0, 333.0), Vec2::new(45.0, 288.0)),
(70, Vec2::new(267.0, 292.0), Vec2::new(267.0, 337.0)),
(72, Vec2::new(261.0, 344.0), Vec2::new(214.0, 366.0)),
(74, Vec2::new(205.0, 356.0), Vec2::new(255.0, 285.0)),
] {
let wall = WALLS
.iter()
.find(|wall| wall.id == id)
.expect("relative line record must be present");
assert_eq!(
(wall.segment.start, wall.segment.end),
(expected_start, expected_end)
);
}
for (id, expected_center) in [
(54, Vec2::new(53.0, 292.0)),
(56, Vec2::new(98.0, 359.0)),
(58, Vec2::new(53.0, 333.0)),
(69, Vec2::new(260.0, 292.0)),
(71, Vec2::new(260.0, 336.0)),
(73, Vec2::new(214.0, 358.0)),
] {
let circle = PASSIVE_CIRCLES
.iter()
.find(|circle| circle.id == id)
.expect("relative circle record must be present");
assert_eq!(circle.center, expected_center);
}
}
#[test]
fn every_recovered_wall_has_its_registered_one_sided_contact() {
for wall in WALLS {
let line = wall.segment.end - wall.segment.start;
assert!(
line.length_squared() > 0.0,
"object {} is degenerate",
wall.id
);
let normal = Vec2::new(-line.y, line.x).normalize();
let midpoint = (wall.segment.start + wall.segment.end) * 0.5;
let mut front_velocity = MilliVec::from_velocity_per_second(normal * 200.0);
assert!(
collide_with_line(
MilliVec::from_position(midpoint - normal),
&mut front_velocity,
wall.segment.start,
wall.segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
),
"object {} missed its registered front side",
wall.id
);
let mut back_velocity = MilliVec::from_velocity_per_second(-normal * 200.0);
assert!(
!collide_with_line(
MilliVec::from_position(midpoint + normal),
&mut back_velocity,
wall.segment.start,
wall.segment.end,
f64::from(wall.normal_rebound),
f64::from(wall.tangent_coupling),
),
"object {} accepted contact from its back side",
wall.id
);
}
}
#[test]
fn every_recovered_circle_uses_its_fixed_point_response() {
for circle in PASSIVE_CIRCLES.iter().chain(BUMPERS.iter()) {
let old_position = MilliVec::from_position(
circle.center + Vec2::new(circle.contact_radius + 1.0, 0.0),
);
let mut velocity = MilliVec { x: -2_000, y: 0 };
let hit = collide_with_circle(
old_position,
&mut velocity,
circle.center,
circle.contact_radius,
f64::from(circle.normal_rebound),
f64::from(circle.tangent_coupling),
f64::from(circle.normal_kick),
);
assert!(hit, "object {} missed an entering path", circle.id);
assert!(
velocity.x > 0,
"object {} did not reflect inward velocity",
circle.id
);
assert!(
old_position.add(velocity).x > old_position.x,
"object {} did not advance away from its surface",
circle.id
);
}
}
}