1use common::{
2 comp::{
3 CharacterState, Collider, Mass, Ori, PhysicsState, Pos, PreviousPhysCache, Scale, Vel,
4 body::ship::figuredata::VoxelCollider,
5 fluid_dynamics::{Fluid, LiquidKind},
6 },
7 consts::FRIC_GROUND,
8 outcome::Outcome,
9 resources::DeltaTime,
10 terrain::{Block, BlockKind},
11 uid::Uid,
12 vol::{BaseVol, ReadVol},
13};
14use specs::Entity;
15use std::ops::Range;
16use vek::*;
17
18use super::PhysicsRead;
19
20#[expect(clippy::too_many_lines)]
21pub(super) fn box_voxel_collision<T: BaseVol<Vox = Block> + ReadVol>(
22 cylinder: (f32, f32, f32), terrain: &T,
24 entity: Entity,
25 pos: &mut Pos,
26 tgt_pos: Vec3<f32>,
27 vel: &mut Vel,
28 physics_state: &mut PhysicsState,
29 dt: &DeltaTime,
30 was_on_ground: bool,
31 block_snap: bool,
32 climbing: bool,
33 mut land_on_ground: impl FnMut(Entity, Vel, Vec3<f32>),
34 read: &PhysicsRead,
35 ori: &Ori,
36 friction_factor: impl Fn(Vec3<f32>) -> f32,
38) {
39 let scale = read.scales.get(entity).map_or(1.0, |s| s.0.min(10.0));
41
42 fn player_aabb(pos: Vec3<f32>, radius: f32, z_range: Range<f32>) -> Aabb<f32> {
46 Aabb {
47 min: pos + Vec3::new(-radius, -radius, z_range.start),
48 max: pos + Vec3::new(radius, radius, z_range.end),
49 }
50 }
51
52 fn move_aabb(aabb: Aabb<i32>, pos: Vec3<f32>) -> Aabb<i32> {
54 Aabb {
55 min: aabb.min + pos.map(|e| e.floor() as i32),
56 max: aabb.max + pos.map(|e| e.floor() as i32),
57 }
58 }
59
60 fn collision_with<T: BaseVol<Vox = Block> + ReadVol>(
63 pos: Vec3<f32>,
64 terrain: &T,
65 near_aabb: Aabb<i32>,
66 radius: f32,
67 z_range: Range<f32>,
68 move_dir: Vec3<f32>,
69 ) -> bool {
70 let player_aabb = player_aabb(pos, radius, z_range);
71
72 let near_aabb = move_aabb(near_aabb, pos);
74
75 let mut collision = false;
76 terrain.for_each_in(near_aabb, |block_pos, block| {
78 if block.is_solid() {
79 let block_aabb = Aabb {
80 min: block_pos.map(|e| e as f32),
81 max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()),
82 };
83 if player_aabb.collides_with_aabb(block_aabb)
84 && block.valid_collision_dir(player_aabb, block_aabb, move_dir)
85 {
86 collision = true;
87 }
88 }
89 });
90
91 collision
92 }
93
94 let (radius, z_min, z_max) = (Vec3::from(cylinder) * scale).into_tuple();
95
96 let hdist = radius.ceil() as i32;
98
99 let near_aabb = Aabb {
101 min: Vec3::new(
102 -hdist,
103 -hdist,
104 1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32,
105 ),
106 max: Vec3::new(hdist, hdist, z_max.ceil() as i32),
107 };
108
109 let z_range = z_min..z_max;
110
111 physics_state.on_ground = None;
113 physics_state.on_ceiling = false;
114
115 let mut on_ground = None::<Block>;
116 let mut on_ceiling = false;
117 let mut attempts = 0;
119
120 let mut pos_delta = tgt_pos - pos.0;
121
122 const MAX_INCREMENTS: usize = 100; let min_step = (radius / 2.0).min(z_max - z_min).clamped(0.01, 0.3);
125 let increments = ((pos_delta.map(|e| e.abs()).reduce_partial_max() / min_step).ceil() as usize)
126 .clamped(1, MAX_INCREMENTS);
127 let old_pos = pos.0;
128 for _ in 0..increments {
129 const MAX_ATTEMPTS: usize = 16;
131 pos.0 += pos_delta / increments as f32;
132
133 let vel2 = *vel;
134 let try_colliding_block = |pos: &Pos| {
135 let player_aabb = player_aabb(pos.0, radius, z_range.clone());
138
139 let mut most_colliding = None;
143 let near_aabb = move_aabb(near_aabb, pos.0);
145 let player_overlap = |block_aabb: Aabb<f32>| {
146 (block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5)
147 .map(f32::abs)
148 .sum()
149 };
150
151 terrain.for_each_in(near_aabb, |block_pos, block| {
152 if block.is_solid() {
154 let block_aabb = Aabb {
156 min: block_pos.map(|e| e as f32),
157 max: block_pos.map(|e| e as f32)
158 + Vec3::new(1.0, 1.0, block.solid_height()),
159 };
160
161 if player_aabb.collides_with_aabb(block_aabb)
163 && block.valid_collision_dir(player_aabb, block_aabb, vel2.0)
164 {
165 match &most_colliding {
166 Some((_, other_block_aabb, _))
168 if player_overlap(block_aabb)
169 >= player_overlap(*other_block_aabb) => {},
170 _ => most_colliding = Some((block_pos, block_aabb, block)),
171 }
172 }
173 }
174 });
175
176 most_colliding
177 };
178
179 while let Some((_block_pos, block_aabb, block)) = (attempts < MAX_ATTEMPTS)
181 .then(|| try_colliding_block(pos))
182 .flatten()
183 {
184 let player_aabb = player_aabb(pos.0, radius, z_range.clone());
186
187 let dir = player_aabb.collision_vector_with_aabb(block_aabb);
189
190 let max_axis = dir.map(|e| e.abs()).reduce_partial_min();
193 let resolve_dir = -dir.map(|e| {
194 if e.abs().to_bits() == max_axis.to_bits() {
195 e
196 } else {
197 0.0
198 }
199 });
200
201 if resolve_dir.z > 0.0 {
205 on_ground = Some(block);
206 } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 {
207 on_ceiling = true;
208 }
209
210 if resolve_dir.z == 0.0
215 && dir.z < -0.1
217 && {
219 !collision_with(
221 Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()),
222 &terrain,
223 near_aabb,
224 radius,
225 z_range.clone(),
226 vel.0,
227 )
228 }
229 && {
231 collision_with(
233 pos.0 + resolve_dir - Vec3::unit_z() * 1.25,
234 &terrain,
235 near_aabb,
236 radius,
237 z_range.clone(),
238 vel.0,
239 )
240 } {
241 pos.0.z = pos.0.z.max(block_aabb.max.z);
243
244 land_on_ground(entity, *vel, Vec3::unit_z());
246 vel.0.z = vel.0.z.max(0.0);
247
248 if (vel.0 * resolve_dir).xy().magnitude_squared() < 1.0_f32.powi(2) {
251 pos.0 -= resolve_dir.normalized() * 0.05;
252 }
253 on_ground = Some(block);
254 break;
255 }
256
257 if resolve_dir.magnitude_squared() > 0.0 {
259 land_on_ground(entity, *vel, resolve_dir.normalized());
260 }
261 vel.0 = vel.0.map2(
262 resolve_dir,
263 |e, d| {
264 if d * e.signum() < 0.0 { 0.0 } else { e }
265 },
266 );
267
268 pos_delta *= resolve_dir.map(|e| if e == 0.0 { 1.0 } else { 0.0 });
269
270 pos.0 += resolve_dir;
272
273 attempts += 1;
274 }
275
276 if attempts == MAX_ATTEMPTS {
277 vel.0 = Vec3::zero();
278 pos.0 = old_pos;
279 break;
280 }
281 }
282
283 if on_ceiling {
285 physics_state.on_ceiling = true;
286 }
287
288 if on_ground.is_some() {
289 physics_state.on_ground = on_ground;
290 } else if vel.0.z <= 0.0
292 && was_on_ground
293 && block_snap
294 && physics_state.in_liquid().is_none()
295 && {
296 collision_with(
298 pos.0 - Vec3::unit_z() * 1.1,
299 &terrain,
300 near_aabb,
301 radius,
302 z_range.clone(),
303 vel.0,
304 )
305 }
306 {
307 let snap_height = terrain
309 .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32))
310 .ok()
311 .filter(|block| block.is_solid())
312 .map_or(0.0, Block::solid_height);
313 vel.0.z = 0.0;
314 pos.0.z = (pos.0.z - 0.1).floor() + snap_height;
315 physics_state.on_ground = terrain
316 .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.01).map(|e| e.floor() as i32))
317 .ok()
318 .copied();
319 }
320
321 let player_aabb = player_aabb(pos.0, radius, z_range.clone());
323 let near_aabb = move_aabb(near_aabb, pos.0);
325
326 let dirs = [
327 Vec3::unit_x(),
328 Vec3::unit_y(),
329 -Vec3::unit_x(),
330 -Vec3::unit_y(),
331 ];
332
333 let player_wall_aabbs = dirs.map(|dir| {
335 let pos = pos.0 + dir * 0.01;
336 Aabb {
337 min: pos + Vec3::new(-radius, -radius, z_range.start),
338 max: pos + Vec3::new(radius, radius, z_range.end),
339 }
340 });
341
342 let mut liquid = None::<(LiquidKind, f32)>;
343 let mut wall_dir_collisions = [false; 4];
344 terrain.for_each_in(near_aabb, |block_pos, block| {
346 if let Some(block_liquid) = block.liquid_kind() {
348 let liquid_aabb = Aabb {
349 min: block_pos.map(|e| e as f32),
350 max: block_pos.map(|e| e as f32) + Vec3::one(),
352 };
353 if player_aabb.collides_with_aabb(liquid_aabb) {
354 liquid = match liquid {
355 Some((kind, max_liquid_z)) => Some((
356 kind.merge(block_liquid),
360 max_liquid_z.max(liquid_aabb.max.z),
361 )),
362 None => Some((block_liquid, liquid_aabb.max.z)),
363 };
364 }
365 }
366
367 if block.is_solid() {
369 let block_aabb = Aabb {
370 min: block_pos.map(|e| e as f32),
371 max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()),
372 };
373
374 for dir in 0..4 {
375 if player_wall_aabbs[dir].collides_with_aabb(block_aabb)
376 && block.valid_collision_dir(player_wall_aabbs[dir], block_aabb, vel.0)
377 {
378 wall_dir_collisions[dir] = true;
379 }
380 }
381 }
382 });
383 let mut on_wall = None;
387 for dir in 0..4 {
388 if wall_dir_collisions[dir] {
389 on_wall = Some(match on_wall {
390 Some(acc) => acc + dirs[dir],
391 None => dirs[dir],
392 });
393 }
394 }
395
396 physics_state.on_wall = on_wall;
397 let fric_mod = read.stats.get(entity).map_or(1.0, |s| s.friction_modifier);
398
399 physics_state.in_fluid = liquid
400 .map(|(kind, max_z)| {
401 let depth = max_z - pos.0.z;
403
404 let new_depth = physics_state.in_liquid().map_or(depth, |old_depth| {
408 (old_depth + old_pos.z - pos.0.z).max(depth)
409 });
410
411 let vel = Vel::zero();
413
414 if depth > 0.0 {
415 physics_state.ground_vel = vel.0;
416 }
417
418 Fluid::Liquid {
419 kind,
420 depth: new_depth,
421 vel,
422 }
423 })
424 .or_else(|| match physics_state.in_fluid {
425 Some(Fluid::Liquid { .. }) | None => Some(Fluid::Air {
426 elevation: pos.0.z,
427 vel: Vel::default(),
428 }),
429 fluid => fluid,
430 });
431
432 if !vel.0.xy().is_approx_zero()
434 && physics_state
435 .on_ground
436 .is_some_and(|g| physics_state.footwear.can_skate_on(g.kind()))
437 {
438 const DT_SCALE: f32 = 1.0; const POTENTIAL_TO_KINETIC: f32 = 8.0; let kind = physics_state.on_ground.map_or(BlockKind::Air, |g| g.kind());
442 let (longitudinal_friction, lateral_friction) = physics_state.footwear.get_friction(kind);
443 let longitudinal_friction_factor_squared =
445 (1.0 - longitudinal_friction).powf(dt.0 * DT_SCALE * 2.0);
446 let lateral_friction_factor = (1.0 - lateral_friction).powf(dt.0 * DT_SCALE);
447 let groundplane_velocity = vel.0.xy();
448 let mut longitudinal_dir = ori.look_vec().xy();
449 if longitudinal_dir.is_approx_zero() {
450 longitudinal_dir = groundplane_velocity;
452 }
453 let longitudinal_dir = longitudinal_dir.normalized();
454 let lateral_dir = Vec2::new(longitudinal_dir.y, -longitudinal_dir.x);
455 let squared_velocity = groundplane_velocity.magnitude_squared();
456 let vertical_difference = physics_state.skating_last_height - pos.0.z;
460 let height_factor_squared = if vertical_difference != 0.0 {
462 let kinetic = squared_velocity;
464 let delta_potential = vertical_difference.clamp(-1.0, 2.0) * POTENTIAL_TO_KINETIC;
466 let new_energy = kinetic + delta_potential;
467 physics_state.skating_last_height = pos.0.z;
468 new_energy / kinetic
469 } else {
470 1.0
471 };
472
473 let long_speed = groundplane_velocity.dot(longitudinal_dir);
476 let lat_speed = groundplane_velocity.dot(lateral_dir);
477 let long_speed_squared = long_speed.powi(2);
478
479 let new_lateral = lat_speed * lateral_friction_factor;
481 let lateral_speed_reduction = lat_speed - new_lateral;
482 let cosine_squared_aoa = long_speed_squared / squared_velocity;
485 let converted_lateral_squared = cosine_squared_aoa * lateral_speed_reduction.powi(2);
486 let new_longitudinal_squared = longitudinal_friction_factor_squared
487 * (long_speed_squared + converted_lateral_squared)
488 * height_factor_squared;
489 let new_longitudinal =
490 new_longitudinal_squared.signum() * new_longitudinal_squared.abs().sqrt();
491 let new_ground_speed = new_longitudinal * longitudinal_dir + new_lateral * lateral_dir;
492 physics_state.skating_active = true;
493 vel.0 = Vec3::new(new_ground_speed.x, new_ground_speed.y, 0.0);
494 } else {
495 let ground_fric = if physics_state.in_liquid().is_some() {
496 0.1
505 } else {
506 1.0
507 } * physics_state
508 .on_ground
509 .map(|b| b.get_friction())
510 .unwrap_or(0.0)
511 * friction_factor(vel.0);
512 let wall_fric = if physics_state.on_wall.is_some() && climbing {
513 FRIC_GROUND
514 } else {
515 0.0
516 };
517 let fric = ground_fric.max(wall_fric);
518 if fric > 0.0 {
519 vel.0 *= (1.0 - fric.min(1.0) * fric_mod).powf(dt.0 * 60.0);
520 physics_state.ground_vel = Vec3::zero();
521 }
522 physics_state.skating_active = false;
523 }
524}
525
526pub(super) fn point_voxel_collision(
527 entity: Entity,
528 pos: &mut Pos,
529 pos_delta: Vec3<f32>,
530 vel: &mut Vel,
531 physics_state: &mut PhysicsState,
532 sticky: bool,
533 outcomes: &mut Vec<Outcome>,
534 read: &PhysicsRead,
535) {
536 let (dist, block) = if let Some(block) = read
541 .terrain
542 .get(pos.0.map(|e| e.floor() as i32))
543 .ok()
544 .filter(|b| b.is_solid())
545 {
546 (0.0, Some(block))
547 } else {
548 let (dist, block) = read
549 .terrain
550 .ray(pos.0, pos.0 + pos_delta)
551 .until(|block: &Block| block.is_solid())
552 .ignore_error()
553 .cast();
554 (dist, block.unwrap())
556 };
557
558 pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist;
559
560 if sticky {
562 if let Some((projectile, body)) = read
563 .projectiles
564 .get(entity)
565 .filter(|_| vel.0.magnitude_squared() > 1.0 && block.is_some())
566 .zip(read.bodies.get(entity).copied())
567 {
568 outcomes.push(Outcome::ProjectileHit {
569 pos: pos.0 + pos_delta * dist,
570 body,
571 vel: vel.0,
572 source: projectile.owner,
573 target: None,
574 });
575 }
576 }
577
578 if block.is_some() {
579 let block_center = pos.0.map(|e| e.floor()) + 0.5;
580 let block_rpos = (pos.0 - block_center)
581 .try_normalized()
582 .unwrap_or_else(Vec3::zero);
583
584 if block_rpos.z.abs() > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() {
587 if block_rpos.z > 0.0 {
588 physics_state.on_ground = block.copied();
589 } else {
590 physics_state.on_ceiling = true;
591 }
592 vel.0.z = 0.0;
593 } else {
594 physics_state.on_wall = Some(if block_rpos.x.abs() > block_rpos.y.abs() {
595 vel.0.x = 0.0;
596 Vec3::unit_x() * -block_rpos.x.signum()
597 } else {
598 vel.0.y = 0.0;
599 Vec3::unit_y() * -block_rpos.y.signum()
600 });
601 }
602
603 if sticky {
605 vel.0 = physics_state.ground_vel;
606 }
607 }
608
609 physics_state.in_fluid = read
610 .terrain
611 .get(pos.0.map(|e| e.floor() as i32))
612 .ok()
613 .and_then(|vox| {
614 vox.liquid_kind().map(|kind| Fluid::Liquid {
615 kind,
616 depth: 1.0,
617 vel: Vel::zero(),
618 })
619 })
620 .or_else(|| match physics_state.in_fluid {
621 Some(Fluid::Liquid { .. }) | None => Some(Fluid::Air {
622 elevation: pos.0.z,
623 vel: Vel::default(),
624 }),
625 fluid => fluid,
626 });
627}
628
629pub(super) fn voxel_collider_bounding_sphere(
630 voxel_collider: &VoxelCollider,
631 pos: &Pos,
632 ori: &Ori,
633 scale: Option<&Scale>,
634) -> Sphere<f32, f32> {
635 let origin_offset = voxel_collider.translation;
636 use common::vol::SizedVol;
637 let lower_bound = voxel_collider.volume().lower_bound().map(|e| e as f32);
638 let upper_bound = voxel_collider.volume().upper_bound().map(|e| e as f32);
639 let center = (lower_bound + upper_bound) / 2.0;
640 let center_offset = center + origin_offset;
643 let oriented_center_offset = ori.local_to_global(center_offset);
645 let wpos_center = oriented_center_offset + pos.0;
647
648 const SPRITE_AND_MAYBE_OTHER_THINGS: f32 = 4.0;
650 let radius = ((upper_bound - lower_bound) / 2.0
651 + Vec3::broadcast(SPRITE_AND_MAYBE_OTHER_THINGS))
652 .magnitude();
653
654 Sphere {
655 center: wpos_center,
656 radius: radius * scale.map_or(1.0, |s| s.0),
657 }
658}
659
660pub(super) struct ColliderData<'a> {
661 pub pos: &'a Pos,
662 pub previous_cache: &'a PreviousPhysCache,
663 pub z_limits: (f32, f32),
664 pub collider: &'a Collider,
665 pub mass: Mass,
666}
667
668#[expect(clippy::too_many_arguments)]
670pub(super) fn resolve_e2e_collision(
671 collision_registered: &mut bool,
673 entity_entity_collisions: &mut u64,
674 factor: f32,
675 physics: &mut PhysicsState,
676 char_state_maybe: Option<&CharacterState>,
677 vel_delta: &mut Vec3<f32>,
678 step_delta: f32,
679 is_mid_air: bool,
681 is_sticky: bool,
682 is_immovable: bool,
683 is_projectile: bool,
684 other: Uid,
686 our_data: ColliderData,
688 other_data: ColliderData,
689 vel: &Vel,
690 is_riding: bool,
691) -> bool {
692 let pos = our_data.pos.0 + our_data.previous_cache.velocity_dt * factor;
699 let pos_other = other_data.pos.0 + other_data.previous_cache.velocity_dt * factor;
700
701 let (z_min, z_max) = our_data.z_limits;
703 let ceiling = pos.z + z_max * our_data.previous_cache.scale;
704 let floor = pos.z + z_min * our_data.previous_cache.scale;
705
706 let (z_min_other, z_max_other) = other_data.z_limits;
707 let ceiling_other = pos_other.z + z_max_other * other_data.previous_cache.scale;
708 let floor_other = pos_other.z + z_min_other * other_data.previous_cache.scale;
709
710 let in_z_range = ceiling >= floor_other && floor <= ceiling_other;
711
712 if !in_z_range {
713 return false;
714 }
715
716 let ours = ColliderContext {
717 pos,
718 previous_cache: our_data.previous_cache,
719 };
720 let theirs = ColliderContext {
721 pos: pos_other,
722 previous_cache: other_data.previous_cache,
723 };
724 let (diff, collision_dist) = projection_between(ours, theirs);
725 let in_collision_range = diff.magnitude_squared() <= collision_dist.powi(2);
726
727 if !in_collision_range {
728 return false;
729 }
730
731 if !*collision_registered && (is_mid_air || !is_sticky) {
735 physics.touch_entities.insert(other, pos);
736 *entity_entity_collisions += 1;
737 }
738
739 let forced_movement =
749 matches!(char_state_maybe, Some(cs) if cs.is_forced_movement()) || is_riding;
750
751 if !forced_movement
758 && (!is_sticky || is_mid_air)
759 && diff.magnitude_squared() > 0.0
760 && !is_projectile
761 && !is_immovable
762 && !other_data.collider.is_voxel()
763 && !our_data.collider.is_voxel()
764 {
765 const ELASTIC_FORCE_COEFFICIENT: f32 = 400.0;
766 let mass_coefficient = other_data.mass.0 / (our_data.mass.0 + other_data.mass.0);
767 let distance_coefficient = collision_dist - diff.magnitude();
768 let force = ELASTIC_FORCE_COEFFICIENT * distance_coefficient * mass_coefficient;
769
770 let diff = diff.normalized();
771
772 *vel_delta += Vec3::from(diff)
773 * force
774 * step_delta
775 * vel
776 .0
777 .xy()
778 .try_normalized()
779 .map_or(1.0, |dir| diff.dot(-dir).max(0.025));
780 }
781
782 *collision_registered = true;
783
784 true
785}
786
787struct ColliderContext<'a> {
788 pos: Vec3<f32>,
789 previous_cache: &'a PreviousPhysCache,
790}
791
792fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {
795 const DIFF_THRESHOLD: f32 = f32::EPSILON;
796 let our_radius = c0.previous_cache.neighborhood_radius;
797 let their_radius = c1.previous_cache.neighborhood_radius;
798 let collision_dist = our_radius + their_radius;
799
800 let we = c0.pos.xy();
801 let other = c1.pos.xy();
802
803 let (p0_offset, p1_offset) = match c0.previous_cache.origins {
804 Some(origins) => origins,
805 None => return capsule2cylinder(c0, c1),
807 };
808 let segment = LineSegment2 {
809 start: we + p0_offset,
810 end: we + p1_offset,
811 };
812
813 let (p0_offset_other, p1_offset_other) = match c1.previous_cache.origins {
814 Some(origins) => origins,
815 None => return capsule2cylinder(c0, c1),
817 };
818 let segment_other = LineSegment2 {
819 start: other + p0_offset_other,
820 end: other + p1_offset_other,
821 };
822
823 let (our, their) = closest_points(segment, segment_other);
824 let diff = our - their;
825
826 if diff.magnitude_squared() < DIFF_THRESHOLD {
827 capsule2cylinder(c0, c1)
828 } else {
829 (diff, collision_dist)
830 }
831}
832
833fn closest_points(n: LineSegment2<f32>, m: LineSegment2<f32>) -> (Vec2<f32>, Vec2<f32>) {
837 let a = n.start;
839 let b = n.end - n.start;
840 let c = m.start;
841 let d = m.end - m.start;
842
843 let t = if d.x > d.y {
848 (d.y / d.x * (c.x - a.x) + a.y - c.y) / (b.x * d.y / d.x - b.y)
849 } else {
850 (d.x / d.y * (c.y - a.y) + a.x - c.x) / (b.y * d.x / d.y - b.x)
851 };
852 let u = if d.y > d.x {
853 (a.y + t * b.y - c.y) / d.y
854 } else {
855 (a.x + t * b.x - c.x) / d.x
856 };
857
858 if !t.is_finite() || !u.is_finite() {
860 [
861 (n.projected_point(m.start), m.start),
862 (n.projected_point(m.end), m.end),
863 (n.start, m.projected_point(n.start)),
864 (n.end, m.projected_point(n.end)),
865 ]
866 .into_iter()
867 .min_by_key(|(a, b)| ordered_float::OrderedFloat(a.distance_squared(*b)))
868 .expect("Lines had non-finite elements")
869 } else {
870 let t = t.clamped(0.0, 1.0);
871 let u = u.clamped(0.0, 1.0);
872
873 let close_n = a + b * t;
874 let close_m = c + d * u;
875
876 let proj_n = n.projected_point(close_m);
877 let proj_m = m.projected_point(close_n);
878
879 if proj_n.distance_squared(close_m) < proj_m.distance_squared(close_n) {
880 (proj_n, close_m)
881 } else {
882 (close_n, proj_m)
883 }
884 }
885}
886
887fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {
890 let we = c0.pos.xy();
912 let other = c1.pos.xy();
913 let calculate_projection_and_collision_dist = |our_radius: f32,
914 their_radius: f32,
915 origins: Option<(Vec2<f32>, Vec2<f32>)>,
916 start_point: Vec2<f32>,
917 end_point: Vec2<f32>,
918 coefficient: f32|
919 -> (Vec2<f32>, f32) {
920 let collision_dist = our_radius + their_radius;
921
922 let (p0_offset, p1_offset) = match origins {
923 Some(origins) => origins,
924 None => return (we - other, collision_dist),
925 };
926 let segment = LineSegment2 {
927 start: start_point + p0_offset,
928 end: start_point + p1_offset,
929 };
930
931 let projection = coefficient * (segment.projected_point(end_point) - end_point);
932
933 (projection, collision_dist)
934 };
935
936 if c0.previous_cache.scaled_radius > c1.previous_cache.scaled_radius {
937 calculate_projection_and_collision_dist(
938 c0.previous_cache.neighborhood_radius,
939 c1.previous_cache.scaled_radius,
940 c0.previous_cache.origins,
941 we,
942 other,
943 1.0,
944 )
945 } else {
946 calculate_projection_and_collision_dist(
947 c0.previous_cache.scaled_radius,
948 c1.previous_cache.neighborhood_radius,
949 c1.previous_cache.origins,
950 other,
951 we,
952 -1.0,
953 )
954 }
955}