Skip to main content

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}