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 {
353 crate::lang::Lang::new().describe_speed(self)
354 }
355
356 pub fn is_reversing(&self) -> bool {
358 self.speed < -0.05
359 }
360
361 pub fn gear(&self) -> i32 {
363 self.gear
364 }
365
366 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 pub fn rpm(&self) -> f32 {
377 self.rpm
378 }
379
380 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 pub fn engine_pitch(&self) -> f32 {
389 0.8 + 1.2 * self.rpm_fraction()
390 }
391
392 pub fn is_engine_on(&self) -> bool {
394 self.engine_on
395 }
396
397 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 pub fn stop_engine(&mut self) {
413 self.engine_on = false;
414 self.gear = 0;
415 self.rpm = 0.0;
416 }
417
418 pub fn fuel(&self) -> Option<f32> {
420 self.fuel_capacity.map(|_| self.fuel)
421 }
422
423 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 pub fn is_out_of_fuel(&self) -> bool {
434 self.fuel_capacity.is_some() && self.fuel <= 0.0
435 }
436
437 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 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 pub fn distance(&self) -> f32 {
456 self.distance
457 }
458
459 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 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 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 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 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 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 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
592pub(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 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}