Skip to main content

audio_code/
physics.rs

1//! Simple, predictable 3D physics. Available as `ctx.physics`.
2//!
3//! Made for the needs of audio games: walls you bump into, floors you stand on, things that
4//! fall, triggers that tell you when the player enters a room, and ray casts to "look"
5//! around. There are only two shapes (boxes and spheres) and no rotation, which keeps
6//! everything easy to reason about.
7//!
8//! The engine steps the world automatically at a fixed rate (60 Hz by default), so you
9//! only add bodies and react to [`PhysicsEvent`]s.
10//!
11//! ```no_run
12//! # use audio_code::prelude::*;
13//! # fn f(ctx: &mut Context) {
14//! ctx.physics.set_gravity(EARTH_GRAVITY);
15//! ctx.physics.add(Body::fixed(vec3(0.0, 0.0, -0.5)).with_box(100.0, 100.0, 1.0).with_tag("floor"));
16//! let player = ctx.physics.add(Body::dynamic(vec3(0.0, 0.0, 1.0)).with_box(0.6, 0.6, 1.8).with_tag("player"));
17//!
18//! // Later: walk forward and find out if a wall was in the way.
19//! let result = ctx.physics.move_by(player, vec3(0.0, 1.0, 0.0));
20//! if result.blocked {
21//!     ctx.audio.play("bump");
22//! }
23//! # }
24//! ```
25//! See [the coordinate system](crate::math) for axis directions.
26
27use std::collections::{BTreeMap, HashSet};
28
29use glam::Vec3;
30
31use crate::vehicle::Vehicle;
32
33/// Gravity on earth: 9.81 m/s² downwards (towards -z).
34pub const EARTH_GRAVITY: Vec3 = Vec3::new(0.0, 0.0, -9.81);
35
36/// Identifies a body in the [`World`].
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct BodyId(u32);
39
40/// Collision shape. Shapes never rotate.
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum Shape {
43    /// Axis aligned box given by its half size along x, y and z.
44    Box(Vec3),
45    /// Sphere with a radius.
46    Sphere(f32),
47}
48
49impl Shape {
50    fn half_extents(&self) -> Vec3 {
51        match *self {
52            Shape::Box(h) => h,
53            Shape::Sphere(r) => Vec3::splat(r),
54        }
55    }
56}
57
58/// How a body takes part in the simulation.
59///
60/// | Kind | Moved by | Gravity | Blocks others |
61/// |------|----------|---------|---------------|
62/// | `Fixed` | nothing (only [`World::set_position`]) | no | yes |
63/// | `Dynamic` | velocity, gravity, forces, collisions | yes | yes |
64/// | `Kinematic` | your code ([`World::move_by`]) or its velocity, which ignores walls | no | yes |
65/// | `Sensor` | nothing (only [`World::set_position`]) | no | no, it only reports [`PhysicsEvent::Entered`] and [`PhysicsEvent::Left`] |
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum BodyKind {
68    /// Moved by velocity and gravity, pushed out of other solids. The only kind that has
69    /// `on_ground`, can [`jump`](World::jump) and creates collision and landing events.
70    Dynamic,
71    /// Never moves on its own (walls, floors). Other bodies collide with it.
72    Fixed,
73    /// Moved only by your code. Its velocity is applied without collision checks, so it
74    /// passes through walls (right for lifts and platforms); move a kinematic player with
75    /// [`World::move_by`], which stops at walls. Gravity does not pull it. Blocks dynamic
76    /// bodies and pushes them when it moves into them.
77    Kinematic,
78    /// Does not block anything, only reports when bodies enter and leave it.
79    Sensor,
80}
81
82/// A physical object.
83#[derive(Debug, Clone)]
84pub struct Body {
85    /// Center of the body in meters (`+x` east, `+y` north, `+z` up).
86    pub position: Vec3,
87    /// Velocity in meters per second. Ignored for fixed bodies and sensors.
88    pub velocity: Vec3,
89    /// Collision shape.
90    pub shape: Shape,
91    /// How the body is simulated.
92    pub kind: BodyKind,
93    /// Multiplier for world gravity (dynamic bodies only). Default 1.0; 0.0 ignores
94    /// gravity, 0.5 floats, negative values rise.
95    pub gravity_scale: f32,
96    /// How much speed is kept when bouncing off something, 0.0 (stops dead, the default)
97    /// to 1.0 (bounces back at full speed). When two bodies touch, the larger value counts.
98    pub bounciness: f32,
99    /// Fraction of the sideways speed removed per second while standing on the ground.
100    /// Default 8.0 (stops within about a quarter second), 0.0 = ice. Never slows down jumps
101    /// or falls. Dynamic bodies only.
102    pub friction: f32,
103    /// Fraction of the speed removed per second at all times and in every direction (air or
104    /// water resistance). Default 0.0; 0.1–0.5 is light air drag, 2–5 feels like water.
105    /// Dynamic bodies only.
106    pub damping: f32,
107    /// Free text to recognize bodies in events, e.g. `"wall"` or `"coin"`.
108    pub tag: String,
109    /// Free number for your own data, e.g. an index into your entity list.
110    pub user_data: u64,
111    /// Compass direction the body faces in degrees (0 = north, 90 = east). Shapes never
112    /// rotate; this is used by [`World::move_forward`], [`World::turn`] and the listener
113    /// when a body is followed with [`Context::follow`](crate::Context::follow).
114    pub facing: f32,
115    /// How far the body looks up or down in degrees: 0 = level (the default), 90 = straight
116    /// up, -90 = straight down. Like `facing`, it does not rotate the shape. Used by
117    /// [`World::move_forward`] (so a body can fly or swim up and down) and by the listener
118    /// when the body is followed.
119    pub pitch: f32,
120    /// Mass (weight) in kilograms, default 1.0. For dynamic bodies: heavier bodies push
121    /// lighter ones further and lose less speed when they collide, and forces move them
122    /// less. Kinematic bodies push dynamic ones regardless of mass. For vehicles it is the
123    /// empty weight, see [`Body::vehicle`].
124    pub mass: f32,
125    /// Speed limit in meters per second, e.g. a terminal velocity when falling.
126    /// `f32::INFINITY` (the default) means no limit.
127    pub max_speed: f32,
128    /// Whether the body touched the ground (something below it) during the last step.
129    /// Only updated for dynamic bodies.
130    pub on_ground: bool,
131    /// The body this one stands on, if any. Bodies standing on a moving kinematic body
132    /// (a lift, a platform) are carried along.
133    pub ground: Option<BodyId>,
134    /// Makes the body a car or truck (optional, `None` for every other body). The physics
135    /// world then drives it from the vehicle's pedals and steering; see
136    /// [`crate::vehicle`]. Vehicle bodies are kinematic.
137    pub vehicle: Option<Vehicle>,
138    force: Vec3,
139}
140
141impl Body {
142    fn new(kind: BodyKind, position: Vec3) -> Self {
143        Self {
144            position,
145            velocity: Vec3::ZERO,
146            shape: Shape::Box(Vec3::splat(0.5)),
147            kind,
148            gravity_scale: 1.0,
149            bounciness: 0.0,
150            friction: 8.0,
151            damping: 0.0,
152            tag: String::new(),
153            user_data: 0,
154            facing: 0.0,
155            pitch: 0.0,
156            mass: 1.0,
157            max_speed: f32::INFINITY,
158            on_ground: false,
159            ground: None,
160            vehicle: None,
161            force: Vec3::ZERO,
162        }
163    }
164
165    /// A body moved by velocity, gravity, forces and collisions, such as a ball, a crate or
166    /// the player in a platformer. Default shape: 1 meter cube.
167    pub fn dynamic(position: Vec3) -> Self {
168        Self::new(BodyKind::Dynamic, position)
169    }
170
171    /// An immovable body such as a wall or floor.
172    pub fn fixed(position: Vec3) -> Self {
173        Self::new(BodyKind::Fixed, position)
174    }
175
176    /// A body only moved by your code: the player in a top down game (move it with
177    /// [`World::move_by`]), doors, lifts and moving platforms (move them with
178    /// [`World::set_velocity`]). Default shape: 1 meter cube.
179    pub fn kinematic(position: Vec3) -> Self {
180        Self::new(BodyKind::Kinematic, position)
181    }
182
183    /// An invisible trigger area that does not block anything. Read
184    /// [`World::entered`] / [`World::entered_tag`] to learn when a body walks in.
185    pub fn sensor(position: Vec3) -> Self {
186        Self::new(BodyKind::Sensor, position)
187    }
188
189    /// A car at `position`, tagged `"car"`: a kinematic body with [`Vehicle::car`], a mass
190    /// of 1300 kg and a sphere of 1 m radius. Shapes never rotate, so a sphere fits a car
191    /// facing any direction. Change the settings with [`Body::with_vehicle`] and
192    /// [`Body::with_mass`].
193    pub fn car(position: Vec3) -> Self {
194        Self::kinematic(position).with_sphere(1.0).with_mass(1300.0).with_tag("car").with_vehicle(Vehicle::car())
195    }
196
197    /// A truck at `position`, tagged `"truck"`: a kinematic body with [`Vehicle::truck`],
198    /// an empty mass of 12 t and a sphere of 1.5 m radius.
199    pub fn truck(position: Vec3) -> Self {
200        Self::kinematic(position)
201            .with_sphere(1.5)
202            .with_mass(12_000.0)
203            .with_tag("truck")
204            .with_vehicle(Vehicle::truck())
205    }
206
207    /// Makes the body a vehicle (car, truck, bus, ...) and kinematic. The weight is the
208    /// body's [`mass`](Body::mass) plus the vehicle's load.
209    pub fn with_vehicle(mut self, vehicle: Vehicle) -> Self {
210        self.kind = BodyKind::Kinematic;
211        self.vehicle = Some(vehicle);
212        self
213    }
214
215    /// Box shape with full width (x, east-west), depth (y, north-south) and height (z) in
216    /// meters. `with_box(2.0, 2.0, 2.0)` reaches 1 m from the center in every direction.
217    pub fn with_box(mut self, width: f32, depth: f32, height: f32) -> Self {
218        self.shape = Shape::Box(Vec3::new(width, depth, height) * 0.5);
219        self
220    }
221
222    /// Sphere shape with a radius in meters. 0.3–0.5 is a good player size.
223    pub fn with_sphere(mut self, radius: f32) -> Self {
224        self.shape = Shape::Sphere(radius);
225        self
226    }
227
228    /// Sets a tag, free text to recognize the body in events and queries
229    /// (e.g. `"wall"`, `"coin"`). See [`World::tag`] and [`World::entered_tag`].
230    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
231        self.tag = tag.into();
232        self
233    }
234
235    /// Sets the initial velocity in meters per second.
236    pub fn with_velocity(mut self, velocity: Vec3) -> Self {
237        self.velocity = velocity;
238        self
239    }
240
241    /// Sets bounciness: 0.0 stops dead (default), 1.0 bounces back at full speed. A ball
242    /// is about 0.6–0.9, a crate 0.1–0.3. See [`Body::bounciness`].
243    pub fn with_bounciness(mut self, bounciness: f32) -> Self {
244        self.bounciness = bounciness.clamp(0.0, 1.0);
245        self
246    }
247
248    /// Sets ground friction: the fraction of sideways speed removed per second while on the
249    /// ground. Default 8.0 (stops within about 0.3 s), 2.0 slides for about a second, 0.0 is
250    /// ice. See [`Body::friction`].
251    pub fn with_friction(mut self, friction: f32) -> Self {
252        self.friction = friction.max(0.0);
253        self
254    }
255
256    /// Sets air damping: the fraction of speed removed per second, always and in every
257    /// direction. Default 0.0. See [`Body::damping`].
258    pub fn with_damping(mut self, damping: f32) -> Self {
259        self.damping = damping.max(0.0);
260        self
261    }
262
263    /// Sets the gravity multiplier: 1.0 normal (default), 0.5 floats, 0.0 no gravity,
264    /// negative values rise.
265    pub fn with_gravity_scale(mut self, scale: f32) -> Self {
266        self.gravity_scale = scale;
267        self
268    }
269
270    /// Sets the mass in kilograms (default 1.0).
271    pub fn with_mass(mut self, mass: f32) -> Self {
272        self.mass = mass.max(0.001);
273        self
274    }
275
276    /// Sets a speed limit in meters per second (terminal velocity).
277    pub fn with_max_speed(mut self, max_speed: f32) -> Self {
278        self.max_speed = max_speed.max(0.0);
279        self
280    }
281
282    /// Sets the compass direction the body faces in degrees (0 = north, 90 = east). The
283    /// shape does not rotate.
284    pub fn with_facing(mut self, degrees: f32) -> Self {
285        self.facing = crate::math::normalize_angle(degrees);
286        self
287    }
288
289    /// Sets how far the body looks up (positive) or down (negative) in degrees, -90 to 90.
290    pub fn with_pitch(mut self, degrees: f32) -> Self {
291        self.pitch = degrees.clamp(-90.0, 90.0);
292        self
293    }
294
295    /// Sets a free number for your own data, e.g. an index into your enemy list.
296    pub fn with_user_data(mut self, data: u64) -> Self {
297        self.user_data = data;
298        self
299    }
300
301    fn min(&self) -> Vec3 {
302        self.position - self.shape.half_extents()
303    }
304
305    fn max(&self) -> Vec3 {
306        self.position + self.shape.half_extents()
307    }
308
309    fn is_solid(&self) -> bool {
310        self.kind != BodyKind::Sensor
311    }
312
313    /// Whether this body overlaps a point.
314    pub fn contains(&self, point: Vec3) -> bool {
315        match self.shape {
316            Shape::Box(_) => point.cmpge(self.min()).all() && point.cmple(self.max()).all(),
317            Shape::Sphere(r) => point.distance_squared(self.position) <= r * r,
318        }
319    }
320}
321
322/// Something that happened during the physics steps of this frame.
323#[derive(Debug, Clone, PartialEq)]
324pub enum PhysicsEvent {
325    /// A dynamic body started touching another solid body during a physics step.
326    /// [`World::move_by`] does not create this event; use [`MoveResult::hits`] instead.
327    CollisionStarted {
328        /// The dynamic body that moved.
329        a: BodyId,
330        /// The body it hit.
331        b: BodyId,
332        /// Impact speed in meters per second: how fast the two came together along the
333        /// contact direction. Sliding along a wall gives a low value, running into it a high
334        /// one. Good for the volume of impact sounds.
335        speed: f32,
336    },
337    /// Two solid bodies stopped touching.
338    CollisionEnded {
339        /// First body.
340        a: BodyId,
341        /// Second body.
342        b: BodyId,
343    },
344    /// A dynamic body touched the ground after being in the air. Perfect for landing sounds
345    /// and fall damage.
346    Landed {
347        /// The body that landed.
348        body: BodyId,
349        /// What it landed on.
350        on: BodyId,
351        /// Falling speed at impact in meters per second.
352        speed: f32,
353    },
354    /// A solid body entered a sensor.
355    Entered {
356        /// The sensor.
357        sensor: BodyId,
358        /// The body that entered.
359        body: BodyId,
360    },
361    /// A body left a sensor.
362    Left {
363        /// The sensor.
364        sensor: BodyId,
365        /// The body that left.
366        body: BodyId,
367    },
368    /// A vehicle drove into a solid body and lost speed.
369    Crashed {
370        /// The vehicle.
371        vehicle: BodyId,
372        /// What it hit.
373        other: BodyId,
374        /// Speed lost in the crash, in meters per second (36 km/h = 10 m/s).
375        speed: f32,
376    },
377    /// A vehicle changed gear: -1 reverse, 0 neutral (engine stopped), 1 and up forward.
378    GearChanged {
379        /// The vehicle.
380        vehicle: BodyId,
381        /// The new gear.
382        gear: i32,
383    },
384}
385
386impl PhysicsEvent {
387    /// Whether the event involves this body.
388    pub fn involves(&self, id: BodyId) -> bool {
389        match *self {
390            PhysicsEvent::CollisionStarted { a, b, .. } | PhysicsEvent::CollisionEnded { a, b } => a == id || b == id,
391            PhysicsEvent::Landed { body, on, .. } => body == id || on == id,
392            PhysicsEvent::Entered { sensor, body } | PhysicsEvent::Left { sensor, body } => sensor == id || body == id,
393            PhysicsEvent::Crashed { vehicle, other, .. } => vehicle == id || other == id,
394            PhysicsEvent::GearChanged { vehicle, .. } => vehicle == id,
395        }
396    }
397
398    /// For events involving `id`, the other body.
399    pub fn other(&self, id: BodyId) -> Option<BodyId> {
400        let (x, y) = match *self {
401            PhysicsEvent::CollisionStarted { a, b, .. } | PhysicsEvent::CollisionEnded { a, b } => (a, b),
402            PhysicsEvent::Landed { body, on, .. } => (body, on),
403            PhysicsEvent::Entered { sensor, body } | PhysicsEvent::Left { sensor, body } => (sensor, body),
404            PhysicsEvent::Crashed { vehicle, other, .. } => (vehicle, other),
405            PhysicsEvent::GearChanged { .. } => return None,
406        };
407        if x == id {
408            Some(y)
409        } else if y == id {
410            Some(x)
411        } else {
412            None
413        }
414    }
415}
416
417/// Result of [`World::raycast`].
418#[derive(Debug, Clone, Copy, PartialEq)]
419pub struct RayHit {
420    /// The body hit.
421    pub body: BodyId,
422    /// Where the ray hit.
423    pub point: Vec3,
424    /// Distance from the ray origin in meters.
425    pub distance: f32,
426    /// Direction the hit surface faces (length 1), e.g. `-y` for the south side of a wall.
427    pub normal: Vec3,
428}
429
430/// Result of [`World::move_by`].
431#[derive(Debug, Clone, Default, PartialEq)]
432pub struct MoveResult {
433    /// How far the body actually moved, in meters. Shorter than the requested delta when a
434    /// wall was in the way.
435    pub moved: Vec3,
436    /// `true` if something stopped part of the movement. Also `true` when the body slid
437    /// along a wall it walked into at an angle.
438    pub blocked: bool,
439    /// Solid bodies that were touched on the way, in order.
440    pub hits: Vec<BodyId>,
441}
442
443/// The physics world. Reach it through `ctx.physics`.
444#[derive(Debug, Clone)]
445pub struct World {
446    bodies: BTreeMap<BodyId, Body>,
447    next_id: u32,
448    gravity: Vec3,
449    contacts: HashSet<(BodyId, BodyId)>,
450    sensor_overlaps: HashSet<(BodyId, BodyId)>,
451    events: Vec<PhysicsEvent>,
452}
453
454impl Default for World {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460/// Minimum translation to separate two overlapping shapes: (direction to push `a`, depth).
461fn penetration(a: &Body, b: &Body) -> Option<(Vec3, f32)> {
462    match (a.shape, b.shape) {
463        (Shape::Sphere(ra), Shape::Sphere(rb)) => {
464            let d = a.position - b.position;
465            let dist = d.length();
466            let depth = ra + rb - dist;
467            if depth <= 0.0 {
468                return None;
469            }
470            let n = if dist > 1e-6 { d / dist } else { Vec3::Z };
471            Some((n, depth))
472        }
473        (Shape::Sphere(r), Shape::Box(_)) => sphere_box(a.position, r, b),
474        (Shape::Box(_), Shape::Sphere(r)) => sphere_box(b.position, r, a).map(|(n, d)| (-n, d)),
475        (Shape::Box(_), Shape::Box(_)) => {
476            let overlap = a.max().min(b.max()) - a.min().max(b.min());
477            if overlap.x <= 0.0 || overlap.y <= 0.0 || overlap.z <= 0.0 {
478                return None;
479            }
480            let d = a.position - b.position;
481            let sign = |v: f32| if v >= 0.0 { 1.0 } else { -1.0 };
482            if overlap.x < overlap.y && overlap.x < overlap.z {
483                Some((Vec3::X * sign(d.x), overlap.x))
484            } else if overlap.y < overlap.z {
485                Some((Vec3::Y * sign(d.y), overlap.y))
486            } else {
487                Some((Vec3::Z * sign(d.z), overlap.z))
488            }
489        }
490    }
491}
492
493/// Sphere at `center` against a box body: direction to push the sphere and depth.
494fn sphere_box(center: Vec3, radius: f32, b: &Body) -> Option<(Vec3, f32)> {
495    let closest = center.clamp(b.min(), b.max());
496    let d = center - closest;
497    let dist = d.length();
498    if dist > 1e-6 {
499        let depth = radius - dist;
500        return (depth > 0.0).then(|| (d / dist, depth));
501    }
502    // Center inside the box: push out through the nearest face.
503    let to_max = b.max() - center;
504    let to_min = center - b.min();
505    let candidates = [
506        (Vec3::X, to_max.x),
507        (-Vec3::X, to_min.x),
508        (Vec3::Y, to_max.y),
509        (-Vec3::Y, to_min.y),
510        (Vec3::Z, to_max.z),
511        (-Vec3::Z, to_min.z),
512    ];
513    let (n, d) = candidates.into_iter().min_by(|a, b| a.1.total_cmp(&b.1))?;
514    Some((n, d + radius))
515}
516
517fn pair(a: BodyId, b: BodyId) -> (BodyId, BodyId) {
518    if a < b { (a, b) } else { (b, a) }
519}
520
521impl World {
522    /// Creates an empty world without gravity.
523    pub fn new() -> Self {
524        Self {
525            bodies: BTreeMap::new(),
526            next_id: 1,
527            gravity: Vec3::ZERO,
528            contacts: HashSet::new(),
529            sensor_overlaps: HashSet::new(),
530            events: Vec::new(),
531        }
532    }
533
534    /// Sets gravity, e.g. [`EARTH_GRAVITY`]. Default is no gravity, which suits top down
535    /// games. Also available as [`Engine::gravity`](crate::Engine::gravity).
536    pub fn set_gravity(&mut self, gravity: Vec3) {
537        self.gravity = gravity;
538    }
539
540    /// Direction pointing away from gravity (`+z` when there is no gravity).
541    pub fn up(&self) -> Vec3 {
542        (-self.gravity).normalize_or(Vec3::Z)
543    }
544
545    /// Current gravity.
546    pub fn gravity(&self) -> Vec3 {
547        self.gravity
548    }
549
550    /// Adds a body and returns its id.
551    pub fn add(&mut self, body: Body) -> BodyId {
552        let id = BodyId(self.next_id);
553        self.next_id += 1;
554        self.bodies.insert(id, body);
555        id
556    }
557
558    /// Removes a body. Returns it if it existed.
559    pub fn remove(&mut self, id: BodyId) -> Option<Body> {
560        self.contacts.retain(|&(a, b)| a != id && b != id);
561        self.sensor_overlaps.retain(|&(a, b)| a != id && b != id);
562        self.bodies.remove(&id)
563    }
564
565    /// Removes every body.
566    pub fn clear(&mut self) {
567        self.bodies.clear();
568        self.contacts.clear();
569        self.sensor_overlaps.clear();
570        self.events.clear();
571    }
572
573    /// Access a body.
574    pub fn get(&self, id: BodyId) -> Option<&Body> {
575        self.bodies.get(&id)
576    }
577
578    /// Mutable access to a body.
579    pub fn get_mut(&mut self, id: BodyId) -> Option<&mut Body> {
580        self.bodies.get_mut(&id)
581    }
582
583    /// Position of a body (zero if it does not exist).
584    pub fn position(&self, id: BodyId) -> Vec3 {
585        self.get(id).map_or(Vec3::ZERO, |b| b.position)
586    }
587
588    /// Teleports a body without collision checks.
589    pub fn set_position(&mut self, id: BodyId, position: Vec3) {
590        if let Some(b) = self.get_mut(id) {
591            b.position = position;
592        }
593    }
594
595    /// Velocity of a body (zero if it does not exist).
596    pub fn velocity(&self, id: BodyId) -> Vec3 {
597        self.get(id).map_or(Vec3::ZERO, |b| b.velocity)
598    }
599
600    /// Sets the velocity of a body.
601    pub fn set_velocity(&mut self, id: BodyId, velocity: Vec3) {
602        if let Some(b) = self.get_mut(id) {
603            b.velocity = velocity;
604        }
605    }
606
607    /// Adds `impulse` (meters per second) to the velocity at once, regardless of mass,
608    /// e.g. a kick or knockback: `apply_impulse(ball, vec3(0.0, 10.0, 0.0))`.
609    pub fn apply_impulse(&mut self, id: BodyId, impulse: Vec3) {
610        if let Some(b) = self.get_mut(id) {
611            b.velocity += impulse;
612        }
613    }
614
615    /// Pushes a body with a force in newtons during the next physics step, e.g. wind,
616    /// a conveyor belt or a rocket. Each second, a force adds `force / mass` meters per
617    /// second to the velocity, so 20 N speed up a 1 kg body by 20 m/s per second and a
618    /// 10 kg body by 2 m/s per second. Call it every frame for a continuous force.
619    pub fn apply_force(&mut self, id: BodyId, force: Vec3) {
620        if let Some(b) = self.get_mut(id) {
621            b.force += force;
622        }
623    }
624
625    /// Makes a dynamic body standing on the ground jump so that it reaches `height` meters
626    /// above where it stands, taking the current gravity into account. Returns `false` (and
627    /// does nothing) while in the air, without gravity, or for other body kinds.
628    ///
629    /// ```no_run
630    /// # use audio_code::prelude::*;
631    /// # fn f(ctx: &mut Context, player: BodyId) {
632    /// if ctx.input.pressed(Key::Space) && ctx.physics.jump(player, 1.5) {
633    ///     ctx.audio.play("jump");
634    /// }
635    /// # }
636    /// ```
637    pub fn jump(&mut self, id: BodyId, height: f32) -> bool {
638        let (up, gravity) = (self.up(), self.gravity.length());
639        let Some(b) = self.get_mut(id) else {
640            return false;
641        };
642        let g = gravity * b.gravity_scale;
643        if !b.on_ground || g <= 0.0 {
644            return false;
645        }
646        let speed = (2.0 * g * height.max(0.0)).sqrt();
647        b.velocity += up * (speed - b.velocity.dot(up));
648        b.on_ground = false;
649        true
650    }
651
652    /// Whether a dynamic body stands on something (always `false` for other kinds).
653    pub fn on_ground(&self, id: BodyId) -> bool {
654        self.get(id).is_some_and(|b| b.on_ground)
655    }
656
657    /// The tag of a body ("" if it does not exist).
658    pub fn tag(&self, id: BodyId) -> &str {
659        self.get(id).map_or("", |b| b.tag.as_str())
660    }
661
662    /// Iterates over all bodies.
663    pub fn bodies(&self) -> impl Iterator<Item = (BodyId, &Body)> {
664        self.bodies.iter().map(|(id, b)| (*id, b))
665    }
666
667    /// All bodies with a tag.
668    pub fn with_tag<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = BodyId> + 'a {
669        self.bodies.iter().filter(move |(_, b)| b.tag == tag).map(|(id, _)| *id)
670    }
671
672    /// Number of bodies.
673    pub fn len(&self) -> usize {
674        self.bodies.len()
675    }
676
677    /// Whether the world is empty.
678    pub fn is_empty(&self) -> bool {
679        self.bodies.is_empty()
680    }
681
682    /// Events from the physics steps of this frame.
683    pub fn events(&self) -> &[PhysicsEvent] {
684        &self.events
685    }
686
687    pub(crate) fn clear_events(&mut self) {
688        self.events.clear();
689    }
690
691    /// Sensors `body` is currently inside.
692    pub fn sensors_of(&self, body: BodyId) -> Vec<BodyId> {
693        self.sensor_overlaps.iter().filter(|(_, b)| *b == body).map(|(s, _)| *s).collect()
694    }
695
696    /// Whether two bodies overlap right now.
697    pub fn overlaps(&self, a: BodyId, b: BodyId) -> bool {
698        match (self.get(a), self.get(b)) {
699            (Some(x), Some(y)) => penetration(x, y).is_some(),
700            _ => false,
701        }
702    }
703
704    /// Bodies whose shape contains `point`.
705    pub fn at_point(&self, point: Vec3) -> Vec<BodyId> {
706        self.bodies.iter().filter(|(_, b)| b.contains(point)).map(|(id, _)| *id).collect()
707    }
708
709    /// Bodies whose center lies within `radius` of `center`, nearest first.
710    pub fn in_radius(&self, center: Vec3, radius: f32) -> Vec<BodyId> {
711        let mut found: Vec<(f32, BodyId)> = self
712            .bodies
713            .iter()
714            .map(|(id, b)| (b.position.distance(center), *id))
715            .filter(|(d, _)| *d <= radius)
716            .collect();
717        found.sort_by(|a, b| a.0.total_cmp(&b.0));
718        found.into_iter().map(|(_, id)| id).collect()
719    }
720
721    /// Nearest body with the given tag.
722    pub fn nearest_with_tag(&self, center: Vec3, tag: &str) -> Option<BodyId> {
723        self.bodies
724            .iter()
725            .filter(|(_, b)| b.tag == tag)
726            .min_by(|a, b| a.1.position.distance_squared(center).total_cmp(&b.1.position.distance_squared(center)))
727            .map(|(id, _)| *id)
728    }
729
730    /// Sends a straight line from `origin` in `direction` (any length) and returns the first
731    /// solid body hit within `max_distance` meters. `ignore` is skipped: pass the caster
732    /// itself, otherwise the ray hits it first. Sensors are ignored.
733    pub fn raycast(&self, origin: Vec3, direction: Vec3, max_distance: f32, ignore: Option<BodyId>) -> Option<RayHit> {
734        let dir = direction.normalize_or_zero();
735        if dir == Vec3::ZERO {
736            return None;
737        }
738        let mut best: Option<RayHit> = None;
739        for (id, body) in &self.bodies {
740            if Some(*id) == ignore || !body.is_solid() {
741                continue;
742            }
743            let hit = match body.shape {
744                Shape::Box(_) => ray_box(origin, dir, body.min(), body.max()),
745                Shape::Sphere(r) => ray_sphere(origin, dir, body.position, r),
746            };
747            if let Some((t, normal)) = hit
748                && t <= max_distance
749                && best.as_ref().is_none_or(|b| t < b.distance)
750            {
751                best = Some(RayHit { body: *id, point: origin + dir * t, distance: t, normal });
752            }
753        }
754        best
755    }
756
757    /// Moves a body by `delta` meters right now, sliding along and stopping at solid bodies,
758    /// and pushing dynamic bodies out of the way. This is the easiest way to move a player:
759    /// walls block, and you learn what was hit. Sensor events are updated at once;
760    /// [`PhysicsEvent::CollisionStarted`] is not created.
761    pub fn move_by(&mut self, id: BodyId, delta: Vec3) -> MoveResult {
762        let Some(body) = self.bodies.get(&id) else {
763            return MoveResult::default();
764        };
765        let start = body.position;
766        let size = body.shape.half_extents().min_element().max(0.01);
767        let steps = ((delta.length() / size).ceil() as usize).clamp(1, 1000);
768        let step = delta / steps as f32;
769        let mut result = MoveResult::default();
770        for _ in 0..steps {
771            if let Some(b) = self.bodies.get_mut(&id) {
772                b.position += step;
773            }
774            for (other, normal, _) in self.resolve(id) {
775                if !result.hits.contains(&other) {
776                    result.hits.push(other);
777                }
778                // Stop moving into the obstacle; slide along it.
779                if step.dot(normal) < 0.0 {
780                    result.blocked = true;
781                }
782            }
783        }
784        result.moved = self.position(id) - start;
785        self.update_sensors_for(id);
786        result
787    }
788
789    /// Pushes body `id` out of every solid it overlaps.
790    /// Returns (other body, push direction, depth) for each contact.
791    fn resolve(&mut self, id: BodyId) -> Vec<(BodyId, Vec3, f32)> {
792        let mut contacts = Vec::new();
793        let Some(body) = self.bodies.get(&id) else {
794            return contacts;
795        };
796        if !body.is_solid() {
797            return contacts;
798        }
799        // A few iterations settle corners where two walls meet.
800        for _ in 0..4 {
801            let body = &self.bodies[&id];
802            let mut push = None;
803            for (other_id, other) in &self.bodies {
804                if *other_id == id || !other.is_solid() {
805                    continue;
806                }
807                if let Some((n, depth)) = penetration(body, other)
808                    && push.as_ref().is_none_or(|(_, _, d)| depth > *d)
809                {
810                    push = Some((*other_id, n, depth));
811                }
812            }
813            let Some((other, n, depth)) = push else {
814                break;
815            };
816            // How much of the overlap the other body takes: none for walls, all of it when a
817            // kinematic body pushes a dynamic one, shared by mass between dynamic bodies.
818            let other_body = &self.bodies[&other];
819            let share = match (body.kind, other_body.kind) {
820                (BodyKind::Kinematic, BodyKind::Dynamic) => 1.0,
821                (BodyKind::Dynamic, BodyKind::Dynamic) => body.mass / (body.mass + other_body.mass),
822                _ => 0.0,
823            };
824            let mut moved_other = 0.0;
825            if share > 0.0 {
826                let old = other_body.position;
827                if let Some(o) = self.bodies.get_mut(&other) {
828                    o.position -= n * depth * share;
829                }
830                if self.obstructed(other, id, -n) {
831                    // Something solid is behind it (a crate against a wall): it does not move.
832                    if let Some(o) = self.bodies.get_mut(&other) {
833                        o.position = old;
834                    }
835                } else {
836                    moved_other = share;
837                }
838            }
839            if let Some(b) = self.bodies.get_mut(&id) {
840                b.position += n * depth * (1.0 - moved_other);
841            }
842            contacts.push((other, n, depth));
843        }
844        contacts
845    }
846
847    /// Whether body `id`, pushed in `direction`, now overlaps a solid in that direction
848    /// (ignoring `pusher`).
849    fn obstructed(&self, id: BodyId, pusher: BodyId, direction: Vec3) -> bool {
850        let body = &self.bodies[&id];
851        self.bodies.iter().any(|(other_id, other)| {
852            *other_id != id
853                && *other_id != pusher
854                && other.is_solid()
855                && penetration(body, other).is_some_and(|(n, _)| n.dot(direction) < -0.5)
856        })
857    }
858
859    fn update_sensors_for(&mut self, id: BodyId) {
860        let Some(body) = self.bodies.get(&id) else {
861            return;
862        };
863        if !body.is_solid() {
864            return;
865        }
866        for (sid, sensor) in &self.bodies {
867            if sensor.kind != BodyKind::Sensor {
868                continue;
869            }
870            let inside = penetration(body, sensor).is_some();
871            let key = (*sid, id);
872            if inside && self.sensor_overlaps.insert(key) {
873                self.events.push(PhysicsEvent::Entered { sensor: *sid, body: id });
874            } else if !inside && self.sensor_overlaps.remove(&key) {
875                self.events.push(PhysicsEvent::Left { sensor: *sid, body: id });
876            }
877        }
878    }
879
880    /// Advances the simulation by `dt` seconds. The engine calls this for you.
881    pub fn step(&mut self, dt: f32) {
882        if dt <= 0.0 {
883            return;
884        }
885        let ids: Vec<BodyId> = self.bodies.keys().copied().collect();
886        let gravity = self.gravity;
887        let up = self.up();
888        let mut touching = HashSet::new();
889
890        // Vehicles drive like characters (move_by), so walls stop them.
891        for &id in &ids {
892            self.step_vehicle(id, dt);
893        }
894
895        // Kinematic bodies move first, so bodies standing on them can be carried along.
896        let mut carried = Vec::new();
897        for (id, body) in self.bodies.iter_mut() {
898            if body.kind == BodyKind::Kinematic && body.vehicle.is_none() {
899                let delta = body.velocity * dt;
900                body.position += delta;
901                if delta != Vec3::ZERO {
902                    carried.push((*id, delta));
903                }
904            }
905        }
906
907        for &id in &ids {
908            let Some(body) = self.bodies.get_mut(&id) else {
909                continue;
910            };
911            if body.kind != BodyKind::Dynamic {
912                continue;
913            }
914            if let Some((_, delta)) = carried.iter().find(|(platform, _)| Some(*platform) == body.ground) {
915                body.position += *delta;
916            }
917
918            let force = std::mem::take(&mut body.force);
919            body.velocity += (gravity * body.gravity_scale + force / body.mass) * dt;
920            if body.damping > 0.0 {
921                body.velocity *= (1.0 - body.damping * dt).max(0.0);
922            }
923            if body.on_ground && body.friction > 0.0 {
924                let keep = (1.0 - body.friction * dt).max(0.0);
925                let vertical = up * body.velocity.dot(up);
926                body.velocity = vertical + (body.velocity - vertical) * keep;
927            }
928            if body.max_speed.is_finite() {
929                body.velocity = body.velocity.clamp_length_max(body.max_speed);
930            }
931
932            let was_on_ground = body.on_ground;
933            let delta = body.velocity * dt;
934            let size = body.shape.half_extents().min_element().max(0.01);
935            let substeps = ((delta.length() / size).ceil() as usize).clamp(1, 64);
936            let step = delta / substeps as f32;
937            let mut ground = None;
938            let mut landing_speed = 0.0f32;
939            for _ in 0..substeps {
940                if let Some(b) = self.bodies.get_mut(&id) {
941                    b.position += step;
942                }
943                for (other, n, _) in self.resolve(id) {
944                    let impact = self.bounce(id, other, n);
945                    if impact > 0.0 {
946                        let key = pair(id, other);
947                        if !self.contacts.contains(&key) && !touching.contains(&key) {
948                            self.events.push(PhysicsEvent::CollisionStarted { a: id, b: other, speed: impact });
949                        }
950                    }
951                    if n.dot(up) > 0.7 {
952                        ground = Some(other);
953                        landing_speed = landing_speed.max(impact);
954                    }
955                    touching.insert(pair(id, other));
956                }
957            }
958            // A body resting on the floor is pushed into it by gravity every step and so keeps
959            // its contact; a body that moved away loses it.
960            if let Some(b) = self.bodies.get_mut(&id) {
961                b.on_ground = ground.is_some();
962                b.ground = ground;
963            }
964            if let (false, Some(on)) = (was_on_ground, ground)
965                && landing_speed > 0.0
966            {
967                self.events.push(PhysicsEvent::Landed { body: id, on, speed: landing_speed });
968            }
969        }
970
971        for &(a, b) in self.contacts.difference(&touching) {
972            self.events.push(PhysicsEvent::CollisionEnded { a, b });
973        }
974        self.contacts = touching;
975
976        for &id in &ids {
977            self.update_sensors_for(id);
978        }
979    }
980
981    /// Drives one vehicle body for `dt` seconds.
982    fn step_vehicle(&mut self, id: BodyId, dt: f32) {
983        let Some(body) = self.bodies.get_mut(&id) else {
984            return;
985        };
986        let Some(mut vehicle) = body.vehicle.take() else {
987            return;
988        };
989        let (turn, report) = vehicle.simulate(body.mass, dt);
990        body.facing = crate::math::normalize_angle(body.facing + turn);
991        let facing = body.facing;
992        if let Some(gear) = report.gear_changed {
993            self.events.push(PhysicsEvent::GearChanged { vehicle: id, gear });
994        }
995
996        let delta = crate::vehicle::velocity(facing, vehicle.speed()) * dt;
997        if delta != Vec3::ZERO {
998            let result = self.move_by(id, delta);
999            let along = result.moved.dot(crate::math::forward(facing));
1000            vehicle.add_distance(along);
1001            if result.blocked
1002                && let Some((other, normal)) = self.contact_ahead(id, delta)
1003            {
1004                let impact = vehicle.blocked(facing, normal);
1005                if impact > 0.5 {
1006                    self.events.push(PhysicsEvent::Crashed { vehicle: id, other, speed: impact });
1007                }
1008            }
1009        }
1010        if let Some(body) = self.bodies.get_mut(&id) {
1011            body.velocity = crate::vehicle::velocity(facing, vehicle.speed());
1012            body.vehicle = Some(vehicle);
1013        }
1014    }
1015
1016    /// The solid body that `id` would run into when moved a little further along `delta`,
1017    /// and the direction its surface faces.
1018    fn contact_ahead(&self, id: BodyId, delta: Vec3) -> Option<(BodyId, Vec3)> {
1019        let mut probe = self.bodies.get(&id)?.clone();
1020        probe.position += delta.normalize_or_zero() * 0.02;
1021        self.bodies
1022            .iter()
1023            .filter(|(other, b)| **other != id && b.is_solid())
1024            .filter_map(|(other, b)| penetration(&probe, b).map(|(n, depth)| (*other, n, depth)))
1025            .max_by(|a, b| a.2.total_cmp(&b.2))
1026            .map(|(other, n, _)| (other, n))
1027    }
1028
1029    /// Changes the velocities of `id` and `other` after they touched along normal `n`
1030    /// (pointing towards `id`). Returns the impact speed, 0.0 if they were moving apart.
1031    fn bounce(&mut self, id: BodyId, other: BodyId, n: Vec3) -> f32 {
1032        let (Some(a), Some(b)) = (self.bodies.get(&id), self.bodies.get(&other)) else {
1033            return 0.0;
1034        };
1035        let other_velocity = if b.kind == BodyKind::Fixed { Vec3::ZERO } else { b.velocity };
1036        let closing = (a.velocity - other_velocity).dot(n);
1037        if closing >= 0.0 {
1038            return 0.0;
1039        }
1040        let restitution = a.bounciness.max(b.bounciness);
1041        if b.kind == BodyKind::Dynamic {
1042            // Exchange momentum like billiard balls.
1043            let (ma, mb) = (a.mass, b.mass);
1044            let j = -(1.0 + restitution) * closing / (1.0 / ma + 1.0 / mb);
1045            if let Some(a) = self.bodies.get_mut(&id) {
1046                a.velocity += n * (j / ma);
1047            }
1048            if let Some(b) = self.bodies.get_mut(&other) {
1049                b.velocity -= n * (j / mb);
1050            }
1051        } else if let Some(a) = self.bodies.get_mut(&id) {
1052            // Walls and platforms do not move: remove the speed going into them.
1053            a.velocity -= n * closing * (1.0 + restitution);
1054        }
1055        -closing
1056    }
1057}
1058
1059
1060/// Convenience helpers for typical audio game code.
1061impl World {
1062    /// Compass direction a body faces (0 if it does not exist).
1063    pub fn facing(&self, id: BodyId) -> f32 {
1064        self.get(id).map_or(0.0, |b| b.facing)
1065    }
1066
1067    /// Sets the compass direction a body faces.
1068    pub fn set_facing(&mut self, id: BodyId, degrees: f32) {
1069        if let Some(b) = self.get_mut(id) {
1070            b.facing = crate::math::normalize_angle(degrees);
1071        }
1072    }
1073
1074    /// Turns a body by `degrees` (positive = clockwise / right, negative = left) and returns
1075    /// the new facing, 0.0 to 360.0.
1076    pub fn turn(&mut self, id: BodyId, degrees: f32) -> f32 {
1077        let facing = crate::math::normalize_angle(self.facing(id) + degrees);
1078        self.set_facing(id, facing);
1079        facing
1080    }
1081
1082    /// How far a body looks up (positive) or down (negative) in degrees (0 if it does not
1083    /// exist).
1084    pub fn pitch(&self, id: BodyId) -> f32 {
1085        self.get(id).map_or(0.0, |b| b.pitch)
1086    }
1087
1088    /// Sets how far a body looks up or down in degrees, clamped to -90..=90.
1089    pub fn set_pitch(&mut self, id: BodyId, degrees: f32) {
1090        if let Some(b) = self.get_mut(id) {
1091            b.pitch = degrees.clamp(-90.0, 90.0);
1092        }
1093    }
1094
1095    /// Tilts a body's view up (positive degrees) or down (negative) and returns the new
1096    /// pitch, -90.0 to 90.0. Together with [`World::turn`] this aims in any 3D direction.
1097    pub fn tilt(&mut self, id: BodyId, degrees: f32) -> f32 {
1098        let pitch = (self.pitch(id) + degrees).clamp(-90.0, 90.0);
1099        self.set_pitch(id, pitch);
1100        pitch
1101    }
1102
1103    /// Turns a body so that it faces `point`: sets both the facing and the pitch.
1104    pub fn look_at(&mut self, id: BodyId, point: Vec3) {
1105        let (yaw, pitch) = crate::math::look_at(self.position(id), point);
1106        self.set_facing(id, yaw);
1107        self.set_pitch(id, pitch);
1108    }
1109
1110    /// Moves a body `distance` meters in the direction it faces (negative = backwards),
1111    /// sliding along walls. The pitch counts too: a body looking 30° up also rises. With the
1112    /// default pitch of 0 it moves level. See [`World::move_by`].
1113    pub fn move_forward(&mut self, id: BodyId, distance: f32) -> MoveResult {
1114        let delta = crate::math::forward_3d(self.facing(id), self.pitch(id)) * distance;
1115        self.move_by(id, delta)
1116    }
1117
1118    /// Moves a body `distance` meters straight up (negative = down), sliding along walls,
1119    /// e.g. to fly, swim, climb a ladder or take a lift. "Up" is `+z`, or against gravity if
1120    /// gravity points elsewhere. See [`World::move_by`].
1121    pub fn move_up(&mut self, id: BodyId, distance: f32) -> MoveResult {
1122        let delta = self.up() * distance;
1123        self.move_by(id, delta)
1124    }
1125
1126    /// Moves a body sideways, `distance` meters to its right (negative = left).
1127    pub fn strafe(&mut self, id: BodyId, distance: f32) -> MoveResult {
1128        let delta = crate::math::right(self.facing(id)) * distance;
1129        self.move_by(id, delta)
1130    }
1131
1132    /// Distance between two bodies' centers (infinity if one does not exist).
1133    pub fn distance(&self, a: BodyId, b: BodyId) -> f32 {
1134        match (self.get(a), self.get(b)) {
1135            (Some(a), Some(b)) => a.position.distance(b.position),
1136            _ => f32::INFINITY,
1137        }
1138    }
1139
1140    /// Where `target` is from the point of view of `from`, ready to speak:
1141    /// `"3 o'clock, 12 meters"`. Uses the facing of `from`: 12 o'clock is straight ahead,
1142    /// 3 o'clock to the right, 6 o'clock behind. A height difference of a meter or more is
1143    /// added (`"12 o'clock, 11 meters, 4 meters above"`), and a target straight above or
1144    /// below is `"directly above, 5 meters"`. See [`math::describe_position`](crate::math::describe_position).
1145    ///
1146    /// ```no_run
1147    /// # use audio_code::prelude::*;
1148    /// # fn f(ctx: &mut Context, player: BodyId, beacon: BodyId) {
1149    /// ctx.speak(ctx.physics.describe(player, beacon));
1150    /// # }
1151    /// ```
1152    pub fn describe(&self, from: BodyId, target: BodyId) -> String {
1153        crate::math::describe_position(self.position(from), self.facing(from), self.position(target))
1154    }
1155
1156    // ------------------------------------------------------------- event shortcuts
1157
1158    /// If `body` landed on something this frame: the falling speed in meters per second.
1159    /// With earth gravity, a 1 m fall lands at about 4.4 m/s, a 3 m fall at 7.7 m/s.
1160    pub fn landed(&self, body: BodyId) -> Option<f32> {
1161        self.events.iter().find_map(|e| match *e {
1162            PhysicsEvent::Landed { body: b, speed, .. } if b == body => Some(speed),
1163            _ => None,
1164        })
1165    }
1166
1167    /// If `body` started touching another solid body this frame: that body and the impact
1168    /// speed.
1169    pub fn collided(&self, body: BodyId) -> Option<(BodyId, f32)> {
1170        self.events.iter().find_map(|e| match *e {
1171            PhysicsEvent::CollisionStarted { a, b, speed } if a == body => Some((b, speed)),
1172            PhysicsEvent::CollisionStarted { a, b, speed } if b == body => Some((a, speed)),
1173            _ => None,
1174        })
1175    }
1176
1177    /// If `body` walked into a sensor this frame: the sensor.
1178    pub fn entered(&self, body: BodyId) -> Option<BodyId> {
1179        self.events.iter().find_map(|e| match *e {
1180            PhysicsEvent::Entered { sensor, body: b } if b == body => Some(sensor),
1181            _ => None,
1182        })
1183    }
1184
1185    /// If `body` walked into a sensor with this tag this frame: the sensor.
1186    ///
1187    /// ```no_run
1188    /// # use audio_code::prelude::*;
1189    /// # fn f(ctx: &mut Context, player: BodyId) {
1190    /// if let Some(coin) = ctx.physics.entered_tag(player, "coin") {
1191    ///     ctx.physics.remove(coin);   // sounds attached with ctx.loop_on stop by themselves
1192    ///     ctx.play("pickup");
1193    /// }
1194    /// # }
1195    /// ```
1196    pub fn entered_tag(&self, body: BodyId, tag: &str) -> Option<BodyId> {
1197        self.events.iter().find_map(|e| match *e {
1198            PhysicsEvent::Entered { sensor, body: b } if b == body && self.tag(sensor) == tag => Some(sensor),
1199            _ => None,
1200        })
1201    }
1202
1203    /// If `body` left a sensor this frame: the sensor.
1204    pub fn left(&self, body: BodyId) -> Option<BodyId> {
1205        self.events.iter().find_map(|e| match *e {
1206            PhysicsEvent::Left { sensor, body: b } if b == body => Some(sensor),
1207            _ => None,
1208        })
1209    }
1210
1211    // ------------------------------------------------------------- vehicles
1212
1213    /// The vehicle part of a body, if it is a car or truck.
1214    ///
1215    /// ```no_run
1216    /// # use audio_code::prelude::*;
1217    /// # fn f(ctx: &mut Context, car: BodyId) {
1218    /// if let Some(v) = ctx.physics.vehicle(car) {
1219    ///     let text = v.describe_speed();
1220    ///     ctx.speak(text);
1221    /// }
1222    /// # }
1223    /// ```
1224    pub fn vehicle(&self, id: BodyId) -> Option<&Vehicle> {
1225        self.get(id).and_then(|b| b.vehicle.as_ref())
1226    }
1227
1228    /// Mutable access to the vehicle part of a body: pedals, steering, load, fuel, settings.
1229    pub fn vehicle_mut(&mut self, id: BodyId) -> Option<&mut Vehicle> {
1230        self.get_mut(id).and_then(|b| b.vehicle.as_mut())
1231    }
1232
1233    /// Total weight of a body in kilograms: its mass plus the load of its vehicle (0 if the
1234    /// body does not exist).
1235    pub fn weight(&self, id: BodyId) -> f32 {
1236        self.get(id).map_or(0.0, |b| b.mass + b.vehicle.as_ref().map_or(0.0, |v| v.load))
1237    }
1238
1239    /// How fast a body moves in meters per second: the length of its velocity. For
1240    /// vehicles this is their driving speed. Kinematic bodies moved with `move_by` have no
1241    /// velocity, so they report 0.
1242    pub fn speed(&self, id: BodyId) -> f32 {
1243        self.velocity(id).length()
1244    }
1245
1246    /// If `vehicle` crashed into something this frame: what it hit and the speed lost in
1247    /// meters per second.
1248    pub fn crashed(&self, vehicle: BodyId) -> Option<(BodyId, f32)> {
1249        self.events.iter().find_map(|e| match *e {
1250            PhysicsEvent::Crashed { vehicle: v, other, speed } if v == vehicle => Some((other, speed)),
1251            _ => None,
1252        })
1253    }
1254
1255    /// If `vehicle` changed gear this frame: the new gear (-1 reverse, 0 neutral, 1 and up).
1256    pub fn gear_changed(&self, vehicle: BodyId) -> Option<i32> {
1257        self.events.iter().rev().find_map(|e| match *e {
1258            PhysicsEvent::GearChanged { vehicle: v, gear } if v == vehicle => Some(gear),
1259            _ => None,
1260        })
1261    }
1262
1263    /// Whether anything entered `sensor` this frame; returns the body.
1264    pub fn triggered(&self, sensor: BodyId) -> Option<BodyId> {
1265        self.events.iter().find_map(|e| match *e {
1266            PhysicsEvent::Entered { sensor: s, body } if s == sensor => Some(body),
1267            _ => None,
1268        })
1269    }
1270}
1271
1272/// Ray against axis aligned box (slab method). Returns distance and normal.
1273fn ray_box(origin: Vec3, dir: Vec3, min: Vec3, max: Vec3) -> Option<(f32, Vec3)> {
1274    let mut t_min = 0.0f32;
1275    let mut t_max = f32::INFINITY;
1276    let mut normal = Vec3::ZERO;
1277    for axis in 0..3 {
1278        let (o, d, lo, hi) = (origin[axis], dir[axis], min[axis], max[axis]);
1279        if d.abs() < 1e-8 {
1280            if o < lo || o > hi {
1281                return None;
1282            }
1283            continue;
1284        }
1285        let (mut t1, mut t2) = ((lo - o) / d, (hi - o) / d);
1286        let mut n = Vec3::ZERO;
1287        n[axis] = -d.signum();
1288        if t1 > t2 {
1289            std::mem::swap(&mut t1, &mut t2);
1290        }
1291        if t1 > t_min {
1292            t_min = t1;
1293            normal = n;
1294        }
1295        t_max = t_max.min(t2);
1296        if t_min > t_max {
1297            return None;
1298        }
1299    }
1300    Some((t_min, normal))
1301}
1302
1303fn ray_sphere(origin: Vec3, dir: Vec3, center: Vec3, radius: f32) -> Option<(f32, Vec3)> {
1304    let oc = origin - center;
1305    let b = oc.dot(dir);
1306    let c = oc.length_squared() - radius * radius;
1307    let disc = b * b - c;
1308    if disc < 0.0 {
1309        return None;
1310    }
1311    let sq = disc.sqrt();
1312    let t = if -b - sq >= 0.0 { -b - sq } else { -b + sq };
1313    if t < 0.0 {
1314        return None;
1315    }
1316    let point = origin + dir * t;
1317    Some((t, (point - center).normalize_or_zero()))
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322    use super::*;
1323    use glam::vec3;
1324
1325    #[test]
1326    fn falls_and_lands() {
1327        let mut w = World::new();
1328        w.set_gravity(vec3(0.0, 0.0, -9.81));
1329        let floor = w.add(Body::fixed(vec3(0.0, 0.0, -0.5)).with_box(10.0, 10.0, 1.0));
1330        let ball = w.add(Body::dynamic(vec3(0.0, 0.0, 2.0)).with_sphere(0.5));
1331        let mut landed = false;
1332        for _ in 0..180 {
1333            w.clear_events();
1334            w.step(1.0 / 60.0);
1335            landed |= w.events().iter().any(|e| matches!(e, PhysicsEvent::CollisionStarted { b, .. } if *b == floor));
1336        }
1337        assert!(landed);
1338        assert!(w.on_ground(ball));
1339        assert!((w.position(ball).z - 0.5).abs() < 0.05, "z = {}", w.position(ball).z);
1340    }
1341
1342    #[test]
1343    fn walls_block_movement() {
1344        let mut w = World::new();
1345        let wall = w.add(Body::fixed(vec3(0.0, 3.0, 0.0)).with_box(10.0, 1.0, 3.0).with_tag("wall"));
1346        let player = w.add(Body::kinematic(Vec3::ZERO).with_box(0.5, 0.5, 1.8));
1347        let r = w.move_by(player, vec3(0.0, 5.0, 0.0));
1348        assert!(r.blocked);
1349        assert_eq!(r.hits, vec![wall]);
1350        assert!(w.position(player).y < 2.3);
1351        let r = w.move_by(player, vec3(0.0, -1.0, 0.0));
1352        assert!(!r.blocked);
1353    }
1354
1355    #[test]
1356    fn sensors_and_rays() {
1357        let mut w = World::new();
1358        let zone = w.add(Body::sensor(vec3(5.0, 0.0, 0.0)).with_box(2.0, 2.0, 2.0));
1359        let p = w.add(Body::kinematic(Vec3::ZERO).with_sphere(0.3));
1360        w.move_by(p, vec3(5.0, 0.0, 0.0));
1361        assert!(w.events().contains(&PhysicsEvent::Entered { sensor: zone, body: p }));
1362        let target = w.add(Body::fixed(vec3(5.0, 10.0, 0.0)).with_box(1.0, 1.0, 1.0));
1363        let hit = w.raycast(w.position(p), Vec3::Y, 50.0, Some(p)).unwrap();
1364        assert_eq!(hit.body, target);
1365        assert!((hit.distance - 9.5).abs() < 1e-3);
1366    }
1367
1368    fn earth() -> (World, BodyId) {
1369        let mut w = World::new();
1370        w.set_gravity(EARTH_GRAVITY);
1371        let floor = w.add(Body::fixed(vec3(0.0, 0.0, -0.5)).with_box(100.0, 100.0, 1.0));
1372        (w, floor)
1373    }
1374
1375    fn run(w: &mut World, seconds: f32) -> Vec<PhysicsEvent> {
1376        let mut events = Vec::new();
1377        for _ in 0..(seconds * 60.0) as usize {
1378            w.clear_events();
1379            w.step(1.0 / 60.0);
1380            events.extend_from_slice(w.events());
1381        }
1382        events
1383    }
1384
1385    #[test]
1386    fn landed_event_reports_fall_speed() {
1387        let (mut w, floor) = earth();
1388        let ball = w.add(Body::dynamic(vec3(0.0, 0.0, 5.5)).with_sphere(0.5));
1389        let events = run(&mut w, 2.0);
1390        let landed: Vec<_> = events.iter().filter(|e| matches!(e, PhysicsEvent::Landed { .. })).collect();
1391        assert_eq!(landed.len(), 1);
1392        let PhysicsEvent::Landed { body, on, speed } = landed[0] else { unreachable!() };
1393        assert_eq!((*body, *on), (ball, floor));
1394        // Falling 5 m: v = sqrt(2 * 9.81 * 5) ≈ 9.9 m/s.
1395        assert!((speed - 9.9).abs() < 0.6, "speed {speed}");
1396    }
1397
1398    #[test]
1399    fn jump_reaches_height() {
1400        let (mut w, _) = earth();
1401        let p = w.add(Body::dynamic(vec3(0.0, 0.0, 1.0)).with_box(1.0, 1.0, 2.0));
1402        run(&mut w, 0.5);
1403        assert!(w.on_ground(p));
1404        assert!(w.jump(p, 2.0));
1405        assert!(!w.jump(p, 2.0), "no double jump");
1406        let mut highest: f32 = 0.0;
1407        for _ in 0..120 {
1408            w.step(1.0 / 60.0);
1409            highest = highest.max(w.position(p).z);
1410        }
1411        assert!((highest - 3.0).abs() < 0.15, "highest {highest}");
1412    }
1413
1414    #[test]
1415    fn forces_mass_and_speed_limit() {
1416        let mut w = World::new();
1417        let light = w.add(Body::dynamic(Vec3::ZERO));
1418        let heavy = w.add(Body::dynamic(vec3(10.0, 0.0, 0.0)).with_mass(10.0));
1419        w.apply_force(light, vec3(60.0, 0.0, 0.0));
1420        w.apply_force(heavy, vec3(60.0, 0.0, 0.0));
1421        w.step(1.0 / 60.0);
1422        assert!((w.velocity(light).x - 1.0).abs() < 1e-4);
1423        assert!((w.velocity(heavy).x - 0.1).abs() < 1e-4);
1424        // The force only lasts one step.
1425        w.step(1.0 / 60.0);
1426        assert!((w.velocity(light).x - 1.0).abs() < 1e-4);
1427
1428        let (mut w, _) = earth();
1429        let rock = w.add(Body::dynamic(vec3(0.0, 0.0, 100.0)).with_max_speed(5.0));
1430        run(&mut w, 1.0);
1431        assert!(w.velocity(rock).length() <= 5.0 + 1e-3);
1432    }
1433
1434    #[test]
1435    fn heavy_ball_pushes_light_ball() {
1436        let mut w = World::new();
1437        let heavy = w.add(
1438            Body::dynamic(Vec3::ZERO).with_sphere(0.5).with_mass(10.0).with_bounciness(1.0).with_velocity(vec3(5.0, 0.0, 0.0)),
1439        );
1440        let light = w.add(Body::dynamic(vec3(2.0, 0.0, 0.0)).with_sphere(0.5).with_bounciness(1.0));
1441        let events = run(&mut w, 0.5);
1442        assert!(events.iter().any(|e| matches!(e, PhysicsEvent::CollisionStarted { .. })));
1443        // Elastic collision: the light ball flies off at about 9.1 m/s, the heavy one keeps 4.1.
1444        assert!((w.velocity(light).x - 9.09).abs() < 0.2, "light {}", w.velocity(light).x);
1445        assert!((w.velocity(heavy).x - 4.09).abs() < 0.2, "heavy {}", w.velocity(heavy).x);
1446    }
1447
1448    #[test]
1449    fn player_pushes_crate_until_wall() {
1450        let (mut w, _) = earth();
1451        w.add(Body::fixed(vec3(5.0, 0.0, 1.0)).with_box(1.0, 10.0, 2.0));
1452        let crate_ = w.add(Body::dynamic(vec3(2.0, 0.0, 0.5)).with_box(1.0, 1.0, 1.0));
1453        let player = w.add(Body::kinematic(vec3(0.0, 0.0, 0.5)).with_box(1.0, 1.0, 1.0));
1454        run(&mut w, 0.3);
1455        let mut blocked = false;
1456        for _ in 0..40 {
1457            blocked = w.move_by(player, vec3(0.1, 0.0, 0.0)).blocked;
1458            run(&mut w, 1.0 / 60.0);
1459        }
1460        // The crate ends at the wall (x = 4.0) and the player behind it.
1461        assert!((w.position(crate_).x - 4.0).abs() < 0.1, "crate {}", w.position(crate_).x);
1462        assert!(w.position(player).x < 3.1);
1463        assert!(blocked);
1464    }
1465
1466    #[test]
1467    fn platforms_carry_bodies() {
1468        let mut w = World::new();
1469        w.set_gravity(EARTH_GRAVITY);
1470        let lift = w.add(Body::kinematic(Vec3::ZERO).with_box(4.0, 4.0, 1.0).with_velocity(vec3(1.0, 0.0, 0.0)));
1471        let p = w.add(Body::dynamic(vec3(0.0, 0.0, 1.5)).with_box(1.0, 1.0, 1.0));
1472        run(&mut w, 0.5);
1473        assert_eq!(w.get(p).unwrap().ground, Some(lift));
1474        let offset = w.position(p).x - w.position(lift).x;
1475        run(&mut w, 1.5);
1476        assert!((w.position(p).x - w.position(lift).x - offset).abs() < 0.02);
1477    }
1478
1479    #[test]
1480    fn facing_and_event_helpers() {
1481        let mut w = World::new();
1482        let p = w.add(Body::kinematic(Vec3::ZERO).with_sphere(0.3).with_facing(90.0));
1483        let coin = w.add(Body::sensor(vec3(2.0, 0.0, 0.0)).with_sphere(0.5).with_tag("coin"));
1484        assert_eq!(w.turn(p, -90.0), 0.0);
1485        w.turn(p, 90.0);
1486        assert_eq!(w.describe(p, coin), "12 o'clock, 2 meters");
1487        w.move_forward(p, 2.0);
1488        assert_eq!(w.entered_tag(p, "coin"), Some(coin));
1489        assert_eq!(w.triggered(coin), Some(p));
1490        assert_eq!(w.entered_tag(p, "door"), None);
1491        assert!((w.position(p).x - 2.0).abs() < 1e-4);
1492    }
1493
1494    #[test]
1495    fn three_d_movement() {
1496        let mut w = World::new();
1497        let p = w.add(Body::kinematic(Vec3::ZERO).with_sphere(0.3));
1498        let bird = w.add(Body::sensor(vec3(0.0, 4.0, 4.0)).with_sphere(0.3));
1499        assert_eq!(w.describe(p, bird), "12 o'clock, 6 meters, 4 meters above");
1500        assert_eq!(w.tilt(p, 45.0), 45.0);
1501        assert_eq!(w.tilt(p, 100.0), 90.0);
1502        w.set_pitch(p, 45.0);
1503        w.move_forward(p, 2.0_f32.sqrt());
1504        assert!((w.position(p) - vec3(0.0, 1.0, 1.0)).length() < 1e-4);
1505        w.move_up(p, -3.0);
1506        assert!((w.position(p).z + 2.0).abs() < 1e-4);
1507        w.look_at(p, vec3(0.0, 1.0, 5.0));
1508        assert!((w.pitch(p) - 90.0).abs() < 1e-3);
1509        // A ceiling stops flying upwards.
1510        w.add(Body::fixed(vec3(0.0, 1.0, 3.0)).with_box(10.0, 10.0, 1.0));
1511        assert!(w.move_forward(p, 10.0).blocked);
1512        assert!(w.position(p).z < 2.5);
1513    }
1514}