veloren_common_systems/phys/
collision.rs

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), // effective collision cylinder
23    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    // Get the proportion of surface friction that should be applied based on the current velocity
37    friction_factor: impl Fn(Vec3<f32>) -> f32,
38) {
39    // We cap out scale at 10.0 to prevent an enormous amount of lag
40    let scale = read.scales.get(entity).map_or(1.0, |s| s.0.min(10.0));
41
42    //prof_span!("box_voxel_collision");
43
44    // Convience function to compute the player aabb
45    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    // Convience function to translate the near_aabb into the world space
53    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    // Function for determining whether the player at a specific position collides
61    // with blocks with the given criteria
62    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        // Calculate the world space near aabb
73        let near_aabb = move_aabb(near_aabb, pos);
74
75        let mut collision = false;
76        // TODO: could short-circuit here
77        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    // Probe distances
97    let hdist = radius.ceil() as i32;
98
99    // Neighbouring blocks Aabb
100    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    // Setup values for the loop below
112    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    // Don't loop infinitely here
118    let mut attempts = 0;
119
120    let mut pos_delta = tgt_pos - pos.0;
121
122    // Don't jump too far at once
123    const MAX_INCREMENTS: usize = 100; // The maximum number of collision tests per tick
124    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        //prof_span!("increment");
130        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            //prof_span!("most colliding check");
136            // Calculate the player's AABB
137            let player_aabb = player_aabb(pos.0, radius, z_range.clone());
138
139            // Determine the block that we are colliding with most
140            // (based on minimum collision axis)
141            // (if we are colliding with one)
142            let mut most_colliding = None;
143            // Calculate the world space near aabb
144            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                // Make sure the block is actually solid
153                if block.is_solid() {
154                    // Calculate block AABB
155                    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                    // Determine whether the block's AABB collides with the player's AABB
162                    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                            // Select the minimum of the value from `player_overlap`
167                            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 the player is colliding with the terrain...
180        while let Some((_block_pos, block_aabb, block)) = (attempts < MAX_ATTEMPTS)
181            .then(|| try_colliding_block(pos))
182            .flatten()
183        {
184            // Calculate the player's AABB
185            let player_aabb = player_aabb(pos.0, radius, z_range.clone());
186
187            // Find the intrusion vector of the collision
188            let dir = player_aabb.collision_vector_with_aabb(block_aabb);
189
190            // Determine an appropriate resolution vector (i.e: the minimum distance
191            // needed to push out of the block)
192            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            // When the resolution direction is pointing upwards, we must be on the
202            // ground
203            /* if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { */
204            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            // When the resolution direction is non-vertical, we must be colliding
211            // with a wall
212            //
213            // If we're being pushed out horizontally...
214            if resolve_dir.z == 0.0
215            // ...and the vertical resolution direction is sufficiently great...
216            && dir.z < -0.1
217            // ...and the space above is free...
218            && {
219                //prof_span!("space above free");
220                !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            // ...and there is a collision with a block beneath our current hitbox...
230            && {
231                //prof_span!("collision beneath");
232                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                // ...block-hop!
242                pos.0.z = pos.0.z.max(block_aabb.max.z);
243
244                // Apply fall damage, in the vertical axis, and correct velocity
245                land_on_ground(entity, *vel, Vec3::unit_z());
246                vel.0.z = vel.0.z.max(0.0);
247
248                // Push the character on to the block very slightly
249                // to avoid jitter due to imprecision
250                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 not, correct the velocity, applying collision damage as we do
258            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            // Resolve the collision normally
271            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    // Report on_ceiling state
284    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    // If the space below us is free, then "snap" to the ground
291    } else if vel.0.z <= 0.0
292        && was_on_ground
293        && block_snap
294        && physics_state.in_liquid().is_none()
295        && {
296            //prof_span!("snap check");
297            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        //prof_span!("snap!!");
308        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    // Find liquid immersion and wall collision all in one round of iteration
322    let player_aabb = player_aabb(pos.0, radius, z_range.clone());
323    // Calculate the world space near_aabb
324    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    // Compute a list of aabbs to check for collision with nearby walls
334    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    //prof_span!(guard, "liquid/walls");
345    terrain.for_each_in(near_aabb, |block_pos, block| {
346        // Check for liquid blocks
347        if let Some(block_liquid) = block.liquid_kind() {
348            let liquid_aabb = Aabb {
349                min: block_pos.map(|e| e as f32),
350                // The liquid part of a liquid block always extends 1 block high.
351                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                        // TODO: merging of liquid kinds and max_liquid_z are done
357                        // independently which allows mix and
358                        // matching them
359                        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        // Check for walls
368        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    //drop(guard);
384
385    // Use wall collision results to determine if we are against a wall
386    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            // NOTE: assumes min_z == 0.0
402            let depth = max_z - pos.0.z;
403
404            // This is suboptimal because it doesn't check for true depth,
405            // so it can cause problems for situations like swimming down
406            // a river and spawning or teleporting in(/to) water
407            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            // TODO: Change this at some point to allow entities to be moved by liquids?
412            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    // skating (ski)
433    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; // other areas use 60.0???
439        const POTENTIAL_TO_KINETIC: f32 = 8.0; // * 2.0 * GRAVITY;
440
441        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        // the amount of longitudinal speed preserved
444        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            // fall back to travelling dir (in case we look up)
451            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        // if we crossed an edge up or down accelerate in travelling direction,
457        // as potential energy is converted into kinetic energy we compare it with the
458        // square of velocity
459        let vertical_difference = physics_state.skating_last_height - pos.0.z;
460        // might become negative when skating slowly uphill
461        let height_factor_squared = if vertical_difference != 0.0 {
462            // E=½mv², we scale both energies by ½m
463            let kinetic = squared_velocity;
464            // positive accelerate, negative decelerate, ΔE=mgΔh
465            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        // we calculate these squared as we need to combined them Euclidianly anyway,
474        // skiing: separate speed into longitudinal and lateral component
475        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        // lateral speed is reduced by lateral_friction,
480        let new_lateral = lat_speed * lateral_friction_factor;
481        let lateral_speed_reduction = lat_speed - new_lateral;
482        // we convert this reduction partically (by the cosine of the angle) into
483        // longitudinal (elastic collision) and the remainder into heat
484        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            // HACK:
497            // If we're in a liquid, radically reduce ground friction (i.e: assume that
498            // contact force is negligible due to buoyancy) Note that this might
499            // not be realistic for very dense entities (currently no entities in Veloren
500            // are sufficiently negatively buoyant for this to matter). We
501            // should really make friction be proportional to net downward force, but
502            // that means taking into account buoyancy which is a bit difficult to do here
503            // for now.
504            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    // TODO: If the velocity is exactly 0,
537    // a raycast may not pick up the current block.
538    //
539    // Handle this.
540    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        // Can't fail since we do ignore_error above
555        (dist, block.unwrap())
556    };
557
558    pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist;
559
560    // TODO: Not all projectiles should count as sticky!
561    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        // See whether we're on the top/bottom of a block,
585        // or the side
586        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        // Sticky things shouldn't move
604        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    // Compute vector from the origin (where pos value corresponds to) and the model
641    // center
642    let center_offset = center + origin_offset;
643    // Rotate
644    let oriented_center_offset = ori.local_to_global(center_offset);
645    // Add to pos to get world coordinates of the center
646    let wpos_center = oriented_center_offset + pos.0;
647
648    // Note: to not get too fine grained we use a 2D grid for now
649    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/// Returns whether interesction between entities occured
669#[expect(clippy::too_many_arguments)]
670pub(super) fn resolve_e2e_collision(
671    // utility variables for our entity
672    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    // physics flags
680    is_mid_air: bool,
681    is_sticky: bool,
682    is_immovable: bool,
683    is_projectile: bool,
684    // entity we colliding with
685    other: Uid,
686    // symetrical collider context
687    our_data: ColliderData,
688    other_data: ColliderData,
689    vel: &Vel,
690    is_riding: bool,
691) -> bool {
692    // Find the distance betwen our collider and
693    // collider we collide with and get vector of pushback.
694    //
695    // If we aren't colliding, just skip step.
696
697    // Get positions
698    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    // Compare Z ranges
702    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 entities have not yet collided this tick (but just did) and if entity
732    // is either in mid air or is not sticky, then mark them as colliding with
733    // the other entity.
734    if !*collision_registered && (is_mid_air || !is_sticky) {
735        physics.touch_entities.insert(other, pos);
736        *entity_entity_collisions += 1;
737    }
738
739    // Don't apply e2e pushback to entities that are in a forced movement state
740    // (e.g. roll, leapmelee).
741    //
742    // This allows leaps to work properly (since you won't get pushed away
743    // before delivering the hit), and allows rolling through an enemy when
744    // trapped (e.g. with minotaur).
745    //
746    // This allows using e2e pushback to gain speed by jumping out of a roll
747    // while in the middle of a collider, this is an intentional combat mechanic.
748    let forced_movement =
749        matches!(char_state_maybe, Some(cs) if cs.is_forced_movement()) || is_riding;
750
751    // Don't apply repulsive force to projectiles,
752    // or if we're colliding with a terrain-like entity,
753    // or if we are a terrain-like entity.
754    //
755    // Don't apply force when entity is immovable, or a sticky which is on the
756    // ground (or on the wall).
757    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
792/// Find pushback vector and collision_distance we assume between this
793/// colliders.
794fn 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        // fallback to simpler model
806        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        // fallback to simpler model
816        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
833/// Returns the points on line segments n and m respectively that are the
834/// closest to one-another. If the lines are parallel, an arbitrary,
835/// unspecified pair of points that sit on the line segments will be chosen.
836fn closest_points(n: LineSegment2<f32>, m: LineSegment2<f32>) -> (Vec2<f32>, Vec2<f32>) {
837    // TODO: Rewrite this to something reasonable, if you have faith
838    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    // Check to prevent div by 0.0 (produces NaNs) and minimize precision
844    // loss from dividing by small values.
845    // If both d.x and d.y are 0.0 then the segment is a point and we are fine
846    // to fallback to the end point projection.
847    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    // Check to see whether the lines are parallel
859    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
887/// Find pushback vector and collision_distance we assume between this
888/// colliders assuming that only one of them is capsule prism.
889fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {
890    // "Proper" way to do this would be handle the case when both our colliders
891    // are capsule prisms by building origins from p0, p1 offsets and our
892    // positions and find some sort of projection between line segments of
893    // both colliders.
894    // While it's possible, it's not a trivial operation especially
895    // in the case when they are intersect. Because in such case,
896    // even when you found intersection and you should push entities back
897    // from each other, you get then difference between them is 0 vector.
898    //
899    // Considering that we won't fully simulate collision of capsule prism.
900    // As intermediate solution, we would assume that bigger collider
901    // (with bigger scaled_radius) is capsule prism (cylinder is special
902    // case of capsule prism too) and smaller collider is cylinder (point is
903    // special case of cylinder).
904    // So in the end our model of collision and pushback vector is simplified
905    // to checking distance of the point between segment of capsule.
906    //
907    // NOTE: no matter if we consider our collider capsule prism or cylinder
908    // we should always build pushback vector to have direction
909    // of motion from our target collider to our collider.
910    //
911    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}