1use glam::Vec3;
45
46const STOPPED: f32 = 0.3;
49
50const AIR_DRAG: f32 = 0.3;
52
53#[derive(Debug, Clone, PartialEq)]
58pub struct Vehicle {
59 pub top_speed: f32,
62 pub reverse_speed: f32,
64 pub engine_force: f32,
67 pub brake_force: f32,
70 pub grip: f32,
73 pub rolling_resistance: f32,
75 pub engine_braking: f32,
77 pub turn_radius: f32,
79 pub max_turn_rate: f32,
81 pub gears: u32,
83 pub idle_rpm: f32,
85 pub max_rpm: f32,
87 pub load: f32,
90 pub max_load: f32,
92 pub fuel_capacity: Option<f32>,
94 pub consumption: f32,
97 pub engine_sound: Option<String>,
100 pub horn_sound: Option<String>,
102 pub start_sound: Option<String>,
104 pub shift_sound: Option<String>,
106 pub crash_sound: Option<String>,
108
109 pub throttle: f32,
111 pub brake: f32,
114 pub steer: f32,
116 pub handbrake: bool,
118
119 speed: f32,
120 gear: i32,
121 rpm: f32,
122 engine_on: bool,
123 fuel: f32,
124 distance: f32,
125 hold: (bool, bool),
128}
129
130impl Default for Vehicle {
131 fn default() -> Self {
132 Self::car()
133 }
134}
135
136#[derive(Debug, Default)]
138pub(crate) struct StepReport {
139 pub gear_changed: Option<i32>,
140}
141
142impl Vehicle {
143 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 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 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 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 pub fn with_engine_force(mut self, newtons: f32) -> Self {
222 self.engine_force = newtons.max(0.0);
223 self
224 }
225
226 pub fn with_brake_force(mut self, newtons: f32) -> Self {
228 self.brake_force = newtons.max(0.0);
229 self
230 }
231
232 pub fn with_grip(mut self, grip: f32) -> Self {
234 self.grip = grip.max(0.01);
235 self
236 }
237
238 pub fn with_rolling_resistance(mut self, resistance: f32) -> Self {
240 self.rolling_resistance = resistance.max(0.0);
241 self
242 }
243
244 pub fn with_engine_braking(mut self, braking: f32) -> Self {
246 self.engine_braking = braking.max(0.0);
247 self
248 }
249
250 pub fn with_turn_radius(mut self, meters: f32) -> Self {
252 self.turn_radius = meters.max(0.1);
253 self
254 }
255
256 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 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 pub fn with_load(mut self, kg: f32) -> Self {
273 self.set_load(kg);
274 self
275 }
276
277 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 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 pub fn with_engine_sound(mut self, name: impl Into<String>) -> Self {
295 self.engine_sound = Some(name.into());
296 self
297 }
298
299 pub fn with_horn_sound(mut self, name: impl Into<String>) -> Self {
301 self.horn_sound = Some(name.into());
302 self
303 }
304
305 pub fn with_start_sound(mut self, name: impl Into<String>) -> Self {
307 self.start_sound = Some(name.into());
308 self
309 }
310
311 pub fn with_shift_sound(mut self, name: impl Into<String>) -> Self {
313 self.shift_sound = Some(name.into());
314 self
315 }
316
317 pub fn with_crash_sound(mut self, name: impl Into<String>) -> Self {
319 self.crash_sound = Some(name.into());
320 self
321 }
322
323 pub fn engine_off(mut self) -> Self {
326 self.engine_on = false;
327 self.rpm = 0.0;
328 self
329 }
330
331 pub fn speed(&self) -> f32 {
335 self.speed
336 }
337
338 pub fn speed_kmh(&self) -> f32 {
340 self.speed * 3.6
341 }
342
343 pub fn set_speed(&mut self, speed: f32) {
346 self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
347 }
348
349 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 pub fn is_reversing(&self) -> bool {
362 self.speed < -0.05
363 }
364
365 pub fn gear(&self) -> i32 {
367 self.gear
368 }
369
370 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 pub fn rpm(&self) -> f32 {
381 self.rpm
382 }
383
384 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 pub fn engine_pitch(&self) -> f32 {
393 0.8 + 1.2 * self.rpm_fraction()
394 }
395
396 pub fn is_engine_on(&self) -> bool {
398 self.engine_on
399 }
400
401 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 pub fn stop_engine(&mut self) {
417 self.engine_on = false;
418 self.gear = 0;
419 self.rpm = 0.0;
420 }
421
422 pub fn fuel(&self) -> Option<f32> {
424 self.fuel_capacity.map(|_| self.fuel)
425 }
426
427 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 pub fn is_out_of_fuel(&self) -> bool {
438 self.fuel_capacity.is_some() && self.fuel <= 0.0
439 }
440
441 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 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 pub fn distance(&self) -> f32 {
460 self.distance
461 }
462
463 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 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 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 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 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 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 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
596pub(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 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}