Skip to main content

proof_engine/physics/
joints.rs

1//! 3D Physics joints, rigid bodies, collision detection, broadphase, ray casting,
2//! impulse resolution, and sleep system.
3//!
4//! ## Components
5//! - `RigidBody`          — full 3D rigid body with Quat orientation, Mat3 inertia tensor
6//! - `RigidBodyHandle`    — newtype u32 handle
7//! - `PhysicsWorld3D`     — arena of RigidBody, gravity, timestep, broadphase, `step()`
8//! - `CollisionShape`     — Sphere, Box, Capsule, ConvexHull
9//! - `CollisionDetector`  — sphere-sphere, sphere-box, box-box (SAT), capsule-capsule
10//! - `Broadphase`         — sweep-and-prune on X-axis
11//! - `RayCast`            — ray vs shape intersection
12//! - `ImpulseResolver`    — sequential impulse with friction cone, restitution, warm-start
13//! - `SleepSystem`        — kinetic energy threshold, sleep counter, island wake propagation
14
15use glam::{Vec2, Vec3, Quat, Mat3};
16use std::collections::HashMap;
17
18// ── RigidBodyHandle ────────────────────────────────────────────────────────────
19
20/// Opaque handle into a PhysicsWorld3D body arena.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct RigidBodyHandle(pub u32);
23
24// ── SleepState ────────────────────────────────────────────────────────────────
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum SleepState {
28    Awake,
29    Drowsy { timer: f32 },
30    Asleep,
31}
32
33// ── RigidBody ─────────────────────────────────────────────────────────────────
34
35/// Full 3D rigid body.
36#[derive(Debug, Clone)]
37pub struct RigidBody {
38    /// World-space position.
39    pub position:       Vec3,
40    /// Linear velocity.
41    pub velocity:       Vec3,
42    /// Angular velocity (world space, in rad/s around each axis).
43    pub angular_vel:    Vec3,
44    /// Orientation quaternion.
45    pub orientation:    Quat,
46    /// Total mass (kg). 0 = static.
47    pub mass:           f32,
48    /// Inverse mass (0 = static).
49    pub inv_mass:       f32,
50    /// Inverse inertia tensor in local body space.
51    pub inv_inertia_local: Mat3,
52    /// Accumulated forces this step (world space).
53    pub force_accum:    Vec3,
54    /// Accumulated torques this step (world space).
55    pub torque_accum:   Vec3,
56    /// Coefficient of restitution.
57    pub restitution:    f32,
58    /// Friction coefficient.
59    pub friction:       f32,
60    /// Linear damping (0–1).
61    pub linear_damping: f32,
62    /// Angular damping (0–1).
63    pub angular_damping: f32,
64    /// Sleep / drowsy / awake state.
65    pub sleep_state:    SleepState,
66    /// Collision group mask.
67    pub collision_group: u32,
68    /// Which groups this body collides with.
69    pub collision_mask:  u32,
70    /// User tag.
71    pub tag:            u32,
72    /// Shape.
73    pub shape:          CollisionShape,
74}
75
76impl RigidBody {
77    /// Create a dynamic rigid body.
78    pub fn dynamic(position: Vec3, mass: f32, shape: CollisionShape) -> Self {
79        let inv_inertia_local = shape.compute_inverse_inertia_tensor(mass);
80        Self {
81            position,
82            velocity:         Vec3::ZERO,
83            angular_vel:      Vec3::ZERO,
84            orientation:      Quat::IDENTITY,
85            mass,
86            inv_mass:         if mass > 0.0 { 1.0 / mass } else { 0.0 },
87            inv_inertia_local,
88            force_accum:      Vec3::ZERO,
89            torque_accum:     Vec3::ZERO,
90            restitution:      0.3,
91            friction:         0.5,
92            linear_damping:   0.01,
93            angular_damping:  0.05,
94            sleep_state:      SleepState::Awake,
95            collision_group:  1,
96            collision_mask:   u32::MAX,
97            tag:              0,
98            shape,
99        }
100    }
101
102    /// Create a static rigid body (inv_mass = 0, won't move).
103    pub fn static_body(position: Vec3, shape: CollisionShape) -> Self {
104        Self {
105            inv_mass:           0.0,
106            mass:               f32::INFINITY,
107            inv_inertia_local:  Mat3::ZERO,
108            ..Self::dynamic(position, 1.0, shape)
109        }
110    }
111
112    pub fn is_static(&self) -> bool { self.inv_mass < 1e-12 }
113    pub fn is_sleeping(&self) -> bool { matches!(self.sleep_state, SleepState::Asleep) }
114
115    /// Wake the body.
116    pub fn wake(&mut self) {
117        self.sleep_state = SleepState::Awake;
118    }
119
120    /// Apply a force at center of mass.
121    pub fn apply_force(&mut self, force: Vec3) {
122        if !self.is_static() { self.force_accum += force; }
123    }
124
125    /// Apply a torque.
126    pub fn apply_torque(&mut self, torque: Vec3) {
127        if !self.is_static() { self.torque_accum += torque; }
128    }
129
130    /// Apply force at a world-space point (generates torque).
131    pub fn apply_force_at_point(&mut self, force: Vec3, world_point: Vec3) {
132        self.apply_force(force);
133        let r = world_point - self.position;
134        self.apply_torque(r.cross(force));
135    }
136
137    /// Apply an instantaneous linear impulse.
138    pub fn apply_linear_impulse(&mut self, impulse: Vec3) {
139        self.velocity += impulse * self.inv_mass;
140    }
141
142    /// Apply an instantaneous angular impulse.
143    pub fn apply_angular_impulse(&mut self, impulse: Vec3) {
144        let iw = self.inv_inertia_world();
145        self.angular_vel += iw * impulse;
146    }
147
148    /// Apply an impulse at a world-space point.
149    pub fn apply_impulse_at_point(&mut self, impulse: Vec3, world_point: Vec3) {
150        self.apply_linear_impulse(impulse);
151        let r = world_point - self.position;
152        self.apply_angular_impulse(r.cross(impulse));
153    }
154
155    /// Clear accumulated forces and torques.
156    pub fn clear_forces(&mut self) {
157        self.force_accum  = Vec3::ZERO;
158        self.torque_accum = Vec3::ZERO;
159    }
160
161    /// Inverse inertia tensor in world space.
162    pub fn inv_inertia_world(&self) -> Mat3 {
163        let rot = Mat3::from_quat(self.orientation);
164        let rot_t = rot.transpose();
165        rot * self.inv_inertia_local * rot_t
166    }
167
168    /// Velocity at a world-space point on the body.
169    pub fn velocity_at_point(&self, world_point: Vec3) -> Vec3 {
170        let r = world_point - self.position;
171        self.velocity + self.angular_vel.cross(r)
172    }
173
174    /// Semi-implicit Euler integration.
175    pub fn integrate(&mut self, dt: f32, gravity: Vec3) {
176        if self.is_static() || self.is_sleeping() { return; }
177
178        // Linear integration
179        let accel = self.force_accum * self.inv_mass + gravity;
180        self.velocity += accel * dt;
181        self.velocity *= (1.0 - self.linear_damping * dt).max(0.0);
182        self.position += self.velocity * dt;
183
184        // Angular integration
185        let iw = self.inv_inertia_world();
186        let alpha = iw * self.torque_accum;
187        self.angular_vel += alpha * dt;
188        self.angular_vel *= (1.0 - self.angular_damping * dt).max(0.0);
189
190        // Update orientation
191        let half_omega = self.angular_vel * 0.5;
192        let dq = Quat::from_xyzw(half_omega.x, half_omega.y, half_omega.z, 0.0) * self.orientation;
193        self.orientation = (self.orientation + dq * dt).normalize();
194
195        self.clear_forces();
196    }
197
198    /// Kinetic energy (linear + rotational).
199    pub fn kinetic_energy(&self) -> f32 {
200        let linear = 0.5 * self.mass * self.velocity.length_squared();
201        let rot_mat = Mat3::from_quat(self.orientation);
202        let inertia_local = pseudo_inverse_mat3(self.inv_inertia_local);
203        let ang_local = rot_mat.transpose() * self.angular_vel;
204        let rotational = 0.5 * ang_local.dot(inertia_local * ang_local);
205        linear + rotational
206    }
207}
208
209/// Approximate inverse of a diagonal-ish inertia tensor.
210fn pseudo_inverse_mat3(m: Mat3) -> Mat3 {
211    // For diagonal matrices: invert each diagonal element
212    let d = Vec3::new(m.x_axis.x, m.y_axis.y, m.z_axis.z);
213    Mat3::from_diagonal(Vec3::new(
214        if d.x.abs() > 1e-10 { 1.0 / d.x } else { 0.0 },
215        if d.y.abs() > 1e-10 { 1.0 / d.y } else { 0.0 },
216        if d.z.abs() > 1e-10 { 1.0 / d.z } else { 0.0 },
217    ))
218}
219
220// ── CollisionShape ────────────────────────────────────────────────────────────
221
222/// Collision shapes supported by the physics engine.
223#[derive(Debug, Clone)]
224pub enum CollisionShape {
225    Sphere  { radius: f32 },
226    Box     { half_extents: Vec3 },
227    Capsule { radius: f32, half_height: f32 },
228    ConvexHull(Vec<Vec3>),
229}
230
231impl CollisionShape {
232    pub fn sphere(radius: f32) -> Self { Self::Sphere { radius } }
233    pub fn box_shape(half: Vec3) -> Self { Self::Box { half_extents: half } }
234    pub fn capsule(radius: f32, half_height: f32) -> Self { Self::Capsule { radius, half_height } }
235
236    /// Compute the inverse inertia tensor for this shape and mass.
237    pub fn compute_inverse_inertia_tensor(&self, mass: f32) -> Mat3 {
238        if mass < 1e-12 { return Mat3::ZERO; }
239        let inv_m = 1.0 / mass;
240        match self {
241            Self::Sphere { radius } => {
242                let i = 2.0 / 5.0 * mass * radius * radius;
243                Mat3::from_diagonal(Vec3::splat(1.0 / i))
244            }
245            Self::Box { half_extents: h } => {
246                let ix = mass * (h.y * h.y + h.z * h.z) / 3.0;
247                let iy = mass * (h.x * h.x + h.z * h.z) / 3.0;
248                let iz = mass * (h.x * h.x + h.y * h.y) / 3.0;
249                Mat3::from_diagonal(Vec3::new(
250                    if ix > 1e-10 { 1.0 / ix } else { 0.0 },
251                    if iy > 1e-10 { 1.0 / iy } else { 0.0 },
252                    if iz > 1e-10 { 1.0 / iz } else { 0.0 },
253                ))
254            }
255            Self::Capsule { radius, half_height } => {
256                let r2 = radius * radius;
257                let h2 = half_height * half_height;
258                let iy = mass * (r2 * 2.0 / 5.0 + h2 / 3.0 + r2 / 2.0);
259                let ixz = mass * (r2 * 2.0 / 5.0 + h2 / 3.0);
260                Mat3::from_diagonal(Vec3::new(
261                    if ixz > 1e-10 { 1.0 / ixz } else { 0.0 },
262                    if iy  > 1e-10 { 1.0 / iy  } else { 0.0 },
263                    if ixz > 1e-10 { 1.0 / ixz } else { 0.0 },
264                ))
265            }
266            Self::ConvexHull(pts) => {
267                if pts.is_empty() { return Mat3::ZERO; }
268                // Approximate with bounding box
269                let mut mn = Vec3::splat(f32::INFINITY);
270                let mut mx = Vec3::splat(f32::NEG_INFINITY);
271                for p in pts {
272                    mn = mn.min(*p);
273                    mx = mx.max(*p);
274                }
275                let h = (mx - mn) * 0.5;
276                let ix = mass * (h.y * h.y + h.z * h.z) / 3.0;
277                let iy = mass * (h.x * h.x + h.z * h.z) / 3.0;
278                let iz = mass * (h.x * h.x + h.y * h.y) / 3.0;
279                Mat3::from_diagonal(Vec3::new(
280                    if ix > 1e-10 { 1.0 / ix } else { 0.0 },
281                    if iy > 1e-10 { 1.0 / iy } else { 0.0 },
282                    if iz > 1e-10 { 1.0 / iz } else { 0.0 },
283                ))
284            }
285        }
286    }
287
288    /// Bounding radius.
289    pub fn bounding_radius(&self) -> f32 {
290        match self {
291            Self::Sphere { radius }        => *radius,
292            Self::Box { half_extents }     => half_extents.length(),
293            Self::Capsule { radius, half_height } => radius + half_height,
294            Self::ConvexHull(pts) => pts.iter().map(|p| p.length()).fold(0.0_f32, f32::max),
295        }
296    }
297
298    /// AABB half-extents.
299    pub fn aabb_half_extents(&self) -> Vec3 {
300        match self {
301            Self::Sphere { radius }        => Vec3::splat(*radius),
302            Self::Box { half_extents }     => *half_extents,
303            Self::Capsule { radius, half_height } => Vec3::new(*radius, half_height + radius, *radius),
304            Self::ConvexHull(pts) => {
305                let mut mx = Vec3::ZERO;
306                for p in pts { mx = mx.max(p.abs()); }
307                mx
308            }
309        }
310    }
311}
312
313// ── ContactPoint ──────────────────────────────────────────────────────────────
314
315/// A single contact point in a collision manifold.
316#[derive(Debug, Clone, Copy)]
317pub struct ContactPoint {
318    /// Contact point in world space.
319    pub point:          Vec3,
320    /// Contact normal (from A toward B).
321    pub normal:         Vec3,
322    /// Penetration depth (positive = overlap).
323    pub depth:          f32,
324    /// Cached normal impulse (warm starting).
325    pub cached_normal:  f32,
326    /// Cached tangent impulses (friction, warm starting).
327    pub cached_tangent: [f32; 2],
328}
329
330impl ContactPoint {
331    pub fn new(point: Vec3, normal: Vec3, depth: f32) -> Self {
332        Self { point, normal, depth, cached_normal: 0.0, cached_tangent: [0.0; 2] }
333    }
334}
335
336/// Result of collision detection between two bodies.
337#[derive(Debug, Clone)]
338pub struct ContactManifold3D {
339    pub handle_a:  RigidBodyHandle,
340    pub handle_b:  RigidBodyHandle,
341    /// Up to 4 contact points.
342    pub contacts:  Vec<ContactPoint>,
343}
344
345impl ContactManifold3D {
346    pub fn new(a: RigidBodyHandle, b: RigidBodyHandle) -> Self {
347        Self { handle_a: a, handle_b: b, contacts: Vec::with_capacity(4) }
348    }
349
350    pub fn add_contact(&mut self, pt: ContactPoint) {
351        if self.contacts.len() < 4 {
352            self.contacts.push(pt);
353        }
354    }
355}
356
357// ── CollisionDetector ─────────────────────────────────────────────────────────
358
359/// Narrow-phase collision detection.
360pub struct CollisionDetector;
361
362impl CollisionDetector {
363    /// Sphere vs sphere.
364    pub fn sphere_sphere(
365        pos_a: Vec3, ra: f32, ha: RigidBodyHandle,
366        pos_b: Vec3, rb: f32, hb: RigidBodyHandle,
367    ) -> Option<ContactManifold3D> {
368        let delta = pos_b - pos_a;
369        let dist  = delta.length();
370        let sum_r = ra + rb;
371        if dist >= sum_r || dist < 1e-8 { return None; }
372        let normal = if dist > 1e-7 { delta / dist } else { Vec3::Y };
373        let mut m = ContactManifold3D::new(ha, hb);
374        m.add_contact(ContactPoint::new(
375            pos_a + normal * ra,
376            normal,
377            sum_r - dist,
378        ));
379        Some(m)
380    }
381
382    /// Sphere vs axis-aligned box (fallback uses sphere-point clamping).
383    pub fn sphere_box(
384        sphere_pos: Vec3, sphere_r: f32, hs: RigidBodyHandle,
385        box_pos: Vec3, box_rot: Quat, box_half: Vec3, hb: RigidBodyHandle,
386    ) -> Option<ContactManifold3D> {
387        // Transform sphere center into box local space
388        let box_inv_rot = box_rot.inverse();
389        let local_sphere = box_inv_rot * (sphere_pos - box_pos);
390
391        // Clamp to box extents
392        let clamped = local_sphere.clamp(-box_half, box_half);
393        let local_delta = local_sphere - clamped;
394        let dist = local_delta.length();
395
396        if dist >= sphere_r { return None; }
397
398        // If sphere center is inside box
399        if dist < 1e-8 {
400            // Find the closest face normal
401            let overlap = box_half - local_sphere.abs();
402            let min_overlap_axis = if overlap.x < overlap.y && overlap.x < overlap.z {
403                Vec3::X * local_sphere.x.signum()
404            } else if overlap.y < overlap.z {
405                Vec3::Y * local_sphere.y.signum()
406            } else {
407                Vec3::Z * local_sphere.z.signum()
408            };
409            let world_normal = box_rot * min_overlap_axis;
410            let penetration = overlap.min_element() + sphere_r;
411            let mut m = ContactManifold3D::new(hs, hb);
412            m.add_contact(ContactPoint::new(sphere_pos - world_normal * sphere_r, world_normal, penetration));
413            return Some(m);
414        }
415
416        let local_normal = local_delta / dist;
417        let world_normal = box_rot * local_normal;
418        let depth = sphere_r - dist;
419        let contact_point = sphere_pos - world_normal * sphere_r;
420        let mut m = ContactManifold3D::new(hs, hb);
421        m.add_contact(ContactPoint::new(contact_point, world_normal, depth));
422        Some(m)
423    }
424
425    /// Box vs box using SAT (Separating Axis Theorem).
426    pub fn box_box(
427        pos_a: Vec3, rot_a: Quat, half_a: Vec3, ha: RigidBodyHandle,
428        pos_b: Vec3, rot_b: Quat, half_b: Vec3, hb: RigidBodyHandle,
429    ) -> Option<ContactManifold3D> {
430        let axes_a = [
431            rot_a * Vec3::X, rot_a * Vec3::Y, rot_a * Vec3::Z,
432        ];
433        let axes_b = [
434            rot_b * Vec3::X, rot_b * Vec3::Y, rot_b * Vec3::Z,
435        ];
436
437        let t = pos_b - pos_a;
438
439        let mut min_depth = f32::MAX;
440        let mut best_normal = Vec3::X;
441
442        // Test 15 SAT axes: 3 face-A, 3 face-B, 9 edge-edge
443        let test_axes: Vec<Vec3> = {
444            let mut v: Vec<Vec3> = Vec::with_capacity(15);
445            v.extend_from_slice(&axes_a);
446            v.extend_from_slice(&axes_b);
447            for &aa in &axes_a {
448                for &ab in &axes_b {
449                    let cross = aa.cross(ab);
450                    if cross.length_squared() > 1e-10 {
451                        v.push(cross.normalize());
452                    }
453                }
454            }
455            v
456        };
457
458        for axis in test_axes {
459            let proj_a = project_box_onto_axis(half_a, axes_a, axis);
460            let proj_b = project_box_onto_axis(half_b, axes_b, axis);
461            let t_proj = t.dot(axis).abs();
462            let depth = proj_a + proj_b - t_proj;
463            if depth < 0.0 { return None; }  // Separating axis found
464            if depth < min_depth {
465                min_depth = depth;
466                // Normal should point from A to B
467                best_normal = if t.dot(axis) < 0.0 { -axis } else { axis };
468            }
469        }
470
471        let mut m = ContactManifold3D::new(ha, hb);
472        // Approximate contact point at midpoint between centers projected onto normal
473        let contact = pos_a + best_normal * (min_depth * 0.5);
474        m.add_contact(ContactPoint::new(contact, best_normal, min_depth));
475        Some(m)
476    }
477
478    /// Capsule vs capsule.
479    pub fn capsule_capsule(
480        pos_a: Vec3, axis_a: Vec3, r_a: f32, hh_a: f32, ha: RigidBodyHandle,
481        pos_b: Vec3, axis_b: Vec3, r_b: f32, hh_b: f32, hb: RigidBodyHandle,
482    ) -> Option<ContactManifold3D> {
483        // Find closest points on the two line segments
484        let p1 = pos_a - axis_a * hh_a;
485        let p2 = pos_a + axis_a * hh_a;
486        let p3 = pos_b - axis_b * hh_b;
487        let p4 = pos_b + axis_b * hh_b;
488
489        let (cp_a, cp_b) = closest_points_on_segments(p1, p2, p3, p4);
490        let delta = cp_b - cp_a;
491        let dist = delta.length();
492        let sum_r = r_a + r_b;
493        if dist >= sum_r { return None; }
494
495        let normal = if dist > 1e-7 { delta / dist } else { Vec3::Y };
496        let depth = sum_r - dist;
497        let contact_point = cp_a + normal * r_a;
498
499        let mut m = ContactManifold3D::new(ha, hb);
500        m.add_contact(ContactPoint::new(contact_point, normal, depth));
501        Some(m)
502    }
503
504    /// Dispatch collision detection based on shape types.
505    pub fn detect(
506        body_a: &RigidBody, ha: RigidBodyHandle,
507        body_b: &RigidBody, hb: RigidBodyHandle,
508    ) -> Option<ContactManifold3D> {
509        match (&body_a.shape, &body_b.shape) {
510            (CollisionShape::Sphere { radius: ra }, CollisionShape::Sphere { radius: rb }) => {
511                Self::sphere_sphere(body_a.position, *ra, ha, body_b.position, *rb, hb)
512            }
513            (CollisionShape::Sphere { radius: rs }, CollisionShape::Box { half_extents }) => {
514                Self::sphere_box(body_a.position, *rs, ha, body_b.position, body_b.orientation, *half_extents, hb)
515            }
516            (CollisionShape::Box { half_extents }, CollisionShape::Sphere { radius: rs }) => {
517                let m = Self::sphere_box(body_b.position, *rs, hb, body_a.position, body_a.orientation, *half_extents, ha)?;
518                // Flip normals
519                let mut flipped = ContactManifold3D::new(ha, hb);
520                for c in m.contacts {
521                    flipped.add_contact(ContactPoint::new(c.point, -c.normal, c.depth));
522                }
523                Some(flipped)
524            }
525            (CollisionShape::Box { half_extents: ha_ext }, CollisionShape::Box { half_extents: hb_ext }) => {
526                Self::box_box(body_a.position, body_a.orientation, *ha_ext, ha, body_b.position, body_b.orientation, *hb_ext, hb)
527            }
528            (CollisionShape::Capsule { radius: ra, half_height: hha }, CollisionShape::Capsule { radius: rb, half_height: hhb }) => {
529                let axis_a = body_a.orientation * Vec3::Y;
530                let axis_b = body_b.orientation * Vec3::Y;
531                Self::capsule_capsule(body_a.position, axis_a, *ra, *hha, ha, body_b.position, axis_b, *rb, *hhb, hb)
532            }
533            _ => {
534                // GJK fallback: use bounding sphere approximation
535                let ra = body_a.shape.bounding_radius();
536                let rb = body_b.shape.bounding_radius();
537                Self::sphere_sphere(body_a.position, ra, ha, body_b.position, rb, hb)
538            }
539        }
540    }
541}
542
543/// Project OBB half-extents onto an axis.
544fn project_box_onto_axis(half: Vec3, axes: [Vec3; 3], axis: Vec3) -> f32 {
545    half.x * axes[0].dot(axis).abs()
546        + half.y * axes[1].dot(axis).abs()
547        + half.z * axes[2].dot(axis).abs()
548}
549
550/// Find the closest points on two line segments.
551fn closest_points_on_segments(p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3) -> (Vec3, Vec3) {
552    let d1 = p2 - p1;
553    let d2 = p4 - p3;
554    let r  = p1 - p3;
555    let a  = d1.dot(d1);
556    let e  = d2.dot(d2);
557    let f  = d2.dot(r);
558
559    let (s, t) = if a < 1e-10 && e < 1e-10 {
560        (0.0, 0.0)
561    } else if a < 1e-10 {
562        (0.0, (f / e).clamp(0.0, 1.0))
563    } else {
564        let c = d1.dot(r);
565        if e < 1e-10 {
566            ((-c / a).clamp(0.0, 1.0), 0.0)
567        } else {
568            let b = d1.dot(d2);
569            let denom = a * e - b * b;
570            let s = if denom.abs() > 1e-10 { ((b * f - c * e) / denom).clamp(0.0, 1.0) } else { 0.0 };
571            let t = (b * s + f) / e;
572            if t < 0.0 {
573                ((-c / a).clamp(0.0, 1.0), 0.0)
574            } else if t > 1.0 {
575                (((b - c) / a).clamp(0.0, 1.0), 1.0)
576            } else {
577                (s, t)
578            }
579        }
580    };
581
582    (p1 + d1 * s, p3 + d2 * t)
583}
584
585// ── Broadphase ────────────────────────────────────────────────────────────────
586
587/// AABB entry for broadphase sweep-and-prune.
588#[derive(Debug, Clone)]
589pub struct BroadphaseEntry {
590    pub handle:  RigidBodyHandle,
591    pub min_x:   f32,
592    pub max_x:   f32,
593    pub min_y:   f32,
594    pub max_y:   f32,
595    pub min_z:   f32,
596    pub max_z:   f32,
597}
598
599/// Sweep-and-prune broadphase on the X axis with insertion sort.
600pub struct Broadphase {
601    entries: Vec<BroadphaseEntry>,
602}
603
604impl Broadphase {
605    pub fn new() -> Self { Self { entries: Vec::new() } }
606
607    /// Rebuild from all bodies.
608    pub fn rebuild(&mut self, bodies: &HashMap<RigidBodyHandle, RigidBody>) {
609        self.entries.clear();
610        for (h, b) in bodies {
611            let half = b.shape.aabb_half_extents();
612            self.entries.push(BroadphaseEntry {
613                handle: *h,
614                min_x: b.position.x - half.x,
615                max_x: b.position.x + half.x,
616                min_y: b.position.y - half.y,
617                max_y: b.position.y + half.y,
618                min_z: b.position.z - half.z,
619                max_z: b.position.z + half.z,
620            });
621        }
622        // Insertion sort by min_x (nearly sorted after rebuild)
623        let n = self.entries.len();
624        for i in 1..n {
625            let mut j = i;
626            while j > 0 && self.entries[j - 1].min_x > self.entries[j].min_x {
627                self.entries.swap(j - 1, j);
628                j -= 1;
629            }
630        }
631    }
632
633    /// Returns all overlapping pairs (handle_a, handle_b).
634    pub fn overlapping_pairs(&self) -> Vec<(RigidBodyHandle, RigidBodyHandle)> {
635        let mut pairs = Vec::new();
636        let n = self.entries.len();
637        for i in 0..n {
638            for j in (i + 1)..n {
639                let a = &self.entries[i];
640                let b = &self.entries[j];
641                // Early-out on X axis
642                if b.min_x > a.max_x { break; }
643                // Check Y and Z overlap
644                if a.min_y > b.max_y || b.min_y > a.max_y { continue; }
645                if a.min_z > b.max_z || b.min_z > a.max_z { continue; }
646                pairs.push((a.handle, b.handle));
647            }
648        }
649        pairs
650    }
651
652    pub fn entry_count(&self) -> usize { self.entries.len() }
653}
654
655impl Default for Broadphase {
656    fn default() -> Self { Self::new() }
657}
658
659// ── RayCast ───────────────────────────────────────────────────────────────────
660
661/// Ray for intersection tests.
662#[derive(Debug, Clone, Copy)]
663pub struct Ray {
664    pub origin:    Vec3,
665    pub direction: Vec3,  // should be normalized
666}
667
668impl Ray {
669    pub fn new(origin: Vec3, direction: Vec3) -> Self {
670        Self { origin, direction: direction.normalize_or_zero() }
671    }
672
673    pub fn at(&self, t: f32) -> Vec3 { self.origin + self.direction * t }
674}
675
676/// Result of a ray-cast hit.
677#[derive(Debug, Clone, Copy)]
678pub struct RayHit {
679    pub handle:  RigidBodyHandle,
680    pub t:       f32,       // distance along ray
681    pub point:   Vec3,
682    pub normal:  Vec3,
683}
684
685/// Ray intersection routines.
686pub struct RayCast;
687
688impl RayCast {
689    /// Ray vs sphere.
690    pub fn ray_sphere(ray: &Ray, center: Vec3, radius: f32) -> Option<f32> {
691        let oc = ray.origin - center;
692        let b = oc.dot(ray.direction);
693        let c = oc.dot(oc) - radius * radius;
694        let disc = b * b - c;
695        if disc < 0.0 { return None; }
696        let sqrt_disc = disc.sqrt();
697        let t1 = -b - sqrt_disc;
698        let t2 = -b + sqrt_disc;
699        if t1 > 1e-4 { Some(t1) }
700        else if t2 > 1e-4 { Some(t2) }
701        else { None }
702    }
703
704    /// Ray vs AABB (slab method), transformed into box local space.
705    pub fn ray_box(ray: &Ray, box_pos: Vec3, box_rot: Quat, box_half: Vec3) -> Option<f32> {
706        let inv_rot = box_rot.inverse();
707        let local_origin = inv_rot * (ray.origin - box_pos);
708        let local_dir    = inv_rot * ray.direction;
709
710        let t_min_arr = (-box_half - local_origin) / local_dir;
711        let t_max_arr = ( box_half - local_origin) / local_dir;
712
713        let t_min_v = t_min_arr.min(t_max_arr);
714        let t_max_v = t_min_arr.max(t_max_arr);
715
716        let t_enter = t_min_v.x.max(t_min_v.y).max(t_min_v.z);
717        let t_exit  = t_max_v.x.min(t_max_v.y).min(t_max_v.z);
718
719        if t_enter > t_exit || t_exit < 1e-4 { return None; }
720        let t = if t_enter > 1e-4 { t_enter } else { t_exit };
721        Some(t)
722    }
723
724    /// Ray vs capsule.
725    pub fn ray_capsule(ray: &Ray, cap_pos: Vec3, cap_axis: Vec3, radius: f32, half_height: f32) -> Option<f32> {
726        // Test against the cylinder body, then the two end-caps
727        let p = ray.origin - cap_pos;
728        let axis = cap_axis.normalize_or_zero();
729        let d = ray.direction - ray.direction.dot(axis) * axis;
730        let e = p - p.dot(axis) * axis;
731
732        let a = d.dot(d);
733        let b = 2.0 * d.dot(e);
734        let c = e.dot(e) - radius * radius;
735
736        let mut best_t = f32::MAX;
737
738        if a > 1e-10 {
739            let disc = b * b - 4.0 * a * c;
740            if disc >= 0.0 {
741                let sqrt_d = disc.sqrt();
742                for t in [(-b - sqrt_d) / (2.0 * a), (-b + sqrt_d) / (2.0 * a)] {
743                    if t > 1e-4 {
744                        let pt = ray.at(t) - cap_pos;
745                        let h = pt.dot(axis);
746                        if h.abs() <= half_height && t < best_t {
747                            best_t = t;
748                        }
749                    }
750                }
751            }
752        }
753
754        // End caps
755        for sign in [-1.0_f32, 1.0] {
756            let cap_center = cap_pos + axis * (sign * half_height);
757            if let Some(t) = Self::ray_sphere(ray, cap_center, radius) {
758                if t < best_t { best_t = t; }
759            }
760        }
761
762        if best_t < f32::MAX { Some(best_t) } else { None }
763    }
764
765    /// Test a ray against a body's shape. Returns t if hit.
766    pub fn ray_vs_body(ray: &Ray, body: &RigidBody) -> Option<f32> {
767        match &body.shape {
768            CollisionShape::Sphere { radius } =>
769                Self::ray_sphere(ray, body.position, *radius),
770            CollisionShape::Box { half_extents } =>
771                Self::ray_box(ray, body.position, body.orientation, *half_extents),
772            CollisionShape::Capsule { radius, half_height } => {
773                let axis = body.orientation * Vec3::Y;
774                Self::ray_capsule(ray, body.position, axis, *radius, *half_height)
775            }
776            CollisionShape::ConvexHull(_) => {
777                // Approximate with bounding sphere
778                let r = body.shape.bounding_radius();
779                Self::ray_sphere(ray, body.position, r)
780            }
781        }
782    }
783}
784
785// ── ImpulseResolver ───────────────────────────────────────────────────────────
786
787/// Sequential impulse resolver with friction cone projection, restitution,
788/// and warm-starting.
789pub struct ImpulseResolver {
790    /// Number of resolution iterations per step.
791    pub iterations:    u32,
792    /// Baumgarte stabilization factor.
793    pub baumgarte:     f32,
794    /// Penetration slop.
795    pub slop:          f32,
796    /// Enable warm starting.
797    pub warm_start:    bool,
798}
799
800impl Default for ImpulseResolver {
801    fn default() -> Self {
802        Self { iterations: 10, baumgarte: 0.2, slop: 0.005, warm_start: true }
803    }
804}
805
806impl ImpulseResolver {
807    pub fn new() -> Self { Self::default() }
808
809    pub fn with_iterations(mut self, n: u32) -> Self { self.iterations = n; self }
810
811    /// Resolve a single contact point between two bodies.
812    pub fn resolve_contact(
813        &self,
814        a: &mut RigidBody, b: &mut RigidBody,
815        contact: &mut ContactPoint,
816        dt: f32,
817    ) {
818        let ra = contact.point - a.position;
819        let rb = contact.point - b.position;
820        let n  = contact.normal;
821
822        let va = a.velocity_at_point(contact.point);
823        let vb = b.velocity_at_point(contact.point);
824        let rel_vel = vb - va;
825        let vn = rel_vel.dot(n);
826
827        // Don't resolve if separating
828        if vn > 0.0 { return; }
829
830        let e = a.restitution.min(b.restitution);
831        let restitution_term = if vn.abs() > 1.0 { -e * vn } else { 0.0 };
832        let baumgarte_bias = self.baumgarte / dt * (contact.depth - self.slop).max(0.0);
833
834        // Effective mass along normal
835        let iwa = a.inv_inertia_world();
836        let iwb = b.inv_inertia_world();
837        let k_n = a.inv_mass + b.inv_mass
838            + (iwa * ra.cross(n)).cross(ra).dot(n)
839            + (iwb * rb.cross(n)).cross(rb).dot(n);
840
841        if k_n < 1e-10 { return; }
842
843        let lambda_n = -(vn + restitution_term + baumgarte_bias) / k_n;
844        let prev_n = contact.cached_normal;
845        let new_n  = (prev_n + lambda_n).max(0.0);
846        let actual_n = new_n - prev_n;
847        contact.cached_normal = new_n;
848
849        let impulse_n = n * actual_n;
850        a.apply_impulse_at_point(-impulse_n, contact.point);
851        b.apply_impulse_at_point( impulse_n, contact.point);
852
853        // Friction
854        let rel_vel2 = b.velocity_at_point(contact.point) - a.velocity_at_point(contact.point);
855        let tangential = rel_vel2 - n * rel_vel2.dot(n);
856        let tangential_len = tangential.length();
857        if tangential_len < 1e-7 { return; }
858        let t1 = tangential / tangential_len;
859        let t2 = n.cross(t1);
860
861        let mu = (a.friction + b.friction) * 0.5;
862        let max_friction = mu * contact.cached_normal;
863
864        for (ti, tangent) in [t1, t2].iter().enumerate() {
865            let k_t = a.inv_mass + b.inv_mass
866                + (iwa * ra.cross(*tangent)).cross(ra).dot(*tangent)
867                + (iwb * rb.cross(*tangent)).cross(rb).dot(*tangent);
868            if k_t < 1e-10 { continue; }
869            let vt = rel_vel2.dot(*tangent);
870            let lambda_t = -vt / k_t;
871            let prev_t = contact.cached_tangent[ti];
872            let new_t = (prev_t + lambda_t).clamp(-max_friction, max_friction);
873            let actual_t = new_t - prev_t;
874            contact.cached_tangent[ti] = new_t;
875            let imp_t = *tangent * actual_t;
876            a.apply_impulse_at_point(-imp_t, contact.point);
877            b.apply_impulse_at_point( imp_t, contact.point);
878        }
879    }
880
881    /// Resolve all contacts in a manifold.
882    pub fn resolve_manifold(
883        &self,
884        a: &mut RigidBody, b: &mut RigidBody,
885        manifold: &mut ContactManifold3D,
886        dt: f32,
887    ) {
888        // Warm-start: apply cached impulses
889        if self.warm_start {
890            for contact in &manifold.contacts {
891                let impulse_n = contact.normal * contact.cached_normal * 0.8;
892                a.apply_impulse_at_point(-impulse_n, contact.point);
893                b.apply_impulse_at_point( impulse_n, contact.point);
894            }
895        }
896
897        for _ in 0..self.iterations {
898            for contact in &mut manifold.contacts {
899                self.resolve_contact(a, b, contact, dt);
900            }
901        }
902    }
903}
904
905// ── SleepSystem ───────────────────────────────────────────────────────────────
906
907/// Manages sleeping bodies to skip expensive simulation when at rest.
908pub struct SleepSystem {
909    /// Kinetic energy threshold below which bodies become drowsy.
910    pub energy_threshold: f32,
911    /// Time a body must remain below threshold before sleeping (seconds).
912    pub sleep_delay:      f32,
913    /// Drowsy timers: handle -> seconds_below_threshold.
914    drowsy_timers:        HashMap<RigidBodyHandle, f32>,
915}
916
917impl SleepSystem {
918    pub fn new(energy_threshold: f32, sleep_delay: f32) -> Self {
919        Self { energy_threshold, sleep_delay, drowsy_timers: HashMap::new() }
920    }
921
922    /// Update sleep states for all bodies. Returns handles that just went to sleep.
923    pub fn update(
924        &mut self,
925        bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
926        dt: f32,
927    ) -> Vec<RigidBodyHandle> {
928        let mut newly_slept = Vec::new();
929
930        for (h, body) in bodies.iter_mut() {
931            if body.is_static() { continue; }
932
933            let ke = body.kinetic_energy();
934            if ke < self.energy_threshold {
935                let timer = self.drowsy_timers.entry(*h).or_insert(0.0);
936                *timer += dt;
937                if *timer >= self.sleep_delay {
938                    body.sleep_state = SleepState::Asleep;
939                    body.velocity    = Vec3::ZERO;
940                    body.angular_vel = Vec3::ZERO;
941                    newly_slept.push(*h);
942                } else {
943                    body.sleep_state = SleepState::Drowsy { timer: *timer };
944                }
945            } else {
946                self.drowsy_timers.remove(h);
947                body.sleep_state = SleepState::Awake;
948            }
949        }
950
951        newly_slept
952    }
953
954    /// Wake a body and propagate to all bodies connected by contacts.
955    pub fn wake_body(
956        &mut self,
957        handle: RigidBodyHandle,
958        bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
959        manifolds: &[ContactManifold3D],
960    ) {
961        let mut to_wake = vec![handle];
962        let mut visited = std::collections::HashSet::new();
963
964        while let Some(h) = to_wake.pop() {
965            if visited.contains(&h) { continue; }
966            visited.insert(h);
967
968            if let Some(body) = bodies.get_mut(&h) {
969                body.wake();
970                self.drowsy_timers.remove(&h);
971            }
972
973            // Propagate to connected bodies
974            for manifold in manifolds {
975                if manifold.handle_a == h && !visited.contains(&manifold.handle_b) {
976                    to_wake.push(manifold.handle_b);
977                }
978                if manifold.handle_b == h && !visited.contains(&manifold.handle_a) {
979                    to_wake.push(manifold.handle_a);
980                }
981            }
982        }
983    }
984
985    /// Wake all bodies touching the given AABB.
986    pub fn wake_in_region(
987        &mut self,
988        region_min: Vec3, region_max: Vec3,
989        bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
990        manifolds: &[ContactManifold3D],
991    ) {
992        let to_wake: Vec<RigidBodyHandle> = bodies.iter()
993            .filter(|(_, b)| {
994                let h = b.shape.aabb_half_extents();
995                b.position.x + h.x >= region_min.x && b.position.x - h.x <= region_max.x &&
996                b.position.y + h.y >= region_min.y && b.position.y - h.y <= region_max.y &&
997                b.position.z + h.z >= region_min.z && b.position.z - h.z <= region_max.z
998            })
999            .map(|(h, _)| *h)
1000            .collect();
1001
1002        for h in to_wake {
1003            self.wake_body(h, bodies, manifolds);
1004        }
1005    }
1006
1007    pub fn sleeping_count(&self, bodies: &HashMap<RigidBodyHandle, RigidBody>) -> usize {
1008        bodies.values().filter(|b| b.is_sleeping()).count()
1009    }
1010}
1011
1012// ── PhysicsWorld3D ────────────────────────────────────────────────────────────
1013
1014/// The 3D physics simulation world.
1015pub struct PhysicsWorld3D {
1016    pub bodies:        HashMap<RigidBodyHandle, RigidBody>,
1017    pub gravity:       Vec3,
1018    pub fixed_dt:      f32,
1019    pub substeps:      u32,
1020    accumulator:       f32,
1021    next_id:           u32,
1022    broadphase:        Broadphase,
1023    resolver:          ImpulseResolver,
1024    pub sleep_system:  SleepSystem,
1025    manifolds:         Vec<ContactManifold3D>,
1026}
1027
1028impl PhysicsWorld3D {
1029    pub fn new() -> Self {
1030        Self {
1031            bodies:      HashMap::new(),
1032            gravity:     Vec3::new(0.0, -9.81, 0.0),
1033            fixed_dt:    1.0 / 60.0,
1034            substeps:    2,
1035            accumulator: 0.0,
1036            next_id:     1,
1037            broadphase:  Broadphase::new(),
1038            resolver:    ImpulseResolver::default(),
1039            sleep_system: SleepSystem::new(0.1, 0.5),
1040            manifolds:   Vec::new(),
1041        }
1042    }
1043
1044    /// Add a body, returning its handle.
1045    pub fn add_body(&mut self, body: RigidBody) -> RigidBodyHandle {
1046        let h = RigidBodyHandle(self.next_id);
1047        self.next_id += 1;
1048        self.bodies.insert(h, body);
1049        h
1050    }
1051
1052    /// Remove a body.
1053    pub fn remove_body(&mut self, h: RigidBodyHandle) {
1054        self.bodies.remove(&h);
1055    }
1056
1057    pub fn body(&self, h: RigidBodyHandle) -> Option<&RigidBody> { self.bodies.get(&h) }
1058    pub fn body_mut(&mut self, h: RigidBodyHandle) -> Option<&mut RigidBody> { self.bodies.get_mut(&h) }
1059
1060    pub fn body_count(&self) -> usize { self.bodies.len() }
1061
1062    /// Cast a ray, returning the closest hit.
1063    pub fn raycast(&self, ray: &Ray) -> Option<RayHit> {
1064        let mut best: Option<RayHit> = None;
1065        for (h, body) in &self.bodies {
1066            if let Some(t) = RayCast::ray_vs_body(ray, body) {
1067                let is_better = best.as_ref().map(|b| t < b.t).unwrap_or(true);
1068                if is_better {
1069                    let point  = ray.at(t);
1070                    let normal = (point - body.position).normalize_or_zero();
1071                    best = Some(RayHit { handle: *h, t, point, normal });
1072                }
1073            }
1074        }
1075        best
1076    }
1077
1078    /// Step the simulation by `dt` seconds.
1079    pub fn step(&mut self, dt: f32) {
1080        self.accumulator += dt;
1081        let fixed = self.fixed_dt;
1082        while self.accumulator >= fixed {
1083            let sub_dt = fixed / self.substeps as f32;
1084            for _ in 0..self.substeps {
1085                self.fixed_step(sub_dt);
1086            }
1087            self.accumulator -= fixed;
1088        }
1089    }
1090
1091    fn fixed_step(&mut self, dt: f32) {
1092        let gravity = self.gravity;
1093
1094        // Integrate all bodies
1095        for body in self.bodies.values_mut() {
1096            body.integrate(dt, gravity);
1097        }
1098
1099        // Broadphase
1100        self.broadphase.rebuild(&self.bodies);
1101        let pairs = self.broadphase.overlapping_pairs();
1102
1103        // Narrow phase + resolve
1104        self.manifolds.clear();
1105        for (ha, hb) in pairs {
1106            // Skip both static
1107            let (a_static, b_static) = {
1108                let a = self.bodies.get(&ha);
1109                let b = self.bodies.get(&hb);
1110                (a.map(|b| b.is_static()).unwrap_or(true), b.map(|b| b.is_static()).unwrap_or(true))
1111            };
1112            if a_static && b_static { continue; }
1113
1114            // Collision group check
1115            let (a_group, a_mask, b_group, b_mask) = {
1116                let a = self.bodies.get(&ha);
1117                let b = self.bodies.get(&hb);
1118                (
1119                    a.map(|b| b.collision_group).unwrap_or(0),
1120                    a.map(|b| b.collision_mask).unwrap_or(0),
1121                    b.map(|b| b.collision_group).unwrap_or(0),
1122                    b.map(|b| b.collision_mask).unwrap_or(0),
1123                )
1124            };
1125            if a_group & b_mask == 0 && b_group & a_mask == 0 { continue; }
1126
1127            let body_a_copy = self.bodies.get(&ha).cloned();
1128            let body_b_copy = self.bodies.get(&hb).cloned();
1129
1130            if let (Some(ba), Some(bb)) = (body_a_copy, body_b_copy) {
1131                if let Some(mut manifold) = CollisionDetector::detect(&ba, ha, &bb, hb) {
1132                    // Wake sleeping bodies
1133                    if ba.is_sleeping() || bb.is_sleeping() {
1134                        if let Some(b) = self.bodies.get_mut(&ha) { b.wake(); }
1135                        if let Some(b) = self.bodies.get_mut(&hb) { b.wake(); }
1136                    }
1137
1138                    // Resolve impulses
1139                    let body_a = self.bodies.get_mut(&ha).unwrap() as *mut RigidBody;
1140                    let body_b = self.bodies.get_mut(&hb).unwrap() as *mut RigidBody;
1141                    // Safety: ha != hb (different handles in same HashMap), so these are distinct.
1142                    unsafe {
1143                        self.resolver.resolve_manifold(&mut *body_a, &mut *body_b, &mut manifold, dt);
1144                    }
1145
1146                    self.manifolds.push(manifold);
1147                }
1148            }
1149        }
1150
1151        // Update sleep system
1152        self.sleep_system.update(&mut self.bodies, dt);
1153    }
1154
1155    pub fn manifolds(&self) -> &[ContactManifold3D] { &self.manifolds }
1156
1157    pub fn sleeping_count(&self) -> usize {
1158        self.sleep_system.sleeping_count(&self.bodies)
1159    }
1160
1161    /// Apply a global force field (e.g. wind) to all non-sleeping bodies.
1162    pub fn apply_global_force(&mut self, force: Vec3) {
1163        for body in self.bodies.values_mut() {
1164            if !body.is_static() && !body.is_sleeping() {
1165                body.apply_force(force);
1166            }
1167        }
1168    }
1169}
1170
1171impl Default for PhysicsWorld3D {
1172    fn default() -> Self { Self::new() }
1173}
1174
1175// ── Legacy 2D types (kept for compatibility) ──────────────────────────────────
1176
1177#[derive(Debug, Clone, Copy, PartialEq)]
1178pub enum JointType {
1179    Distance, Revolute, Prismatic, Fixed, Spring, BallSocket, Weld, Pulley, Gear,
1180}
1181
1182#[derive(Debug, Clone, Copy)]
1183pub struct JointAnchor {
1184    pub local_a: Vec2,
1185    pub local_b: Vec2,
1186}
1187
1188impl JointAnchor {
1189    pub fn at_origins() -> Self { Self { local_a: Vec2::ZERO, local_b: Vec2::ZERO } }
1190    pub fn new(local_a: Vec2, local_b: Vec2) -> Self { Self { local_a, local_b } }
1191}
1192
1193#[derive(Debug, Clone, Copy)]
1194pub struct JointLimits {
1195    pub min: f32,
1196    pub max: f32,
1197    pub motor_speed: f32,
1198    pub max_motor_force: f32,
1199    pub motor_enabled: bool,
1200}
1201
1202impl JointLimits {
1203    pub fn none() -> Self {
1204        Self { min: f32::NEG_INFINITY, max: f32::INFINITY, motor_speed: 0.0, max_motor_force: 0.0, motor_enabled: false }
1205    }
1206
1207    pub fn range(min: f32, max: f32) -> Self {
1208        Self { min, max, motor_speed: 0.0, max_motor_force: 0.0, motor_enabled: false }
1209    }
1210
1211    pub fn with_motor(mut self, speed: f32, max_force: f32) -> Self {
1212        self.motor_speed = speed; self.max_motor_force = max_force; self.motor_enabled = true; self
1213    }
1214
1215    pub fn clamp(&self, value: f32) -> f32 { value.clamp(self.min, self.max) }
1216}
1217
1218#[derive(Debug, Clone)]
1219pub struct Joint {
1220    pub id:            u32,
1221    pub kind:          JointType,
1222    pub body_a:        u32,
1223    pub body_b:        Option<u32>,
1224    pub anchor:        JointAnchor,
1225    pub limits:        JointLimits,
1226    pub stiffness:     f32,
1227    pub damping:       f32,
1228    pub rest_length:   f32,
1229    pub frequency:     f32,
1230    pub collide_connected: bool,
1231    pub break_force:   f32,
1232    pub broken:        bool,
1233    pub gear_ratio:    f32,
1234    pub pulley_ratio:  f32,
1235    pub pulley_anchor_a: Vec2,
1236    pub pulley_anchor_b: Vec2,
1237}
1238
1239impl Joint {
1240    pub fn distance(id: u32, body_a: u32, body_b: u32, local_a: Vec2, local_b: Vec2, length: f32) -> Self {
1241        Self {
1242            id, kind: JointType::Distance, body_a, body_b: Some(body_b),
1243            anchor: JointAnchor::new(local_a, local_b),
1244            limits: JointLimits::range(length * 0.9, length * 1.1),
1245            stiffness: 1.0, damping: 0.1, rest_length: length,
1246            frequency: 10.0, collide_connected: false,
1247            break_force: 0.0, broken: false, gear_ratio: 1.0,
1248            pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1249        }
1250    }
1251
1252    pub fn spring(id: u32, body_a: u32, body_b: u32, local_a: Vec2, local_b: Vec2, rest_len: f32, stiffness: f32, damping: f32) -> Self {
1253        Self {
1254            id, kind: JointType::Spring, body_a, body_b: Some(body_b),
1255            anchor: JointAnchor::new(local_a, local_b),
1256            limits: JointLimits::none(),
1257            stiffness, damping, rest_length: rest_len,
1258            frequency: 5.0, collide_connected: true,
1259            break_force: 0.0, broken: false, gear_ratio: 1.0,
1260            pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1261        }
1262    }
1263
1264    pub fn revolute(id: u32, body_a: u32, body_b: u32, anchor: Vec2) -> Self {
1265        Self {
1266            id, kind: JointType::Revolute, body_a, body_b: Some(body_b),
1267            anchor: JointAnchor { local_a: anchor, local_b: anchor },
1268            limits: JointLimits::none(),
1269            stiffness: 1.0, damping: 0.05, rest_length: 0.0,
1270            frequency: 10.0, collide_connected: false,
1271            break_force: 0.0, broken: false, gear_ratio: 1.0,
1272            pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1273        }
1274    }
1275
1276    pub fn fixed(id: u32, body_a: u32, body_b: u32) -> Self {
1277        Self {
1278            id, kind: JointType::Fixed, body_a, body_b: Some(body_b),
1279            anchor: JointAnchor::at_origins(),
1280            limits: JointLimits::none(),
1281            stiffness: 1.0, damping: 1.0, rest_length: 0.0,
1282            frequency: 60.0, collide_connected: false,
1283            break_force: 0.0, broken: false, gear_ratio: 1.0,
1284            pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1285        }
1286    }
1287
1288    pub fn with_limits(mut self, min: f32, max: f32) -> Self { self.limits = JointLimits::range(min, max); self }
1289    pub fn with_motor(mut self, speed: f32, max_force: f32) -> Self { self.limits = self.limits.with_motor(speed, max_force); self }
1290    pub fn with_break_force(mut self, force: f32) -> Self { self.break_force = force; self }
1291    pub fn collide_connected(mut self) -> Self { self.collide_connected = true; self }
1292    pub fn is_active(&self) -> bool { !self.broken }
1293}
1294
1295#[derive(Debug, Clone, Default)]
1296pub struct JointImpulse {
1297    pub impulse:         Vec2,
1298    pub angular_impulse: f32,
1299    pub lambda:          f32,
1300}
1301
1302/// Positional constraint solver using Sequential Impulses.
1303#[derive(Debug, Clone)]
1304pub struct JointSolver {
1305    pub iterations:    u32,
1306    pub position_slop: f32,
1307    pub baumgarte:     f32,
1308}
1309
1310impl JointSolver {
1311    pub fn new() -> Self { Self { iterations: 10, position_slop: 0.005, baumgarte: 0.2 } }
1312
1313    pub fn solve_distance(
1314        &self,
1315        pos_a: &mut Vec2, vel_a: &mut Vec2, inv_mass_a: f32,
1316        pos_b: &mut Vec2, vel_b: &mut Vec2, inv_mass_b: f32,
1317        rest_len: f32, stiffness: f32, damping: f32, dt: f32,
1318    ) {
1319        let delta = *pos_b - *pos_a;
1320        let dist = delta.length();
1321        if dist < 1e-6 { return; }
1322        let dir = delta / dist;
1323        let stretch = dist - rest_len;
1324        let relative_vel = (*vel_b - *vel_a).dot(dir);
1325        let force_mag = -stiffness * stretch - damping * relative_vel;
1326        let total_inv_mass = inv_mass_a + inv_mass_b;
1327        if total_inv_mass < 1e-6 { return; }
1328        let impulse = dir * (-force_mag * dt / total_inv_mass);
1329        *vel_a -= impulse * inv_mass_a;
1330        *vel_b += impulse * inv_mass_b;
1331    }
1332
1333    pub fn solve_rigid_distance(
1334        &self,
1335        pos_a: &mut Vec2, vel_a: &mut Vec2, inv_mass_a: f32,
1336        pos_b: &mut Vec2, vel_b: &mut Vec2, inv_mass_b: f32,
1337        target_dist: f32, dt: f32,
1338    ) {
1339        for _ in 0..self.iterations {
1340            let delta = *pos_b - *pos_a;
1341            let dist = delta.length();
1342            if dist < 1e-6 { continue; }
1343            let dir = delta / dist;
1344            let error = dist - target_dist;
1345            let total_inv_mass = inv_mass_a + inv_mass_b;
1346            if total_inv_mass < 1e-6 { break; }
1347            let correction = dir * (error * self.baumgarte / (total_inv_mass * dt));
1348            *pos_a += correction * inv_mass_a;
1349            *pos_b -= correction * inv_mass_b;
1350            let vel_along = (*vel_b - *vel_a).dot(dir);
1351            let lambda = -vel_along / total_inv_mass;
1352            *vel_a -= dir * (lambda * inv_mass_a);
1353            *vel_b += dir * (lambda * inv_mass_b);
1354        }
1355    }
1356}
1357
1358impl Default for JointSolver {
1359    fn default() -> Self { Self::new() }
1360}
1361
1362// ── RagdollBone ───────────────────────────────────────────────────────────────
1363
1364#[derive(Debug, Clone)]
1365pub struct RagdollBone {
1366    pub name:        String,
1367    pub body_id:     u32,
1368    pub position:    Vec2,
1369    pub velocity:    Vec2,
1370    pub angle:       f32,
1371    pub angular_vel: f32,
1372    pub mass:        f32,
1373    pub inv_mass:    f32,
1374    pub half_extents: Vec2,
1375    pub damping:     f32,
1376    pub restitution: f32,
1377    pub friction:    f32,
1378}
1379
1380impl RagdollBone {
1381    pub fn new(name: impl Into<String>, id: u32, pos: Vec2, mass: f32, half: Vec2) -> Self {
1382        Self {
1383            name: name.into(), body_id: id, position: pos,
1384            velocity: Vec2::ZERO, angle: 0.0, angular_vel: 0.0,
1385            mass, inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
1386            half_extents: half, damping: 0.1, restitution: 0.3, friction: 0.5,
1387        }
1388    }
1389
1390    pub fn apply_impulse(&mut self, impulse: Vec2, point: Vec2) {
1391        self.velocity += impulse * self.inv_mass;
1392        let r = point - self.position;
1393        let inertia_inv = 1.0 / (self.mass * (self.half_extents.x.powi(2) + self.half_extents.y.powi(2)) / 3.0).max(1e-6);
1394        self.angular_vel += (r.x * impulse.y - r.y * impulse.x) * inertia_inv;
1395    }
1396
1397    pub fn integrate(&mut self, dt: f32, gravity: Vec2) {
1398        if self.inv_mass <= 0.0 { return; }
1399        self.velocity += gravity * dt;
1400        self.velocity *= (1.0 - self.damping * dt).max(0.0);
1401        self.position += self.velocity * dt;
1402        self.angle += self.angular_vel * dt;
1403        self.angular_vel *= (1.0 - self.damping * dt).max(0.0);
1404    }
1405
1406    pub fn world_point(&self, local: Vec2) -> Vec2 {
1407        let (sin, cos) = self.angle.sin_cos();
1408        self.position + Vec2::new(local.x * cos - local.y * sin, local.x * sin + local.y * cos)
1409    }
1410}
1411
1412// ── Ragdoll ───────────────────────────────────────────────────────────────────
1413
1414#[derive(Debug, Clone)]
1415pub struct Ragdoll {
1416    pub bones:   Vec<RagdollBone>,
1417    pub joints:  Vec<Joint>,
1418    pub active:  bool,
1419    pub gravity: Vec2,
1420    solver:      JointSolver,
1421}
1422
1423impl Ragdoll {
1424    pub fn new() -> Self {
1425        Self { bones: Vec::new(), joints: Vec::new(), active: false, gravity: Vec2::new(0.0, -9.81), solver: JointSolver::new() }
1426    }
1427
1428    pub fn add_bone(&mut self, bone: RagdollBone) -> u32 {
1429        let id = self.bones.len() as u32;
1430        self.bones.push(bone);
1431        id
1432    }
1433
1434    pub fn add_joint(&mut self, joint: Joint) { self.joints.push(joint); }
1435
1436    pub fn activate(&mut self, impulse: Vec2, contact_bone: usize) {
1437        self.active = true;
1438        if let Some(bone) = self.bones.get_mut(contact_bone) {
1439            bone.apply_impulse(impulse, bone.position);
1440        }
1441    }
1442
1443    pub fn deactivate(&mut self) {
1444        self.active = false;
1445        for bone in &mut self.bones {
1446            bone.velocity    = Vec2::ZERO;
1447            bone.angular_vel = 0.0;
1448        }
1449    }
1450
1451    pub fn update(&mut self, dt: f32) {
1452        if !self.active { return; }
1453        for bone in &mut self.bones { bone.integrate(dt, self.gravity); }
1454
1455        let joint_snapshot: Vec<Joint> = self.joints.iter().filter(|j| j.is_active()).cloned().collect();
1456        for joint in &joint_snapshot {
1457            let Some(body_b_id) = joint.body_b else { continue };
1458            let a_idx = self.bones.iter().position(|b| b.body_id == joint.body_a);
1459            let b_idx = self.bones.iter().position(|b| b.body_id == body_b_id);
1460            if a_idx.is_none() || b_idx.is_none() { continue; }
1461            let ai = a_idx.unwrap();
1462            let bi = b_idx.unwrap();
1463            match joint.kind {
1464                JointType::Distance | JointType::Spring => {
1465                    let (left, right) = self.bones.split_at_mut(ai.max(bi));
1466                    let (bone_a, bone_b) = if ai < bi { (&mut left[ai], &mut right[0]) } else { (&mut right[0], &mut left[bi]) };
1467                    self.solver.solve_distance(
1468                        &mut bone_a.position, &mut bone_a.velocity, bone_a.inv_mass,
1469                        &mut bone_b.position, &mut bone_b.velocity, bone_b.inv_mass,
1470                        joint.rest_length, joint.stiffness, joint.damping, dt,
1471                    );
1472                }
1473                JointType::Fixed | JointType::Weld => {
1474                    let (left, right) = self.bones.split_at_mut(ai.max(bi));
1475                    let (bone_a, bone_b) = if ai < bi { (&mut left[ai], &mut right[0]) } else { (&mut right[0], &mut left[bi]) };
1476                    self.solver.solve_rigid_distance(
1477                        &mut bone_a.position, &mut bone_a.velocity, bone_a.inv_mass,
1478                        &mut bone_b.position, &mut bone_b.velocity, bone_b.inv_mass,
1479                        joint.rest_length, dt,
1480                    );
1481                }
1482                _ => {}
1483            }
1484        }
1485
1486        for joint in &mut self.joints {
1487            if joint.broken || joint.break_force <= 0.0 { continue; }
1488            let Some(body_b_id) = joint.body_b else { continue };
1489            let a_pos = self.bones.iter().find(|b| b.body_id == joint.body_a).map(|b| b.position);
1490            let b_pos = self.bones.iter().find(|b| b.body_id == body_b_id).map(|b| b.position);
1491            if let (Some(pa), Some(pb)) = (a_pos, b_pos) {
1492                let stretch = ((pb - pa).length() - joint.rest_length).abs();
1493                if stretch * joint.stiffness > joint.break_force {
1494                    joint.broken = true;
1495                }
1496            }
1497        }
1498    }
1499
1500    pub fn bone_by_name(&self, name: &str) -> Option<&RagdollBone> {
1501        self.bones.iter().find(|b| b.name == name)
1502    }
1503
1504    pub fn bone_by_name_mut(&mut self, name: &str) -> Option<&mut RagdollBone> {
1505        self.bones.iter_mut().find(|b| b.name == name)
1506    }
1507
1508    pub fn humanoid(position: Vec2) -> Self {
1509        let mut r = Ragdoll::new();
1510        let (tw, th) = (Vec2::new(0.2, 0.3), Vec2::new(0.15, 0.15));
1511        let (uh, lh) = (Vec2::new(0.08, 0.25), Vec2::new(0.07, 0.22));
1512        let (fh, ua, la) = (Vec2::new(0.10, 0.06), Vec2::new(0.07, 0.22), Vec2::new(0.06, 0.18));
1513        r.add_bone(RagdollBone::new("torso",      0,  position + Vec2::new( 0.0,  0.0),  15.0, tw));
1514        r.add_bone(RagdollBone::new("head",       1,  position + Vec2::new( 0.0,  0.55),  5.0, th));
1515        r.add_bone(RagdollBone::new("upper_arm_l",2,  position + Vec2::new(-0.35, 0.2),   3.0, ua));
1516        r.add_bone(RagdollBone::new("lower_arm_l",3,  position + Vec2::new(-0.35,-0.1),   2.0, la));
1517        r.add_bone(RagdollBone::new("upper_arm_r",4,  position + Vec2::new( 0.35, 0.2),   3.0, ua));
1518        r.add_bone(RagdollBone::new("lower_arm_r",5,  position + Vec2::new( 0.35,-0.1),   2.0, la));
1519        r.add_bone(RagdollBone::new("upper_leg_l",6,  position + Vec2::new(-0.12,-0.5),   5.0, uh));
1520        r.add_bone(RagdollBone::new("lower_leg_l",7,  position + Vec2::new(-0.12,-0.9),   4.0, lh));
1521        r.add_bone(RagdollBone::new("foot_l",     8,  position + Vec2::new(-0.12,-1.2),   1.5, fh));
1522        r.add_bone(RagdollBone::new("upper_leg_r",9,  position + Vec2::new( 0.12,-0.5),   5.0, uh));
1523        r.add_bone(RagdollBone::new("lower_leg_r",10, position + Vec2::new( 0.12,-0.9),   4.0, lh));
1524        r.add_bone(RagdollBone::new("foot_r",     11, position + Vec2::new( 0.12,-1.2),   1.5, fh));
1525        r.add_joint(Joint::revolute(0, 0, 1, Vec2::new(0.0, 0.45)).with_limits(-0.5, 0.5));
1526        r.add_joint(Joint::revolute(1, 0, 2, Vec2::new(-0.25, 0.25)).with_limits(-2.0, 0.5));
1527        r.add_joint(Joint::revolute(2, 2, 3, Vec2::new(-0.35,-0.05)).with_limits(-2.2, 0.0));
1528        r.add_joint(Joint::revolute(3, 0, 4, Vec2::new( 0.25, 0.25)).with_limits(-0.5, 2.0));
1529        r.add_joint(Joint::revolute(4, 4, 5, Vec2::new( 0.35,-0.05)).with_limits(0.0, 2.2));
1530        r.add_joint(Joint::revolute(5, 0, 6, Vec2::new(-0.12,-0.35)).with_limits(-0.5, 1.5));
1531        r.add_joint(Joint::revolute(6, 6, 7, Vec2::new(-0.12,-0.75)).with_limits(0.0, 2.5));
1532        r.add_joint(Joint::revolute(7, 7, 8, Vec2::new(-0.12,-1.12)).with_limits(-0.8, 0.8));
1533        r.add_joint(Joint::revolute(8, 0, 9, Vec2::new( 0.12,-0.35)).with_limits(-0.5, 1.5));
1534        r.add_joint(Joint::revolute(9, 9, 10,Vec2::new( 0.12,-0.75)).with_limits(0.0, 2.5));
1535        r.add_joint(Joint::revolute(10,10,11,Vec2::new( 0.12,-1.12)).with_limits(-0.8, 0.8));
1536        r
1537    }
1538
1539    pub fn center_of_mass(&self) -> Vec2 {
1540        let (sm, sp) = self.bones.iter().fold((0.0f32, Vec2::ZERO), |(tm, tp), b| (tm + b.mass, tp + b.position * b.mass));
1541        if sm > 0.0 { sp / sm } else { Vec2::ZERO }
1542    }
1543
1544    pub fn is_at_rest(&self) -> bool {
1545        const T: f32 = 0.05;
1546        self.bones.iter().all(|b| b.velocity.length() < T && b.angular_vel.abs() < T)
1547    }
1548}
1549
1550impl Default for Ragdoll {
1551    fn default() -> Self { Self::new() }
1552}
1553
1554// ── CharacterController ────────────────────────────────────────────────────────
1555
1556#[derive(Debug, Clone)]
1557pub struct CharacterController {
1558    pub position:          Vec2,
1559    pub velocity:          Vec2,
1560    pub size:              Vec2,
1561    pub speed:             f32,
1562    pub jump_force:        f32,
1563    pub gravity:           f32,
1564    pub on_ground:         bool,
1565    pub on_slope:          bool,
1566    pub max_slope_angle:   f32,
1567    pub step_height:       f32,
1568    pub coyote_time:       f32,
1569    pub coyote_timer:      f32,
1570    pub jump_buffer:       f32,
1571    pub jump_buffer_timer: f32,
1572    pub wall_stick_time:   f32,
1573    pub wall_stick_timer:  f32,
1574    pub is_wall_sliding:   bool,
1575    pub wall_normal:       Vec2,
1576    pub wall_jump_force:   Vec2,
1577    pub move_input:        f32,
1578    pub jump_requested:    bool,
1579    pub crouch:            bool,
1580    pub dash_velocity:     Vec2,
1581    pub dash_timer:        f32,
1582    pub dash_cooldown:     f32,
1583    pub dash_speed:        f32,
1584    pub dashes_remaining:  u32,
1585    pub max_dashes:        u32,
1586}
1587
1588impl CharacterController {
1589    pub fn new(position: Vec2, size: Vec2) -> Self {
1590        Self {
1591            position, velocity: Vec2::ZERO, size,
1592            speed: 6.0, jump_force: 12.0, gravity: -20.0,
1593            on_ground: false, on_slope: false, max_slope_angle: 0.7,
1594            step_height: 0.25,
1595            coyote_time: 0.15, coyote_timer: 0.0,
1596            jump_buffer: 0.12, jump_buffer_timer: 0.0,
1597            wall_stick_time: 0.3, wall_stick_timer: 0.0,
1598            is_wall_sliding: false, wall_normal: Vec2::ZERO, wall_jump_force: Vec2::new(6.0, 10.0),
1599            move_input: 0.0, jump_requested: false, crouch: false,
1600            dash_velocity: Vec2::ZERO, dash_timer: 0.0, dash_cooldown: 0.8,
1601            dash_speed: 15.0, dashes_remaining: 2, max_dashes: 2,
1602        }
1603    }
1604
1605    pub fn move_input(&mut self, x: f32) { self.move_input = x.clamp(-1.0, 1.0); }
1606    pub fn request_jump(&mut self) { self.jump_requested = true; self.jump_buffer_timer = self.jump_buffer; }
1607
1608    pub fn request_dash(&mut self) {
1609        if self.dashes_remaining > 0 && self.dash_timer <= 0.0 {
1610            self.dash_velocity = Vec2::new(self.move_input * self.dash_speed, 0.0);
1611            if self.move_input == 0.0 { self.dash_velocity.x = self.dash_speed; }
1612            self.dash_timer = 0.2; self.dash_cooldown = 0.8;
1613            self.dashes_remaining -= 1;
1614        }
1615    }
1616
1617    pub fn update(&mut self, dt: f32, is_grounded: bool, slope_normal: Option<Vec2>) {
1618        if self.on_ground && !is_grounded { self.coyote_timer = self.coyote_time; }
1619        self.on_ground = is_grounded;
1620        if self.coyote_timer > 0.0 { self.coyote_timer -= dt; }
1621        if self.jump_buffer_timer > 0.0 { self.jump_buffer_timer -= dt; }
1622        if self.dash_cooldown > 0.0 { self.dash_cooldown -= dt; }
1623        if self.dash_timer > 0.0 { self.dash_timer -= dt; self.velocity.x = self.dash_velocity.x; }
1624        if is_grounded { self.dashes_remaining = self.max_dashes; }
1625
1626        if !is_grounded {
1627            let grav = if self.is_wall_sliding { self.gravity * 0.3 } else { self.gravity };
1628            self.velocity.y += grav * dt;
1629        } else if self.velocity.y < 0.0 {
1630            self.velocity.y = 0.0;
1631        }
1632
1633        if self.dash_timer <= 0.0 {
1634            let target = self.move_input * self.speed * if self.crouch { 0.5 } else { 1.0 };
1635            let accel = if is_grounded { 20.0 } else { 8.0 };
1636            self.velocity.x += (target - self.velocity.x) * (accel * dt).min(1.0);
1637        }
1638
1639        let can_jump  = is_grounded || self.coyote_timer > 0.0;
1640        let wants_jump = self.jump_requested || self.jump_buffer_timer > 0.0;
1641        if can_jump && wants_jump {
1642            self.velocity.y = self.jump_force;
1643            self.coyote_timer = 0.0; self.jump_buffer_timer = 0.0; self.jump_requested = false;
1644        } else if !is_grounded && self.is_wall_sliding && wants_jump {
1645            self.velocity = self.wall_normal * self.wall_jump_force.x + Vec2::Y * self.wall_jump_force.y;
1646            self.is_wall_sliding = false; self.jump_buffer_timer = 0.0; self.jump_requested = false;
1647        }
1648        self.jump_requested = false;
1649
1650        if let Some(normal) = slope_normal {
1651            let slope_angle = normal.dot(Vec2::Y).acos();
1652            self.on_slope = slope_angle > 0.1;
1653            if self.on_slope && is_grounded && self.dash_timer <= 0.0 {
1654                let right = Vec2::new(normal.y, -normal.x);
1655                self.velocity = right * self.velocity.dot(right);
1656            }
1657        }
1658        self.position += self.velocity * dt;
1659    }
1660
1661    pub fn is_moving(&self) -> bool { self.velocity.length() > 0.1 }
1662    pub fn is_falling(&self) -> bool { self.velocity.y < -0.5 && !self.on_ground }
1663    pub fn is_jumping(&self) -> bool { self.velocity.y > 0.5 }
1664    pub fn facing_right(&self) -> bool { self.velocity.x >= 0.0 }
1665    pub fn aabb_min(&self) -> Vec2 { self.position - self.size }
1666    pub fn aabb_max(&self) -> Vec2 { self.position + self.size }
1667}
1668
1669// ── Tests ─────────────────────────────────────────────────────────────────────
1670
1671#[cfg(test)]
1672mod tests {
1673    use super::*;
1674
1675    #[test]
1676    fn rigid_body_falls_under_gravity() {
1677        let mut world = PhysicsWorld3D::new();
1678        let h = world.add_body(RigidBody::dynamic(Vec3::new(0.0, 10.0, 0.0), 1.0, CollisionShape::sphere(0.5)));
1679        world.step(0.5);
1680        let body = world.body(h).unwrap();
1681        assert!(body.position.y < 10.0, "body should fall: y={}", body.position.y);
1682    }
1683
1684    #[test]
1685    fn static_body_does_not_move() {
1686        let mut world = PhysicsWorld3D::new();
1687        let h = world.add_body(RigidBody::static_body(Vec3::ZERO, CollisionShape::sphere(1.0)));
1688        world.step(1.0);
1689        assert_eq!(world.body(h).unwrap().position, Vec3::ZERO);
1690    }
1691
1692    #[test]
1693    fn sphere_sphere_collision_detected() {
1694        let m = CollisionDetector::sphere_sphere(
1695            Vec3::ZERO, 1.0, RigidBodyHandle(1),
1696            Vec3::new(1.5, 0.0, 0.0), 1.0, RigidBodyHandle(2),
1697        );
1698        assert!(m.is_some(), "spheres should overlap");
1699        let m = CollisionDetector::sphere_sphere(
1700            Vec3::ZERO, 1.0, RigidBodyHandle(1),
1701            Vec3::new(3.0, 0.0, 0.0), 1.0, RigidBodyHandle(2),
1702        );
1703        assert!(m.is_none(), "spheres should not overlap");
1704    }
1705
1706    #[test]
1707    fn sphere_box_collision() {
1708        let m = CollisionDetector::sphere_box(
1709            Vec3::new(0.0, 1.4, 0.0), 0.6, RigidBodyHandle(1),
1710            Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(2),
1711        );
1712        assert!(m.is_some(), "sphere should touch box");
1713    }
1714
1715    #[test]
1716    fn box_box_collision_sat() {
1717        let m = CollisionDetector::box_box(
1718            Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(1),
1719            Vec3::new(1.5, 0.0, 0.0), Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(2),
1720        );
1721        assert!(m.is_some(), "boxes should overlap");
1722    }
1723
1724    #[test]
1725    fn capsule_capsule_collision() {
1726        let m = CollisionDetector::capsule_capsule(
1727            Vec3::ZERO, Vec3::Y, 0.5, 1.0, RigidBodyHandle(1),
1728            Vec3::new(0.8, 0.0, 0.0), Vec3::Y, 0.5, 1.0, RigidBodyHandle(2),
1729        );
1730        assert!(m.is_some(), "capsules should overlap");
1731    }
1732
1733    #[test]
1734    fn broadphase_finds_overlapping_pairs() {
1735        let mut bp = Broadphase::new();
1736        let mut bodies = HashMap::new();
1737        bodies.insert(RigidBodyHandle(1), RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(1.0)));
1738        bodies.insert(RigidBodyHandle(2), RigidBody::dynamic(Vec3::new(1.5, 0.0, 0.0), 1.0, CollisionShape::sphere(1.0)));
1739        bodies.insert(RigidBodyHandle(3), RigidBody::dynamic(Vec3::new(100.0, 0.0, 0.0), 1.0, CollisionShape::sphere(1.0)));
1740        bp.rebuild(&bodies);
1741        let pairs = bp.overlapping_pairs();
1742        assert!(!pairs.is_empty(), "should find at least one overlapping pair");
1743        let has_far = pairs.iter().any(|(a, b)| *a == RigidBodyHandle(3) || *b == RigidBodyHandle(3));
1744        assert!(!has_far, "far body should not be in any pair");
1745    }
1746
1747    #[test]
1748    fn raycast_hits_sphere() {
1749        let mut world = PhysicsWorld3D::new();
1750        let h = world.add_body(RigidBody::dynamic(Vec3::new(0.0, 0.0, 5.0), 1.0, CollisionShape::sphere(1.0)));
1751        let ray = Ray::new(Vec3::ZERO, Vec3::Z);
1752        let hit = world.raycast(&ray);
1753        assert!(hit.is_some(), "ray should hit sphere");
1754        assert_eq!(hit.unwrap().handle, h);
1755    }
1756
1757    #[test]
1758    fn raycast_misses_sphere() {
1759        let mut world = PhysicsWorld3D::new();
1760        world.add_body(RigidBody::dynamic(Vec3::new(0.0, 5.0, 0.0), 1.0, CollisionShape::sphere(0.5)));
1761        let ray = Ray::new(Vec3::ZERO, Vec3::Z);
1762        let hit = world.raycast(&ray);
1763        assert!(hit.is_none(), "ray going Z should miss sphere at Y=5");
1764    }
1765
1766    #[test]
1767    fn sleep_system_sleeps_body() {
1768        let mut bodies = HashMap::new();
1769        let h = RigidBodyHandle(1);
1770        bodies.insert(h, RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(0.5)));
1771        let mut sys = SleepSystem::new(10.0, 0.1); // high threshold so zero-velocity body sleeps quickly
1772        for _ in 0..10 {
1773            sys.update(&mut bodies, 0.05);
1774        }
1775        assert!(bodies[&h].is_sleeping(), "body should have slept");
1776    }
1777
1778    #[test]
1779    fn sleep_wake_propagation() {
1780        let mut bodies = HashMap::new();
1781        let h1 = RigidBodyHandle(1);
1782        let h2 = RigidBodyHandle(2);
1783        let mut b1 = RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(0.5));
1784        b1.sleep_state = SleepState::Asleep;
1785        let mut b2 = RigidBody::dynamic(Vec3::new(0.5, 0.0, 0.0), 1.0, CollisionShape::sphere(0.5));
1786        b2.sleep_state = SleepState::Asleep;
1787        bodies.insert(h1, b1);
1788        bodies.insert(h2, b2);
1789        let manifolds = vec![ContactManifold3D { handle_a: h1, handle_b: h2, contacts: vec![] }];
1790        let mut sys = SleepSystem::new(0.01, 0.5);
1791        sys.wake_body(h1, &mut bodies, &manifolds);
1792        assert!(!bodies[&h1].is_sleeping(), "h1 should be awake");
1793        assert!(!bodies[&h2].is_sleeping(), "h2 should have been woken by propagation");
1794    }
1795
1796    #[test]
1797    fn joint_spring_velocity_change() {
1798        let solver = JointSolver::new();
1799        let mut pa = Vec2::ZERO;
1800        let mut va = Vec2::ZERO;
1801        let mut pb = Vec2::new(5.0, 0.0);
1802        let mut vb = Vec2::ZERO;
1803        solver.solve_distance(&mut pa, &mut va, 1.0, &mut pb, &mut vb, 1.0, 2.0, 100.0, 0.5, 0.016);
1804        assert!(va.length() > 0.0 || vb.length() > 0.0, "velocities should change");
1805    }
1806
1807    #[test]
1808    fn ragdoll_humanoid_bone_count() {
1809        let r = Ragdoll::humanoid(Vec2::new(0.0, 5.0));
1810        assert_eq!(r.bones.len(), 12);
1811        assert!(!r.joints.is_empty());
1812        assert!(r.bone_by_name("torso").is_some());
1813    }
1814
1815    #[test]
1816    fn ragdoll_integrates() {
1817        let mut r = Ragdoll::humanoid(Vec2::ZERO);
1818        r.activate(Vec2::new(5.0, 8.0), 0);
1819        r.update(0.016);
1820        assert!(r.active);
1821    }
1822
1823    #[test]
1824    fn character_controller_moves() {
1825        let mut cc = CharacterController::new(Vec2::ZERO, Vec2::new(0.4, 0.8));
1826        cc.move_input(1.0);
1827        cc.update(0.016, true, None);
1828        assert!(cc.position.x > 0.0, "controller should move right");
1829    }
1830
1831    #[test]
1832    fn character_controller_jumps() {
1833        let mut cc = CharacterController::new(Vec2::ZERO, Vec2::new(0.4, 0.8));
1834        cc.request_jump();
1835        cc.update(0.016, true, None);
1836        assert!(cc.velocity.y > 0.0, "should be jumping");
1837    }
1838
1839    #[test]
1840    fn collision_shape_bounding_radius() {
1841        let s = CollisionShape::sphere(2.0);
1842        assert!((s.bounding_radius() - 2.0).abs() < 1e-5);
1843        let b = CollisionShape::box_shape(Vec3::splat(1.0));
1844        assert!(b.bounding_radius() > 1.0);
1845    }
1846
1847    #[test]
1848    fn rigid_body_kinetic_energy() {
1849        let mut b = RigidBody::dynamic(Vec3::ZERO, 2.0, CollisionShape::sphere(1.0));
1850        b.velocity = Vec3::new(3.0, 0.0, 0.0);
1851        let ke = b.kinetic_energy();
1852        // KE = 0.5 * 2 * 9 = 9
1853        assert!((ke - 9.0).abs() < 0.1, "KE should be ~9, got {ke}");
1854    }
1855}