Skip to main content

audio_code/
vehicle.rs

1//! Cars and trucks: weight, speed, engine, gears, steering, brakes, fuel and sounds.
2//!
3//! A vehicle is an optional part of a physics [`Body`](crate::physics::Body). Add one with
4//! [`Body::car`](crate::physics::Body::car), [`Body::truck`](crate::physics::Body::truck) or
5//! [`Body::with_vehicle`](crate::physics::Body::with_vehicle); every other body is not
6//! affected. The physics world drives it at the fixed physics rate: the pedals and the
7//! steering wheel are plain fields you set every frame (or let
8//! [`Context::drive`](crate::Context::drive) set them from the arrow keys).
9//!
10//! ```no_run
11//! # use audio_code::prelude::*;
12//! # fn f(ctx: &mut Context) {
13//! let car = ctx.physics.add(Body::car(vec3(0.0, 0.0, 0.0)).with_vehicle(
14//!     Vehicle::car().with_engine_sound("engine").with_horn_sound("horn"),
15//! ));
16//! ctx.follow(car);
17//! // every frame:
18//! ctx.drive(car);   // Up = gas, Down = brake / reverse, Left / Right = steer, Space = handbrake
19//! if let Some(v) = ctx.physics.vehicle(car) {
20//!     let kmh = v.speed_kmh();
21//! }
22//! # }
23//! ```
24//!
25//! ## The model
26//!
27//! * The **weight** is the body's [`mass`](crate::physics::Body::mass) plus the vehicle's
28//!   [`load`](Vehicle::load), in kilograms. The engine force and the brake force are fixed,
29//!   so a heavier vehicle speeds up more slowly and needs a longer way to stop.
30//! * The **speed** is a signed number along the facing of the body (negative when
31//!   reversing), limited by [`top_speed`](Vehicle::top_speed) and
32//!   [`reverse_speed`](Vehicle::reverse_speed). Engine force fades out towards the top speed.
33//! * **Steering** turns the body's facing. The turn rate grows with speed (a parked car
34//!   cannot turn) up to [`max_turn_rate`](Vehicle::max_turn_rate); the tightest circle has
35//!   the radius [`turn_radius`](Vehicle::turn_radius).
36//! * The vehicle moves with [`World::move_by`](crate::physics::World::move_by), so it stops
37//!   at walls, slides along them, pushes loose objects and triggers sensors (checkpoints).
38//!   Running into something loses the speed going into it and reports
39//!   [`PhysicsEvent::Crashed`](crate::physics::PhysicsEvent::Crashed).
40//! * Vehicles drive level (they keep their height) and are not pulled by gravity.
41//! * An automatic gearbox picks gears from the speed; the engine speed (rpm) follows.
42//!   [`Vehicle::engine_pitch`] turns it into a pitch for a looping engine sound.
43
44use glam::Vec3;
45
46/// Drives at more than this speed (m/s) count as moving forward or backward. Slower than
47/// that, the brake pedal starts reversing.
48const STOPPED: f32 = 0.3;
49
50/// Air drag at top speed as a share of the engine force.
51const AIR_DRAG: f32 = 0.3;
52
53/// The vehicle part of a body: settings, pedals and the driving state.
54///
55/// Settings are public fields with `with_*` builder methods. Speeds are stored in meters per
56/// second; the builders that take km/h say so in their name.
57#[derive(Debug, Clone, PartialEq)]
58pub struct Vehicle {
59    /// Highest forward speed in m/s. The engine gets weaker towards it, so full throttle
60    /// approaches it slowly.
61    pub top_speed: f32,
62    /// Highest reverse speed in m/s.
63    pub reverse_speed: f32,
64    /// Driving force of the engine at full throttle in newtons. Acceleration is this divided
65    /// by the weight.
66    pub engine_force: f32,
67    /// Braking force at full brake pedal in newtons. Deceleration is this divided by the
68    /// weight.
69    pub brake_force: f32,
70    /// Most acceleration or deceleration the tyres can transfer, in m/s². Caps engine,
71    /// brakes and handbrake. 9 is a car on dry asphalt, 3 is a wet road, 1 is ice.
72    pub grip: f32,
73    /// Speed lost per second while rolling, in m/s² (tyres, air). Always active.
74    pub rolling_resistance: f32,
75    /// Extra speed lost per second when no pedal is pressed and the engine runs, in m/s².
76    pub engine_braking: f32,
77    /// Radius of the tightest circle the vehicle can drive, in meters.
78    pub turn_radius: f32,
79    /// Fastest the vehicle can turn, in degrees per second.
80    pub max_turn_rate: f32,
81    /// Number of forward gears (at least 1).
82    pub gears: u32,
83    /// Engine speed when idling, in revolutions per minute.
84    pub idle_rpm: f32,
85    /// Highest engine speed, in revolutions per minute.
86    pub max_rpm: f32,
87    /// Cargo in kilograms, added to the body's mass. Change it with [`Vehicle::set_load`] to
88    /// respect [`Vehicle::max_load`].
89    pub load: f32,
90    /// Most cargo in kilograms. Default unlimited.
91    pub max_load: f32,
92    /// Tank size in liters, `None` (the default) for unlimited fuel.
93    pub fuel_capacity: Option<f32>,
94    /// Fuel use in liters per 100 km at full throttle with an empty vehicle. Light throttle
95    /// uses less, cargo uses more.
96    pub consumption: f32,
97    /// Looping engine sound, played at the vehicle while the engine runs. Its pitch follows
98    /// the engine speed and its volume the throttle.
99    pub engine_sound: Option<String>,
100    /// Sound for [`Context::horn`](crate::Context::horn).
101    pub horn_sound: Option<String>,
102    /// Sound for [`Context::start_engine`](crate::Context::start_engine).
103    pub start_sound: Option<String>,
104    /// Sound played on every gear change.
105    pub shift_sound: Option<String>,
106    /// Sound played when the vehicle crashes into something, louder for harder crashes.
107    pub crash_sound: Option<String>,
108
109    /// Gas pedal, 0.0 to 1.0. Set it every frame. While reversing it brakes.
110    pub throttle: f32,
111    /// Brake pedal, 0.0 to 1.0. Set it every frame. Pressed while the vehicle stands still,
112    /// it drives backwards (after braking to a stop, release it and press it again).
113    pub brake: f32,
114    /// Steering wheel, -1.0 (full left) to 1.0 (full right).
115    pub steer: f32,
116    /// Handbrake: stops the vehicle as hard as the grip allows.
117    pub handbrake: bool,
118
119    speed: f32,
120    gear: i32,
121    rpm: f32,
122    engine_on: bool,
123    fuel: f32,
124    distance: f32,
125    /// The pedal that braked the vehicle to a stop must be released before it drives
126    /// the other way: (brake pedal, gas pedal).
127    hold: (bool, bool),
128}
129
130impl Default for Vehicle {
131    fn default() -> Self {
132        Self::car()
133    }
134}
135
136/// What happened during one step, reported by the physics world as events.
137#[derive(Debug, Default)]
138pub(crate) struct StepReport {
139    pub gear_changed: Option<i32>,
140}
141
142impl Vehicle {
143    /// A car: top speed 180 km/h, 0 to 100 km/h in about 10 seconds with
144    /// [`Body::car`](crate::physics::Body::car)'s 1300 kg, 5 gears, turns in a 5 m circle.
145    pub fn car() -> Self {
146        Self {
147            top_speed: 180.0 / 3.6,
148            reverse_speed: 25.0 / 3.6,
149            engine_force: 4500.0,
150            brake_force: 11_000.0,
151            grip: 9.0,
152            rolling_resistance: 0.15,
153            engine_braking: 0.8,
154            turn_radius: 5.0,
155            max_turn_rate: 90.0,
156            gears: 5,
157            idle_rpm: 800.0,
158            max_rpm: 6500.0,
159            load: 0.0,
160            max_load: f32::INFINITY,
161            fuel_capacity: None,
162            consumption: 12.0,
163            engine_sound: None,
164            horn_sound: None,
165            start_sound: None,
166            shift_sound: None,
167            crash_sound: None,
168            throttle: 0.0,
169            brake: 0.0,
170            steer: 0.0,
171            handbrake: false,
172            speed: 0.0,
173            gear: 1,
174            rpm: 800.0,
175            engine_on: true,
176            fuel: 0.0,
177            distance: 0.0,
178            hold: (false, false),
179        }
180    }
181
182    /// A truck: top speed 90 km/h, 12 gears, turns in an 11 m circle, carries up to 25 t.
183    /// With [`Body::truck`](crate::physics::Body::truck)'s 12 t it reaches 60 km/h in about
184    /// 13 seconds empty and in about 50 seconds fully loaded.
185    pub fn truck() -> Self {
186        Self {
187            top_speed: 90.0 / 3.6,
188            reverse_speed: 10.0 / 3.6,
189            engine_force: 20_000.0,
190            brake_force: 110_000.0,
191            grip: 7.0,
192            rolling_resistance: 0.1,
193            engine_braking: 0.5,
194            turn_radius: 11.0,
195            max_turn_rate: 35.0,
196            gears: 12,
197            idle_rpm: 600.0,
198            max_rpm: 2200.0,
199            max_load: 25_000.0,
200            consumption: 35.0,
201            gear: 1,
202            rpm: 600.0,
203            ..Self::car()
204        }
205    }
206
207    /// Sets the top speed in km/h.
208    pub fn with_top_speed_kmh(mut self, kmh: f32) -> Self {
209        self.top_speed = (kmh / 3.6).max(0.1);
210        self
211    }
212
213    /// Sets the top reverse speed in km/h.
214    pub fn with_reverse_speed_kmh(mut self, kmh: f32) -> Self {
215        self.reverse_speed = (kmh / 3.6).max(0.0);
216        self
217    }
218
219    /// Sets the engine force in newtons (car 4500, truck 20000). More force, faster
220    /// acceleration.
221    pub fn with_engine_force(mut self, newtons: f32) -> Self {
222        self.engine_force = newtons.max(0.0);
223        self
224    }
225
226    /// Sets the brake force in newtons (car 11000, truck 110000).
227    pub fn with_brake_force(mut self, newtons: f32) -> Self {
228        self.brake_force = newtons.max(0.0);
229        self
230    }
231
232    /// Sets the tyre grip in m/s² (9 dry, 3 wet, 1 ice).
233    pub fn with_grip(mut self, grip: f32) -> Self {
234        self.grip = grip.max(0.01);
235        self
236    }
237
238    /// Sets the rolling resistance in m/s².
239    pub fn with_rolling_resistance(mut self, resistance: f32) -> Self {
240        self.rolling_resistance = resistance.max(0.0);
241        self
242    }
243
244    /// Sets the engine braking in m/s² (how fast the vehicle slows down off the gas).
245    pub fn with_engine_braking(mut self, braking: f32) -> Self {
246        self.engine_braking = braking.max(0.0);
247        self
248    }
249
250    /// Sets the radius of the tightest circle in meters.
251    pub fn with_turn_radius(mut self, meters: f32) -> Self {
252        self.turn_radius = meters.max(0.1);
253        self
254    }
255
256    /// Sets the fastest turn rate in degrees per second.
257    pub fn with_max_turn_rate(mut self, degrees_per_second: f32) -> Self {
258        self.max_turn_rate = degrees_per_second.max(0.0);
259        self
260    }
261
262    /// Sets the gearbox: number of forward gears, idle and highest engine speed in rpm.
263    pub fn with_gears(mut self, gears: u32, idle_rpm: f32, max_rpm: f32) -> Self {
264        self.gears = gears.max(1);
265        self.idle_rpm = idle_rpm.max(0.0);
266        self.max_rpm = max_rpm.max(self.idle_rpm + 1.0);
267        self.rpm = self.idle_rpm;
268        self
269    }
270
271    /// Sets the cargo in kilograms (limited by [`Vehicle::max_load`]).
272    pub fn with_load(mut self, kg: f32) -> Self {
273        self.set_load(kg);
274        self
275    }
276
277    /// Sets the most cargo in kilograms.
278    pub fn with_max_load(mut self, kg: f32) -> Self {
279        self.max_load = kg.max(0.0);
280        self.load = self.load.min(self.max_load);
281        self
282    }
283
284    /// Gives the vehicle a tank of `capacity` liters, starting full, that uses
285    /// `liters_per_100km` at full throttle. Without this, fuel is unlimited.
286    pub fn with_fuel(mut self, capacity: f32, liters_per_100km: f32) -> Self {
287        self.fuel_capacity = Some(capacity.max(0.0));
288        self.fuel = capacity.max(0.0);
289        self.consumption = liters_per_100km.max(0.0);
290        self
291    }
292
293    /// Sets the looping engine sound.
294    pub fn with_engine_sound(mut self, name: impl Into<String>) -> Self {
295        self.engine_sound = Some(name.into());
296        self
297    }
298
299    /// Sets the horn sound.
300    pub fn with_horn_sound(mut self, name: impl Into<String>) -> Self {
301        self.horn_sound = Some(name.into());
302        self
303    }
304
305    /// Sets the engine start sound.
306    pub fn with_start_sound(mut self, name: impl Into<String>) -> Self {
307        self.start_sound = Some(name.into());
308        self
309    }
310
311    /// Sets the gear change sound.
312    pub fn with_shift_sound(mut self, name: impl Into<String>) -> Self {
313        self.shift_sound = Some(name.into());
314        self
315    }
316
317    /// Sets the crash sound.
318    pub fn with_crash_sound(mut self, name: impl Into<String>) -> Self {
319        self.crash_sound = Some(name.into());
320        self
321    }
322
323    /// Starts with the engine off. Start it with [`Vehicle::start_engine`] or
324    /// [`Context::start_engine`](crate::Context::start_engine).
325    pub fn engine_off(mut self) -> Self {
326        self.engine_on = false;
327        self.rpm = 0.0;
328        self
329    }
330
331    // ----------------------------------------------------------------- state
332
333    /// Speed along the facing in m/s, negative when reversing.
334    pub fn speed(&self) -> f32 {
335        self.speed
336    }
337
338    /// Speed in km/h, negative when reversing.
339    pub fn speed_kmh(&self) -> f32 {
340        self.speed * 3.6
341    }
342
343    /// Sets the speed in m/s, e.g. to start a race already rolling. Limited to the top
344    /// speeds.
345    pub fn set_speed(&mut self, speed: f32) {
346        self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
347    }
348
349    /// The speed ready to speak, e.g. `"54 kilometers per hour"` (reversing included).
350    pub fn describe_speed(&self) -> String {
351        let kmh = self.speed_kmh().abs().round() as i64;
352        let unit = if kmh == 1 { "kilometer" } else { "kilometers" };
353        if self.is_reversing() {
354            format!("reversing, {kmh} {unit} per hour")
355        } else {
356            format!("{kmh} {unit} per hour")
357        }
358    }
359
360    /// Whether the vehicle is rolling backwards.
361    pub fn is_reversing(&self) -> bool {
362        self.speed < -0.05
363    }
364
365    /// Current gear: -1 reverse, 0 neutral (engine off), 1 and up forward.
366    pub fn gear(&self) -> i32 {
367        self.gear
368    }
369
370    /// The gear ready to speak: `"reverse"`, `"neutral"` or the number.
371    pub fn gear_name(&self) -> String {
372        match self.gear {
373            -1 => "reverse".to_string(),
374            0 => "neutral".to_string(),
375            g => g.to_string(),
376        }
377    }
378
379    /// Engine speed in revolutions per minute (0 when the engine is off).
380    pub fn rpm(&self) -> f32 {
381        self.rpm
382    }
383
384    /// Engine speed from 0.0 (idle) to 1.0 (highest rpm).
385    pub fn rpm_fraction(&self) -> f32 {
386        ((self.rpm - self.idle_rpm) / (self.max_rpm - self.idle_rpm)).clamp(0.0, 1.0)
387    }
388
389    /// A playback pitch for a looping engine sound recorded at idle: 0.8 at idle, 2.0 at
390    /// the highest rpm. The engine sound set with [`Vehicle::with_engine_sound`] uses it
391    /// automatically.
392    pub fn engine_pitch(&self) -> f32 {
393        0.8 + 1.2 * self.rpm_fraction()
394    }
395
396    /// Whether the engine runs.
397    pub fn is_engine_on(&self) -> bool {
398        self.engine_on
399    }
400
401    /// Starts the engine (silently; [`Context::start_engine`](crate::Context::start_engine)
402    /// also plays the start sound). Returns `false` if the tank is empty.
403    pub fn start_engine(&mut self) -> bool {
404        if self.is_out_of_fuel() {
405            return false;
406        }
407        self.engine_on = true;
408        if self.gear == 0 {
409            self.gear = 1;
410        }
411        self.rpm = self.rpm.max(self.idle_rpm);
412        true
413    }
414
415    /// Stops the engine. The vehicle rolls out.
416    pub fn stop_engine(&mut self) {
417        self.engine_on = false;
418        self.gear = 0;
419        self.rpm = 0.0;
420    }
421
422    /// Liters left in the tank, `None` for unlimited fuel.
423    pub fn fuel(&self) -> Option<f32> {
424        self.fuel_capacity.map(|_| self.fuel)
425    }
426
427    /// Tank level from 0.0 (empty) to 1.0 (full); always 1.0 for unlimited fuel.
428    pub fn fuel_fraction(&self) -> f32 {
429        match self.fuel_capacity {
430            Some(capacity) if capacity > 0.0 => self.fuel / capacity,
431            Some(_) => 0.0,
432            None => 1.0,
433        }
434    }
435
436    /// Whether the tank is empty (never with unlimited fuel).
437    pub fn is_out_of_fuel(&self) -> bool {
438        self.fuel_capacity.is_some() && self.fuel <= 0.0
439    }
440
441    /// Adds fuel in liters, up to the tank size. Returns how much was added.
442    pub fn refuel(&mut self, liters: f32) -> f32 {
443        let Some(capacity) = self.fuel_capacity else {
444            return 0.0;
445        };
446        let added = liters.max(0.0).min(capacity - self.fuel);
447        self.fuel += added;
448        added
449    }
450
451    /// Sets the cargo in kilograms, limited to 0..=[`Vehicle::max_load`]. Returns the new
452    /// load.
453    pub fn set_load(&mut self, kg: f32) -> f32 {
454        self.load = kg.clamp(0.0, self.max_load);
455        self.load
456    }
457
458    /// Meters driven so far (forward and backward), like an odometer.
459    pub fn distance(&self) -> f32 {
460        self.distance
461    }
462
463    // ----------------------------------------------------------------- simulation
464
465    /// Updates speed, gear, rpm and fuel for one step of `dt` seconds. `mass` is the
466    /// body's mass. Returns the turn in degrees and what changed.
467    pub(crate) fn simulate(&mut self, mass: f32, dt: f32) -> (f32, StepReport) {
468        let weight = (mass + self.load).max(1.0);
469        let throttle = self.throttle.clamp(0.0, 1.0);
470        let brake = self.brake.clamp(0.0, 1.0);
471        let v = self.speed;
472        if brake == 0.0 {
473            self.hold.0 = false;
474        }
475        if throttle == 0.0 {
476            self.hold.1 = false;
477        }
478
479        // Arcade pedals: moving backwards, gas brakes; standing still, the brake reverses.
480        let (drive, pedal_brake) = if v > STOPPED {
481            self.hold.0 |= brake > 0.0;
482            (throttle, brake)
483        } else if v < -STOPPED {
484            self.hold.1 |= throttle > 0.0;
485            (-brake, throttle)
486        } else {
487            let (brake_held, gas_held) = self.hold;
488            let gas = if gas_held { 0.0 } else { throttle };
489            let reverse = if brake_held { 0.0 } else { brake };
490            let holding = if brake_held { brake } else { 0.0 }.max(if gas_held { throttle } else { 0.0 });
491            (gas - reverse, holding)
492        };
493        let drive = if self.engine_on && !self.handbrake { drive } else { 0.0 };
494
495        let mut accel = 0.0;
496        if drive > 0.0 {
497            let falloff = 1.0 - (v.max(0.0) / self.top_speed).powi(2);
498            accel = self.engine_force * drive * falloff.max(0.0) / weight;
499        } else if drive < 0.0 {
500            let falloff = 1.0 - ((-v).max(0.0) / self.reverse_speed.max(0.01)).powi(2);
501            accel = -self.engine_force * -drive * falloff.max(0.0) / weight;
502        }
503        accel = accel.clamp(-self.grip, self.grip);
504
505        let mut slow = self.brake_force * pedal_brake / weight;
506        if self.handbrake {
507            slow = self.grip;
508        }
509        slow = slow.min(self.grip) + self.rolling_resistance;
510        if drive == 0.0 && pedal_brake == 0.0 && self.engine_on {
511            slow += self.engine_braking;
512        }
513        // Air drag grows with the square of the speed. Under power, the engine falloff
514        // above already includes it.
515        if drive == 0.0 {
516            let top = if v >= 0.0 { self.top_speed } else { self.reverse_speed.max(0.01) };
517            slow += AIR_DRAG * self.engine_force / weight * (v / top).powi(2);
518        }
519
520        let mut speed = v + accel * dt;
521        let drop = slow * dt;
522        if speed > 0.0 {
523            speed = (speed - drop).max(0.0);
524        } else if speed < 0.0 {
525            speed = (speed + drop).min(0.0);
526        }
527        self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
528
529        // Steering: turning needs speed, up to the turn rate limit.
530        let rate = (self.speed.abs() / self.turn_radius).to_degrees().min(self.max_turn_rate);
531        let turn = rate * self.steer.clamp(-1.0, 1.0) * self.speed.signum() * dt;
532
533        // Fuel.
534        if self.fuel_capacity.is_some() && self.engine_on {
535            let load_factor = weight / mass.max(1.0);
536            let per_meter = self.consumption / 100_000.0 * (0.3 + 0.7 * throttle.max(brake)) * load_factor;
537            self.fuel = (self.fuel - per_meter * self.speed.abs() * dt).max(0.0);
538            if self.fuel <= 0.0 {
539                self.stop_engine();
540            }
541        }
542
543        let report = StepReport { gear_changed: self.update_gearbox(drive) };
544        (turn, report)
545    }
546
547    /// Picks the gear from the speed and computes the rpm. Returns the new gear if it changed.
548    fn update_gearbox(&mut self, drive: f32) -> Option<i32> {
549        let before = self.gear;
550        if !self.engine_on {
551            self.gear = 0;
552            self.rpm = 0.0;
553            return (before != 0).then_some(0);
554        }
555        let gears = self.gears.max(1) as i32;
556        let band = |g: i32| self.top_speed * g as f32 / gears as f32;
557        let fraction = if self.speed < -0.05 || (self.speed.abs() <= 0.05 && drive < 0.0) {
558            self.gear = -1;
559            -self.speed / self.reverse_speed.max(0.01)
560        } else {
561            if self.gear < 1 {
562                self.gear = 1;
563            }
564            while self.gear < gears && self.speed > band(self.gear) * 0.95 {
565                self.gear += 1;
566            }
567            while self.gear > 1 && self.speed < band(self.gear - 1) * 0.7 {
568                self.gear -= 1;
569            }
570            self.speed / band(self.gear)
571        };
572        let revving = drive.abs() * 0.3;
573        let fraction = fraction.clamp(0.0, 1.0).max(revving);
574        self.rpm = self.idle_rpm + (self.max_rpm - self.idle_rpm) * fraction;
575        (self.gear != before).then_some(self.gear)
576    }
577
578    /// Removes the speed going into an obstacle whose surface faces `normal`, keeping the
579    /// part that slides along it. Returns the impact speed.
580    pub(crate) fn blocked(&mut self, facing: f32, normal: Vec3) -> f32 {
581        let velocity = velocity(facing, self.speed);
582        let into = velocity.dot(normal);
583        if into >= 0.0 {
584            return 0.0;
585        }
586        let slide = velocity - normal * into;
587        self.speed = slide.dot(crate::math::forward(facing)).clamp(-self.reverse_speed, self.top_speed);
588        -into
589    }
590
591    pub(crate) fn add_distance(&mut self, meters: f32) {
592        self.distance += meters.abs();
593    }
594}
595
596/// Velocity of a vehicle body facing `facing` with signed `speed`.
597pub(crate) fn velocity(facing: f32, speed: f32) -> Vec3 {
598    crate::math::forward(facing) * speed
599}
600
601#[cfg(test)]
602mod tests {
603    use crate::prelude::*;
604
605    const DT: f32 = 1.0 / 60.0;
606
607    fn run(world: &mut World, seconds: f32) {
608        for _ in 0..(seconds / DT).round() as usize {
609            world.step(DT);
610        }
611    }
612
613    /// Seconds a vehicle with full throttle needs to reach `kmh`.
614    fn time_to(world: &mut World, id: BodyId, kmh: f32) -> f32 {
615        world.vehicle_mut(id).unwrap().throttle = 1.0;
616        let mut t = 0.0;
617        while world.vehicle(id).unwrap().speed_kmh() < kmh && t < 300.0 {
618            world.step(DT);
619            t += DT;
620        }
621        t
622    }
623
624    #[test]
625    fn car_accelerates_and_stops() {
626        let mut world = World::new();
627        let car = world.add(Body::car(Vec3::ZERO));
628        let t = time_to(&mut world, car, 100.0);
629        assert!((7.0..14.0).contains(&t), "0-100 km/h took {t} s");
630        assert!(world.position(car).y > 50.0, "drives north, the default facing");
631        assert!(world.vehicle(car).unwrap().gear() > 1, "shifted up");
632        assert!((world.speed(car) * 3.6 - 100.0).abs() < 2.0);
633
634        let v = world.vehicle_mut(car).unwrap();
635        v.throttle = 0.0;
636        v.brake = 1.0;
637        run(&mut world, 5.0);
638        assert!(world.vehicle(car).unwrap().speed().abs() < 0.01, "stopped, not reversing yet");
639        world.vehicle_mut(car).unwrap().brake = 0.0;
640        world.step(DT);
641        world.vehicle_mut(car).unwrap().brake = 1.0;
642        run(&mut world, 1.0);
643        assert!(world.vehicle(car).unwrap().is_reversing(), "pressed again: reverses");
644    }
645
646    #[test]
647    fn top_speed_is_respected() {
648        let mut world = World::new();
649        let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_top_speed_kmh(50.0)));
650        world.vehicle_mut(car).unwrap().throttle = 1.0;
651        run(&mut world, 30.0);
652        assert!(world.vehicle(car).unwrap().speed_kmh() <= 50.0 + 1e-3);
653    }
654
655    #[test]
656    fn load_makes_a_truck_slower_and_brake_longer() {
657        let mut world = World::new();
658        let empty = world.add(Body::truck(Vec3::ZERO));
659        let full = world.add(Body::truck(vec3(100.0, 0.0, 0.0)));
660        world.vehicle_mut(full).unwrap().set_load(25_000.0);
661        assert_eq!(world.weight(full), 37_000.0);
662        let t_empty = time_to(&mut world, empty, 60.0);
663        world.vehicle_mut(empty).unwrap().throttle = 0.0;
664        let t_full = time_to(&mut world, full, 60.0);
665        assert!(t_full > t_empty * 2.0, "empty {t_empty} s, full {t_full} s");
666
667        for id in [empty, full] {
668            let v = world.vehicle_mut(id).unwrap();
669            v.set_speed(60.0 / 3.6);
670            v.throttle = 0.0;
671            v.brake = 1.0;
672        }
673        let (y_empty, y_full) = (world.position(empty).y, world.position(full).y);
674        run(&mut world, 10.0);
675        let stop_empty = world.position(empty).y - y_empty;
676        let stop_full = world.position(full).y - y_full;
677        assert!(stop_full > stop_empty * 2.0, "braking: empty {stop_empty} m, full {stop_full} m");
678    }
679
680    #[test]
681    fn brake_reverses_when_stopped() {
682        let mut world = World::new();
683        let car = world.add(Body::car(Vec3::ZERO));
684        world.vehicle_mut(car).unwrap().brake = 1.0;
685        run(&mut world, 3.0);
686        let v = world.vehicle(car).unwrap();
687        assert!(v.is_reversing());
688        assert_eq!(v.gear(), -1);
689        assert!(v.speed_kmh() >= -25.0 - 1e-3);
690        assert!(world.position(car).y < 0.0);
691        assert!(v.describe_speed().starts_with("reversing"));
692    }
693
694    #[test]
695    fn steering_needs_speed() {
696        let mut world = World::new();
697        let car = world.add(Body::car(Vec3::ZERO));
698        world.vehicle_mut(car).unwrap().steer = 1.0;
699        run(&mut world, 1.0);
700        assert_eq!(world.facing(car), 0.0, "a parked car does not turn");
701        world.vehicle_mut(car).unwrap().set_speed(10.0);
702        run(&mut world, 0.5);
703        let facing = world.facing(car);
704        assert!(facing > 20.0 && facing < 90.0, "turned right to {facing}");
705    }
706
707    #[test]
708    fn crash_into_wall() {
709        let mut world = World::new();
710        let wall = world.add(Body::fixed(vec3(0.0, 20.0, 0.0)).with_box(10.0, 1.0, 3.0));
711        let car = world.add(Body::car(Vec3::ZERO));
712        world.vehicle_mut(car).unwrap().set_speed(15.0);
713        let mut crash = None;
714        for _ in 0..200 {
715            world.clear_events();
716            world.step(DT);
717            if let Some(c) = world.crashed(car) {
718                crash = Some(c);
719                break;
720            }
721        }
722        let (other, speed) = crash.expect("crashed");
723        assert_eq!(other, wall);
724        assert!(speed > 10.0);
725        assert!(world.vehicle(car).unwrap().speed().abs() < 1.0, "lost its speed");
726        assert!(world.position(car).y < 19.0, "did not pass the wall");
727    }
728
729    #[test]
730    fn fuel_runs_out() {
731        let mut world = World::new();
732        let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_fuel(0.05, 10.0)));
733        world.vehicle_mut(car).unwrap().throttle = 1.0;
734        let mut t = 0.0;
735        while world.vehicle(car).unwrap().is_engine_on() && t < 120.0 {
736            world.step(DT);
737            t += DT;
738        }
739        let v = world.vehicle(car).unwrap();
740        assert!(v.is_out_of_fuel());
741        assert!(!v.is_engine_on());
742        assert_eq!(v.gear(), 0);
743        let driven = v.distance();
744        assert!((400.0..600.0).contains(&driven), "0.05 l at 10 l/100 km is 500 m, drove {driven}");
745        let v = world.vehicle_mut(car).unwrap();
746        assert!(v.refuel(10.0) > 0.0);
747        assert!(v.start_engine());
748    }
749
750    #[test]
751    fn context_shortcuts() {
752        let mut runner = Engine::new("t").headless().build_fn(|_| {});
753        runner.frame();
754        let ctx = &mut runner.ctx;
755        let car = ctx.physics.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().engine_off()));
756        assert!(!ctx.physics.vehicle(car).unwrap().is_engine_on());
757        assert!(ctx.start_engine(car));
758        assert!(!ctx.start_engine(car), "already running");
759        runner.key_down(Key::Up, false);
760        for _ in 0..30 {
761            runner.ctx.drive(car);
762            std::thread::sleep(std::time::Duration::from_millis(5));
763            runner.frame();
764        }
765        assert!(runner.ctx.physics.vehicle(car).unwrap().speed() > 0.0);
766        runner.ctx.stop_engine(car);
767        assert_eq!(runner.ctx.physics.vehicle(car).unwrap().gear(), 0);
768    }
769}