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). Always
350    /// English; [`Lang::describe_speed`](crate::lang::Lang::describe_speed) uses the current
351    /// language.
352    pub fn describe_speed(&self) -> String {
353        crate::lang::Lang::new().describe_speed(self)
354    }
355
356    /// Whether the vehicle is rolling backwards.
357    pub fn is_reversing(&self) -> bool {
358        self.speed < -0.05
359    }
360
361    /// Current gear: -1 reverse, 0 neutral (engine off), 1 and up forward.
362    pub fn gear(&self) -> i32 {
363        self.gear
364    }
365
366    /// The gear ready to speak: `"reverse"`, `"neutral"` or the number.
367    pub fn gear_name(&self) -> String {
368        match self.gear {
369            -1 => "reverse".to_string(),
370            0 => "neutral".to_string(),
371            g => g.to_string(),
372        }
373    }
374
375    /// Engine speed in revolutions per minute (0 when the engine is off).
376    pub fn rpm(&self) -> f32 {
377        self.rpm
378    }
379
380    /// Engine speed from 0.0 (idle) to 1.0 (highest rpm).
381    pub fn rpm_fraction(&self) -> f32 {
382        ((self.rpm - self.idle_rpm) / (self.max_rpm - self.idle_rpm)).clamp(0.0, 1.0)
383    }
384
385    /// A playback pitch for a looping engine sound recorded at idle: 0.8 at idle, 2.0 at
386    /// the highest rpm. The engine sound set with [`Vehicle::with_engine_sound`] uses it
387    /// automatically.
388    pub fn engine_pitch(&self) -> f32 {
389        0.8 + 1.2 * self.rpm_fraction()
390    }
391
392    /// Whether the engine runs.
393    pub fn is_engine_on(&self) -> bool {
394        self.engine_on
395    }
396
397    /// Starts the engine (silently; [`Context::start_engine`](crate::Context::start_engine)
398    /// also plays the start sound). Returns `false` if the tank is empty.
399    pub fn start_engine(&mut self) -> bool {
400        if self.is_out_of_fuel() {
401            return false;
402        }
403        self.engine_on = true;
404        if self.gear == 0 {
405            self.gear = 1;
406        }
407        self.rpm = self.rpm.max(self.idle_rpm);
408        true
409    }
410
411    /// Stops the engine. The vehicle rolls out.
412    pub fn stop_engine(&mut self) {
413        self.engine_on = false;
414        self.gear = 0;
415        self.rpm = 0.0;
416    }
417
418    /// Liters left in the tank, `None` for unlimited fuel.
419    pub fn fuel(&self) -> Option<f32> {
420        self.fuel_capacity.map(|_| self.fuel)
421    }
422
423    /// Tank level from 0.0 (empty) to 1.0 (full); always 1.0 for unlimited fuel.
424    pub fn fuel_fraction(&self) -> f32 {
425        match self.fuel_capacity {
426            Some(capacity) if capacity > 0.0 => self.fuel / capacity,
427            Some(_) => 0.0,
428            None => 1.0,
429        }
430    }
431
432    /// Whether the tank is empty (never with unlimited fuel).
433    pub fn is_out_of_fuel(&self) -> bool {
434        self.fuel_capacity.is_some() && self.fuel <= 0.0
435    }
436
437    /// Adds fuel in liters, up to the tank size. Returns how much was added.
438    pub fn refuel(&mut self, liters: f32) -> f32 {
439        let Some(capacity) = self.fuel_capacity else {
440            return 0.0;
441        };
442        let added = liters.max(0.0).min(capacity - self.fuel);
443        self.fuel += added;
444        added
445    }
446
447    /// Sets the cargo in kilograms, limited to 0..=[`Vehicle::max_load`]. Returns the new
448    /// load.
449    pub fn set_load(&mut self, kg: f32) -> f32 {
450        self.load = kg.clamp(0.0, self.max_load);
451        self.load
452    }
453
454    /// Meters driven so far (forward and backward), like an odometer.
455    pub fn distance(&self) -> f32 {
456        self.distance
457    }
458
459    // ----------------------------------------------------------------- simulation
460
461    /// Updates speed, gear, rpm and fuel for one step of `dt` seconds. `mass` is the
462    /// body's mass. Returns the turn in degrees and what changed.
463    pub(crate) fn simulate(&mut self, mass: f32, dt: f32) -> (f32, StepReport) {
464        let weight = (mass + self.load).max(1.0);
465        let throttle = self.throttle.clamp(0.0, 1.0);
466        let brake = self.brake.clamp(0.0, 1.0);
467        let v = self.speed;
468        if brake == 0.0 {
469            self.hold.0 = false;
470        }
471        if throttle == 0.0 {
472            self.hold.1 = false;
473        }
474
475        // Arcade pedals: moving backwards, gas brakes; standing still, the brake reverses.
476        let (drive, pedal_brake) = if v > STOPPED {
477            self.hold.0 |= brake > 0.0;
478            (throttle, brake)
479        } else if v < -STOPPED {
480            self.hold.1 |= throttle > 0.0;
481            (-brake, throttle)
482        } else {
483            let (brake_held, gas_held) = self.hold;
484            let gas = if gas_held { 0.0 } else { throttle };
485            let reverse = if brake_held { 0.0 } else { brake };
486            let holding = if brake_held { brake } else { 0.0 }.max(if gas_held { throttle } else { 0.0 });
487            (gas - reverse, holding)
488        };
489        let drive = if self.engine_on && !self.handbrake { drive } else { 0.0 };
490
491        let mut accel = 0.0;
492        if drive > 0.0 {
493            let falloff = 1.0 - (v.max(0.0) / self.top_speed).powi(2);
494            accel = self.engine_force * drive * falloff.max(0.0) / weight;
495        } else if drive < 0.0 {
496            let falloff = 1.0 - ((-v).max(0.0) / self.reverse_speed.max(0.01)).powi(2);
497            accel = -self.engine_force * -drive * falloff.max(0.0) / weight;
498        }
499        accel = accel.clamp(-self.grip, self.grip);
500
501        let mut slow = self.brake_force * pedal_brake / weight;
502        if self.handbrake {
503            slow = self.grip;
504        }
505        slow = slow.min(self.grip) + self.rolling_resistance;
506        if drive == 0.0 && pedal_brake == 0.0 && self.engine_on {
507            slow += self.engine_braking;
508        }
509        // Air drag grows with the square of the speed. Under power, the engine falloff
510        // above already includes it.
511        if drive == 0.0 {
512            let top = if v >= 0.0 { self.top_speed } else { self.reverse_speed.max(0.01) };
513            slow += AIR_DRAG * self.engine_force / weight * (v / top).powi(2);
514        }
515
516        let mut speed = v + accel * dt;
517        let drop = slow * dt;
518        if speed > 0.0 {
519            speed = (speed - drop).max(0.0);
520        } else if speed < 0.0 {
521            speed = (speed + drop).min(0.0);
522        }
523        self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
524
525        // Steering: turning needs speed, up to the turn rate limit.
526        let rate = (self.speed.abs() / self.turn_radius).to_degrees().min(self.max_turn_rate);
527        let turn = rate * self.steer.clamp(-1.0, 1.0) * self.speed.signum() * dt;
528
529        // Fuel.
530        if self.fuel_capacity.is_some() && self.engine_on {
531            let load_factor = weight / mass.max(1.0);
532            let per_meter = self.consumption / 100_000.0 * (0.3 + 0.7 * throttle.max(brake)) * load_factor;
533            self.fuel = (self.fuel - per_meter * self.speed.abs() * dt).max(0.0);
534            if self.fuel <= 0.0 {
535                self.stop_engine();
536            }
537        }
538
539        let report = StepReport { gear_changed: self.update_gearbox(drive) };
540        (turn, report)
541    }
542
543    /// Picks the gear from the speed and computes the rpm. Returns the new gear if it changed.
544    fn update_gearbox(&mut self, drive: f32) -> Option<i32> {
545        let before = self.gear;
546        if !self.engine_on {
547            self.gear = 0;
548            self.rpm = 0.0;
549            return (before != 0).then_some(0);
550        }
551        let gears = self.gears.max(1) as i32;
552        let band = |g: i32| self.top_speed * g as f32 / gears as f32;
553        let fraction = if self.speed < -0.05 || (self.speed.abs() <= 0.05 && drive < 0.0) {
554            self.gear = -1;
555            -self.speed / self.reverse_speed.max(0.01)
556        } else {
557            if self.gear < 1 {
558                self.gear = 1;
559            }
560            while self.gear < gears && self.speed > band(self.gear) * 0.95 {
561                self.gear += 1;
562            }
563            while self.gear > 1 && self.speed < band(self.gear - 1) * 0.7 {
564                self.gear -= 1;
565            }
566            self.speed / band(self.gear)
567        };
568        let revving = drive.abs() * 0.3;
569        let fraction = fraction.clamp(0.0, 1.0).max(revving);
570        self.rpm = self.idle_rpm + (self.max_rpm - self.idle_rpm) * fraction;
571        (self.gear != before).then_some(self.gear)
572    }
573
574    /// Removes the speed going into an obstacle whose surface faces `normal`, keeping the
575    /// part that slides along it. Returns the impact speed.
576    pub(crate) fn blocked(&mut self, facing: f32, normal: Vec3) -> f32 {
577        let velocity = velocity(facing, self.speed);
578        let into = velocity.dot(normal);
579        if into >= 0.0 {
580            return 0.0;
581        }
582        let slide = velocity - normal * into;
583        self.speed = slide.dot(crate::math::forward(facing)).clamp(-self.reverse_speed, self.top_speed);
584        -into
585    }
586
587    pub(crate) fn add_distance(&mut self, meters: f32) {
588        self.distance += meters.abs();
589    }
590}
591
592/// Velocity of a vehicle body facing `facing` with signed `speed`.
593pub(crate) fn velocity(facing: f32, speed: f32) -> Vec3 {
594    crate::math::forward(facing) * speed
595}
596
597#[cfg(test)]
598mod tests {
599    use crate::prelude::*;
600
601    const DT: f32 = 1.0 / 60.0;
602
603    fn run(world: &mut World, seconds: f32) {
604        for _ in 0..(seconds / DT).round() as usize {
605            world.step(DT);
606        }
607    }
608
609    /// Seconds a vehicle with full throttle needs to reach `kmh`.
610    fn time_to(world: &mut World, id: BodyId, kmh: f32) -> f32 {
611        world.vehicle_mut(id).unwrap().throttle = 1.0;
612        let mut t = 0.0;
613        while world.vehicle(id).unwrap().speed_kmh() < kmh && t < 300.0 {
614            world.step(DT);
615            t += DT;
616        }
617        t
618    }
619
620    #[test]
621    fn car_accelerates_and_stops() {
622        let mut world = World::new();
623        let car = world.add(Body::car(Vec3::ZERO));
624        let t = time_to(&mut world, car, 100.0);
625        assert!((7.0..14.0).contains(&t), "0-100 km/h took {t} s");
626        assert!(world.position(car).y > 50.0, "drives north, the default facing");
627        assert!(world.vehicle(car).unwrap().gear() > 1, "shifted up");
628        assert!((world.speed(car) * 3.6 - 100.0).abs() < 2.0);
629
630        let v = world.vehicle_mut(car).unwrap();
631        v.throttle = 0.0;
632        v.brake = 1.0;
633        run(&mut world, 5.0);
634        assert!(world.vehicle(car).unwrap().speed().abs() < 0.01, "stopped, not reversing yet");
635        world.vehicle_mut(car).unwrap().brake = 0.0;
636        world.step(DT);
637        world.vehicle_mut(car).unwrap().brake = 1.0;
638        run(&mut world, 1.0);
639        assert!(world.vehicle(car).unwrap().is_reversing(), "pressed again: reverses");
640    }
641
642    #[test]
643    fn top_speed_is_respected() {
644        let mut world = World::new();
645        let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_top_speed_kmh(50.0)));
646        world.vehicle_mut(car).unwrap().throttle = 1.0;
647        run(&mut world, 30.0);
648        assert!(world.vehicle(car).unwrap().speed_kmh() <= 50.0 + 1e-3);
649    }
650
651    #[test]
652    fn load_makes_a_truck_slower_and_brake_longer() {
653        let mut world = World::new();
654        let empty = world.add(Body::truck(Vec3::ZERO));
655        let full = world.add(Body::truck(vec3(100.0, 0.0, 0.0)));
656        world.vehicle_mut(full).unwrap().set_load(25_000.0);
657        assert_eq!(world.weight(full), 37_000.0);
658        let t_empty = time_to(&mut world, empty, 60.0);
659        world.vehicle_mut(empty).unwrap().throttle = 0.0;
660        let t_full = time_to(&mut world, full, 60.0);
661        assert!(t_full > t_empty * 2.0, "empty {t_empty} s, full {t_full} s");
662
663        for id in [empty, full] {
664            let v = world.vehicle_mut(id).unwrap();
665            v.set_speed(60.0 / 3.6);
666            v.throttle = 0.0;
667            v.brake = 1.0;
668        }
669        let (y_empty, y_full) = (world.position(empty).y, world.position(full).y);
670        run(&mut world, 10.0);
671        let stop_empty = world.position(empty).y - y_empty;
672        let stop_full = world.position(full).y - y_full;
673        assert!(stop_full > stop_empty * 2.0, "braking: empty {stop_empty} m, full {stop_full} m");
674    }
675
676    #[test]
677    fn brake_reverses_when_stopped() {
678        let mut world = World::new();
679        let car = world.add(Body::car(Vec3::ZERO));
680        world.vehicle_mut(car).unwrap().brake = 1.0;
681        run(&mut world, 3.0);
682        let v = world.vehicle(car).unwrap();
683        assert!(v.is_reversing());
684        assert_eq!(v.gear(), -1);
685        assert!(v.speed_kmh() >= -25.0 - 1e-3);
686        assert!(world.position(car).y < 0.0);
687        assert!(v.describe_speed().starts_with("reversing"));
688    }
689
690    #[test]
691    fn steering_needs_speed() {
692        let mut world = World::new();
693        let car = world.add(Body::car(Vec3::ZERO));
694        world.vehicle_mut(car).unwrap().steer = 1.0;
695        run(&mut world, 1.0);
696        assert_eq!(world.facing(car), 0.0, "a parked car does not turn");
697        world.vehicle_mut(car).unwrap().set_speed(10.0);
698        run(&mut world, 0.5);
699        let facing = world.facing(car);
700        assert!(facing > 20.0 && facing < 90.0, "turned right to {facing}");
701    }
702
703    #[test]
704    fn crash_into_wall() {
705        let mut world = World::new();
706        let wall = world.add(Body::fixed(vec3(0.0, 20.0, 0.0)).with_box(10.0, 1.0, 3.0));
707        let car = world.add(Body::car(Vec3::ZERO));
708        world.vehicle_mut(car).unwrap().set_speed(15.0);
709        let mut crash = None;
710        for _ in 0..200 {
711            world.clear_events();
712            world.step(DT);
713            if let Some(c) = world.crashed(car) {
714                crash = Some(c);
715                break;
716            }
717        }
718        let (other, speed) = crash.expect("crashed");
719        assert_eq!(other, wall);
720        assert!(speed > 10.0);
721        assert!(world.vehicle(car).unwrap().speed().abs() < 1.0, "lost its speed");
722        assert!(world.position(car).y < 19.0, "did not pass the wall");
723    }
724
725    #[test]
726    fn fuel_runs_out() {
727        let mut world = World::new();
728        let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_fuel(0.05, 10.0)));
729        world.vehicle_mut(car).unwrap().throttle = 1.0;
730        let mut t = 0.0;
731        while world.vehicle(car).unwrap().is_engine_on() && t < 120.0 {
732            world.step(DT);
733            t += DT;
734        }
735        let v = world.vehicle(car).unwrap();
736        assert!(v.is_out_of_fuel());
737        assert!(!v.is_engine_on());
738        assert_eq!(v.gear(), 0);
739        let driven = v.distance();
740        assert!((400.0..600.0).contains(&driven), "0.05 l at 10 l/100 km is 500 m, drove {driven}");
741        let v = world.vehicle_mut(car).unwrap();
742        assert!(v.refuel(10.0) > 0.0);
743        assert!(v.start_engine());
744    }
745
746    #[test]
747    fn context_shortcuts() {
748        let mut runner = Engine::new("t").headless().build_fn(|_| {});
749        runner.frame();
750        let ctx = &mut runner.ctx;
751        let car = ctx.physics.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().engine_off()));
752        assert!(!ctx.physics.vehicle(car).unwrap().is_engine_on());
753        assert!(ctx.start_engine(car));
754        assert!(!ctx.start_engine(car), "already running");
755        runner.key_down(Key::Up, false);
756        for _ in 0..30 {
757            runner.ctx.drive(car);
758            std::thread::sleep(std::time::Duration::from_millis(5));
759            runner.frame();
760        }
761        assert!(runner.ctx.physics.vehicle(car).unwrap().speed() > 0.0);
762        runner.ctx.stop_engine(car);
763        assert_eq!(runner.ctx.physics.vehicle(car).unwrap().gear(), 0);
764    }
765}