Skip to main content

symtropy_physics/
world.rs

1// Copyright (C) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Commercial licensing: see COMMERCIAL_LICENSE.md at repository root
4//! Physics world: owns bodies, steps simulation, resolves collisions.
5
6use std::collections::BTreeMap;
7use std::collections::HashMap;
8
9use nalgebra::SVector;
10use symtropy_math::{Capsule, HalfSpace, HyperBox, Point, Sphere};
11
12use crate::body::{BodyHandle, NetId, RigidBody};
13use crate::broadphase;
14use crate::constraint::Constraint;
15use crate::contact::{CollisionEvent, ContactCache, ContactManifold};
16use crate::gjk;
17use crate::integrator;
18use crate::manifold_gen;
19
20/// Quantize a float to Q16.16 fixed-point (range ±32768, resolution 1/65536).
21///
22/// Used by the `deterministic-net` feature to guarantee bit-identical simulation
23/// across heterogeneous hardware for P2P multiplayer and replay files.
24#[cfg(feature = "deterministic-net")]
25#[inline]
26fn quantize_q16_16(v: f64) -> f64 {
27    (v * 65536.0).round() / 65536.0
28}
29
30/// Callback trait for consciousness-physics coupling.
31///
32/// The physics world calls these methods during collision resolution,
33/// allowing consciousness to modulate forces, impulses, and friction
34/// without creating a circular dependency between crates.
35pub trait PhysicsCallback<const D: usize> {
36    /// Modulate a force by the entity's consciousness level.
37    /// Returns the modified force vector.
38    fn modulate_force(&self, body: BodyHandle, force: &SVector<f64, D>) -> SVector<f64, D>;
39
40    /// Modulate a collision impulse at a contact point.
41    /// Returns the modified impulse magnitude.
42    fn modulate_impulse(&self, impulse: f64, contact_point: &SVector<f64, D>) -> f64;
43
44    /// Friction multiplier at a contact point (harmony field effect).
45    /// 1.0 = normal, <1.0 = reduced (resonance), >1.0 = increased (dissonance).
46    fn friction_multiplier(&self, contact_point: &SVector<f64, D>, body: BodyHandle) -> f64;
47
48    /// Called after collision resolution with the collision event.
49    /// This is the primary hook for consciousness to observe and react to
50    /// physical impacts (e.g., calculating trauma, adjusting internal state).
51    fn on_collision(&mut self, event: &CollisionEvent<D>);
52
53    /// Called after each physics step to record energy dissipated.
54    fn record_dissipation(&mut self, energy: f64);
55
56    /// Record mechanical work performed (or energy recovered) by an actuator.
57    /// Positive work = energy consumed (motor driving motion).
58    /// Negative work = energy recovered (regenerative braking).
59    fn record_work(&mut self, body: BodyHandle, work_joules: f64);
60
61    /// Calculates and applies the long-term effect of a collision event
62    /// (e.g., trauma, fatigue, shock) to the entity's internal state.
63    /// This is the primary hook for persistent state change based on impact.
64    fn apply_trauma(&mut self, event: &CollisionEvent<D>);
65}
66
67/// No-op callback for physics-only usage (no consciousness coupling).
68pub struct NoOpCallback;
69
70impl<const D: usize> PhysicsCallback<D> for NoOpCallback {
71    fn modulate_force(&self, _: BodyHandle, force: &SVector<f64, D>) -> SVector<f64, D> {
72        *force
73    }
74    fn modulate_impulse(&self, impulse: f64, _: &SVector<f64, D>) -> f64 {
75        impulse
76    }
77    fn friction_multiplier(&self, _: &SVector<f64, D>, _: BodyHandle) -> f64 {
78        1.0
79    }
80    fn on_collision(&mut self, _: &CollisionEvent<D>) {}
81    fn record_dissipation(&mut self, _: f64) {}
82    fn record_work(&mut self, _: BodyHandle, _: f64) {}
83    fn apply_trauma(&mut self, _: &CollisionEvent<D>) {}
84}
85
86/// The physics world manages all rigid bodies and steps the simulation.
87pub struct PhysicsWorld<const D: usize> {
88    pub bodies: Vec<RigidBody<D>>,
89    pub constraints: Vec<Box<dyn Constraint<D>>>,
90    pub gravity: SVector<f64, D>,
91    /// Contacts from the last step.
92    pub contacts: Vec<ContactManifold<D>>,
93    /// Collision events from the last step (for game logic callbacks).
94    pub collision_events: Vec<CollisionEvent<D>>,
95    /// Sensor overlap events from the last step.
96    pub sensor_events: Vec<crate::contact::SensorEvent>,
97    /// Contact cache for warm-starting (previous frame's impulses).
98    contact_cache: ContactCache<D>,
99    /// Warm-starting from the previous frame (swapped at frame boundaries).
100    prev_cache: ContactCache<D>,
101    /// Position correction iterations per step.
102    pub solver_iterations: usize,
103    /// Sleep velocity threshold.
104    pub sleep_threshold: f64,
105    /// Ticks below sleep threshold before sleeping.
106    pub sleep_ticks: u32,
107    /// Penetration slop (small overlap allowed to prevent jitter).
108    pub slop: f64,
109    /// Baumgarte stabilization factor (TGS Soft position-bias coefficient).
110    ///
111    /// Used as `bias = baumgarte * (depth - slop).max(0) / dt` in TGS Soft.
112    /// Typical values: 0.1–0.4. Default: 0.2.
113    pub baumgarte: f64,
114    /// Constraint compliance (softness). 0.0 = rigid (default). Higher values
115    /// make contacts softer (spring-like). Applied as `α = compliance / dt²`.
116    pub compliance: f64,
117    /// NetId → BodyHandle mapping for cross-machine replay determinism.
118    net_id_map: BTreeMap<NetId, BodyHandle>,
119    /// BodyHandle → Vec index for O(1) body lookup.
120    handle_to_index: HashMap<BodyHandle, usize>,
121    next_handle: usize,
122    /// Cached broadphase data for Static/Kinematic bodies (rebuilt only on add/remove).
123    static_broadphase: broadphase::StaticBroadphase<D>,
124    /// Set to true when a static or kinematic body is added or removed.
125    static_tree_dirty: bool,
126}
127
128impl<const D: usize> Default for PhysicsWorld<D> {
129    fn default() -> Self {
130        Self::new(SVector::zeros())
131    }
132}
133
134impl<const D: usize> PhysicsWorld<D> {
135    /// Create an empty physics world.
136    pub fn new(gravity: SVector<f64, D>) -> Self {
137        Self {
138            bodies: Vec::new(),
139            constraints: Vec::new(),
140            gravity,
141            contacts: Vec::new(),
142            collision_events: Vec::new(),
143            sensor_events: Vec::new(),
144            contact_cache: ContactCache::new(),
145            prev_cache: ContactCache::new(),
146            solver_iterations: 4,
147            slop: 0.01,
148            baumgarte: 0.2,
149            compliance: 0.0,
150            sleep_threshold: 0.5,
151            sleep_ticks: 60, // ~1 second at 64Hz
152            net_id_map: BTreeMap::new(),
153            handle_to_index: HashMap::new(),
154            next_handle: 0,
155            static_broadphase: broadphase::StaticBroadphase::new(),
156            static_tree_dirty: false,
157        }
158    }
159
160    /// Get the total number of bodies in the world.
161    pub fn body_count(&self) -> usize {
162        self.bodies.len()
163    }
164
165    /// Get the total kinetic energy of all dynamic bodies in the world.
166    pub fn total_kinetic_energy(&self) -> f64 {
167        self.bodies
168            .iter()
169            .filter(|b| b.body_type == crate::body::BodyType::Dynamic)
170            .map(|b| b.kinetic_energy())
171            .sum()
172    }
173
174    /// Count how many bodies are currently sleeping.
175    pub fn sleeping_count(&self) -> usize {
176        self.bodies.iter().filter(|b| b.sleeping).count()
177    }
178
179    /// Add a dynamic sphere body and return its handle.
180    pub fn add_sphere(&mut self, position: Point<D>, radius: f64, mass: f64) -> BodyHandle {
181        let handle = self.allocate_handle();
182        let body = RigidBody::dynamic_sphere(handle, position, radius, mass);
183        let idx = self.bodies.len();
184        self.bodies.push(body);
185        self.handle_to_index.insert(handle, idx);
186        handle
187    }
188
189    /// Add a dynamic body with a custom collider.
190    pub fn add_body(&mut self, mut body: RigidBody<D>) -> BodyHandle {
191        use crate::body::BodyType;
192        let handle = self.allocate_handle();
193        body.handle = handle;
194        if body.body_type == BodyType::Static || body.body_type == BodyType::Kinematic {
195            self.static_tree_dirty = true;
196        }
197        let idx = self.bodies.len();
198        self.bodies.push(body);
199        self.handle_to_index.insert(handle, idx);
200        handle
201    }
202
203    /// Add multiple bodies with stable network IDs in deterministic order.
204    ///
205    /// Bodies are sorted by `NetId` before insertion, ensuring the same
206    /// `BodyHandle` assignment regardless of the caller's iteration order.
207    /// Returns an error if any `NetId` is duplicated.
208    pub fn add_bodies_deterministic(
209        &mut self,
210        mut bodies: Vec<(NetId, RigidBody<D>)>,
211    ) -> Result<Vec<BodyHandle>, String> {
212        bodies.sort_by_key(|(id, _)| *id);
213        let mut handles = Vec::with_capacity(bodies.len());
214        for (net_id, mut body) in bodies {
215            if self.net_id_map.contains_key(&net_id) {
216                return Err(format!("duplicate NetId({})", net_id.0));
217            }
218            let handle = self.allocate_handle();
219            body.handle = handle;
220            body.net_id = Some(net_id);
221            self.net_id_map.insert(net_id, handle);
222            let idx = self.bodies.len();
223            self.bodies.push(body);
224            self.handle_to_index.insert(handle, idx);
225            handles.push(handle);
226        }
227        Ok(handles)
228    }
229
230    /// Resolve a stable `NetId` to its `BodyHandle`.
231    pub fn handle_for_net_id(&self, net_id: NetId) -> Option<BodyHandle> {
232        self.net_id_map.get(&net_id).copied()
233    }
234
235    /// Resolve a `BodyHandle` to its stable `NetId`.
236    pub fn net_id_for_handle(&self, handle: BodyHandle) -> Option<NetId> {
237        self.body(handle).and_then(|b| b.net_id)
238    }
239
240    /// Assign a stable network identifier to a body.
241    pub fn set_net_id(&mut self, handle: BodyHandle, net_id: NetId) {
242        let old_id = self.body(handle).and_then(|b| b.net_id);
243
244        if let Some(old) = old_id {
245            self.net_id_map.remove(&old);
246        }
247
248        if let Some(body) = self.body_mut(handle) {
249            body.net_id = Some(net_id);
250            self.net_id_map.insert(net_id, handle);
251        }
252    }
253
254    /// Add a constraint between two bodies.
255    pub fn add_constraint(&mut self, constraint: Box<dyn Constraint<D>>) {
256        self.constraints.push(constraint);
257    }
258
259    /// Get a reference to a body by handle.
260    pub fn body(&self, handle: BodyHandle) -> Option<&RigidBody<D>> {
261        self.handle_to_index
262            .get(&handle)
263            .and_then(|&idx| self.bodies.get(idx))
264    }
265
266    /// Get a mutable reference to a body by handle.
267    pub fn body_mut(&mut self, handle: BodyHandle) -> Option<&mut RigidBody<D>> {
268        self.handle_to_index
269            .get(&handle)
270            .copied()
271            .and_then(|idx| self.bodies.get_mut(idx))
272    }
273
274    /// Step with consciousness-physics callback.
275    ///
276    /// The callback modulates forces, impulses, and friction based on
277    /// consciousness state, closing the consciousness-physics loop.
278    pub fn step_with_callback(&mut self, dt: f64, callback: &mut dyn PhysicsCallback<D>) {
279        self.step_internal(dt, callback);
280    }
281
282    /// Step without consciousness coupling (pure physics).
283    pub fn step(&mut self, dt: f64) {
284        let mut noop = NoOpCallback;
285        self.step_internal(dt, &mut noop);
286    }
287
288    fn step_internal(&mut self, dt: f64, callback: &mut dyn PhysicsCallback<D>) {
289        // 0. Clear events from previous step
290        self.collision_events.clear();
291        self.sensor_events.clear();
292        // Warm-starting: swap caches so prev_cache has last frame's impulses
293        std::mem::swap(&mut self.contact_cache, &mut self.prev_cache);
294        self.contact_cache.begin_frame();
295
296        // 0b. Rebuild static broadphase cache if bodies have been added/removed.
297        if self.static_tree_dirty {
298            self.static_broadphase.rebuild(&self.bodies);
299            self.static_tree_dirty = false;
300        }
301
302        // 1. Integrate all bodies (skip sleeping)
303        #[cfg(feature = "deterministic-net")]
304        for body in &mut self.bodies {
305            // Snap accumulated forces to Q16.16 before integration for
306            // bit-identical simulation across platforms (P2P / replay).
307            if !body.sleeping {
308                for i in 0..D {
309                    body.force_accumulator[i] = quantize_q16_16(body.force_accumulator[i]);
310                }
311            }
312        }
313        for body in &mut self.bodies {
314            if !body.sleeping {
315                body.force_accumulator =
316                    callback.modulate_force(body.handle, &body.force_accumulator);
317                integrator::integrate(body, &self.gravity, dt);
318            }
319        }
320        #[cfg(feature = "deterministic-net")]
321        for body in &mut self.bodies {
322            // Snap positions and velocities after integration.
323            for i in 0..D {
324                body.transform.translation.0[i] = quantize_q16_16(body.transform.translation.0[i]);
325                body.linear_velocity[i] = quantize_q16_16(body.linear_velocity[i]);
326            }
327        }
328
329        // 2. Broadphase: find potentially colliding pairs (incremental static cache)
330        let pairs = broadphase::find_pairs_incremental(&self.bodies, &self.static_broadphase);
331
332        // 3. Narrowphase: GJK for each pair
333        self.contacts.clear();
334        for pair in &pairs {
335            let (idx_a, idx_b) = self.find_body_indices(pair.0, pair.1);
336            if let (Some(a), Some(b)) = (idx_a, idx_b) {
337                let pos_a = self.bodies[a].transform.translation.0;
338                let pos_b = self.bodies[b].transform.translation.0;
339
340                // Fast path: analytical HalfSpace contacts (bypass GJK+EPA)
341                if let Some(manifold) = self.try_halfspace_contact(a, b, pair.0, pair.1) {
342                    if self.bodies[a].is_sensor || self.bodies[b].is_sensor {
343                        let (sensor, other) = if self.bodies[a].is_sensor {
344                            (pair.0, pair.1)
345                        } else {
346                            (pair.1, pair.0)
347                        };
348                        self.sensor_events
349                            .push(crate::contact::SensorEvent { sensor, other });
350                        continue;
351                    }
352                    self.contacts.push(manifold);
353                    continue;
354                }
355
356                // Special path: Mesh collisions (if implemented by the shape)
357                if let Some(manifolds) = self.try_mesh_contact(a, b, pair.0, pair.1) {
358                    for manifold in manifolds {
359                        if self.bodies[a].is_sensor || self.bodies[b].is_sensor {
360                            let (sensor, other) = if self.bodies[a].is_sensor {
361                                (pair.0, pair.1)
362                            } else {
363                                (pair.1, pair.0)
364                            };
365                            self.sensor_events
366                                .push(crate::contact::SensorEvent { sensor, other });
367                            continue;
368                        }
369                        self.contacts.push(manifold);
370                    }
371                    continue;
372                }
373
374                let result = gjk::intersects(
375                    self.bodies[a].collider.as_ref(),
376                    &pos_a,
377                    self.bodies[b].collider.as_ref(),
378                    &pos_b,
379                );
380
381                if result.intersecting {
382                    // Sensor detection: emit event but skip collision resolution
383                    if self.bodies[a].is_sensor || self.bodies[b].is_sensor {
384                        let (sensor, other) = if self.bodies[a].is_sensor {
385                            (pair.0, pair.1)
386                        } else {
387                            (pair.1, pair.0)
388                        };
389                        self.sensor_events
390                            .push(crate::contact::SensorEvent { sensor, other });
391                        continue;
392                    }
393
394                    // EPA for accurate penetration depth and normal
395                    if let Some(epa_result) = crate::epa::penetration(
396                        self.bodies[a].collider.as_ref(),
397                        &pos_a,
398                        self.bodies[b].collider.as_ref(),
399                        &pos_b,
400                        &result.simplex,
401                    ) {
402                        if epa_result.depth > 0.0 {
403                            // Multi-point manifold: contact perturbation for stable stacking
404                            let manifold = manifold_gen::generate_contact_manifold(
405                                self.bodies[a].collider.as_ref(),
406                                &pos_a,
407                                self.bodies[b].collider.as_ref(),
408                                &pos_b,
409                                epa_result.normal,
410                                epa_result.depth,
411                                pair.0,
412                                pair.1,
413                            );
414                            self.contacts.push(manifold);
415                        }
416                    }
417                }
418            }
419        }
420
421        // 4. Build islands and skip sleeping ones
422        let islands = crate::island::build_islands(
423            &self.bodies,
424            &self.contacts,
425            &self.constraints,
426            &self.handle_to_index,
427        );
428        let active_contact_indices: Vec<usize> = islands
429            .iter()
430            .filter(|island| !island.sleeping)
431            .flat_map(|island| island.contact_indices.iter().copied())
432            .collect();
433
434        // 5a. Warm-start: apply previous-frame impulse ONCE before the solver loop.
435        //     (The old code accidentally applied warm-starting N times per frame.)
436        for &ci in &active_contact_indices {
437            if ci < self.contacts.len() {
438                self.warm_start_contact(ci);
439            }
440        }
441
442        // 5b. TGS Soft velocity solver: position-correction bias folded into impulse.
443        //     Each iteration accumulates per-point lambda; write updated manifold back.
444        for _ in 0..self.solver_iterations {
445            for &ci in &active_contact_indices {
446                if ci < self.contacts.len() {
447                    let contact = self.contacts[ci].clone();
448                    let updated = self.resolve_contact(contact, dt, callback);
449                    self.contacts[ci] = updated;
450                }
451            }
452        }
453
454        // 5c. After the solve, cache per-manifold total impulse for next frame's warm-start.
455        for &ci in &active_contact_indices {
456            if ci < self.contacts.len() {
457                let c = &self.contacts[ci];
458                let total_lambda: f64 = c.points.iter().map(|p| p.lambda).sum();
459                self.contact_cache
460                    .store(c.body_a, c.body_b, c.point(), total_lambda, 0.0);
461            }
462        }
463
464        // 6. Solve constraints (active islands only)
465        let active_constraint_indices: Vec<usize> = islands
466            .iter()
467            .filter(|island| !island.sleeping)
468            .flat_map(|island| island.constraint_indices.iter().copied())
469            .collect();
470
471        for _ in 0..self.solver_iterations {
472            for &ci in &active_constraint_indices {
473                if ci >= self.constraints.len() {
474                    continue;
475                }
476                let (ha, hb) = self.constraints[ci].bodies();
477                let (idx_a, idx_b) = self.find_body_indices(ha, hb);
478                if let (Some(a), Some(b)) = (idx_a, idx_b) {
479                    if a < b {
480                        let (left, right) = self.bodies.split_at_mut(b);
481                        self.constraints[ci].solve(&mut left[a], &mut right[0], dt);
482                    } else {
483                        let (left, right) = self.bodies.split_at_mut(a);
484                        self.constraints[ci].solve(&mut right[0], &mut left[b], dt);
485                    }
486                }
487            }
488        }
489
490        // 6b. Velocity-level constraint solve (active islands only)
491        for _ in 0..self.solver_iterations {
492            for &ci in &active_constraint_indices {
493                if ci >= self.constraints.len() {
494                    continue;
495                }
496                let (ha, hb) = self.constraints[ci].bodies();
497                let (idx_a, idx_b) = self.find_body_indices(ha, hb);
498                if let (Some(a), Some(b)) = (idx_a, idx_b) {
499                    if a < b {
500                        let (left, right) = self.bodies.split_at_mut(b);
501                        self.constraints[ci].solve_velocity(
502                            &mut left[a],
503                            &mut right[0],
504                            dt,
505                            Some(callback),
506                        );
507                    } else {
508                        let (left, right) = self.bodies.split_at_mut(a);
509                        self.constraints[ci].solve_velocity(
510                            &mut right[0],
511                            &mut left[b],
512                            dt,
513                            Some(callback),
514                        );
515                    }
516                }
517            }
518        }
519
520        // 7. Body sleeping: deactivate near-stationary bodies
521        let threshold = self.sleep_threshold;
522        let ticks = self.sleep_ticks;
523        for body in &mut self.bodies {
524            body.try_sleep(threshold, ticks);
525        }
526
527        // 8. State Decay: Apply natural recovery/decay to all conscious entities.
528        self.decay_state(callback, dt);
529    }
530
531    /// Decay the consciousness state over time.
532    ///
533    /// This simulates natural recovery (Trauma fades, Fatigue dissipates)
534    /// and stress dissipation.
535    fn decay_state(&mut self, _callback: &mut dyn PhysicsCallback<D>, _dt: f64) {
536        // Note: In a real system, we would iterate over all conscious bodies
537        // and update their individual state. Here, we assume the callback
538        // handles the global state update for simplicity.
539
540        // The callback implementation must handle the actual state update.
541        // We pass a dummy event since decay is time-based, not impact-based.
542        let _dummy_event: CollisionEvent<D> = CollisionEvent {
543            body_a: BodyHandle(0),
544            body_b: BodyHandle(0),
545            impulse: 0.0,
546            normal: SVector::zeros(),
547            depth: 0.0,
548        };
549
550        // We call a specialized decay method on the callback trait (if available)
551        // or, for now, we rely on the callback's internal logic to handle time-based decay.
552        // Since we cannot modify the trait signature here, we assume the callback
553        // implements a method to handle time-based decay, or we simply log the intent.
554
555        // For demonstration, we assume the callback has a method to handle time decay.
556        // If the trait were expanded, we would call:
557        // callback.decay_state(dt);
558
559        // Since we cannot modify the trait here, we will leave this as a comment
560        // and assume the callback implementation handles the time decay internally
561        // based on the physics step time (dt).
562    }
563
564    /// Warm-start a single contact from the previous frame's impulse cache.
565    ///
566    /// Called ONCE per contact before the solver loop. Distributes the cached
567    /// total impulse evenly across all contact points and initialises `lambda`.
568    fn warm_start_contact(&mut self, ci: usize) {
569        let contact = self.contacts[ci].clone();
570        let (idx_a, idx_b) = self.find_body_indices(contact.body_a, contact.body_b);
571        let (Some(a), Some(b)) = (idx_a, idx_b) else {
572            return;
573        };
574
575        let primary_pt = contact.primary_point().position;
576        let cached_total = self
577            .prev_cache
578            .lookup(contact.body_a, contact.body_b, &primary_pt)
579            .map(|c| c.normal_impulse * 0.8) // 80% of previous frame
580            .unwrap_or(0.0);
581
582        if cached_total > 1e-15 {
583            let n_pts = contact.points.len().max(1) as f64;
584            let per_pt = cached_total / n_pts;
585
586            // Apply warm-start impulse
587            let impulse = contact.normal * cached_total;
588            integrator::apply_impulse(&mut self.bodies[a], &(-impulse));
589            integrator::apply_impulse(&mut self.bodies[b], &impulse);
590
591            // Seed lambda so TGS Soft starts from warm-start value
592            for pt in &mut self.contacts[ci].points {
593                pt.lambda = per_pt;
594            }
595        }
596    }
597
598    /// Resolve a single contact using TGS Soft (Temporal Gauss-Seidel with compliance).
599    ///
600    /// Replaces Baumgarte stabilisation: position correction is folded into a
601    /// "bias velocity" `bias = baumgarte * (depth - slop).max(0) / dt`, which is
602    /// added to the velocity constraint. Lambda clamping ensures contacts only push
603    /// (never pull), eliminating ghost-acceleration artefacts.
604    ///
605    /// Returns the updated manifold so accumulated lambdas persist across iterations.
606    fn resolve_contact(
607        &mut self,
608        mut contact: ContactManifold<D>,
609        dt: f64,
610        callback: &mut dyn PhysicsCallback<D>,
611    ) -> ContactManifold<D> {
612        let (idx_a, idx_b) = self.find_body_indices(contact.body_a, contact.body_b);
613        let (Some(a), Some(b)) = (idx_a, idx_b) else {
614            return contact;
615        };
616
617        let inv_mass_a = self.bodies[a].inv_mass;
618        let inv_mass_b = self.bodies[b].inv_mass;
619        let total_inv_mass = inv_mass_a + inv_mass_b;
620        if total_inv_mass < 1e-15 {
621            return contact;
622        }
623
624        // Compliance: α = compliance / dt² (adds softness to the constraint)
625        let alpha = self.compliance / (dt * dt).max(1e-20);
626        let safe_dt = dt.max(1e-10);
627
628        let baumgarte = self.baumgarte;
629        let slop = self.slop;
630
631        // ─── TGS Soft: per-point velocity+position constraint ───
632        let mut total_normal_impulse = 0.0_f64;
633
634        for pt in &mut contact.points {
635            let v_rel_n = {
636                let va = self.bodies[a].linear_velocity;
637                let vb = self.bodies[b].linear_velocity;
638                (vb - va).dot(&contact.normal)
639            };
640
641            // Position-correction bias (replaces Baumgarte teleport)
642            let bias = (pt.depth - slop).max(0.0) * baumgarte / safe_dt;
643
644            // TGS Soft impulse: Δλ = (-v_rel·n + bias) / (w_a + w_b + α)
645            let denom = total_inv_mass + alpha;
646            let delta_lambda = (-v_rel_n + bias) / denom;
647
648            // Clamp: accumulated normal impulse must stay ≥ 0 (no pulling)
649            let new_lambda = (pt.lambda + delta_lambda).max(0.0);
650            let actual_delta = new_lambda - pt.lambda;
651            pt.lambda = new_lambda;
652
653            if actual_delta.abs() > 1e-15 {
654                // ═══ CONSCIOUSNESS MODULATION: sanctuary + harmony fields ═══
655                let modulated_delta = callback.modulate_impulse(actual_delta, &pt.position);
656
657                let impulse = contact.normal * modulated_delta;
658                integrator::apply_impulse(&mut self.bodies[a], &(-impulse));
659                integrator::apply_impulse(&mut self.bodies[b], &impulse);
660                total_normal_impulse += modulated_delta.abs();
661            }
662        }
663
664        // ─── Coulomb friction (once per manifold, based on total normal impulse) ───
665        if total_normal_impulse > 1e-15 {
666            let primary_pt = contact.point();
667            let v_rel = self.bodies[b].linear_velocity - self.bodies[a].linear_velocity;
668            let v_n = contact.normal * v_rel.dot(&contact.normal);
669            let v_t = v_rel - v_n;
670            let v_t_mag = v_t.norm();
671
672            if v_t_mag > 1e-10 {
673                let tangent = v_t / v_t_mag;
674                let mut mu = (self.bodies[a].friction * self.bodies[b].friction).sqrt();
675
676                // ═══ HARMONY FIELD FRICTION MODULATION ═══
677                mu *= callback.friction_multiplier(&primary_pt, contact.body_a);
678
679                let j_t_desired = -v_t_mag / total_inv_mass;
680                let j_t = j_t_desired.clamp(-mu * total_normal_impulse, mu * total_normal_impulse);
681                let friction_impulse = tangent * j_t;
682                integrator::apply_impulse(&mut self.bodies[a], &(-friction_impulse));
683                integrator::apply_impulse(&mut self.bodies[b], &friction_impulse);
684
685                // Record friction dissipation for consciousness energy budget
686                callback.record_dissipation(j_t.abs() * 0.1);
687            }
688
689            // ─── Emit collision event ───
690            let event = CollisionEvent {
691                body_a: contact.body_a,
692                body_b: contact.body_b,
693                impulse: total_normal_impulse,
694                normal: contact.normal,
695                depth: contact.depth(),
696            };
697
698            // ═══ CONSCIOUSNESS FEEDBACK: collision → prediction error ═══
699            // 1. Call the general on_collision hook (for immediate reaction)
700            callback.on_collision(&event);
701            // 2. Call the dedicated trauma/state update hook (for persistent state)
702            callback.apply_trauma(&event);
703        }
704
705        contact
706    }
707
708    #[inline]
709    fn allocate_handle(&mut self) -> BodyHandle {
710        let handle = BodyHandle(self.next_handle);
711        self.next_handle += 1;
712        handle
713    }
714
715    #[inline]
716    fn find_body_indices(
717        &self,
718        body_a: BodyHandle,
719        body_b: BodyHandle,
720    ) -> (Option<usize>, Option<usize>) {
721        (
722            self.handle_to_index.get(&body_a).copied(),
723            self.handle_to_index.get(&body_b).copied(),
724        )
725    }
726
727    fn try_halfspace_contact(
728        &self,
729        idx_a: usize,
730        idx_b: usize,
731        handle_a: BodyHandle,
732        handle_b: BodyHandle,
733    ) -> Option<ContactManifold<D>> {
734        let body_a = &self.bodies[idx_a];
735        let body_b = &self.bodies[idx_b];
736
737        if let Some(plane) = body_a.collider.as_any().downcast_ref::<HalfSpace<D>>() {
738            return self.contact_against_halfspace(plane, body_b, handle_a, handle_b, true);
739        }
740        if let Some(plane) = body_b.collider.as_any().downcast_ref::<HalfSpace<D>>() {
741            return self.contact_against_halfspace(plane, body_a, handle_a, handle_b, false);
742        }
743
744        None
745    }
746
747    fn try_mesh_contact(
748        &self,
749        idx_a: usize,
750        idx_b: usize,
751        handle_a: BodyHandle,
752        handle_b: BodyHandle,
753    ) -> Option<Vec<ContactManifold<D>>> {
754        use crate::manifold_gen::MeshColliderMetadata;
755        let body_a = &self.bodies[idx_a];
756        let body_b = &self.bodies[idx_b];
757
758        if let Some(mesh) = body_a
759            .collider
760            .as_any()
761            .downcast_ref::<&dyn MeshColliderMetadata<D>>()
762        {
763            return Some(mesh.generate_mesh_contacts(
764                handle_a,
765                &body_a.transform.translation.0,
766                body_b.collider.as_ref(),
767                handle_b,
768                &body_b.transform.translation.0,
769            ));
770        }
771        if let Some(mesh) = body_b
772            .collider
773            .as_any()
774            .downcast_ref::<&dyn MeshColliderMetadata<D>>()
775        {
776            return Some(mesh.generate_mesh_contacts(
777                handle_b,
778                &body_b.transform.translation.0,
779                body_a.collider.as_ref(),
780                handle_a,
781                &body_a.transform.translation.0,
782            ));
783        }
784
785        None
786    }
787
788    fn contact_against_halfspace(
789        &self,
790        plane: &HalfSpace<D>,
791        other: &RigidBody<D>,
792        handle_a: BodyHandle,
793        handle_b: BodyHandle,
794        plane_is_a: bool,
795    ) -> Option<ContactManifold<D>> {
796        let other_pos = other.transform.translation.0;
797        let normal = if plane_is_a {
798            plane.normal
799        } else {
800            -plane.normal
801        };
802
803        if let Some(sphere) = other.collider.as_any().downcast_ref::<Sphere<D>>() {
804            let (point, depth) = plane.contact_sphere(&other_pos, sphere.radius)?;
805            return Some(ContactManifold::single(
806                handle_a, handle_b, normal, point, depth,
807            ));
808        }
809
810        if let Some(capsule) = other.collider.as_any().downcast_ref::<Capsule<D>>() {
811            let contacts = plane.contact_capsule(
812                &other_pos,
813                capsule.half_height,
814                capsule.radius,
815                capsule.axis,
816            );
817            return Self::manifold_from_contacts(handle_a, handle_b, normal, contacts);
818        }
819
820        if let Some(hyperbox) = other.collider.as_any().downcast_ref::<HyperBox<D>>() {
821            let contacts = plane.contact_box(&other_pos, &hyperbox.half_extents);
822            return Self::manifold_from_contacts(handle_a, handle_b, normal, contacts);
823        }
824
825        None
826    }
827
828    fn manifold_from_contacts(
829        body_a: BodyHandle,
830        body_b: BodyHandle,
831        normal: SVector<f64, D>,
832        contacts: Vec<(SVector<f64, D>, f64)>,
833    ) -> Option<ContactManifold<D>> {
834        let mut contacts = contacts.into_iter();
835        let (point, depth) = contacts.next()?;
836        let mut manifold = ContactManifold::single(body_a, body_b, normal, point, depth);
837        for (position, depth) in contacts {
838            manifold.points.push(crate::contact::ContactPoint {
839                position,
840                depth,
841                lambda: 0.0,
842            });
843        }
844        Some(manifold)
845    }
846}