1use crate::{SubtrActorError, SubtrActorErrorVariant, SubtrActorResult};
2
3pub fn vec_to_glam(v: &boxcars::Vector3f) -> glam::f32::Vec3 {
4 glam::f32::Vec3::new(v.x, v.y, v.z)
5}
6
7pub fn glam_to_vec(v: &glam::f32::Vec3) -> boxcars::Vector3f {
8 boxcars::Vector3f {
9 x: v.x,
10 y: v.y,
11 z: v.z,
12 }
13}
14
15pub fn quat_to_glam(q: &boxcars::Quaternion) -> glam::Quat {
16 let rotation = glam::Quat::from_xyzw(q.x, q.y, q.z, q.w);
17 if rotation.x.is_finite()
18 && rotation.y.is_finite()
19 && rotation.z.is_finite()
20 && rotation.w.is_finite()
21 && rotation.length_squared() > 0.0
22 {
23 rotation.normalize()
24 } else {
25 glam::Quat::IDENTITY
26 }
27}
28
29pub fn glam_to_quat(rotation: &glam::Quat) -> boxcars::Quaternion {
30 boxcars::Quaternion {
31 x: rotation.x,
32 y: rotation.y,
33 z: rotation.z,
34 w: rotation.w,
35 }
36}
37
38pub fn apply_velocities_to_rigid_body(
39 rigid_body: &boxcars::RigidBody,
40 time_delta: f32,
41) -> boxcars::RigidBody {
42 let mut interpolated = *rigid_body;
43 if time_delta == 0.0 {
44 return interpolated;
45 }
46 let linear_velocity = interpolated.linear_velocity.unwrap_or(boxcars::Vector3f {
47 x: 0.0,
48 y: 0.0,
49 z: 0.0,
50 });
51 let location = vec_to_glam(&rigid_body.location) + (time_delta * vec_to_glam(&linear_velocity));
52 interpolated.location = glam_to_vec(&location);
53 interpolated.rotation = apply_angular_velocity(rigid_body, time_delta);
54 interpolated
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum CarHitboxFamily {
59 Breakout,
60 Dominus,
61 Hybrid,
62 Merc,
63 Octane,
64 Plank,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct CarHitbox {
69 pub family: CarHitboxFamily,
70 pub length: f32,
71 pub width: f32,
72 pub height: f32,
73 pub angle: f32,
74 pub front_height: f32,
75 pub back_height: f32,
76 pub offset: f32,
77 pub elevation: f32,
78}
79
80impl CarHitbox {
81 const DEFAULT_OFFSET: f32 = 13.88;
82 const DEFAULT_ELEVATION: f32 = 17.05;
83
84 const fn from_preset(
85 family: CarHitboxFamily,
86 length: f32,
87 width: f32,
88 height: f32,
89 angle: f32,
90 front_height: f32,
91 back_height: f32,
92 ) -> Self {
93 Self {
94 family,
95 length,
96 width,
97 height,
98 angle,
99 front_height,
100 back_height,
101 offset: Self::DEFAULT_OFFSET,
102 elevation: Self::DEFAULT_ELEVATION,
103 }
104 }
105
106 pub const fn breakout() -> Self {
107 Self::from_preset(
108 CarHitboxFamily::Breakout,
109 131.4924,
110 80.521,
111 30.3,
112 -0.9795,
113 43.8976,
114 46.1454,
115 )
116 }
117
118 pub const fn dominus() -> Self {
119 Self::from_preset(
120 CarHitboxFamily::Dominus,
121 127.9268,
122 83.27995,
123 31.3,
124 -0.9635,
125 47.2238,
126 49.3749,
127 )
128 }
129
130 pub const fn hybrid() -> Self {
131 Self::from_preset(
132 CarHitboxFamily::Hybrid,
133 127.0192,
134 82.18787,
135 34.15907,
136 -0.5499,
137 54.0982,
138 55.3173,
139 )
140 }
141
142 pub const fn merc() -> Self {
143 Self::from_preset(
144 CarHitboxFamily::Merc,
145 120.72,
146 76.71,
147 41.66,
148 0.28,
149 60.76,
150 61.35,
151 )
152 }
153
154 pub const fn octane() -> Self {
155 Self::from_preset(
156 CarHitboxFamily::Octane,
157 118.0074,
158 84.19941,
159 36.15907,
160 -0.5518,
161 55.1449,
162 56.2814,
163 )
164 }
165
166 pub const fn plank() -> Self {
167 Self::from_preset(
168 CarHitboxFamily::Plank,
169 128.8198,
170 84.67036,
171 29.3944,
172 -0.3447,
173 44.998,
174 45.773,
175 )
176 }
177
178 pub const fn for_family(family: CarHitboxFamily) -> Self {
179 match family {
180 CarHitboxFamily::Breakout => Self::breakout(),
181 CarHitboxFamily::Dominus => Self::dominus(),
182 CarHitboxFamily::Hybrid => Self::hybrid(),
183 CarHitboxFamily::Merc => Self::merc(),
184 CarHitboxFamily::Octane => Self::octane(),
185 CarHitboxFamily::Plank => Self::plank(),
186 }
187 }
188}
189
190pub fn default_car_hitbox() -> CarHitbox {
191 CarHitbox::octane()
192}
193
194pub const BALL_COLLISION_RADIUS: f32 = 92.75;
195
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct TouchCandidateScoring {
198 pub strict_contact_gap_threshold: f32,
199 pub relaxed_contact_gap_threshold: f32,
200 pub strict_contact_min_position_deviation: f32,
201 pub relaxed_contact_min_position_deviation: f32,
202 pub strict_contact_min_velocity_deviation: f32,
203 pub relaxed_contact_min_velocity_deviation: f32,
204 pub relaxed_contact_score_penalty: f32,
205 pub dodge_contact_score_bonus: f32,
206 pub simultaneous_touch_score_margin: f32,
207 pub contested_touch_score_margin: f32,
208}
209
210impl TouchCandidateScoring {
211 pub const DEFAULT: Self = Self {
212 strict_contact_gap_threshold: 5.0,
213 relaxed_contact_gap_threshold: 25.0,
214 strict_contact_min_position_deviation: 25.0,
215 relaxed_contact_min_position_deviation: 500.0,
216 strict_contact_min_velocity_deviation: 50.0,
217 relaxed_contact_min_velocity_deviation: 1000.0,
218 relaxed_contact_score_penalty: 100.0,
219 dodge_contact_score_bonus: 1.0,
220 simultaneous_touch_score_margin: 5.0,
221 contested_touch_score_margin: 5.0,
222 };
223
224 pub fn accepts_contact_gap(
225 self,
226 closest_contact_gap: f32,
227 position_deviation: f32,
228 velocity_deviation: f32,
229 ) -> bool {
230 (closest_contact_gap <= self.strict_contact_gap_threshold
231 && (position_deviation >= self.strict_contact_min_position_deviation
232 || velocity_deviation >= self.strict_contact_min_velocity_deviation))
233 || (closest_contact_gap <= self.relaxed_contact_gap_threshold
234 && (position_deviation >= self.relaxed_contact_min_position_deviation
235 || velocity_deviation >= self.relaxed_contact_min_velocity_deviation))
236 }
237
238 pub fn score_contact_gap(self, closest_contact_gap: f32, dodge_contact: bool) -> f32 {
239 let relaxed_penalty = if closest_contact_gap > self.strict_contact_gap_threshold {
240 self.relaxed_contact_score_penalty
241 } else {
242 0.0
243 };
244 closest_contact_gap + relaxed_penalty
245 - if dodge_contact {
246 self.dodge_contact_score_bonus
247 } else {
248 0.0
249 }
250 }
251}
252
253pub fn car_hitbox_for_body_name(body_name: &str) -> Option<CarHitbox> {
254 hitbox_family_for_body_name(body_name).map(CarHitbox::for_family)
255}
256
257pub fn car_hitbox_for_body_id(body_id: u32) -> Option<CarHitbox> {
258 hitbox_family_for_body_id(body_id).map(CarHitbox::for_family)
259}
260
261pub fn car_hitbox_for_body_id_or_name(
262 body_id: Option<u32>,
263 body_name: Option<&str>,
264) -> Option<CarHitbox> {
265 hitbox_family_for_body_id_or_name(body_id, body_name).map(CarHitbox::for_family)
266}
267
268pub fn hitbox_family_for_body_id_or_name(
269 body_id: Option<u32>,
270 body_name: Option<&str>,
271) -> Option<CarHitboxFamily> {
272 body_id
273 .and_then(hitbox_family_for_body_id)
274 .or_else(|| body_name.and_then(hitbox_family_for_body_name))
275}
276
277pub fn hitbox_family_for_body_id(body_id: u32) -> Option<CarHitboxFamily> {
278 match body_id {
279 22 | 1416 | 1894 | 1932 | 3031 | 3311 | 6243 | 6489 | 7651 | 7696 | 7890 | 7901 | 8006
280 | 8360 | 8361 | 8565 | 8566 | 8669 | 9357 | 10697 | 10698 | 10817 | 10822 | 11038
281 | 11394 | 11505 | 11677 | 11800 | 11933 | 11949 | 12173 | 12315 | 12361 | 12484 => {
282 Some(CarHitboxFamily::Breakout)
283 }
284 29 | 403 | 597 | 600 | 1018 | 1171 | 1286 | 1675 | 1689 | 1883 | 2070 | 2268 | 2666
285 | 2950 | 2951 | 3155 | 3156 | 3157 | 3265 | 3426 | 3875 | 3879 | 3880 | 4014 | 4155
286 | 4367 | 4472 | 4473 | 4745 | 4770 | 4781 | 4861 | 4864 | 5709 | 5773 | 5823 | 5858
287 | 5964 | 5979 | 6122 | 6244 | 6247 | 6260 | 6836 | 7211 | 7337 | 7338 | 7341 | 7343
288 | 7415 | 7512 | 7532 | 7593 | 7772 | 8454 | 9053 | 9088 | 9089 | 9140 | 9388 | 9894
289 | 10094 | 10440 | 10441 | 10694 | 10695 | 11016 | 11095 | 11315 | 11336 | 11534 | 11941
290 | 11996 | 12106 | 12142 | 12262 | 12286 | 12325 | 12382 | 12563 | 12669 => {
291 Some(CarHitboxFamily::Dominus)
292 }
293 28 | 31 | 1159 | 1317 | 1624 | 1856 | 2269 | 3451 | 3582 | 3702 | 5470 | 5488 | 5879
294 | 7012 | 9084 | 9085 | 9427 | 10044 | 10805 | 11138 | 11141 | 11379 | 11932 | 12569
295 | 12652 => Some(CarHitboxFamily::Hybrid),
296 30 | 4780 | 7336 | 7477 | 7815 | 7979 | 10689 | 11098 | 11736 | 11905 | 11950 | 12318
297 | 12335 => Some(CarHitboxFamily::Merc),
298 21 | 23 | 25 | 26 | 27 | 402 | 404 | 523 | 607 | 625 | 723 | 1172 | 1295 | 1300 | 1475
299 | 1478 | 1533 | 1568 | 1623 | 2665 | 2853 | 2919 | 2949 | 4284 | 4318 | 4319 | 4320
300 | 4782 | 4906 | 5020 | 5039 | 5188 | 5361 | 5547 | 5713 | 5837 | 5951 | 6939 | 7947
301 | 7948 | 8383 | 8806 | 8807 | 10896 | 10897 | 10900 | 10901 | 11314 | 11603 | 12104
302 | 12105 => Some(CarHitboxFamily::Octane),
303 24 | 803 | 1603 | 1691 | 1919 | 3594 | 3614 | 3622 | 4268 | 5265 | 7052 | 8524 => {
304 Some(CarHitboxFamily::Plank)
305 }
306 _ => None,
307 }
308}
309
310pub fn hitbox_family_for_body_name(body_name: &str) -> Option<CarHitboxFamily> {
311 let normalized = normalized_car_body_name(body_name);
312 if normalized.is_empty() {
313 return None;
314 }
315
316 if normalized_body_name_matches(&normalized, BREAKOUT_HITBOX_BODIES) {
317 Some(CarHitboxFamily::Breakout)
318 } else if normalized_body_name_matches(&normalized, DOMINUS_HITBOX_BODIES) {
319 Some(CarHitboxFamily::Dominus)
320 } else if normalized_body_name_matches(&normalized, HYBRID_HITBOX_BODIES) {
321 Some(CarHitboxFamily::Hybrid)
322 } else if normalized_body_name_matches(&normalized, MERC_HITBOX_BODIES) {
323 Some(CarHitboxFamily::Merc)
324 } else if normalized_body_name_matches(&normalized, OCTANE_HITBOX_BODIES) {
325 Some(CarHitboxFamily::Octane)
326 } else if normalized_body_name_matches(&normalized, PLANK_HITBOX_BODIES) {
327 Some(CarHitboxFamily::Plank)
328 } else {
329 None
330 }
331}
332
333fn normalized_body_name_matches(normalized: &str, candidates: &[&str]) -> bool {
334 candidates
335 .iter()
336 .any(|candidate| normalized_car_body_name(candidate) == normalized)
337}
338
339fn normalized_car_body_name(body_name: &str) -> String {
340 body_name
341 .chars()
342 .filter(|character| character.is_ascii_alphanumeric())
343 .flat_map(char::to_lowercase)
344 .collect()
345}
346
347const BREAKOUT_HITBOX_BODIES: &[&str] = &[
353 "1966 Cadillac DeVille",
354 "Ace",
355 "Animus GP",
356 "Aston Martin Valhalla",
357 "Azura",
358 "Breakout",
359 "Breakout Type-S",
360 "Breakout X",
361 "Cyberpunk Quadra",
362 "Cyclone",
363 "Diesel",
364 "Emperor",
365 "Emperor II",
366 "Emperor II: Frozen",
367 "Emperor II: Scorched",
368 "Fast & Furious Mazda RX-7",
369 "Ferrari F40",
370 "Fast and Furious Mazda-RX7",
371 "Fuse",
372 "Havoc",
373 "Chevrolet Corvette Stingray",
374 "Chevrolet Corvette ZR1",
375 "Komodo",
376 "Mako",
377 "McLaren Senna",
378 "Megastar",
379 "Nexus",
380 "Nexus SC",
381 "Pontiac Firebird",
382 "Porsche 918 Spyder",
383 "Quadra Turbo-R",
384 "Redline",
385 "Revolver",
386 "Samurai",
387 "The Incredibile",
388 "Whiplash",
389];
390
391const DOMINUS_HITBOX_BODIES: &[&str] = &[
392 "'89 Batmobile",
393 "007's Aston Martin DBS",
394 "007's Aston Martin Valhalla",
395 "Admiral",
396 "Aftershock",
397 "Back To The Future Time Machine",
398 "Batmobile (1989)",
399 "Batmobile (2022)",
400 "Bumblebee Car",
401 "Bumblebee",
402 "BMW M3 (E30)",
403 "BMW M2 Racing",
404 "BMW M4 GT3 EVO",
405 "BMW M240i",
406 "Chikara",
407 "Chikara G1",
408 "Chikara GXT",
409 "DeLorean Time Machine",
410 "Diestro",
411 "Dodge Charger Daytona Scat Pack",
412 "Dodger Charger Daytona Scat Pack",
413 "Dominus",
414 "Dominus: Neon",
415 "Dominus GT",
416 "Ecto-1",
417 "Ecto-1 (Ghostbusters)",
418 "Fast & Furious Dodge Charger",
419 "Fast and Furious Dodge Charger",
420 "Fast & Furious Dodge Charger SRT Hellcat",
421 "Ferrari 296 GTB",
422 "Ford Mustang Shelby GT350R RLE",
423 "Ford Mustang Shelby GT500",
424 "Ford Mustang GTD",
425 "Gazella GT",
426 "Gazella GT (Hot Wheels)",
427 "Guardian",
428 "Guardian G1",
429 "Guardian GXT",
430 "Homer's Car",
431 "Hotshot",
432 "Ice Charger",
433 "Imperator DT5",
434 "K.I.T.T.",
435 "K.I.T.T. (Knight Rider)",
436 "Lamborghini Countach LPI 800-4",
437 "Lamborghini Huracan STO",
438 "Lamborghini Huracán STO",
439 "Lightning McQueen",
440 "Lightning McQueen Car",
441 "Lockjaw",
442 "Maestro",
443 "Magnifique",
444 "Magnifique GXT",
445 "Mamba",
446 "Masamune",
447 "Maven",
448 "Maverick",
449 "Maverick G1",
450 "Maverick GXT",
451 "McLaren 570S",
452 "McLaren 765LT",
453 "McLaren P1",
454 "Mercedes-AMG GT 63 S",
455 "Mercedes-Benz CLA",
456 "MR11",
457 "MR11 (Hot Wheels)",
458 "NASCAR Chevrolet Camaro",
459 "NASCAR Ford Mustang",
460 "NASCAR Toyota Camry",
461 "NASCAR Next Gen Chevrolet Camaro",
462 "NASCAR Next Gen Chevrolet Camaro (2022)",
463 "NASCAR Next Gen Ford Mustang",
464 "NASCAR Next Gen Ford Mustang (2022)",
465 "NASCAR Next Gen Toyota Camry",
466 "NASCAR Next Gen Toyota Camry (2022)",
467 "Nemesis",
468 "Nissan 350Z",
469 "Nissan Fairlady Z",
470 "Nissan Fairlady Z RLE",
471 "Nissan Z Performance",
472 "Nissan Z Performance Car",
473 "Peregrine TT",
474 "Perigrine TT",
475 "Porsche 911 GT3 RS",
476 "Porsche 911 Turbo",
477 "Porsche 911 Turbo RLE",
478 "Ripper",
479 "Ronin",
480 "Ronin G1",
481 "Ronin GXT",
482 "Samus' Gunship",
483 "Samus' Gunship (Nintendo Exclusive)",
484 "Scorpion",
485 "Tyranno",
486 "Tyranno GXT",
487 "Werewolf",
488 "Zefira",
489];
490
491const HYBRID_HITBOX_BODIES: &[&str] = &[
492 "Beskar",
493 "Chrysler Pacifica",
494 "Endo",
495 "Esper",
496 "Fast & Furious Nissan Skyline",
497 "Fast and Furious Nissan Skyline",
498 "Fast & Furious Pontiac Fiero",
499 "Fast and Furious Pontiac Fiero",
500 "Hearse",
501 "Insidio",
502 "Jager 619",
503 "Jäger 619",
504 "Jäger 619 RS",
505 "Lamborghini Urus",
506 "Lamborghini Urus SE",
507 "Nimbus",
508 "Nissan Silvia",
509 "Nissan Silvia RLE",
510 "Nissan Skyline GT-R",
511 "Nissan Skyline GT-R (R32)",
512 "Primo",
513 "R3MX",
514 "R3MX GXT",
515 "RAM 1500 RHO",
516 "Rivian R1S",
517 "Tesla Cybertruck",
518 "Tygris",
519 "Venom",
520 "Void Burn",
521 "X-Devil",
522 "X-Devil MK2",
523];
524
525const MERC_HITBOX_BODIES: &[&str] = &[
526 "Battle Bus",
527 "Behemoth",
528 "Chevrolet Astro",
529 "Defender D7X-R",
530 "Ford Bronco Raptor RLE",
531 "Merc",
532 "The Mystery Machine",
533 "Nomad",
534 "Nomad GXT",
535 "Pizza Planet Delivery Truck",
536 "Recoil AV",
537 "Stampede",
538 "Turtle Van",
539];
540
541const OCTANE_HITBOX_BODIES: &[&str] = &[
542 "007's Aston Martin DB5",
543 "Armadillo",
544 "Armadillo (Xbox Exclusive)",
545 "Backfire",
546 "BMW 1 Series",
547 "BMW 1 Series RLE",
548 "Bone Shaker",
549 "Corlay",
550 "Dingo",
551 "Fast 4WD",
552 "Fast 4WD (Hot Wheels)",
553 "Fennec",
554 "Fennec ZR-F",
555 "Ford F-150 RLE",
556 "Ford Mustang Mach-E RLE",
557 "Gizmo",
558 "Grog",
559 "Harbinger",
560 "Harbinger GXT",
561 "Hogsticker",
562 "Hogsticker (Xbox Exclusive)",
563 "Honda Civic Type R",
564 "Honda Civic Type R-LE",
565 "Jackal",
566 "Jurassic Jeep Wrangler",
567 "Jeep Wrangler Rubicon",
568 "Marauder",
569 "Mario NSR",
570 "Luigi NSR",
571 "Mudcat",
572 "Mudcat G1",
573 "Mudcat GXT",
574 "Octane",
575 "Octane ZSR",
576 "Outlaw",
577 "Outlaw GXT",
578 "Patty Wagon",
579 "Proteus",
580 "Psyclops",
581 "Road Hog",
582 "Road Hog XL",
583 "Scarab",
584 "Shokunin",
585 "Shokunin GXT",
586 "Sweet Tooth",
587 "Sweet Tooth (PlayStation Exclusive)",
588 "Takumi",
589 "Takumi RX-T",
590 "The Dark Knight's Tumbler",
591 "The Dark Knight Tumbler",
592 "Triton",
593 "Twinzer",
594 "Volkswagen Golf GTI",
595 "Volkswagen Golf GTI RLE",
596 "Vulcan",
597 "Xentari",
598 "Zippy",
599];
600
601const PLANK_HITBOX_BODIES: &[&str] = &[
602 "'16 Batmobile",
603 "Batmobile (2016)",
604 "Bugatti Centodieci",
605 "Artemis",
606 "Artemis G1",
607 "Artemis GXT",
608 "Centio",
609 "Centio V17",
610 "Formula 1 2021",
611 "Formula 1 2022",
612 "Mantis",
613 "Paladin",
614 "Sentinel",
615 "Twin Mill III",
616];
617
618#[derive(Debug, Clone, Copy, PartialEq)]
619pub(crate) struct CarHitboxContactEstimate {
620 pub distance: f32,
621 pub local_ball_position: glam::Vec3,
622 pub local_contact_point: glam::Vec3,
623}
624
625pub(crate) fn car_hitbox_contact_estimate(
626 ball_position: glam::Vec3,
627 player_body: &boxcars::RigidBody,
628 hitbox: CarHitbox,
629) -> Option<CarHitboxContactEstimate> {
630 let car_local_ball_position = quat_to_glam(&player_body.rotation).inverse()
631 * (ball_position - vec_to_glam(&player_body.location));
632 let hitbox_center = glam::Vec3::new(hitbox.offset, 0.0, hitbox.elevation);
633 let hitbox_rotation = glam::Quat::from_rotation_y(hitbox.angle.to_radians());
634 let local_ball_position = hitbox_rotation.inverse() * (car_local_ball_position - hitbox_center);
635 if !local_ball_position.is_finite() {
636 return None;
637 }
638
639 let x_min = -hitbox.length / 2.0;
640 let x_max = hitbox.length / 2.0;
641 let y_min = -hitbox.width / 2.0;
642 let y_max = hitbox.width / 2.0;
643 let z_min = -hitbox.height / 2.0;
644 let z_max = hitbox.height / 2.0;
645 let local_contact_point = glam::Vec3::new(
646 local_ball_position.x.clamp(x_min, x_max),
647 local_ball_position.y.clamp(y_min, y_max),
648 local_ball_position.z.clamp(z_min, z_max),
649 );
650 let distance = (local_ball_position - local_contact_point).length();
651 if !distance.is_finite() {
652 return None;
653 }
654
655 Some(CarHitboxContactEstimate {
656 distance,
657 local_ball_position,
658 local_contact_point,
659 })
660}
661
662pub(crate) fn car_hitbox_distance(
663 ball_position: glam::Vec3,
664 player_body: &boxcars::RigidBody,
665 hitbox: CarHitbox,
666) -> Option<f32> {
667 car_hitbox_contact_estimate(ball_position, player_body, hitbox)
668 .map(|estimate| estimate.distance)
669}
670
671pub fn car_hitbox_ball_contact_gap(
672 ball_position: glam::Vec3,
673 player_body: &boxcars::RigidBody,
674 hitbox: CarHitbox,
675) -> Option<f32> {
676 car_hitbox_distance(ball_position, player_body, hitbox)
677 .map(|center_distance| (center_distance - BALL_COLLISION_RADIUS).max(0.0))
678}
679
680#[derive(Debug, Clone, Copy)]
681struct OrientedCarHitbox {
682 center: glam::Vec3,
683 axes: [glam::Vec3; 3],
684 half_extents: glam::Vec3,
685}
686
687impl OrientedCarHitbox {
688 fn corners(self) -> [glam::Vec3; 8] {
689 let x = self.axes[0] * self.half_extents.x;
690 let y = self.axes[1] * self.half_extents.y;
691 let z = self.axes[2] * self.half_extents.z;
692
693 [
694 self.center - x - y - z,
695 self.center - x - y + z,
696 self.center - x + y - z,
697 self.center - x + y + z,
698 self.center + x - y - z,
699 self.center + x - y + z,
700 self.center + x + y - z,
701 self.center + x + y + z,
702 ]
703 }
704
705 fn edge_segments(self) -> [(glam::Vec3, glam::Vec3); 12] {
706 let corners = self.corners();
707 [
708 (corners[0], corners[1]),
709 (corners[0], corners[2]),
710 (corners[0], corners[4]),
711 (corners[3], corners[1]),
712 (corners[3], corners[2]),
713 (corners[3], corners[7]),
714 (corners[5], corners[1]),
715 (corners[5], corners[4]),
716 (corners[5], corners[7]),
717 (corners[6], corners[2]),
718 (corners[6], corners[4]),
719 (corners[6], corners[7]),
720 ]
721 }
722}
723
724fn oriented_car_hitbox(
725 player_body: &boxcars::RigidBody,
726 hitbox: CarHitbox,
727) -> Option<OrientedCarHitbox> {
728 let car_position = vec_to_glam(&player_body.location);
729 let car_rotation = quat_to_glam(&player_body.rotation);
730 let hitbox_center = glam::Vec3::new(hitbox.offset, 0.0, hitbox.elevation);
731 let hitbox_rotation = glam::Quat::from_rotation_y(hitbox.angle.to_radians());
732 let rotation = car_rotation * hitbox_rotation;
733 let center = car_position + car_rotation * hitbox_center;
734 let axes = [
735 rotation * glam::Vec3::X,
736 rotation * glam::Vec3::Y,
737 rotation * glam::Vec3::Z,
738 ];
739 let half_extents =
740 glam::Vec3::new(hitbox.length / 2.0, hitbox.width / 2.0, hitbox.height / 2.0);
741
742 if center.is_finite() && axes.iter().all(|axis| axis.is_finite()) && half_extents.is_finite() {
743 Some(OrientedCarHitbox {
744 center,
745 axes,
746 half_extents,
747 })
748 } else {
749 None
750 }
751}
752
753fn point_oriented_box_distance(point: glam::Vec3, hitbox: OrientedCarHitbox) -> f32 {
754 let delta = point - hitbox.center;
755 let mut closest = hitbox.center;
756 for (axis, half_extent) in hitbox.axes.into_iter().zip([
757 hitbox.half_extents.x,
758 hitbox.half_extents.y,
759 hitbox.half_extents.z,
760 ]) {
761 let distance_on_axis = delta.dot(axis).clamp(-half_extent, half_extent);
762 closest += axis * distance_on_axis;
763 }
764 (point - closest).length()
765}
766
767fn segment_segment_distance(
768 left_start: glam::Vec3,
769 left_end: glam::Vec3,
770 right_start: glam::Vec3,
771 right_end: glam::Vec3,
772) -> f32 {
773 let left_delta = left_end - left_start;
774 let right_delta = right_end - right_start;
775 let offset = left_start - right_start;
776 let left_length_sq = left_delta.length_squared();
777 let right_length_sq = right_delta.length_squared();
778 let left_right_dot = left_delta.dot(right_delta);
779 let left_offset_dot = left_delta.dot(offset);
780 let right_offset_dot = right_delta.dot(offset);
781 let denominator = left_length_sq * right_length_sq - left_right_dot * left_right_dot;
782
783 let mut left_t;
784 let mut right_t;
785 if denominator.abs() > f32::EPSILON {
786 left_t = ((left_right_dot * right_offset_dot - right_length_sq * left_offset_dot)
787 / denominator)
788 .clamp(0.0, 1.0);
789 } else {
790 left_t = 0.0;
791 }
792
793 right_t = (left_right_dot * left_t + right_offset_dot) / right_length_sq;
794 if right_t < 0.0 {
795 right_t = 0.0;
796 left_t = (-left_offset_dot / left_length_sq).clamp(0.0, 1.0);
797 } else if right_t > 1.0 {
798 right_t = 1.0;
799 left_t = ((left_right_dot - left_offset_dot) / left_length_sq).clamp(0.0, 1.0);
800 }
801
802 let left_closest = left_start + left_delta * left_t;
803 let right_closest = right_start + right_delta * right_t;
804 (left_closest - right_closest).length()
805}
806
807fn projected_interval(points: &[glam::Vec3; 8], axis: glam::Vec3) -> (f32, f32) {
808 points
809 .iter()
810 .map(|point| point.dot(axis))
811 .fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), value| {
812 (min.min(value), max.max(value))
813 })
814}
815
816fn separated_on_axis(
817 left_corners: &[glam::Vec3; 8],
818 right_corners: &[glam::Vec3; 8],
819 axis: glam::Vec3,
820) -> bool {
821 if axis.length_squared() <= f32::EPSILON {
822 return false;
823 }
824 let axis = axis.normalize();
825 let (left_min, left_max) = projected_interval(left_corners, axis);
826 let (right_min, right_max) = projected_interval(right_corners, axis);
827 left_max < right_min || right_max < left_min
828}
829
830fn oriented_boxes_intersect(left: OrientedCarHitbox, right: OrientedCarHitbox) -> bool {
831 let left_corners = left.corners();
832 let right_corners = right.corners();
833 let face_axes = left.axes.into_iter().chain(right.axes);
834 let edge_axes = left
835 .axes
836 .into_iter()
837 .flat_map(|left_axis| right.axes.map(|right_axis| left_axis.cross(right_axis)));
838
839 face_axes
840 .chain(edge_axes)
841 .all(|axis| !separated_on_axis(&left_corners, &right_corners, axis))
842}
843
844fn oriented_box_distance(left: OrientedCarHitbox, right: OrientedCarHitbox) -> f32 {
845 if oriented_boxes_intersect(left, right) {
846 return 0.0;
847 }
848
849 let left_corners = left.corners();
850 let right_corners = right.corners();
851 let mut distance = f32::INFINITY;
852
853 for corner in left_corners {
854 distance = distance.min(point_oriented_box_distance(corner, right));
855 }
856 for corner in right_corners {
857 distance = distance.min(point_oriented_box_distance(corner, left));
858 }
859 for (left_start, left_end) in left.edge_segments() {
860 for (right_start, right_end) in right.edge_segments() {
861 distance = distance.min(segment_segment_distance(
862 left_start,
863 left_end,
864 right_start,
865 right_end,
866 ));
867 }
868 }
869
870 distance
871}
872
873pub fn car_hitbox_pair_contact_gap(
874 left_body: &boxcars::RigidBody,
875 left_hitbox: CarHitbox,
876 right_body: &boxcars::RigidBody,
877 right_hitbox: CarHitbox,
878) -> Option<f32> {
879 let left = oriented_car_hitbox(left_body, left_hitbox)?;
880 let right = oriented_car_hitbox(right_body, right_hitbox)?;
881 let distance = oriented_box_distance(left, right);
882 distance.is_finite().then_some(distance)
883}
884
885pub fn car_hitbox_min_world_z(player_body: &boxcars::RigidBody, hitbox: CarHitbox) -> Option<f32> {
886 let car_position = vec_to_glam(&player_body.location);
887 let car_rotation = quat_to_glam(&player_body.rotation);
888 let hitbox_center = glam::Vec3::new(hitbox.offset, 0.0, hitbox.elevation);
889 let hitbox_rotation = glam::Quat::from_rotation_y(hitbox.angle.to_radians());
890 let mut min_z: Option<f32> = None;
891
892 for x in [-hitbox.length / 2.0, hitbox.length / 2.0] {
893 for y in [-hitbox.width / 2.0, hitbox.width / 2.0] {
894 for z in [-hitbox.height / 2.0, hitbox.height / 2.0] {
895 let local_corner = glam::Vec3::new(x, y, z);
896 let world_corner =
897 car_position + car_rotation * (hitbox_center + hitbox_rotation * local_corner);
898 if !world_corner.z.is_finite() {
899 return None;
900 }
901 min_z = Some(min_z.map_or(world_corner.z, |current| current.min(world_corner.z)));
902 }
903 }
904 }
905
906 min_z
907}
908
909pub fn car_hitbox_touches_floor(player_body: &boxcars::RigidBody, hitbox: CarHitbox) -> bool {
910 const FLOOR_CONTACT_MAX_Z: f32 = 5.0;
911
912 car_hitbox_min_world_z(player_body, hitbox).is_some_and(|min_z| min_z <= FLOOR_CONTACT_MAX_Z)
913}
914
915pub fn touch_candidate_contact_gap_rank_with_hitbox(
916 ball_body: &boxcars::RigidBody,
917 player_body: &boxcars::RigidBody,
918 hitbox: CarHitbox,
919) -> Option<(f32, f32)> {
920 touch_candidate_rank_with_hitbox(ball_body, player_body, hitbox).map(
921 |(closest_center_distance, current_center_distance)| {
922 (
923 (closest_center_distance - BALL_COLLISION_RADIUS).max(0.0),
924 (current_center_distance - BALL_COLLISION_RADIUS).max(0.0),
925 )
926 },
927 )
928}
929
930#[derive(Debug, Clone, Copy, PartialEq)]
931pub struct BallTrajectoryDeviation {
932 pub position_deviation: f32,
933 pub velocity_deviation: f32,
934 pub seconds: f32,
935}
936
937pub fn ball_trajectory_deviation_with_gravity(
938 previous_body: &boxcars::RigidBody,
939 previous_time: f32,
940 actual_body: &boxcars::RigidBody,
941 actual_time: f32,
942 gravity_z: f32,
943) -> Option<BallTrajectoryDeviation> {
944 let seconds = actual_time - previous_time;
945 if !seconds.is_finite() || seconds <= 0.0 {
946 return None;
947 }
948
949 let previous_velocity = vec_to_glam(&previous_body.linear_velocity?);
950 let actual_velocity = vec_to_glam(&actual_body.linear_velocity?);
951 let gravity = glam::Vec3::new(0.0, 0.0, gravity_z);
952 let expected_position = vec_to_glam(&previous_body.location)
953 + previous_velocity * seconds
954 + 0.5 * gravity * seconds * seconds;
955 let expected_velocity = previous_velocity + gravity * seconds;
956 let actual_position = vec_to_glam(&actual_body.location);
957
958 let position_deviation = (actual_position - expected_position).length();
959 let velocity_deviation = (actual_velocity - expected_velocity).length();
960 if !position_deviation.is_finite() || !velocity_deviation.is_finite() {
961 return None;
962 }
963
964 Some(BallTrajectoryDeviation {
965 position_deviation,
966 velocity_deviation,
967 seconds,
968 })
969}
970
971#[cfg_attr(not(test), allow(dead_code))]
979pub(crate) fn touch_candidate_rank(
980 ball_body: &boxcars::RigidBody,
981 player_body: &boxcars::RigidBody,
982) -> Option<(f32, f32)> {
983 touch_candidate_rank_with_hitbox(ball_body, player_body, default_car_hitbox())
984}
985
986pub fn touch_candidate_rank_with_hitbox(
987 ball_body: &boxcars::RigidBody,
988 player_body: &boxcars::RigidBody,
989 hitbox: CarHitbox,
990) -> Option<(f32, f32)> {
991 const TOUCH_LOOKBACK_SECONDS: f32 = 0.12;
992 const TOUCH_LOOKAHEAD_SECONDS: f32 = 0.03;
993 const TOUCH_RANK_SAMPLES: usize = 9;
994
995 let current_distance =
996 car_hitbox_distance(vec_to_glam(&ball_body.location), player_body, hitbox)?;
997
998 let mut closest_distance = current_distance;
999 for sample_index in 0..=TOUCH_RANK_SAMPLES {
1000 let sample_fraction = sample_index as f32 / TOUCH_RANK_SAMPLES as f32;
1001 let sample_time = -TOUCH_LOOKBACK_SECONDS
1002 + sample_fraction * (TOUCH_LOOKBACK_SECONDS + TOUCH_LOOKAHEAD_SECONDS);
1003 let sample_ball_body = apply_velocities_to_rigid_body(ball_body, sample_time);
1004 let sample_player_body = apply_velocities_to_rigid_body(player_body, sample_time);
1005 let sample_distance = car_hitbox_distance(
1006 vec_to_glam(&sample_ball_body.location),
1007 &sample_player_body,
1008 hitbox,
1009 )?;
1010 closest_distance = closest_distance.min(sample_distance);
1011 }
1012
1013 Some((closest_distance, current_distance))
1014}
1015
1016fn apply_angular_velocity(rigid_body: &boxcars::RigidBody, time_delta: f32) -> boxcars::Quaternion {
1017 let rbav = rigid_body.angular_velocity.unwrap_or(boxcars::Vector3f {
1018 x: 0.0,
1019 y: 0.0,
1020 z: 0.0,
1021 });
1022 let angular_velocity = glam::Vec3::new(rbav.x, rbav.y, rbav.z);
1023 let magnitude = angular_velocity.length();
1024 let angular_velocity_unit_vector = angular_velocity.normalize_or_zero();
1025
1026 let mut rotation = glam::Quat::from_xyzw(
1027 rigid_body.rotation.x,
1028 rigid_body.rotation.y,
1029 rigid_body.rotation.z,
1030 rigid_body.rotation.w,
1031 );
1032
1033 if angular_velocity_unit_vector.length() != 0.0 {
1034 let delta_rotation =
1035 glam::Quat::from_axis_angle(angular_velocity_unit_vector, magnitude * time_delta);
1036 rotation *= delta_rotation;
1037 }
1038 rotation = if rotation.length_squared() > 0.0 {
1039 rotation.normalize()
1040 } else {
1041 glam::Quat::IDENTITY
1042 };
1043
1044 boxcars::Quaternion {
1045 x: rotation.x,
1046 y: rotation.y,
1047 z: rotation.z,
1048 w: rotation.w,
1049 }
1050}
1051
1052pub fn get_interpolated_rigid_body(
1066 start_body: &boxcars::RigidBody,
1067 start_time: f32,
1068 end_body: &boxcars::RigidBody,
1069 end_time: f32,
1070 time: f32,
1071) -> SubtrActorResult<boxcars::RigidBody> {
1072 if !(start_time <= time && time <= end_time) {
1073 return SubtrActorError::new_result(SubtrActorErrorVariant::InterpolationTimeOrderError {
1074 start_time,
1075 time,
1076 end_time,
1077 });
1078 }
1079
1080 let duration = end_time - start_time;
1081 let interpolation_amount = (time - start_time) / duration;
1082 let start_position = vec_to_glam(&start_body.location);
1083 let end_position = vec_to_glam(&end_body.location);
1084 let interpolated_location = start_position.lerp(end_position, interpolation_amount);
1085 let start_rotation = quat_to_glam(&start_body.rotation);
1086 let end_rotation = quat_to_glam(&end_body.rotation);
1087 let interpolated_rotation = start_rotation.slerp(end_rotation, interpolation_amount);
1088
1089 Ok(boxcars::RigidBody {
1090 location: glam_to_vec(&interpolated_location),
1091 rotation: glam_to_quat(&interpolated_rotation),
1092 sleeping: start_body.sleeping,
1093 linear_velocity: start_body.linear_velocity,
1094 angular_velocity: start_body.angular_velocity,
1095 })
1096}
1097
1098#[cfg(test)]
1099#[path = "geometry_tests.rs"]
1100mod tests;