goud_engine/ecs/collision.rs
1//! Collision detection algorithms for 2D physics.
2//!
3//! This module provides narrow-phase collision detection between various collider shapes.
4//! Each collision function returns contact information when shapes are intersecting.
5//!
6//! # Supported Shape Pairs
7//!
8//! - Circle-Circle (fastest)
9//! - Circle-AABB
10//! - Circle-OBB
11//! - AABB-AABB
12//! - OBB-OBB (SAT algorithm)
13//! - Capsule-Circle
14//! - Capsule-AABB
15//!
16//! # Usage
17//!
18//! ```
19//! use goud_engine::ecs::collision::{circle_circle_collision, Contact};
20//! use goud_engine::core::math::Vec2;
21//!
22//! let circle_a = Vec2::new(0.0, 0.0);
23//! let radius_a = 1.0;
24//! let circle_b = Vec2::new(1.5, 0.0);
25//! let radius_b = 1.0;
26//!
27//! if let Some(contact) = circle_circle_collision(circle_a, radius_a, circle_b, radius_b) {
28//! println!("Circles colliding! Penetration: {}", contact.penetration);
29//! println!("Normal: {:?}", contact.normal);
30//! }
31//! ```
32//!
33//! # Collision Detection Pipeline
34//!
35//! 1. **Broad Phase**: Spatial hash identifies potentially colliding pairs (see [`crate::ecs::broad_phase`])
36//! 2. **Narrow Phase**: This module's algorithms compute exact contact information
37//! 3. **Response**: Physics system resolves collisions based on contact data
38//!
39//! # Performance Notes
40//!
41//! - Circle-circle is fastest (single distance check)
42//! - AABB-AABB is very fast (no rotation)
43//! - OBB-OBB uses SAT (more expensive but accurate)
44//! - Early exits when no collision detected
45
46use crate::core::math::Vec2;
47
48// =============================================================================
49// Collision Response Module
50// =============================================================================
51
52/// Collision response configuration for impulse resolution.
53///
54/// These settings control how collisions are resolved using impulse-based physics.
55/// Different collision types (bounce, slide, etc.) require different configurations.
56///
57/// # Examples
58///
59/// ```
60/// use goud_engine::ecs::collision::CollisionResponse;
61///
62/// // Bouncy ball (high restitution)
63/// let bouncy = CollisionResponse::bouncy();
64///
65/// // Character controller (no bounce, high friction)
66/// let character = CollisionResponse::character();
67///
68/// // Realistic physics (default)
69/// let realistic = CollisionResponse::default();
70/// ```
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct CollisionResponse {
73 /// How much velocity is retained after collision (0.0 = no bounce, 1.0 = perfect bounce).
74 ///
75 /// Typical values:
76 /// - 0.0-0.1: Non-bouncy (characters, boxes)
77 /// - 0.3-0.5: Moderately bouncy (rubber balls)
78 /// - 0.8-1.0: Very bouncy (super balls)
79 pub restitution: f32,
80
81 /// Resistance to sliding (0.0 = ice, 1.0 = rubber on concrete).
82 ///
83 /// Typical values:
84 /// - 0.0-0.1: Ice, very slippery
85 /// - 0.2-0.4: Wood on wood, metal on metal
86 /// - 0.6-0.8: Rubber on concrete
87 pub friction: f32,
88
89 /// Percentage of overlap to resolve (0.0-1.0).
90 ///
91 /// Helps prevent objects from sinking into each other due to numerical errors.
92 /// - 0.0: No positional correction
93 /// - 0.2-0.8: Recommended range (0.4 is common)
94 /// - 1.0: Full correction (may cause jitter)
95 pub position_correction: f32,
96
97 /// Threshold below which positional correction is not applied.
98 ///
99 /// Prevents over-correction for small penetrations.
100 /// Typical value: 0.01 units
101 pub slop: f32,
102}
103
104impl CollisionResponse {
105 /// Creates a new collision response configuration.
106 ///
107 /// # Arguments
108 ///
109 /// * `restitution` - Bounciness (0.0 = no bounce, 1.0 = perfect bounce)
110 /// * `friction` - Sliding resistance (0.0 = ice, 1.0 = rubber)
111 /// * `position_correction` - Overlap correction percentage (0.0-1.0)
112 /// * `slop` - Minimum penetration for correction
113 ///
114 /// # Example
115 ///
116 /// ```
117 /// use goud_engine::ecs::collision::CollisionResponse;
118 ///
119 /// let response = CollisionResponse::new(0.5, 0.4, 0.4, 0.01);
120 /// ```
121 pub fn new(restitution: f32, friction: f32, position_correction: f32, slop: f32) -> Self {
122 Self {
123 restitution: restitution.clamp(0.0, 1.0),
124 friction: friction.clamp(0.0, 1.0),
125 position_correction: position_correction.clamp(0.0, 1.0),
126 slop: slop.max(0.0),
127 }
128 }
129
130 /// Creates a bouncy collision response (high restitution, low friction).
131 ///
132 /// Good for: balls, projectiles, bouncing objects
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// use goud_engine::ecs::collision::CollisionResponse;
138 ///
139 /// let bouncy = CollisionResponse::bouncy();
140 /// assert_eq!(bouncy.restitution, 0.8);
141 /// ```
142 pub fn bouncy() -> Self {
143 Self::new(0.8, 0.2, 0.4, 0.01)
144 }
145
146 /// Creates a character controller response (no bounce, high friction).
147 ///
148 /// Good for: player characters, NPCs, walking entities
149 ///
150 /// # Example
151 ///
152 /// ```
153 /// use goud_engine::ecs::collision::CollisionResponse;
154 ///
155 /// let character = CollisionResponse::character();
156 /// assert_eq!(character.restitution, 0.0);
157 /// assert_eq!(character.friction, 0.8);
158 /// ```
159 pub fn character() -> Self {
160 Self::new(0.0, 0.8, 0.6, 0.01)
161 }
162
163 /// Creates a slippery collision response (low friction, some bounce).
164 ///
165 /// Good for: ice, smooth surfaces, low-friction materials
166 ///
167 /// # Example
168 ///
169 /// ```
170 /// use goud_engine::ecs::collision::CollisionResponse;
171 ///
172 /// let slippery = CollisionResponse::slippery();
173 /// assert!(slippery.friction < 0.2);
174 /// ```
175 pub fn slippery() -> Self {
176 Self::new(0.3, 0.1, 0.4, 0.01)
177 }
178
179 /// Creates a perfectly elastic collision (no energy loss).
180 ///
181 /// Good for: billiard balls, perfect bounce scenarios
182 ///
183 /// # Example
184 ///
185 /// ```
186 /// use goud_engine::ecs::collision::CollisionResponse;
187 ///
188 /// let elastic = CollisionResponse::elastic();
189 /// assert_eq!(elastic.restitution, 1.0);
190 /// ```
191 pub fn elastic() -> Self {
192 Self::new(1.0, 0.4, 0.4, 0.01)
193 }
194}
195
196impl Default for CollisionResponse {
197 /// Default collision response: moderate bounce and friction.
198 fn default() -> Self {
199 Self::new(0.4, 0.4, 0.4, 0.01)
200 }
201}
202
203/// Resolves a collision between two bodies using impulse-based physics.
204///
205/// This function computes and applies the impulse needed to resolve a collision
206/// between two dynamic bodies. It handles both normal impulse (bounce/separation)
207/// and tangent impulse (friction).
208///
209/// # Algorithm
210///
211/// 1. Compute relative velocity at contact point
212/// 2. Apply restitution (bounce) along normal
213/// 3. Apply friction along tangent
214/// 4. Apply position correction to prevent sinking
215///
216/// # Arguments
217///
218/// * `contact` - Contact information from collision detection
219/// * `velocity_a` - Linear velocity of body A
220/// * `velocity_b` - Linear velocity of body B
221/// * `inv_mass_a` - Inverse mass of body A (0.0 for static)
222/// * `inv_mass_b` - Inverse mass of body B (0.0 for static)
223/// * `response` - Collision response configuration
224///
225/// # Returns
226///
227/// Tuple of velocity changes: `(delta_velocity_a, delta_velocity_b)`
228///
229/// # Examples
230///
231/// ```
232/// use goud_engine::ecs::collision::{Contact, CollisionResponse, resolve_collision};
233/// use goud_engine::core::math::Vec2;
234///
235/// let contact = Contact::new(
236/// Vec2::new(1.0, 0.0), // contact point
237/// Vec2::new(1.0, 0.0), // normal (A to B)
238/// 0.1, // penetration
239/// );
240///
241/// let vel_a = Vec2::new(10.0, 0.0); // A moving right
242/// let vel_b = Vec2::new(-5.0, 0.0); // B moving left
243/// let inv_mass_a = 1.0; // 1 kg
244/// let inv_mass_b = 1.0; // 1 kg
245/// let response = CollisionResponse::default();
246///
247/// let (delta_vel_a, delta_vel_b) = resolve_collision(
248/// &contact,
249/// vel_a,
250/// vel_b,
251/// inv_mass_a,
252/// inv_mass_b,
253/// &response,
254/// );
255///
256/// // Apply velocity changes
257/// let new_vel_a = vel_a + delta_vel_a;
258/// let new_vel_b = vel_b + delta_vel_b;
259/// ```
260///
261/// # Physics Notes
262///
263/// - Static bodies (inv_mass = 0.0) do not move
264/// - Higher mass (lower inv_mass) means less velocity change
265/// - Restitution of 1.0 means perfect elastic collision
266/// - Friction is applied perpendicular to collision normal
267pub fn resolve_collision(
268 contact: &Contact,
269 velocity_a: Vec2,
270 velocity_b: Vec2,
271 inv_mass_a: f32,
272 inv_mass_b: f32,
273 response: &CollisionResponse,
274) -> (Vec2, Vec2) {
275 // Early exit if both bodies are static
276 let total_inv_mass = inv_mass_a + inv_mass_b;
277 if total_inv_mass < 1e-6 {
278 return (Vec2::zero(), Vec2::zero());
279 }
280
281 // Compute relative velocity
282 let relative_velocity = velocity_b - velocity_a;
283 let velocity_along_normal = relative_velocity.dot(contact.normal);
284
285 // Don't resolve if velocities are separating
286 if velocity_along_normal > 0.0 {
287 return (Vec2::zero(), Vec2::zero());
288 }
289
290 // =================================================================
291 // Normal Impulse (bounce/separation)
292 // =================================================================
293
294 // Compute impulse scalar with restitution
295 let restitution = response.restitution;
296 let impulse_scalar = -(1.0 + restitution) * velocity_along_normal / total_inv_mass;
297
298 // Apply impulse along normal
299 let impulse = contact.normal * impulse_scalar;
300 let delta_vel_a_normal = impulse * -inv_mass_a;
301 let delta_vel_b_normal = impulse * inv_mass_b;
302
303 // =================================================================
304 // Tangent Impulse (friction)
305 // =================================================================
306
307 // Compute tangent (perpendicular to normal)
308 let tangent = Vec2::new(-contact.normal.y, contact.normal.x);
309
310 // Relative velocity along tangent
311 let relative_velocity_tangent =
312 (relative_velocity + delta_vel_b_normal - delta_vel_a_normal).dot(tangent);
313
314 // Don't apply friction if already stationary in tangent direction
315 if relative_velocity_tangent.abs() < 1e-6 {
316 return (delta_vel_a_normal, delta_vel_b_normal);
317 }
318
319 // Compute friction impulse (Coulomb friction model)
320 let friction_impulse_scalar = -relative_velocity_tangent / total_inv_mass;
321 let mu = response.friction;
322
323 // Clamp friction to not exceed normal impulse (Coulomb's law)
324 let friction_impulse_scalar =
325 friction_impulse_scalar.clamp(-impulse_scalar * mu, impulse_scalar * mu);
326
327 let friction_impulse = tangent * friction_impulse_scalar;
328 let delta_vel_a_friction = friction_impulse * -inv_mass_a;
329 let delta_vel_b_friction = friction_impulse * inv_mass_b;
330
331 // =================================================================
332 // Combine normal and friction impulses
333 // =================================================================
334
335 let delta_vel_a = delta_vel_a_normal + delta_vel_a_friction;
336 let delta_vel_b = delta_vel_b_normal + delta_vel_b_friction;
337
338 (delta_vel_a, delta_vel_b)
339}
340
341/// Computes positional correction to prevent sinking due to numerical drift.
342///
343/// This function uses Baumgarte stabilization to gradually push objects apart
344/// when they penetrate each other. This prevents accumulation of small errors
345/// that would cause objects to sink over time.
346///
347/// # Arguments
348///
349/// * `contact` - Contact information with penetration depth
350/// * `inv_mass_a` - Inverse mass of body A (0.0 for static)
351/// * `inv_mass_b` - Inverse mass of body B (0.0 for static)
352/// * `response` - Collision response configuration (uses position_correction and slop)
353///
354/// # Returns
355///
356/// Tuple of position corrections: `(correction_a, correction_b)`
357///
358/// # Examples
359///
360/// ```
361/// use goud_engine::ecs::collision::{Contact, CollisionResponse, compute_position_correction};
362/// use goud_engine::core::math::Vec2;
363///
364/// let contact = Contact::new(
365/// Vec2::new(1.0, 0.0),
366/// Vec2::new(1.0, 0.0),
367/// 0.1, // 0.1 units penetration
368/// );
369///
370/// let inv_mass_a = 1.0;
371/// let inv_mass_b = 1.0;
372/// let response = CollisionResponse::default();
373///
374/// let (correction_a, correction_b) = compute_position_correction(
375/// &contact,
376/// inv_mass_a,
377/// inv_mass_b,
378/// &response,
379/// );
380///
381/// // Apply corrections to positions
382/// // position_a += correction_a;
383/// // position_b += correction_b;
384/// ```
385///
386/// # Physics Notes
387///
388/// - Only corrects penetration above `slop` threshold
389/// - Correction percentage controls trade-off between stability and jitter
390/// - Static bodies (inv_mass = 0.0) do not move
391/// - Heavier objects move less than lighter objects
392pub fn compute_position_correction(
393 contact: &Contact,
394 inv_mass_a: f32,
395 inv_mass_b: f32,
396 response: &CollisionResponse,
397) -> (Vec2, Vec2) {
398 // Early exit if both bodies are static
399 let total_inv_mass = inv_mass_a + inv_mass_b;
400 if total_inv_mass < 1e-6 {
401 return (Vec2::zero(), Vec2::zero());
402 }
403
404 // Only correct penetration above slop threshold
405 let penetration = contact.penetration - response.slop;
406 if penetration <= 0.0 {
407 return (Vec2::zero(), Vec2::zero());
408 }
409
410 // Compute correction magnitude (Baumgarte stabilization)
411 let correction_percent = response.position_correction;
412 let correction_magnitude = penetration * correction_percent / total_inv_mass;
413
414 // Apply correction along collision normal
415 let correction = contact.normal * correction_magnitude;
416 let correction_a = correction * -inv_mass_a;
417 let correction_b = correction * inv_mass_b;
418
419 (correction_a, correction_b)
420}
421
422// =============================================================================
423// Contact Information
424// =============================================================================
425
426/// Contact information from a collision.
427///
428/// When two shapes collide, this struct contains all information needed to
429/// resolve the collision:
430///
431/// - **point**: The contact point in world space
432/// - **normal**: The collision normal (points from A to B)
433/// - **penetration**: How deep the shapes overlap (positive = overlapping)
434///
435/// # Example
436///
437/// ```
438/// use goud_engine::ecs::collision::{circle_circle_collision, Contact};
439/// use goud_engine::core::math::Vec2;
440///
441/// let contact = circle_circle_collision(
442/// Vec2::new(0.0, 0.0), 1.0,
443/// Vec2::new(1.5, 0.0), 1.0
444/// ).unwrap();
445///
446/// assert!(contact.penetration > 0.0);
447/// assert_eq!(contact.normal, Vec2::new(1.0, 0.0));
448/// ```
449#[derive(Debug, Clone, Copy, PartialEq)]
450pub struct Contact {
451 /// Contact point in world space.
452 ///
453 /// This is the point where the two shapes are touching. For penetrating
454 /// collisions, this is typically the deepest penetration point.
455 pub point: Vec2,
456
457 /// Collision normal (unit vector from shape A to shape B).
458 ///
459 /// This vector points from the first shape to the second shape and is
460 /// normalized to unit length. It indicates the direction to separate
461 /// the shapes to resolve the collision.
462 pub normal: Vec2,
463
464 /// Penetration depth (positive = overlapping, negative = separated).
465 ///
466 /// This is the distance the shapes overlap. A positive value means the
467 /// shapes are penetrating. To resolve the collision, move the shapes
468 /// apart by this distance along the normal.
469 pub penetration: f32,
470}
471
472impl Contact {
473 /// Creates a new contact.
474 ///
475 /// # Arguments
476 ///
477 /// * `point` - Contact point in world space
478 /// * `normal` - Collision normal (should be normalized)
479 /// * `penetration` - Penetration depth
480 ///
481 /// # Example
482 ///
483 /// ```
484 /// use goud_engine::ecs::collision::Contact;
485 /// use goud_engine::core::math::Vec2;
486 ///
487 /// let contact = Contact::new(
488 /// Vec2::new(1.0, 0.0),
489 /// Vec2::new(1.0, 0.0),
490 /// 0.5
491 /// );
492 /// ```
493 pub fn new(point: Vec2, normal: Vec2, penetration: f32) -> Self {
494 Self {
495 point,
496 normal,
497 penetration,
498 }
499 }
500
501 /// Returns true if the contact represents a collision (positive penetration).
502 ///
503 /// # Example
504 ///
505 /// ```
506 /// use goud_engine::ecs::collision::Contact;
507 /// use goud_engine::core::math::Vec2;
508 ///
509 /// let colliding = Contact::new(Vec2::zero(), Vec2::unit_x(), 0.5);
510 /// let separated = Contact::new(Vec2::zero(), Vec2::unit_x(), -0.1);
511 ///
512 /// assert!(colliding.is_colliding());
513 /// assert!(!separated.is_colliding());
514 /// ```
515 pub fn is_colliding(&self) -> bool {
516 self.penetration > 0.0
517 }
518
519 /// Returns the separation distance needed to resolve the collision.
520 ///
521 /// This is the magnitude of the vector needed to separate the shapes.
522 ///
523 /// # Example
524 ///
525 /// ```
526 /// use goud_engine::ecs::collision::Contact;
527 /// use goud_engine::core::math::Vec2;
528 ///
529 /// let contact = Contact::new(Vec2::zero(), Vec2::unit_x(), 0.5);
530 /// assert_eq!(contact.separation_distance(), 0.5);
531 /// ```
532 pub fn separation_distance(&self) -> f32 {
533 self.penetration.abs()
534 }
535
536 /// Returns the separation vector needed to resolve the collision.
537 ///
538 /// This is the vector (normal * penetration) that would separate the shapes.
539 ///
540 /// # Example
541 ///
542 /// ```
543 /// use goud_engine::ecs::collision::Contact;
544 /// use goud_engine::core::math::Vec2;
545 ///
546 /// let contact = Contact::new(
547 /// Vec2::zero(),
548 /// Vec2::new(1.0, 0.0),
549 /// 0.5
550 /// );
551 /// assert_eq!(contact.separation_vector(), Vec2::new(0.5, 0.0));
552 /// ```
553 pub fn separation_vector(&self) -> Vec2 {
554 self.normal * self.penetration
555 }
556
557 /// Returns a contact with reversed normal (swaps A and B).
558 ///
559 /// This is useful when the collision detection function expects shapes
560 /// in a specific order but you have them reversed.
561 ///
562 /// # Example
563 ///
564 /// ```
565 /// use goud_engine::ecs::collision::Contact;
566 /// use goud_engine::core::math::Vec2;
567 ///
568 /// let contact = Contact::new(
569 /// Vec2::zero(),
570 /// Vec2::new(1.0, 0.0),
571 /// 0.5
572 /// );
573 /// let reversed = contact.reversed();
574 ///
575 /// assert_eq!(reversed.normal, Vec2::new(-1.0, 0.0));
576 /// assert_eq!(reversed.penetration, contact.penetration);
577 /// ```
578 pub fn reversed(&self) -> Self {
579 Self {
580 point: self.point,
581 normal: self.normal * -1.0,
582 penetration: self.penetration,
583 }
584 }
585}
586
587impl Default for Contact {
588 /// Returns a contact with no collision (zero penetration).
589 fn default() -> Self {
590 Self {
591 point: Vec2::zero(),
592 normal: Vec2::unit_x(),
593 penetration: 0.0,
594 }
595 }
596}
597
598// =============================================================================
599// Circle-Circle Collision
600// =============================================================================
601
602/// Detects collision between two circles.
603///
604/// This is the fastest collision detection algorithm, requiring only a single
605/// distance check. Returns contact information if the circles overlap.
606///
607/// # Arguments
608///
609/// * `center_a` - Center of circle A
610/// * `radius_a` - Radius of circle A (must be positive)
611/// * `center_b` - Center of circle B
612/// * `radius_b` - Radius of circle B (must be positive)
613///
614/// # Returns
615///
616/// - `Some(Contact)` if the circles are overlapping
617/// - `None` if the circles are separated
618///
619/// # Example
620///
621/// ```
622/// use goud_engine::ecs::collision::circle_circle_collision;
623/// use goud_engine::core::math::Vec2;
624///
625/// // Overlapping circles
626/// let contact = circle_circle_collision(
627/// Vec2::new(0.0, 0.0), 1.0,
628/// Vec2::new(1.5, 0.0), 1.0
629/// );
630/// assert!(contact.is_some());
631/// assert!(contact.unwrap().penetration > 0.0);
632///
633/// // Separated circles
634/// let no_contact = circle_circle_collision(
635/// Vec2::new(0.0, 0.0), 1.0,
636/// Vec2::new(5.0, 0.0), 1.0
637/// );
638/// assert!(no_contact.is_none());
639/// ```
640///
641/// # Performance
642///
643/// O(1) - Single distance computation and comparison.
644pub fn circle_circle_collision(
645 center_a: Vec2,
646 radius_a: f32,
647 center_b: Vec2,
648 radius_b: f32,
649) -> Option<Contact> {
650 // Vector from A to B
651 let delta = center_b - center_a;
652 let distance_squared = delta.length_squared();
653 let combined_radius = radius_a + radius_b;
654 let combined_radius_squared = combined_radius * combined_radius;
655
656 // Early exit if circles are separated
657 if distance_squared > combined_radius_squared {
658 return None;
659 }
660
661 // Handle edge case: circles at exact same position
662 if distance_squared < 1e-6 {
663 // Circles are basically at the same position, use arbitrary normal
664 return Some(Contact {
665 point: center_a,
666 normal: Vec2::unit_x(),
667 penetration: combined_radius,
668 });
669 }
670
671 // Compute distance and normal
672 let distance = distance_squared.sqrt();
673 let normal = delta / distance;
674
675 // Penetration is how much they overlap
676 let penetration = combined_radius - distance;
677
678 // Contact point is halfway between the closest points on each circle
679 let contact_point = center_a + normal * (radius_a - penetration * 0.5);
680
681 Some(Contact {
682 point: contact_point,
683 normal,
684 penetration,
685 })
686}
687
688// =============================================================================
689// Box-Box Collision (SAT)
690// =============================================================================
691
692/// Detects collision between two oriented bounding boxes (OBBs) using SAT.
693///
694/// The Separating Axis Theorem (SAT) is the standard algorithm for OBB collision
695/// detection. It tests for separation along potential separating axes. If no
696/// separating axis is found, the boxes are colliding.
697///
698/// # Arguments
699///
700/// * `center_a` - Center of box A
701/// * `half_extents_a` - Half-width and half-height of box A
702/// * `rotation_a` - Rotation angle of box A in radians
703/// * `center_b` - Center of box B
704/// * `half_extents_b` - Half-width and half-height of box B
705/// * `rotation_b` - Rotation angle of box B in radians
706///
707/// # Returns
708///
709/// - `Some(Contact)` if the boxes are overlapping
710/// - `None` if the boxes are separated
711///
712/// # Algorithm
713///
714/// SAT tests for separation along 4 potential axes:
715/// 1. Box A's X axis (rotated)
716/// 2. Box A's Y axis (rotated)
717/// 3. Box B's X axis (rotated)
718/// 4. Box B's Y axis (rotated)
719///
720/// For each axis, we project both boxes and check for overlap.
721/// If any axis shows no overlap, the boxes are separated.
722/// Otherwise, we track the axis with minimum overlap (the collision normal).
723///
724/// # Example
725///
726/// ```
727/// use goud_engine::ecs::collision::box_box_collision;
728/// use goud_engine::core::math::Vec2;
729///
730/// // Two axis-aligned boxes overlapping
731/// let contact = box_box_collision(
732/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), 0.0,
733/// Vec2::new(1.5, 0.0), Vec2::new(1.0, 1.0), 0.0
734/// );
735/// assert!(contact.is_some());
736///
737/// // Two rotated boxes separated
738/// let no_contact = box_box_collision(
739/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), 0.0,
740/// Vec2::new(5.0, 0.0), Vec2::new(1.0, 1.0), std::f32::consts::PI / 4.0
741/// );
742/// assert!(no_contact.is_none());
743/// ```
744///
745/// # Performance
746///
747/// O(1) - Tests 4 axes, each with constant-time projection and overlap checks.
748/// More expensive than AABB-AABB due to rotation handling.
749pub fn box_box_collision(
750 center_a: Vec2,
751 half_extents_a: Vec2,
752 rotation_a: f32,
753 center_b: Vec2,
754 half_extents_b: Vec2,
755 rotation_b: f32,
756) -> Option<Contact> {
757 // Compute rotation matrices (cos/sin)
758 let cos_a = rotation_a.cos();
759 let sin_a = rotation_a.sin();
760 let cos_b = rotation_b.cos();
761 let sin_b = rotation_b.sin();
762
763 // Box A axes (rotated)
764 let axis_a_x = Vec2::new(cos_a, sin_a);
765 let axis_a_y = Vec2::new(-sin_a, cos_a);
766
767 // Box B axes (rotated)
768 let axis_b_x = Vec2::new(cos_b, sin_b);
769 let axis_b_y = Vec2::new(-sin_b, cos_b);
770
771 // Center offset
772 let delta = center_b - center_a;
773
774 // Track minimum overlap and collision normal
775 let mut min_overlap = f32::INFINITY;
776 let mut collision_normal = Vec2::unit_x();
777
778 // Test all 4 potential separating axes
779 let axes = [
780 (axis_a_x, half_extents_a.x, half_extents_a.y),
781 (axis_a_y, half_extents_a.x, half_extents_a.y),
782 (axis_b_x, half_extents_b.x, half_extents_b.y),
783 (axis_b_y, half_extents_b.x, half_extents_b.y),
784 ];
785
786 for (axis, _hx_a, _hy_a) in &axes {
787 // Project box A onto axis
788 let r_a = half_extents_a.x * (axis.dot(axis_a_x)).abs()
789 + half_extents_a.y * (axis.dot(axis_a_y)).abs();
790
791 // Project box B onto axis
792 let r_b = half_extents_b.x * (axis.dot(axis_b_x)).abs()
793 + half_extents_b.y * (axis.dot(axis_b_y)).abs();
794
795 // Project center offset onto axis
796 let distance = axis.dot(delta).abs();
797
798 // Check for separation
799 let overlap = r_a + r_b - distance;
800 if overlap < 0.0 {
801 // Found separating axis - no collision
802 return None;
803 }
804
805 // Track minimum overlap (shallowest penetration)
806 if overlap < min_overlap {
807 min_overlap = overlap;
808 collision_normal = *axis;
809
810 // Ensure normal points from A to B
811 if axis.dot(delta) < 0.0 {
812 collision_normal = collision_normal * -1.0;
813 }
814 }
815 }
816
817 // No separating axis found - boxes are colliding
818 let contact_point = center_a + delta * 0.5; // Approximate contact point
819
820 Some(Contact {
821 point: contact_point,
822 normal: collision_normal,
823 penetration: min_overlap,
824 })
825}
826
827/// Detects collision between two axis-aligned bounding boxes (AABBs).
828///
829/// This is a specialized, faster version of box-box collision for axis-aligned
830/// boxes (rotation = 0). It's much simpler than the full SAT algorithm.
831///
832/// # Arguments
833///
834/// * `center_a` - Center of box A
835/// * `half_extents_a` - Half-width and half-height of box A
836/// * `center_b` - Center of box B
837/// * `half_extents_b` - Half-width and half-height of box B
838///
839/// # Returns
840///
841/// - `Some(Contact)` if the boxes are overlapping
842/// - `None` if the boxes are separated
843///
844/// # Example
845///
846/// ```
847/// use goud_engine::ecs::collision::aabb_aabb_collision;
848/// use goud_engine::core::math::Vec2;
849///
850/// // Two AABBs overlapping
851/// let contact = aabb_aabb_collision(
852/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0),
853/// Vec2::new(1.5, 0.0), Vec2::new(1.0, 1.0)
854/// );
855/// assert!(contact.is_some());
856/// ```
857///
858/// # Performance
859///
860/// O(1) - Simple comparison checks, no trigonometry required.
861pub fn aabb_aabb_collision(
862 center_a: Vec2,
863 half_extents_a: Vec2,
864 center_b: Vec2,
865 half_extents_b: Vec2,
866) -> Option<Contact> {
867 // Compute min/max bounds
868 let min_a = center_a - half_extents_a;
869 let max_a = center_a + half_extents_a;
870 let min_b = center_b - half_extents_b;
871 let max_b = center_b + half_extents_b;
872
873 // Check for overlap on X axis
874 if max_a.x < min_b.x || max_b.x < min_a.x {
875 return None;
876 }
877
878 // Check for overlap on Y axis
879 if max_a.y < min_b.y || max_b.y < min_a.y {
880 return None;
881 }
882
883 // Compute overlap on each axis
884 let overlap_x = (max_a.x.min(max_b.x) - min_a.x.max(min_b.x)).abs();
885 let overlap_y = (max_a.y.min(max_b.y) - min_a.y.max(min_b.y)).abs();
886
887 // Find minimum overlap axis (collision normal)
888 let (penetration, normal) = if overlap_x < overlap_y {
889 // X axis is minimum
890 let normal = if center_b.x > center_a.x {
891 Vec2::unit_x()
892 } else {
893 Vec2::new(-1.0, 0.0)
894 };
895 (overlap_x, normal)
896 } else {
897 // Y axis is minimum
898 let normal = if center_b.y > center_a.y {
899 Vec2::unit_y()
900 } else {
901 Vec2::new(0.0, -1.0)
902 };
903 (overlap_y, normal)
904 };
905
906 // Contact point is at the center of overlap region
907 let contact_point = Vec2::new(
908 (min_a.x.max(min_b.x) + max_a.x.min(max_b.x)) * 0.5,
909 (min_a.y.max(min_b.y) + max_a.y.min(max_b.y)) * 0.5,
910 );
911
912 Some(Contact {
913 point: contact_point,
914 normal,
915 penetration,
916 })
917}
918
919// =============================================================================
920// Circle-Box Collision
921// =============================================================================
922
923/// Detects collision between a circle and an axis-aligned bounding box (AABB).
924///
925/// This uses the "closest point" algorithm:
926/// 1. Find the closest point on the AABB to the circle center
927/// 2. Check if that point is within the circle's radius
928///
929/// This is more efficient than the full OBB version since it doesn't require
930/// rotation transformations.
931///
932/// # Arguments
933///
934/// * `circle_center` - Center of the circle
935/// * `circle_radius` - Radius of the circle (must be positive)
936/// * `box_center` - Center of the AABB
937/// * `box_half_extents` - Half-width and half-height of the AABB
938///
939/// # Returns
940///
941/// - `Some(Contact)` if the circle and AABB are overlapping
942/// - `None` if they are separated
943///
944/// # Example
945///
946/// ```
947/// use goud_engine::ecs::collision::circle_aabb_collision;
948/// use goud_engine::core::math::Vec2;
949///
950/// // Circle overlapping with AABB
951/// let contact = circle_aabb_collision(
952/// Vec2::new(1.5, 0.0), 1.0,
953/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)
954/// );
955/// assert!(contact.is_some());
956///
957/// // Circle separated from AABB
958/// let no_contact = circle_aabb_collision(
959/// Vec2::new(5.0, 0.0), 1.0,
960/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)
961/// );
962/// assert!(no_contact.is_none());
963/// ```
964///
965/// # Performance
966///
967/// O(1) - Simple clamping and distance check.
968pub fn circle_aabb_collision(
969 circle_center: Vec2,
970 circle_radius: f32,
971 box_center: Vec2,
972 box_half_extents: Vec2,
973) -> Option<Contact> {
974 // Compute AABB bounds
975 let box_min = box_center - box_half_extents;
976 let box_max = box_center + box_half_extents;
977
978 // Find the closest point on the AABB to the circle center
979 let closest_point = Vec2::new(
980 circle_center.x.clamp(box_min.x, box_max.x),
981 circle_center.y.clamp(box_min.y, box_max.y),
982 );
983
984 // Vector from closest point to circle center
985 let delta = circle_center - closest_point;
986 let distance_squared = delta.length_squared();
987 let radius_squared = circle_radius * circle_radius;
988
989 // Early exit if separated
990 if distance_squared > radius_squared {
991 return None;
992 }
993
994 // Handle edge case: circle center inside box
995 if distance_squared < 1e-6 {
996 // Circle center is inside the box, find the axis of minimum penetration
997 let penetration_x =
998 (box_half_extents.x - (circle_center.x - box_center.x).abs()) + circle_radius;
999 let penetration_y =
1000 (box_half_extents.y - (circle_center.y - box_center.y).abs()) + circle_radius;
1001
1002 if penetration_x < penetration_y {
1003 // Push out along X axis
1004 let normal = if circle_center.x > box_center.x {
1005 Vec2::unit_x()
1006 } else {
1007 Vec2::new(-1.0, 0.0)
1008 };
1009 return Some(Contact {
1010 point: Vec2::new(
1011 if circle_center.x > box_center.x {
1012 box_max.x
1013 } else {
1014 box_min.x
1015 },
1016 circle_center.y,
1017 ),
1018 normal,
1019 penetration: penetration_x,
1020 });
1021 } else {
1022 // Push out along Y axis
1023 let normal = if circle_center.y > box_center.y {
1024 Vec2::unit_y()
1025 } else {
1026 Vec2::new(0.0, -1.0)
1027 };
1028 return Some(Contact {
1029 point: Vec2::new(
1030 circle_center.x,
1031 if circle_center.y > box_center.y {
1032 box_max.y
1033 } else {
1034 box_min.y
1035 },
1036 ),
1037 normal,
1038 penetration: penetration_y,
1039 });
1040 }
1041 }
1042
1043 // Compute distance and normal
1044 let distance = distance_squared.sqrt();
1045 let normal = delta / distance;
1046
1047 // Penetration is how much the circle overlaps with the box
1048 let penetration = circle_radius - distance;
1049
1050 // Contact point is on the surface of the circle
1051 let contact_point = closest_point;
1052
1053 Some(Contact {
1054 point: contact_point,
1055 normal,
1056 penetration,
1057 })
1058}
1059
1060/// Detects collision between a circle and an oriented bounding box (OBB).
1061///
1062/// This is a more general version of circle-AABB collision that handles
1063/// rotated boxes. The algorithm:
1064/// 1. Transform the circle into the box's local coordinate space
1065/// 2. Perform circle-AABB collision in local space
1066/// 3. Transform the contact back to world space
1067///
1068/// # Arguments
1069///
1070/// * `circle_center` - Center of the circle in world space
1071/// * `circle_radius` - Radius of the circle (must be positive)
1072/// * `box_center` - Center of the OBB in world space
1073/// * `box_half_extents` - Half-width and half-height of the OBB
1074/// * `box_rotation` - Rotation angle of the OBB in radians
1075///
1076/// # Returns
1077///
1078/// - `Some(Contact)` if the circle and OBB are overlapping
1079/// - `None` if they are separated
1080///
1081/// # Example
1082///
1083/// ```
1084/// use goud_engine::ecs::collision::circle_obb_collision;
1085/// use goud_engine::core::math::Vec2;
1086/// use std::f32::consts::PI;
1087///
1088/// // Circle overlapping with rotated box
1089/// let contact = circle_obb_collision(
1090/// Vec2::new(1.5, 0.0), 1.0,
1091/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), PI / 4.0
1092/// );
1093/// assert!(contact.is_some());
1094///
1095/// // Circle separated from rotated box
1096/// let no_contact = circle_obb_collision(
1097/// Vec2::new(5.0, 0.0), 1.0,
1098/// Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), PI / 4.0
1099/// );
1100/// assert!(no_contact.is_none());
1101/// ```
1102///
1103/// # Performance
1104///
1105/// O(1) - Coordinate transformation + AABB collision check.
1106/// More expensive than AABB version due to sin/cos calculations.
1107pub fn circle_obb_collision(
1108 circle_center: Vec2,
1109 circle_radius: f32,
1110 box_center: Vec2,
1111 box_half_extents: Vec2,
1112 box_rotation: f32,
1113) -> Option<Contact> {
1114 // If rotation is near zero, use faster AABB version
1115 if box_rotation.abs() < 1e-6 {
1116 return circle_aabb_collision(circle_center, circle_radius, box_center, box_half_extents);
1117 }
1118
1119 // Compute rotation matrix for box (inverse rotation to transform to local space)
1120 let cos_r = box_rotation.cos();
1121 let sin_r = box_rotation.sin();
1122
1123 // Transform circle center to box's local coordinate space
1124 let delta = circle_center - box_center;
1125 let local_circle_center = Vec2::new(
1126 delta.x * cos_r + delta.y * sin_r,
1127 -delta.x * sin_r + delta.y * cos_r,
1128 );
1129
1130 // Perform collision in local space (box is axis-aligned here)
1131 let local_contact = circle_aabb_collision(
1132 local_circle_center,
1133 circle_radius,
1134 Vec2::zero(), // Box is at origin in local space
1135 box_half_extents,
1136 )?;
1137
1138 // Transform contact back to world space
1139 let world_normal = Vec2::new(
1140 local_contact.normal.x * cos_r - local_contact.normal.y * sin_r,
1141 local_contact.normal.x * sin_r + local_contact.normal.y * cos_r,
1142 );
1143
1144 let world_point = Vec2::new(
1145 local_contact.point.x * cos_r - local_contact.point.y * sin_r,
1146 local_contact.point.x * sin_r + local_contact.point.y * cos_r,
1147 ) + box_center;
1148
1149 Some(Contact {
1150 point: world_point,
1151 normal: world_normal,
1152 penetration: local_contact.penetration,
1153 })
1154}
1155
1156// =============================================================================
1157// Tests
1158// =============================================================================
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 // -------------------------------------------------------------------------
1165 // Contact Tests
1166 // -------------------------------------------------------------------------
1167
1168 #[test]
1169 fn test_contact_new() {
1170 let contact = Contact::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, 0.0), 0.5);
1171 assert_eq!(contact.point, Vec2::new(1.0, 2.0));
1172 assert_eq!(contact.normal, Vec2::new(1.0, 0.0));
1173 assert_eq!(contact.penetration, 0.5);
1174 }
1175
1176 #[test]
1177 fn test_contact_default() {
1178 let contact = Contact::default();
1179 assert_eq!(contact.point, Vec2::zero());
1180 assert_eq!(contact.normal, Vec2::unit_x());
1181 assert_eq!(contact.penetration, 0.0);
1182 }
1183
1184 #[test]
1185 fn test_contact_is_colliding() {
1186 let colliding = Contact::new(Vec2::zero(), Vec2::unit_x(), 0.5);
1187 let touching = Contact::new(Vec2::zero(), Vec2::unit_x(), 0.0);
1188 let separated = Contact::new(Vec2::zero(), Vec2::unit_x(), -0.1);
1189
1190 assert!(colliding.is_colliding());
1191 assert!(!touching.is_colliding());
1192 assert!(!separated.is_colliding());
1193 }
1194
1195 #[test]
1196 fn test_contact_separation_distance() {
1197 let contact = Contact::new(Vec2::zero(), Vec2::unit_x(), 0.5);
1198 assert_eq!(contact.separation_distance(), 0.5);
1199
1200 let negative = Contact::new(Vec2::zero(), Vec2::unit_x(), -0.3);
1201 assert_eq!(negative.separation_distance(), 0.3);
1202 }
1203
1204 #[test]
1205 fn test_contact_separation_vector() {
1206 let contact = Contact::new(Vec2::zero(), Vec2::new(1.0, 0.0), 0.5);
1207 assert_eq!(contact.separation_vector(), Vec2::new(0.5, 0.0));
1208
1209 let diagonal = Contact::new(
1210 Vec2::zero(),
1211 Vec2::new(0.6, 0.8), // Normalized (approximately)
1212 2.0,
1213 );
1214 let sep = diagonal.separation_vector();
1215 assert!((sep.x - 1.2).abs() < 1e-5);
1216 assert!((sep.y - 1.6).abs() < 1e-5);
1217 }
1218
1219 #[test]
1220 fn test_contact_reversed() {
1221 let contact = Contact::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, 0.0), 0.5);
1222 let reversed = contact.reversed();
1223
1224 assert_eq!(reversed.point, contact.point);
1225 assert_eq!(reversed.normal, Vec2::new(-1.0, 0.0));
1226 assert_eq!(reversed.penetration, contact.penetration);
1227 }
1228
1229 #[test]
1230 fn test_contact_clone() {
1231 let contact = Contact::new(Vec2::new(1.0, 2.0), Vec2::unit_x(), 0.5);
1232 let cloned = contact.clone();
1233 assert_eq!(contact, cloned);
1234 }
1235
1236 #[test]
1237 fn test_contact_debug() {
1238 let contact = Contact::new(Vec2::new(1.0, 2.0), Vec2::unit_x(), 0.5);
1239 let debug_str = format!("{:?}", contact);
1240 assert!(debug_str.contains("Contact"));
1241 assert!(debug_str.contains("point"));
1242 assert!(debug_str.contains("normal"));
1243 assert!(debug_str.contains("penetration"));
1244 }
1245
1246 // -------------------------------------------------------------------------
1247 // Circle-Circle Collision Tests
1248 // -------------------------------------------------------------------------
1249
1250 #[test]
1251 fn test_circle_circle_collision_overlapping() {
1252 // Two circles overlapping
1253 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(1.5, 0.0), 1.0);
1254 assert!(contact.is_some());
1255
1256 let contact = contact.unwrap();
1257 assert!(contact.penetration > 0.0);
1258 assert_eq!(contact.normal, Vec2::new(1.0, 0.0));
1259 }
1260
1261 #[test]
1262 fn test_circle_circle_collision_separated() {
1263 // Two circles separated
1264 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(5.0, 0.0), 1.0);
1265 assert!(contact.is_none());
1266 }
1267
1268 #[test]
1269 fn test_circle_circle_collision_touching() {
1270 // Two circles exactly touching (edge case)
1271 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(2.0, 0.0), 1.0);
1272 assert!(contact.is_some());
1273
1274 let contact = contact.unwrap();
1275 assert!((contact.penetration).abs() < 1e-5); // Near zero penetration
1276 }
1277
1278 #[test]
1279 fn test_circle_circle_collision_same_position() {
1280 // Circles at the same position (edge case)
1281 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(0.0, 0.0), 1.0);
1282 assert!(contact.is_some());
1283
1284 let contact = contact.unwrap();
1285 assert_eq!(contact.penetration, 2.0); // Full overlap
1286 assert_eq!(contact.point, Vec2::zero());
1287 }
1288
1289 #[test]
1290 fn test_circle_circle_collision_diagonal() {
1291 // Circles offset diagonally
1292 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(1.0, 1.0), 1.0);
1293 assert!(contact.is_some());
1294
1295 let contact = contact.unwrap();
1296 assert!(contact.penetration > 0.0);
1297
1298 // Normal should point from center_a to center_b (diagonal)
1299 let expected_normal = Vec2::new(1.0, 1.0).normalize();
1300 assert!((contact.normal.x - expected_normal.x).abs() < 1e-5);
1301 assert!((contact.normal.y - expected_normal.y).abs() < 1e-5);
1302 }
1303
1304 #[test]
1305 fn test_circle_circle_collision_different_radii() {
1306 // Circles with different radii
1307 let contact = circle_circle_collision(Vec2::new(0.0, 0.0), 2.0, Vec2::new(3.0, 0.0), 1.5);
1308 assert!(contact.is_some());
1309
1310 let contact = contact.unwrap();
1311 assert!(contact.penetration > 0.0);
1312
1313 // Combined radius = 2 + 1.5 = 3.5
1314 // Distance = 3.0
1315 // Penetration = 3.5 - 3.0 = 0.5
1316 assert!((contact.penetration - 0.5).abs() < 1e-5);
1317 }
1318
1319 #[test]
1320 fn test_circle_circle_collision_negative_coordinates() {
1321 // Circles in negative coordinate space
1322 let contact =
1323 circle_circle_collision(Vec2::new(-10.0, -10.0), 1.0, Vec2::new(-9.0, -10.0), 1.0);
1324 assert!(contact.is_some());
1325
1326 let contact = contact.unwrap();
1327 assert!(contact.penetration > 0.0);
1328 assert_eq!(contact.normal, Vec2::new(1.0, 0.0));
1329 }
1330
1331 #[test]
1332 fn test_circle_circle_collision_contact_point() {
1333 // Verify contact point is computed correctly
1334 let contact =
1335 circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(1.5, 0.0), 1.0).unwrap();
1336
1337 // Contact point should be between the two circles
1338 assert!(contact.point.x > 0.0 && contact.point.x < 1.5);
1339 assert_eq!(contact.point.y, 0.0);
1340 }
1341
1342 #[test]
1343 fn test_circle_circle_collision_symmetry() {
1344 // Collision should be symmetric (order shouldn't matter for detection)
1345 let contact_ab =
1346 circle_circle_collision(Vec2::new(0.0, 0.0), 1.0, Vec2::new(1.5, 0.0), 1.0);
1347 let contact_ba =
1348 circle_circle_collision(Vec2::new(1.5, 0.0), 1.0, Vec2::new(0.0, 0.0), 1.0);
1349
1350 assert!(contact_ab.is_some());
1351 assert!(contact_ba.is_some());
1352
1353 let contact_ab = contact_ab.unwrap();
1354 let contact_ba = contact_ba.unwrap();
1355
1356 // Penetration should be the same
1357 assert_eq!(contact_ab.penetration, contact_ba.penetration);
1358
1359 // Normals should be opposite
1360 assert_eq!(contact_ab.normal, contact_ba.normal * -1.0);
1361 }
1362
1363 #[test]
1364 fn test_circle_circle_collision_large_circles() {
1365 // Test with large circles
1366 let contact =
1367 circle_circle_collision(Vec2::new(0.0, 0.0), 100.0, Vec2::new(150.0, 0.0), 100.0);
1368 assert!(contact.is_some());
1369
1370 let contact = contact.unwrap();
1371 assert!((contact.penetration - 50.0).abs() < 1e-3);
1372 }
1373
1374 #[test]
1375 fn test_circle_circle_collision_tiny_circles() {
1376 // Test with tiny circles
1377 let contact =
1378 circle_circle_collision(Vec2::new(0.0, 0.0), 0.01, Vec2::new(0.015, 0.0), 0.01);
1379 assert!(contact.is_some());
1380
1381 let contact = contact.unwrap();
1382 assert!(contact.penetration > 0.0);
1383 }
1384
1385 // -------------------------------------------------------------------------
1386 // Box-Box Collision (SAT) Tests
1387 // -------------------------------------------------------------------------
1388
1389 #[test]
1390 fn test_box_box_collision_axis_aligned_overlapping() {
1391 // Two axis-aligned boxes overlapping
1392 let contact = box_box_collision(
1393 Vec2::new(0.0, 0.0),
1394 Vec2::new(1.0, 1.0),
1395 0.0,
1396 Vec2::new(1.5, 0.0),
1397 Vec2::new(1.0, 1.0),
1398 0.0,
1399 );
1400 assert!(contact.is_some());
1401
1402 let contact = contact.unwrap();
1403 assert!(contact.penetration > 0.0);
1404 assert!((contact.penetration - 0.5).abs() < 1e-5);
1405
1406 // Normal should point in X direction (minimum overlap axis)
1407 assert!((contact.normal.x.abs() - 1.0).abs() < 1e-5);
1408 assert!(contact.normal.y.abs() < 1e-5);
1409 }
1410
1411 #[test]
1412 fn test_box_box_collision_axis_aligned_separated() {
1413 // Two axis-aligned boxes separated
1414 let contact = box_box_collision(
1415 Vec2::new(0.0, 0.0),
1416 Vec2::new(1.0, 1.0),
1417 0.0,
1418 Vec2::new(5.0, 0.0),
1419 Vec2::new(1.0, 1.0),
1420 0.0,
1421 );
1422 assert!(contact.is_none());
1423 }
1424
1425 #[test]
1426 fn test_box_box_collision_rotated_overlapping() {
1427 use std::f32::consts::PI;
1428
1429 // Box B rotated 45 degrees, overlapping with axis-aligned box A
1430 let contact = box_box_collision(
1431 Vec2::new(0.0, 0.0),
1432 Vec2::new(1.0, 1.0),
1433 0.0,
1434 Vec2::new(1.0, 0.0),
1435 Vec2::new(1.0, 1.0),
1436 PI / 4.0,
1437 );
1438 assert!(contact.is_some());
1439
1440 let contact = contact.unwrap();
1441 assert!(contact.penetration > 0.0);
1442 }
1443
1444 #[test]
1445 fn test_box_box_collision_rotated_separated() {
1446 use std::f32::consts::PI;
1447
1448 // Two rotated boxes, separated
1449 let contact = box_box_collision(
1450 Vec2::new(0.0, 0.0),
1451 Vec2::new(1.0, 1.0),
1452 PI / 6.0,
1453 Vec2::new(5.0, 0.0),
1454 Vec2::new(1.0, 1.0),
1455 PI / 4.0,
1456 );
1457 assert!(contact.is_none());
1458 }
1459
1460 #[test]
1461 fn test_box_box_collision_both_rotated() {
1462 use std::f32::consts::PI;
1463
1464 // Both boxes rotated, overlapping
1465 let contact = box_box_collision(
1466 Vec2::new(0.0, 0.0),
1467 Vec2::new(1.0, 1.0),
1468 PI / 6.0,
1469 Vec2::new(1.2, 0.0),
1470 Vec2::new(1.0, 1.0),
1471 PI / 3.0,
1472 );
1473 assert!(contact.is_some());
1474
1475 let contact = contact.unwrap();
1476 assert!(contact.penetration > 0.0);
1477 }
1478
1479 #[test]
1480 fn test_box_box_collision_same_position() {
1481 // Boxes at the same position (full overlap)
1482 let contact = box_box_collision(
1483 Vec2::new(0.0, 0.0),
1484 Vec2::new(1.0, 1.0),
1485 0.0,
1486 Vec2::new(0.0, 0.0),
1487 Vec2::new(1.0, 1.0),
1488 0.0,
1489 );
1490 assert!(contact.is_some());
1491
1492 let contact = contact.unwrap();
1493 // Full overlap, penetration should be equal to full extents
1494 assert!(contact.penetration > 0.0);
1495 }
1496
1497 #[test]
1498 fn test_box_box_collision_touching() {
1499 // Boxes exactly touching (edge case)
1500 let contact = box_box_collision(
1501 Vec2::new(0.0, 0.0),
1502 Vec2::new(1.0, 1.0),
1503 0.0,
1504 Vec2::new(2.0, 0.0),
1505 Vec2::new(1.0, 1.0),
1506 0.0,
1507 );
1508
1509 // Should detect as collision (touching counts as collision)
1510 assert!(contact.is_some());
1511
1512 let contact = contact.unwrap();
1513 // Penetration should be near zero (touching)
1514 assert!(contact.penetration.abs() < 1e-3);
1515 }
1516
1517 #[test]
1518 fn test_box_box_collision_different_sizes() {
1519 // Boxes with different sizes
1520 let contact = box_box_collision(
1521 Vec2::new(0.0, 0.0),
1522 Vec2::new(2.0, 1.0),
1523 0.0,
1524 Vec2::new(2.5, 0.0),
1525 Vec2::new(1.0, 2.0),
1526 0.0,
1527 );
1528 assert!(contact.is_some());
1529
1530 let contact = contact.unwrap();
1531 assert!(contact.penetration > 0.0);
1532 }
1533
1534 #[test]
1535 fn test_box_box_collision_normal_direction() {
1536 // Verify normal points from A to B
1537 let contact = box_box_collision(
1538 Vec2::new(0.0, 0.0),
1539 Vec2::new(1.0, 1.0),
1540 0.0,
1541 Vec2::new(1.5, 0.0),
1542 Vec2::new(1.0, 1.0),
1543 0.0,
1544 )
1545 .unwrap();
1546
1547 // Normal should point right (from A to B)
1548 assert!(contact.normal.x > 0.0);
1549 assert!(contact.normal.y.abs() < 1e-5);
1550 }
1551
1552 #[test]
1553 fn test_box_box_collision_symmetry() {
1554 use std::f32::consts::PI;
1555
1556 // Collision should detect the same penetration regardless of order
1557 let contact_ab = box_box_collision(
1558 Vec2::new(0.0, 0.0),
1559 Vec2::new(1.0, 1.0),
1560 0.0,
1561 Vec2::new(1.5, 0.0),
1562 Vec2::new(1.0, 1.0),
1563 PI / 6.0,
1564 );
1565 let contact_ba = box_box_collision(
1566 Vec2::new(1.5, 0.0),
1567 Vec2::new(1.0, 1.0),
1568 PI / 6.0,
1569 Vec2::new(0.0, 0.0),
1570 Vec2::new(1.0, 1.0),
1571 0.0,
1572 );
1573
1574 assert!(contact_ab.is_some());
1575 assert!(contact_ba.is_some());
1576
1577 let contact_ab = contact_ab.unwrap();
1578 let contact_ba = contact_ba.unwrap();
1579
1580 // Penetration should be the same
1581 assert!((contact_ab.penetration - contact_ba.penetration).abs() < 1e-5);
1582
1583 // Normals should be opposite
1584 assert!((contact_ab.normal.x + contact_ba.normal.x).abs() < 1e-5);
1585 assert!((contact_ab.normal.y + contact_ba.normal.y).abs() < 1e-5);
1586 }
1587
1588 #[test]
1589 fn test_box_box_collision_90_degree_rotation() {
1590 use std::f32::consts::PI;
1591
1592 // Box rotated 90 degrees
1593 let contact = box_box_collision(
1594 Vec2::new(0.0, 0.0),
1595 Vec2::new(2.0, 1.0),
1596 0.0,
1597 Vec2::new(1.5, 0.0),
1598 Vec2::new(2.0, 1.0),
1599 PI / 2.0,
1600 );
1601 assert!(contact.is_some());
1602
1603 let contact = contact.unwrap();
1604 assert!(contact.penetration > 0.0);
1605 }
1606
1607 // -------------------------------------------------------------------------
1608 // AABB-AABB Collision Tests
1609 // -------------------------------------------------------------------------
1610
1611 #[test]
1612 fn test_aabb_aabb_collision_overlapping() {
1613 // Two AABBs overlapping
1614 let contact = aabb_aabb_collision(
1615 Vec2::new(0.0, 0.0),
1616 Vec2::new(1.0, 1.0),
1617 Vec2::new(1.5, 0.0),
1618 Vec2::new(1.0, 1.0),
1619 );
1620 assert!(contact.is_some());
1621
1622 let contact = contact.unwrap();
1623 assert!(contact.penetration > 0.0);
1624 assert!((contact.penetration - 0.5).abs() < 1e-5);
1625
1626 // Normal should point right (X direction)
1627 assert_eq!(contact.normal, Vec2::unit_x());
1628 }
1629
1630 #[test]
1631 fn test_aabb_aabb_collision_separated() {
1632 // Two AABBs separated
1633 let contact = aabb_aabb_collision(
1634 Vec2::new(0.0, 0.0),
1635 Vec2::new(1.0, 1.0),
1636 Vec2::new(5.0, 0.0),
1637 Vec2::new(1.0, 1.0),
1638 );
1639 assert!(contact.is_none());
1640 }
1641
1642 #[test]
1643 fn test_aabb_aabb_collision_touching() {
1644 // Two AABBs exactly touching
1645 let contact = aabb_aabb_collision(
1646 Vec2::new(0.0, 0.0),
1647 Vec2::new(1.0, 1.0),
1648 Vec2::new(2.0, 0.0),
1649 Vec2::new(1.0, 1.0),
1650 );
1651
1652 // Should detect as collision (touching edges)
1653 assert!(contact.is_some());
1654
1655 let contact = contact.unwrap();
1656 // Penetration should be near zero
1657 assert!(contact.penetration.abs() < 1e-5);
1658 }
1659
1660 #[test]
1661 fn test_aabb_aabb_collision_vertical_overlap() {
1662 // AABBs overlapping vertically
1663 let contact = aabb_aabb_collision(
1664 Vec2::new(0.0, 0.0),
1665 Vec2::new(1.0, 1.0),
1666 Vec2::new(0.0, 1.5),
1667 Vec2::new(1.0, 1.0),
1668 );
1669 assert!(contact.is_some());
1670
1671 let contact = contact.unwrap();
1672 assert!(contact.penetration > 0.0);
1673 assert!((contact.penetration - 0.5).abs() < 1e-5);
1674
1675 // Normal should point up (Y direction)
1676 assert_eq!(contact.normal, Vec2::unit_y());
1677 }
1678
1679 #[test]
1680 fn test_aabb_aabb_collision_different_sizes() {
1681 // AABBs with different sizes
1682 let contact = aabb_aabb_collision(
1683 Vec2::new(0.0, 0.0),
1684 Vec2::new(2.0, 1.0),
1685 Vec2::new(2.5, 0.0),
1686 Vec2::new(1.0, 2.0),
1687 );
1688 assert!(contact.is_some());
1689
1690 let contact = contact.unwrap();
1691 assert!(contact.penetration > 0.0);
1692 }
1693
1694 #[test]
1695 fn test_aabb_aabb_collision_same_position() {
1696 // AABBs at the same position
1697 let contact = aabb_aabb_collision(
1698 Vec2::new(0.0, 0.0),
1699 Vec2::new(1.0, 1.0),
1700 Vec2::new(0.0, 0.0),
1701 Vec2::new(1.0, 1.0),
1702 );
1703 assert!(contact.is_some());
1704
1705 let contact = contact.unwrap();
1706 // Full overlap
1707 assert!(contact.penetration > 0.0);
1708 assert!((contact.penetration - 2.0).abs() < 1e-5);
1709 }
1710
1711 #[test]
1712 fn test_aabb_aabb_collision_contact_point() {
1713 // Verify contact point is in the overlap region
1714 let contact = aabb_aabb_collision(
1715 Vec2::new(0.0, 0.0),
1716 Vec2::new(1.0, 1.0),
1717 Vec2::new(1.5, 0.0),
1718 Vec2::new(1.0, 1.0),
1719 )
1720 .unwrap();
1721
1722 // Contact point should be in the overlap region
1723 assert!(contact.point.x > 0.0 && contact.point.x < 2.0);
1724 assert!(contact.point.y >= -1.0 && contact.point.y <= 1.0);
1725 }
1726
1727 #[test]
1728 fn test_aabb_aabb_collision_normal_direction() {
1729 // Verify normal points from A to B
1730 let contact = aabb_aabb_collision(
1731 Vec2::new(0.0, 0.0),
1732 Vec2::new(1.0, 1.0),
1733 Vec2::new(1.5, 0.0),
1734 Vec2::new(1.0, 1.0),
1735 )
1736 .unwrap();
1737
1738 // Normal should point right (from A to B)
1739 assert_eq!(contact.normal, Vec2::unit_x());
1740
1741 // Test vertical case
1742 let contact_vertical = aabb_aabb_collision(
1743 Vec2::new(0.0, 0.0),
1744 Vec2::new(1.0, 1.0),
1745 Vec2::new(0.0, 1.5),
1746 Vec2::new(1.0, 1.0),
1747 )
1748 .unwrap();
1749
1750 // Normal should point up
1751 assert_eq!(contact_vertical.normal, Vec2::unit_y());
1752 }
1753
1754 #[test]
1755 fn test_aabb_aabb_collision_symmetry() {
1756 // Collision should be symmetric (same penetration regardless of order)
1757 let contact_ab = aabb_aabb_collision(
1758 Vec2::new(0.0, 0.0),
1759 Vec2::new(1.0, 1.0),
1760 Vec2::new(1.5, 0.0),
1761 Vec2::new(1.0, 1.0),
1762 );
1763 let contact_ba = aabb_aabb_collision(
1764 Vec2::new(1.5, 0.0),
1765 Vec2::new(1.0, 1.0),
1766 Vec2::new(0.0, 0.0),
1767 Vec2::new(1.0, 1.0),
1768 );
1769
1770 assert!(contact_ab.is_some());
1771 assert!(contact_ba.is_some());
1772
1773 let contact_ab = contact_ab.unwrap();
1774 let contact_ba = contact_ba.unwrap();
1775
1776 // Penetration should be the same
1777 assert_eq!(contact_ab.penetration, contact_ba.penetration);
1778
1779 // Normals should be opposite
1780 assert_eq!(contact_ab.normal, contact_ba.normal * -1.0);
1781 }
1782
1783 #[test]
1784 fn test_aabb_aabb_collision_negative_coordinates() {
1785 // AABBs in negative coordinate space
1786 let contact = aabb_aabb_collision(
1787 Vec2::new(-10.0, -10.0),
1788 Vec2::new(1.0, 1.0),
1789 Vec2::new(-9.0, -10.0),
1790 Vec2::new(1.0, 1.0),
1791 );
1792 assert!(contact.is_some());
1793
1794 let contact = contact.unwrap();
1795 assert!(contact.penetration > 0.0);
1796 assert_eq!(contact.normal, Vec2::unit_x());
1797 }
1798
1799 // -------------------------------------------------------------------------
1800 // Circle-AABB Collision Tests
1801 // -------------------------------------------------------------------------
1802
1803 #[test]
1804 fn test_circle_aabb_collision_overlapping() {
1805 // Circle overlapping with AABB from the side
1806 let contact = circle_aabb_collision(
1807 Vec2::new(1.5, 0.0),
1808 1.0,
1809 Vec2::new(0.0, 0.0),
1810 Vec2::new(1.0, 1.0),
1811 );
1812 assert!(contact.is_some());
1813
1814 let contact = contact.unwrap();
1815 assert!(contact.penetration > 0.0);
1816 assert!((contact.penetration - 0.5).abs() < 1e-5);
1817 assert_eq!(contact.normal, Vec2::unit_x());
1818 }
1819
1820 #[test]
1821 fn test_circle_aabb_collision_separated() {
1822 // Circle separated from AABB
1823 let contact = circle_aabb_collision(
1824 Vec2::new(5.0, 0.0),
1825 1.0,
1826 Vec2::new(0.0, 0.0),
1827 Vec2::new(1.0, 1.0),
1828 );
1829 assert!(contact.is_none());
1830 }
1831
1832 #[test]
1833 fn test_circle_aabb_collision_corner() {
1834 // Circle colliding with corner of AABB
1835 let contact = circle_aabb_collision(
1836 Vec2::new(1.5, 1.5),
1837 1.0,
1838 Vec2::new(0.0, 0.0),
1839 Vec2::new(1.0, 1.0),
1840 );
1841 assert!(contact.is_some());
1842
1843 let contact = contact.unwrap();
1844 assert!(contact.penetration > 0.0);
1845
1846 // Normal should point diagonally (from corner to circle center)
1847 let expected_normal = Vec2::new(0.5, 0.5).normalize();
1848 assert!((contact.normal.x - expected_normal.x).abs() < 1e-5);
1849 assert!((contact.normal.y - expected_normal.y).abs() < 1e-5);
1850 }
1851
1852 #[test]
1853 fn test_circle_aabb_collision_edge() {
1854 // Circle colliding with edge of AABB
1855 let contact = circle_aabb_collision(
1856 Vec2::new(1.8, 0.0),
1857 1.0,
1858 Vec2::new(0.0, 0.0),
1859 Vec2::new(1.0, 1.0),
1860 );
1861 assert!(contact.is_some());
1862
1863 let contact = contact.unwrap();
1864 assert!((contact.penetration - 0.2).abs() < 1e-5);
1865
1866 // Normal should point horizontally (perpendicular to edge)
1867 assert_eq!(contact.normal, Vec2::unit_x());
1868 }
1869
1870 #[test]
1871 fn test_circle_aabb_collision_inside() {
1872 // Circle center inside AABB (penetrating deeply)
1873 let contact = circle_aabb_collision(
1874 Vec2::new(0.5, 0.0),
1875 1.0,
1876 Vec2::new(0.0, 0.0),
1877 Vec2::new(1.0, 1.0),
1878 );
1879 assert!(contact.is_some());
1880
1881 let contact = contact.unwrap();
1882 assert!(contact.penetration > 1.0); // Deep penetration
1883
1884 // Normal should push circle out along minimum axis
1885 assert!(contact.normal.x.abs() > 0.9 || contact.normal.y.abs() > 0.9);
1886 }
1887
1888 #[test]
1889 fn test_circle_aabb_collision_center_coincident() {
1890 // Circle center exactly at AABB center
1891 let contact = circle_aabb_collision(
1892 Vec2::new(0.0, 0.0),
1893 1.0,
1894 Vec2::new(0.0, 0.0),
1895 Vec2::new(1.0, 1.0),
1896 );
1897 assert!(contact.is_some());
1898
1899 let contact = contact.unwrap();
1900 assert!(contact.penetration > 1.0); // Deep penetration
1901 }
1902
1903 #[test]
1904 fn test_circle_aabb_collision_touching() {
1905 // Circle exactly touching AABB edge
1906 let contact = circle_aabb_collision(
1907 Vec2::new(2.0, 0.0),
1908 1.0,
1909 Vec2::new(0.0, 0.0),
1910 Vec2::new(1.0, 1.0),
1911 );
1912 assert!(contact.is_some());
1913
1914 let contact = contact.unwrap();
1915 assert!(contact.penetration.abs() < 1e-5); // Near-zero penetration
1916 }
1917
1918 #[test]
1919 fn test_circle_aabb_collision_different_sizes() {
1920 // Large circle with small AABB
1921 let contact = circle_aabb_collision(
1922 Vec2::new(2.0, 0.0),
1923 2.5,
1924 Vec2::new(0.0, 0.0),
1925 Vec2::new(0.5, 0.5),
1926 );
1927 assert!(contact.is_some());
1928
1929 let contact = contact.unwrap();
1930 assert!(contact.penetration > 0.0);
1931 assert!((contact.penetration - 1.0).abs() < 1e-5); // Circle left edge at -0.5, box at 0.5, overlap = 1.0
1932 }
1933
1934 #[test]
1935 fn test_circle_aabb_collision_vertical() {
1936 // Circle colliding from above
1937 let contact = circle_aabb_collision(
1938 Vec2::new(0.0, 1.8),
1939 1.0,
1940 Vec2::new(0.0, 0.0),
1941 Vec2::new(1.0, 1.0),
1942 );
1943 assert!(contact.is_some());
1944
1945 let contact = contact.unwrap();
1946 assert!((contact.penetration - 0.2).abs() < 1e-5);
1947 assert_eq!(contact.normal, Vec2::unit_y());
1948 }
1949
1950 #[test]
1951 fn test_circle_aabb_collision_contact_point() {
1952 // Verify contact point is on AABB surface
1953 let contact = circle_aabb_collision(
1954 Vec2::new(1.8, 0.0),
1955 1.0,
1956 Vec2::new(0.0, 0.0),
1957 Vec2::new(1.0, 1.0),
1958 )
1959 .unwrap();
1960
1961 // Contact point should be on the right edge of the AABB
1962 assert!((contact.point.x - 1.0).abs() < 1e-5);
1963 assert!((contact.point.y - 0.0).abs() < 1e-5);
1964 }
1965
1966 #[test]
1967 fn test_circle_aabb_collision_symmetry() {
1968 // Similar collision from opposite direction
1969 let contact1 = circle_aabb_collision(
1970 Vec2::new(1.8, 0.0),
1971 1.0,
1972 Vec2::new(0.0, 0.0),
1973 Vec2::new(1.0, 1.0),
1974 );
1975 let contact2 = circle_aabb_collision(
1976 Vec2::new(-1.8, 0.0),
1977 1.0,
1978 Vec2::new(0.0, 0.0),
1979 Vec2::new(1.0, 1.0),
1980 );
1981
1982 assert!(contact1.is_some());
1983 assert!(contact2.is_some());
1984
1985 let c1 = contact1.unwrap();
1986 let c2 = contact2.unwrap();
1987
1988 // Penetrations should be the same
1989 assert_eq!(c1.penetration, c2.penetration);
1990
1991 // Normals should be opposite
1992 assert_eq!(c1.normal.x, -c2.normal.x);
1993 }
1994
1995 // -------------------------------------------------------------------------
1996 // Circle-OBB Collision Tests
1997 // -------------------------------------------------------------------------
1998
1999 #[test]
2000 fn test_circle_obb_collision_no_rotation() {
2001 // Zero rotation should behave like AABB
2002 let contact_obb = circle_obb_collision(
2003 Vec2::new(1.8, 0.0),
2004 1.0,
2005 Vec2::new(0.0, 0.0),
2006 Vec2::new(1.0, 1.0),
2007 0.0,
2008 );
2009 let contact_aabb = circle_aabb_collision(
2010 Vec2::new(1.8, 0.0),
2011 1.0,
2012 Vec2::new(0.0, 0.0),
2013 Vec2::new(1.0, 1.0),
2014 );
2015
2016 assert!(contact_obb.is_some());
2017 assert!(contact_aabb.is_some());
2018
2019 let c_obb = contact_obb.unwrap();
2020 let c_aabb = contact_aabb.unwrap();
2021
2022 assert!((c_obb.penetration - c_aabb.penetration).abs() < 1e-5);
2023 }
2024
2025 #[test]
2026 fn test_circle_obb_collision_45_degree_rotation() {
2027 use std::f32::consts::PI;
2028
2029 // Box rotated 45 degrees
2030 let contact = circle_obb_collision(
2031 Vec2::new(1.8, 0.0),
2032 1.0,
2033 Vec2::new(0.0, 0.0),
2034 Vec2::new(1.0, 1.0),
2035 PI / 4.0,
2036 );
2037 assert!(contact.is_some());
2038
2039 let contact = contact.unwrap();
2040 assert!(contact.penetration > 0.0);
2041 }
2042
2043 #[test]
2044 fn test_circle_obb_collision_90_degree_rotation() {
2045 use std::f32::consts::PI;
2046
2047 // Box rotated 90 degrees (swaps width/height for square, no visual change)
2048 let contact = circle_obb_collision(
2049 Vec2::new(1.8, 0.0),
2050 1.0,
2051 Vec2::new(0.0, 0.0),
2052 Vec2::new(1.0, 1.0),
2053 PI / 2.0,
2054 );
2055 assert!(contact.is_some());
2056
2057 let contact = contact.unwrap();
2058 assert!(contact.penetration > 0.0);
2059 }
2060
2061 #[test]
2062 fn test_circle_obb_collision_rotated_rectangle() {
2063 use std::f32::consts::PI;
2064
2065 // Non-square box rotated 30 degrees
2066 let contact = circle_obb_collision(
2067 Vec2::new(2.5, 0.0),
2068 1.0,
2069 Vec2::new(0.0, 0.0),
2070 Vec2::new(2.0, 0.5),
2071 PI / 6.0,
2072 );
2073 assert!(contact.is_some());
2074
2075 let contact = contact.unwrap();
2076 assert!(contact.penetration > 0.0);
2077 }
2078
2079 #[test]
2080 fn test_circle_obb_collision_separated_rotated() {
2081 use std::f32::consts::PI;
2082
2083 // Circle and rotated box separated
2084 let contact = circle_obb_collision(
2085 Vec2::new(5.0, 0.0),
2086 1.0,
2087 Vec2::new(0.0, 0.0),
2088 Vec2::new(1.0, 1.0),
2089 PI / 4.0,
2090 );
2091 assert!(contact.is_none());
2092 }
2093
2094 #[test]
2095 fn test_circle_obb_collision_inside_rotated() {
2096 use std::f32::consts::PI;
2097
2098 // Circle center inside rotated box
2099 let contact = circle_obb_collision(
2100 Vec2::new(0.5, 0.0),
2101 1.0,
2102 Vec2::new(0.0, 0.0),
2103 Vec2::new(2.0, 2.0),
2104 PI / 4.0,
2105 );
2106 assert!(contact.is_some());
2107
2108 let contact = contact.unwrap();
2109 assert!(contact.penetration > 0.0);
2110 }
2111
2112 #[test]
2113 fn test_circle_obb_collision_corner_rotated() {
2114 use std::f32::consts::PI;
2115
2116 // Circle colliding with corner of rotated box
2117 // With 45-degree rotation, corners move farther out
2118 let contact = circle_obb_collision(
2119 Vec2::new(1.2, 1.2),
2120 1.0,
2121 Vec2::new(0.0, 0.0),
2122 Vec2::new(1.0, 1.0),
2123 PI / 4.0,
2124 );
2125 assert!(contact.is_some());
2126
2127 let contact = contact.unwrap();
2128 assert!(contact.penetration > 0.0);
2129 }
2130
2131 #[test]
2132 fn test_circle_obb_collision_touching_rotated() {
2133 use std::f32::consts::PI;
2134
2135 // Circle close to rotated box
2136 let contact = circle_obb_collision(
2137 Vec2::new(1.8, 0.0),
2138 1.0,
2139 Vec2::new(0.0, 0.0),
2140 Vec2::new(1.0, 1.0),
2141 PI / 4.0,
2142 );
2143
2144 // Should collide since rotated box extends farther
2145 if let Some(c) = contact {
2146 assert!(c.penetration >= 0.0); // Valid penetration
2147 }
2148 }
2149
2150 #[test]
2151 fn test_circle_obb_collision_large_rotation() {
2152 use std::f32::consts::PI;
2153
2154 // Box rotated by large angle (3π/4)
2155 let contact = circle_obb_collision(
2156 Vec2::new(1.5, 0.0),
2157 1.0,
2158 Vec2::new(0.0, 0.0),
2159 Vec2::new(1.0, 1.0),
2160 3.0 * PI / 4.0,
2161 );
2162 assert!(contact.is_some());
2163
2164 let contact = contact.unwrap();
2165 assert!(contact.penetration > 0.0);
2166 }
2167
2168 #[test]
2169 fn test_circle_obb_collision_negative_rotation() {
2170 use std::f32::consts::PI;
2171
2172 // Negative rotation
2173 let contact = circle_obb_collision(
2174 Vec2::new(1.8, 0.0),
2175 1.0,
2176 Vec2::new(0.0, 0.0),
2177 Vec2::new(1.0, 1.0),
2178 -PI / 4.0,
2179 );
2180 assert!(contact.is_some());
2181
2182 let contact = contact.unwrap();
2183 assert!(contact.penetration > 0.0);
2184 }
2185
2186 // -------------------------------------------------------------------------
2187 // CollisionResponse Tests
2188 // -------------------------------------------------------------------------
2189
2190 #[test]
2191 fn test_collision_response_new() {
2192 let response = CollisionResponse::new(0.5, 0.4, 0.6, 0.02);
2193 assert_eq!(response.restitution, 0.5);
2194 assert_eq!(response.friction, 0.4);
2195 assert_eq!(response.position_correction, 0.6);
2196 assert_eq!(response.slop, 0.02);
2197 }
2198
2199 #[test]
2200 fn test_collision_response_new_clamping() {
2201 // Test clamping of invalid values
2202 let response = CollisionResponse::new(1.5, -0.2, 2.0, -0.01);
2203 assert_eq!(response.restitution, 1.0); // Clamped to max
2204 assert_eq!(response.friction, 0.0); // Clamped to min
2205 assert_eq!(response.position_correction, 1.0); // Clamped to max
2206 assert_eq!(response.slop, 0.0); // Clamped to min
2207 }
2208
2209 #[test]
2210 fn test_collision_response_default() {
2211 let response = CollisionResponse::default();
2212 assert_eq!(response.restitution, 0.4);
2213 assert_eq!(response.friction, 0.4);
2214 assert_eq!(response.position_correction, 0.4);
2215 assert_eq!(response.slop, 0.01);
2216 }
2217
2218 #[test]
2219 fn test_collision_response_bouncy() {
2220 let response = CollisionResponse::bouncy();
2221 assert_eq!(response.restitution, 0.8);
2222 assert_eq!(response.friction, 0.2);
2223 }
2224
2225 #[test]
2226 fn test_collision_response_character() {
2227 let response = CollisionResponse::character();
2228 assert_eq!(response.restitution, 0.0);
2229 assert_eq!(response.friction, 0.8);
2230 }
2231
2232 #[test]
2233 fn test_collision_response_slippery() {
2234 let response = CollisionResponse::slippery();
2235 assert!(response.friction < 0.2);
2236 assert!(response.restitution > 0.0);
2237 }
2238
2239 #[test]
2240 fn test_collision_response_elastic() {
2241 let response = CollisionResponse::elastic();
2242 assert_eq!(response.restitution, 1.0);
2243 }
2244
2245 #[test]
2246 fn test_collision_response_clone() {
2247 let response = CollisionResponse::default();
2248 let cloned = response.clone();
2249 assert_eq!(response, cloned);
2250 }
2251
2252 #[test]
2253 fn test_collision_response_debug() {
2254 let response = CollisionResponse::default();
2255 let debug_str = format!("{:?}", response);
2256 assert!(debug_str.contains("CollisionResponse"));
2257 }
2258
2259 // -------------------------------------------------------------------------
2260 // Impulse Resolution Tests
2261 // -------------------------------------------------------------------------
2262
2263 #[test]
2264 fn test_resolve_collision_head_on() {
2265 // Two objects colliding head-on with equal mass
2266 let contact = Contact::new(
2267 Vec2::new(0.0, 0.0),
2268 Vec2::new(1.0, 0.0), // Normal points from A to B
2269 0.1,
2270 );
2271
2272 let vel_a = Vec2::new(10.0, 0.0); // A moving right
2273 let vel_b = Vec2::new(-10.0, 0.0); // B moving left
2274 let inv_mass_a = 1.0; // Equal mass
2275 let inv_mass_b = 1.0;
2276
2277 let response = CollisionResponse::elastic();
2278 let (delta_a, delta_b) =
2279 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2280
2281 // With elastic collision and equal masses, velocities should swap
2282 let new_vel_a = vel_a + delta_a;
2283 let new_vel_b = vel_b + delta_b;
2284
2285 // A should now move left, B should move right
2286 assert!(new_vel_a.x < 0.0);
2287 assert!(new_vel_b.x > 0.0);
2288 }
2289
2290 #[test]
2291 fn test_resolve_collision_static_wall() {
2292 // Object colliding with static wall
2293 // Normal points FROM object TO wall (right)
2294 let contact = Contact::new(
2295 Vec2::new(1.0, 0.0),
2296 Vec2::new(1.0, 0.0), // Normal points right (from A to B/wall)
2297 0.1,
2298 );
2299
2300 let vel_a = Vec2::new(10.0, 0.0); // A moving right into wall
2301 let vel_b = Vec2::zero(); // Wall is static
2302 let inv_mass_a = 1.0;
2303 let inv_mass_b = 0.0; // Infinite mass (static)
2304
2305 let response = CollisionResponse::bouncy();
2306 let (delta_a, delta_b) =
2307 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2308
2309 // Wall doesn't move
2310 assert_eq!(delta_b, Vec2::zero());
2311
2312 // Object bounces back (velocity becomes negative)
2313 let new_vel_a = vel_a + delta_a;
2314 assert!(new_vel_a.x < vel_a.x); // Velocity reduced or reversed
2315 }
2316
2317 #[test]
2318 fn test_resolve_collision_no_bounce() {
2319 // Collision with zero restitution (inelastic)
2320 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2321
2322 let vel_a = Vec2::new(10.0, 0.0);
2323 let vel_b = Vec2::zero();
2324 let inv_mass_a = 1.0;
2325 let inv_mass_b = 1.0;
2326
2327 let response = CollisionResponse::character(); // Zero restitution
2328 let (delta_a, delta_b) =
2329 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2330
2331 let new_vel_a = vel_a + delta_a;
2332 let new_vel_b = vel_b + delta_b;
2333
2334 // Should slow down significantly (no bounce)
2335 assert!(new_vel_a.x < vel_a.x);
2336 }
2337
2338 #[test]
2339 fn test_resolve_collision_separating() {
2340 // Objects already separating (no impulse needed)
2341 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2342
2343 let vel_a = Vec2::new(-5.0, 0.0); // A moving away (left)
2344 let vel_b = Vec2::new(5.0, 0.0); // B moving away (right)
2345 let inv_mass_a = 1.0;
2346 let inv_mass_b = 1.0;
2347
2348 let response = CollisionResponse::default();
2349 let (delta_a, delta_b) =
2350 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2351
2352 // No impulse should be applied (already separating)
2353 assert_eq!(delta_a, Vec2::zero());
2354 assert_eq!(delta_b, Vec2::zero());
2355 }
2356
2357 #[test]
2358 fn test_resolve_collision_two_static() {
2359 // Two static objects (no impulse possible)
2360 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2361
2362 let vel_a = Vec2::zero();
2363 let vel_b = Vec2::zero();
2364 let inv_mass_a = 0.0; // Static
2365 let inv_mass_b = 0.0; // Static
2366
2367 let response = CollisionResponse::default();
2368 let (delta_a, delta_b) =
2369 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2370
2371 // No movement possible
2372 assert_eq!(delta_a, Vec2::zero());
2373 assert_eq!(delta_b, Vec2::zero());
2374 }
2375
2376 #[test]
2377 fn test_resolve_collision_with_friction() {
2378 // Object sliding along surface with friction
2379 // Object A hits surface B from above
2380 let contact = Contact::new(
2381 Vec2::new(0.0, 0.0),
2382 Vec2::new(0.0, -1.0), // Normal points down (from A to B)
2383 0.1,
2384 );
2385
2386 let vel_a = Vec2::new(10.0, -5.0); // A moving right and down
2387 let vel_b = Vec2::zero(); // B is static surface
2388 let inv_mass_a = 1.0;
2389 let inv_mass_b = 0.0;
2390
2391 let response = CollisionResponse::character(); // High friction, zero restitution
2392 let (delta_a, _) =
2393 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2394
2395 // Impulse should be applied (not zero)
2396 assert!(delta_a.length() > 0.0);
2397
2398 // Horizontal velocity should be reduced by friction
2399 let new_vel_a = vel_a + delta_a;
2400 assert!(new_vel_a.x < vel_a.x);
2401 }
2402
2403 #[test]
2404 fn test_resolve_collision_diagonal() {
2405 // Collision at an angle
2406 let contact = Contact::new(
2407 Vec2::new(0.0, 0.0),
2408 Vec2::new(0.707, 0.707), // 45-degree normal
2409 0.1,
2410 );
2411
2412 let vel_a = Vec2::new(10.0, 10.0);
2413 let vel_b = Vec2::zero();
2414 let inv_mass_a = 1.0;
2415 let inv_mass_b = 0.0;
2416
2417 let response = CollisionResponse::default();
2418 let (delta_a, _) =
2419 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2420
2421 let new_vel_a = vel_a + delta_a;
2422
2423 // Velocity should be redirected along the surface
2424 assert!(new_vel_a.length() < vel_a.length()); // Some energy lost
2425 }
2426
2427 #[test]
2428 fn test_resolve_collision_mass_ratio() {
2429 // Heavy object hitting light object
2430 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2431
2432 let vel_a = Vec2::new(10.0, 0.0);
2433 let vel_b = Vec2::zero();
2434 let inv_mass_a = 0.1; // Heavy (10 kg)
2435 let inv_mass_b = 1.0; // Light (1 kg)
2436
2437 let response = CollisionResponse::default();
2438 let (delta_a, delta_b) =
2439 resolve_collision(&contact, vel_a, vel_b, inv_mass_a, inv_mass_b, &response);
2440
2441 // Light object should move more than heavy object
2442 assert!(delta_b.length() > delta_a.length());
2443 }
2444
2445 // -------------------------------------------------------------------------
2446 // Position Correction Tests
2447 // -------------------------------------------------------------------------
2448
2449 #[test]
2450 fn test_position_correction_basic() {
2451 let contact = Contact::new(
2452 Vec2::new(0.0, 0.0),
2453 Vec2::new(1.0, 0.0),
2454 0.1, // 0.1 penetration
2455 );
2456
2457 let inv_mass_a = 1.0;
2458 let inv_mass_b = 1.0;
2459 let response = CollisionResponse::default();
2460
2461 let (corr_a, corr_b) =
2462 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2463
2464 // Both should move (equal mass)
2465 assert!(corr_a.length() > 0.0);
2466 assert!(corr_b.length() > 0.0);
2467
2468 // Should move in opposite directions
2469 assert!(corr_a.x < 0.0); // A moves left
2470 assert!(corr_b.x > 0.0); // B moves right
2471 }
2472
2473 #[test]
2474 fn test_position_correction_below_slop() {
2475 let contact = Contact::new(
2476 Vec2::new(0.0, 0.0),
2477 Vec2::new(1.0, 0.0),
2478 0.005, // Below default slop threshold (0.01)
2479 );
2480
2481 let inv_mass_a = 1.0;
2482 let inv_mass_b = 1.0;
2483 let response = CollisionResponse::default();
2484
2485 let (corr_a, corr_b) =
2486 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2487
2488 // No correction for small penetration
2489 assert_eq!(corr_a, Vec2::zero());
2490 assert_eq!(corr_b, Vec2::zero());
2491 }
2492
2493 #[test]
2494 fn test_position_correction_static_wall() {
2495 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2496
2497 let inv_mass_a = 1.0;
2498 let inv_mass_b = 0.0; // Static
2499 let response = CollisionResponse::default();
2500
2501 let (corr_a, corr_b) =
2502 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2503
2504 // Only dynamic object moves
2505 assert!(corr_a.length() > 0.0);
2506 assert_eq!(corr_b, Vec2::zero());
2507 }
2508
2509 #[test]
2510 fn test_position_correction_two_static() {
2511 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2512
2513 let inv_mass_a = 0.0;
2514 let inv_mass_b = 0.0;
2515 let response = CollisionResponse::default();
2516
2517 let (corr_a, corr_b) =
2518 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2519
2520 // No correction possible
2521 assert_eq!(corr_a, Vec2::zero());
2522 assert_eq!(corr_b, Vec2::zero());
2523 }
2524
2525 #[test]
2526 fn test_position_correction_zero_percent() {
2527 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2528
2529 let inv_mass_a = 1.0;
2530 let inv_mass_b = 1.0;
2531 let response = CollisionResponse::new(0.5, 0.5, 0.0, 0.01); // Zero correction
2532
2533 let (corr_a, corr_b) =
2534 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2535
2536 // No correction applied
2537 assert_eq!(corr_a, Vec2::zero());
2538 assert_eq!(corr_b, Vec2::zero());
2539 }
2540
2541 #[test]
2542 fn test_position_correction_full_percent() {
2543 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2544
2545 let inv_mass_a = 1.0;
2546 let inv_mass_b = 1.0;
2547 let response = CollisionResponse::new(0.5, 0.5, 1.0, 0.01); // Full correction
2548
2549 let (corr_a, corr_b) =
2550 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2551
2552 // Maximum correction applied
2553 assert!(corr_a.length() > 0.0);
2554 assert!(corr_b.length() > 0.0);
2555 }
2556
2557 #[test]
2558 fn test_position_correction_mass_ratio() {
2559 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.1);
2560
2561 let inv_mass_a = 0.1; // Heavy
2562 let inv_mass_b = 1.0; // Light
2563 let response = CollisionResponse::default();
2564
2565 let (corr_a, corr_b) =
2566 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2567
2568 // Light object should move more
2569 assert!(corr_b.length() > corr_a.length());
2570 }
2571
2572 #[test]
2573 fn test_position_correction_direction() {
2574 let contact = Contact::new(
2575 Vec2::new(0.0, 0.0),
2576 Vec2::new(0.0, 1.0), // Normal points up
2577 0.1,
2578 );
2579
2580 let inv_mass_a = 1.0;
2581 let inv_mass_b = 1.0;
2582 let response = CollisionResponse::default();
2583
2584 let (corr_a, corr_b) =
2585 compute_position_correction(&contact, inv_mass_a, inv_mass_b, &response);
2586
2587 // Correction should be along normal
2588 assert!(corr_a.y < 0.0); // A moves down
2589 assert!(corr_b.y > 0.0); // B moves up
2590 assert!(corr_a.x.abs() < 1e-6); // No horizontal movement
2591 assert!(corr_b.x.abs() < 1e-6);
2592 }
2593}
2594
2595// =============================================================================
2596// Collision Events Module
2597// =============================================================================
2598
2599/// Collision events module.
2600///
2601/// This module defines events that are fired when collisions are detected
2602/// between entities. Systems can listen to these events to trigger gameplay
2603/// effects, audio, particles, damage, etc.
2604///
2605/// # Event Types
2606///
2607/// - [`CollisionStarted`]: Fired when two entities start colliding (first contact)
2608/// - [`CollisionEnded`]: Fired when two entities stop colliding (separation)
2609///
2610/// # Usage Example
2611///
2612/// ```rust
2613/// use goud_engine::ecs::collision::{CollisionStarted, CollisionEnded};
2614/// use goud_engine::core::event::Events;
2615/// use goud_engine::ecs::{Entity, World};
2616///
2617/// // System that listens for collision events
2618/// fn handle_collisions(world: &World) {
2619/// // In a real system, you'd use EventReader<CollisionStarted>
2620/// // For now, this shows the concept
2621/// }
2622/// ```
2623pub mod events {
2624 use super::Contact;
2625 use crate::ecs::Entity;
2626
2627 /// Event fired when two entities start colliding.
2628 ///
2629 /// This event is emitted when collision detection determines that two
2630 /// entities have just made contact (were not colliding in the previous
2631 /// frame, but are colliding now).
2632 ///
2633 /// # Fields
2634 ///
2635 /// - `entity_a`, `entity_b`: The two entities involved in the collision
2636 /// - `contact`: Detailed contact information (point, normal, penetration)
2637 ///
2638 /// # Usage
2639 ///
2640 /// ```rust
2641 /// use goud_engine::ecs::collision::events::CollisionStarted;
2642 /// use goud_engine::core::event::{Events, EventReader};
2643 ///
2644 /// fn handle_collision_start(events: &Events<CollisionStarted>) {
2645 /// let mut reader = events.reader();
2646 /// for event in reader.read() {
2647 /// println!("Collision started between {:?} and {:?}", event.entity_a, event.entity_b);
2648 /// println!("Contact point: {:?}", event.contact.point);
2649 /// println!("Penetration depth: {}", event.contact.penetration);
2650 ///
2651 /// // Trigger gameplay effects
2652 /// // - Play collision sound
2653 /// // - Spawn particle effects
2654 /// // - Apply damage
2655 /// // - Trigger game events
2656 /// }
2657 /// }
2658 /// ```
2659 ///
2660 /// # Entity Ordering
2661 ///
2662 /// The order of `entity_a` and `entity_b` is consistent within a frame
2663 /// but may vary between frames. To check if a specific entity is involved:
2664 ///
2665 /// ```rust
2666 /// # use goud_engine::ecs::collision::events::CollisionStarted;
2667 /// # use goud_engine::ecs::Entity;
2668 /// # let event = CollisionStarted {
2669 /// # entity_a: Entity::PLACEHOLDER,
2670 /// # entity_b: Entity::PLACEHOLDER,
2671 /// # contact: goud_engine::ecs::collision::Contact::default(),
2672 /// # };
2673 /// # let my_entity = Entity::PLACEHOLDER;
2674 /// if event.involves(my_entity) {
2675 /// let other = event.other_entity(my_entity);
2676 /// println!("My entity collided with {:?}", other);
2677 /// }
2678 /// ```
2679 #[derive(Debug, Clone, Copy, PartialEq)]
2680 pub struct CollisionStarted {
2681 /// First entity in the collision pair.
2682 pub entity_a: Entity,
2683 /// Second entity in the collision pair.
2684 pub entity_b: Entity,
2685 /// Contact information for this collision.
2686 pub contact: Contact,
2687 }
2688
2689 impl CollisionStarted {
2690 /// Creates a new `CollisionStarted` event.
2691 #[must_use]
2692 pub fn new(entity_a: Entity, entity_b: Entity, contact: Contact) -> Self {
2693 Self {
2694 entity_a,
2695 entity_b,
2696 contact,
2697 }
2698 }
2699
2700 /// Checks if the given entity is involved in this collision.
2701 #[must_use]
2702 pub fn involves(&self, entity: Entity) -> bool {
2703 self.entity_a == entity || self.entity_b == entity
2704 }
2705
2706 /// Returns the other entity in the collision pair.
2707 ///
2708 /// # Returns
2709 ///
2710 /// - `Some(Entity)` if the given entity is involved in the collision
2711 /// - `None` if the given entity is not part of this collision
2712 ///
2713 /// # Example
2714 ///
2715 /// ```rust
2716 /// # use goud_engine::ecs::collision::events::CollisionStarted;
2717 /// # use goud_engine::ecs::Entity;
2718 /// # use goud_engine::ecs::collision::Contact;
2719 /// # let entity_a = Entity::from_bits(1);
2720 /// # let entity_b = Entity::from_bits(2);
2721 /// # let contact = Contact::default();
2722 /// let event = CollisionStarted::new(entity_a, entity_b, contact);
2723 ///
2724 /// assert_eq!(event.other_entity(entity_a), Some(entity_b));
2725 /// assert_eq!(event.other_entity(entity_b), Some(entity_a));
2726 /// assert_eq!(event.other_entity(Entity::from_bits(999)), None);
2727 /// ```
2728 #[must_use]
2729 pub fn other_entity(&self, entity: Entity) -> Option<Entity> {
2730 if self.entity_a == entity {
2731 Some(self.entity_b)
2732 } else if self.entity_b == entity {
2733 Some(self.entity_a)
2734 } else {
2735 None
2736 }
2737 }
2738
2739 /// Returns an ordered pair of entities (lower entity ID first).
2740 ///
2741 /// This is useful for consistent lookups in hash maps or sets
2742 /// regardless of which entity was `entity_a` vs `entity_b`.
2743 #[must_use]
2744 pub fn ordered_pair(&self) -> (Entity, Entity) {
2745 if self.entity_a.to_bits() < self.entity_b.to_bits() {
2746 (self.entity_a, self.entity_b)
2747 } else {
2748 (self.entity_b, self.entity_a)
2749 }
2750 }
2751 }
2752
2753 /// Event fired when two entities stop colliding.
2754 ///
2755 /// This event is emitted when collision detection determines that two
2756 /// entities that were colliding in the previous frame are no longer
2757 /// in contact.
2758 ///
2759 /// # Fields
2760 ///
2761 /// - `entity_a`, `entity_b`: The two entities that separated
2762 ///
2763 /// # Usage
2764 ///
2765 /// ```rust
2766 /// use goud_engine::ecs::collision::events::CollisionEnded;
2767 /// use goud_engine::core::event::{Events, EventReader};
2768 ///
2769 /// fn handle_collision_end(events: &Events<CollisionEnded>) {
2770 /// let mut reader = events.reader();
2771 /// for event in reader.read() {
2772 /// println!("Collision ended between {:?} and {:?}", event.entity_a, event.entity_b);
2773 ///
2774 /// // Stop gameplay effects
2775 /// // - Stop looping collision sounds
2776 /// // - Stop particle emitters
2777 /// // - Update collision state tracking
2778 /// }
2779 /// }
2780 /// ```
2781 ///
2782 /// # Note on Contact Information
2783 ///
2784 /// Unlike `CollisionStarted`, this event does not include contact
2785 /// information since the entities are no longer touching. If you need
2786 /// to track the last known contact point, store it when receiving
2787 /// `CollisionStarted` events.
2788 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2789 pub struct CollisionEnded {
2790 /// First entity in the collision pair.
2791 pub entity_a: Entity,
2792 /// Second entity in the collision pair.
2793 pub entity_b: Entity,
2794 }
2795
2796 impl CollisionEnded {
2797 /// Creates a new `CollisionEnded` event.
2798 #[must_use]
2799 pub fn new(entity_a: Entity, entity_b: Entity) -> Self {
2800 Self { entity_a, entity_b }
2801 }
2802
2803 /// Checks if the given entity is involved in this collision end.
2804 #[must_use]
2805 pub fn involves(&self, entity: Entity) -> bool {
2806 self.entity_a == entity || self.entity_b == entity
2807 }
2808
2809 /// Returns the other entity in the collision pair.
2810 ///
2811 /// # Returns
2812 ///
2813 /// - `Some(Entity)` if the given entity was involved in the collision
2814 /// - `None` if the given entity was not part of this collision
2815 #[must_use]
2816 pub fn other_entity(&self, entity: Entity) -> Option<Entity> {
2817 if self.entity_a == entity {
2818 Some(self.entity_b)
2819 } else if self.entity_b == entity {
2820 Some(self.entity_a)
2821 } else {
2822 None
2823 }
2824 }
2825
2826 /// Returns an ordered pair of entities (lower entity ID first).
2827 ///
2828 /// This is useful for consistent lookups in hash maps or sets
2829 /// regardless of which entity was `entity_a` vs `entity_b`.
2830 #[must_use]
2831 pub fn ordered_pair(&self) -> (Entity, Entity) {
2832 if self.entity_a.to_bits() < self.entity_b.to_bits() {
2833 (self.entity_a, self.entity_b)
2834 } else {
2835 (self.entity_b, self.entity_a)
2836 }
2837 }
2838 }
2839}
2840
2841// Re-export collision events at the collision module level for convenience
2842pub use events::{CollisionEnded, CollisionStarted};
2843
2844#[cfg(test)]
2845mod collision_events_tests {
2846 use super::events::*;
2847 use super::Contact;
2848 use crate::core::event::Event;
2849 use crate::core::math::Vec2;
2850 use crate::ecs::Entity;
2851
2852 // =========================================================================
2853 // CollisionStarted Tests
2854 // =========================================================================
2855
2856 #[test]
2857 fn test_collision_started_new() {
2858 let entity_a = Entity::from_bits(1);
2859 let entity_b = Entity::from_bits(2);
2860 let contact = Contact::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.5);
2861
2862 let event = CollisionStarted::new(entity_a, entity_b, contact);
2863
2864 assert_eq!(event.entity_a, entity_a);
2865 assert_eq!(event.entity_b, entity_b);
2866 assert_eq!(event.contact, contact);
2867 }
2868
2869 #[test]
2870 fn test_collision_started_involves() {
2871 let entity_a = Entity::from_bits(1);
2872 let entity_b = Entity::from_bits(2);
2873 let entity_c = Entity::from_bits(3);
2874 let contact = Contact::default();
2875
2876 let event = CollisionStarted::new(entity_a, entity_b, contact);
2877
2878 assert!(event.involves(entity_a));
2879 assert!(event.involves(entity_b));
2880 assert!(!event.involves(entity_c));
2881 }
2882
2883 #[test]
2884 fn test_collision_started_other_entity() {
2885 let entity_a = Entity::from_bits(1);
2886 let entity_b = Entity::from_bits(2);
2887 let entity_c = Entity::from_bits(3);
2888 let contact = Contact::default();
2889
2890 let event = CollisionStarted::new(entity_a, entity_b, contact);
2891
2892 assert_eq!(event.other_entity(entity_a), Some(entity_b));
2893 assert_eq!(event.other_entity(entity_b), Some(entity_a));
2894 assert_eq!(event.other_entity(entity_c), None);
2895 }
2896
2897 #[test]
2898 fn test_collision_started_ordered_pair() {
2899 let entity_1 = Entity::from_bits(1);
2900 let entity_2 = Entity::from_bits(2);
2901 let contact = Contact::default();
2902
2903 // Test both orderings
2904 let event1 = CollisionStarted::new(entity_1, entity_2, contact);
2905 let event2 = CollisionStarted::new(entity_2, entity_1, contact);
2906
2907 // Both should return the same ordered pair
2908 assert_eq!(event1.ordered_pair(), (entity_1, entity_2));
2909 assert_eq!(event2.ordered_pair(), (entity_1, entity_2));
2910 }
2911
2912 #[test]
2913 fn test_collision_started_implements_event() {
2914 fn accepts_event<E: Event>(_: E) {}
2915
2916 let entity_a = Entity::from_bits(1);
2917 let entity_b = Entity::from_bits(2);
2918 let contact = Contact::default();
2919 let event = CollisionStarted::new(entity_a, entity_b, contact);
2920
2921 accepts_event(event);
2922 }
2923
2924 #[test]
2925 fn test_collision_started_send_sync() {
2926 fn assert_send_sync<T: Send + Sync>() {}
2927 assert_send_sync::<CollisionStarted>();
2928 }
2929
2930 #[test]
2931 fn test_collision_started_clone() {
2932 let entity_a = Entity::from_bits(1);
2933 let entity_b = Entity::from_bits(2);
2934 let contact = Contact::default();
2935
2936 let event = CollisionStarted::new(entity_a, entity_b, contact);
2937 let cloned = event.clone();
2938
2939 assert_eq!(event, cloned);
2940 }
2941
2942 #[test]
2943 fn test_collision_started_debug() {
2944 let entity_a = Entity::from_bits(1);
2945 let entity_b = Entity::from_bits(2);
2946 let contact = Contact::default();
2947
2948 let event = CollisionStarted::new(entity_a, entity_b, contact);
2949 let debug_str = format!("{:?}", event);
2950
2951 assert!(debug_str.contains("CollisionStarted"));
2952 assert!(debug_str.contains("entity_a"));
2953 assert!(debug_str.contains("entity_b"));
2954 assert!(debug_str.contains("contact"));
2955 }
2956
2957 // =========================================================================
2958 // CollisionEnded Tests
2959 // =========================================================================
2960
2961 #[test]
2962 fn test_collision_ended_new() {
2963 let entity_a = Entity::from_bits(1);
2964 let entity_b = Entity::from_bits(2);
2965
2966 let event = CollisionEnded::new(entity_a, entity_b);
2967
2968 assert_eq!(event.entity_a, entity_a);
2969 assert_eq!(event.entity_b, entity_b);
2970 }
2971
2972 #[test]
2973 fn test_collision_ended_involves() {
2974 let entity_a = Entity::from_bits(1);
2975 let entity_b = Entity::from_bits(2);
2976 let entity_c = Entity::from_bits(3);
2977
2978 let event = CollisionEnded::new(entity_a, entity_b);
2979
2980 assert!(event.involves(entity_a));
2981 assert!(event.involves(entity_b));
2982 assert!(!event.involves(entity_c));
2983 }
2984
2985 #[test]
2986 fn test_collision_ended_other_entity() {
2987 let entity_a = Entity::from_bits(1);
2988 let entity_b = Entity::from_bits(2);
2989 let entity_c = Entity::from_bits(3);
2990
2991 let event = CollisionEnded::new(entity_a, entity_b);
2992
2993 assert_eq!(event.other_entity(entity_a), Some(entity_b));
2994 assert_eq!(event.other_entity(entity_b), Some(entity_a));
2995 assert_eq!(event.other_entity(entity_c), None);
2996 }
2997
2998 #[test]
2999 fn test_collision_ended_ordered_pair() {
3000 let entity_1 = Entity::from_bits(1);
3001 let entity_2 = Entity::from_bits(2);
3002
3003 // Test both orderings
3004 let event1 = CollisionEnded::new(entity_1, entity_2);
3005 let event2 = CollisionEnded::new(entity_2, entity_1);
3006
3007 // Both should return the same ordered pair
3008 assert_eq!(event1.ordered_pair(), (entity_1, entity_2));
3009 assert_eq!(event2.ordered_pair(), (entity_1, entity_2));
3010 }
3011
3012 #[test]
3013 fn test_collision_ended_implements_event() {
3014 fn accepts_event<E: Event>(_: E) {}
3015
3016 let entity_a = Entity::from_bits(1);
3017 let entity_b = Entity::from_bits(2);
3018 let event = CollisionEnded::new(entity_a, entity_b);
3019
3020 accepts_event(event);
3021 }
3022
3023 #[test]
3024 fn test_collision_ended_send_sync() {
3025 fn assert_send_sync<T: Send + Sync>() {}
3026 assert_send_sync::<CollisionEnded>();
3027 }
3028
3029 #[test]
3030 fn test_collision_ended_clone() {
3031 let entity_a = Entity::from_bits(1);
3032 let entity_b = Entity::from_bits(2);
3033
3034 let event = CollisionEnded::new(entity_a, entity_b);
3035 let cloned = event.clone();
3036
3037 assert_eq!(event, cloned);
3038 }
3039
3040 #[test]
3041 fn test_collision_ended_debug() {
3042 let entity_a = Entity::from_bits(1);
3043 let entity_b = Entity::from_bits(2);
3044
3045 let event = CollisionEnded::new(entity_a, entity_b);
3046 let debug_str = format!("{:?}", event);
3047
3048 assert!(debug_str.contains("CollisionEnded"));
3049 assert!(debug_str.contains("entity_a"));
3050 assert!(debug_str.contains("entity_b"));
3051 }
3052
3053 #[test]
3054 fn test_collision_ended_hash() {
3055 use std::collections::HashSet;
3056
3057 let entity_a = Entity::from_bits(1);
3058 let entity_b = Entity::from_bits(2);
3059 let entity_c = Entity::from_bits(3);
3060
3061 let mut set: HashSet<CollisionEnded> = HashSet::new();
3062 set.insert(CollisionEnded::new(entity_a, entity_b));
3063 set.insert(CollisionEnded::new(entity_b, entity_c));
3064 set.insert(CollisionEnded::new(entity_a, entity_b)); // Duplicate
3065
3066 // Should only have 2 unique collision pairs
3067 assert_eq!(set.len(), 2);
3068 }
3069
3070 // =========================================================================
3071 // Integration Tests
3072 // =========================================================================
3073
3074 #[test]
3075 fn test_collision_pair_consistency() {
3076 let entity_a = Entity::from_bits(1);
3077 let entity_b = Entity::from_bits(2);
3078 let contact = Contact::default();
3079
3080 // Create events in different orders
3081 let start1 = CollisionStarted::new(entity_a, entity_b, contact);
3082 let start2 = CollisionStarted::new(entity_b, entity_a, contact);
3083 let end1 = CollisionEnded::new(entity_a, entity_b);
3084 let end2 = CollisionEnded::new(entity_b, entity_a);
3085
3086 // Ordered pairs should be consistent
3087 assert_eq!(start1.ordered_pair(), start2.ordered_pair());
3088 assert_eq!(end1.ordered_pair(), end2.ordered_pair());
3089 assert_eq!(start1.ordered_pair(), end1.ordered_pair());
3090 }
3091
3092 #[test]
3093 fn test_collision_event_workflow() {
3094 let player = Entity::from_bits(1);
3095 let enemy = Entity::from_bits(2);
3096 let contact = Contact::new(Vec2::new(5.0, 5.0), Vec2::new(1.0, 0.0), 0.2);
3097
3098 // Collision starts
3099 let start_event = CollisionStarted::new(player, enemy, contact);
3100 assert!(start_event.involves(player));
3101 assert_eq!(start_event.other_entity(player), Some(enemy));
3102 assert!(start_event.contact.is_colliding());
3103
3104 // Collision ends
3105 let end_event = CollisionEnded::new(player, enemy);
3106 assert!(end_event.involves(player));
3107 assert_eq!(end_event.other_entity(player), Some(enemy));
3108
3109 // Both events should have same ordered pair
3110 assert_eq!(start_event.ordered_pair(), end_event.ordered_pair());
3111 }
3112}