Use the original computer drivers, car collisions and towing

Decoded from SKIDMARKS_CD32:

- Computer drivers: the path of nodes laid 2 game units apart along
  the middle of the road, with side points 1 unit in from each wall.
  Drivers aim four nodes past where they'll be in four frames, or cut
  to the other side two nodes on at 70% throttle when running wide.
  Replaces the pure-pursuit AI; laps are quicker on every track.
- Collisions: six points on the player's car (corners and sides, from
  .set words 6 and 7, now known to be half-length and half-width) are
  tested against the other car's box. Hits push the cars apart with a
  strength that grows with their speed difference, and spin the car.
  As in the original, only human players' cars collide; cars at
  different heights don't.
- Towing: the hitch is at the car's back, the caravan sits 0.656 units
  behind it, and past 90 degrees both car and caravan bend back.

The reconstructed collision weight (`mass`) is gone; old asset files
with it still load.
This commit is contained in:
2026-10-20 10:15:33 +00:00
parent 54e146ce36
commit a4445d521f
18 changed files with 511 additions and 253 deletions
+22 -44
View File
@@ -44,10 +44,10 @@ pub const SHEET_H: usize = SPR_H * TILTS;
/// | 2 | steering rate |
/// | 3 | read into the car but never used |
/// | 4 | gravity |
/// | 6 | tow hitch distance behind the car |
/// | 6, 7 | half the car's length and width, for collisions; the tow hitch is at the back |
/// | 8, 9, 10 | human drivers' drag, grip and power |
///
/// Words 7 and 11-15 aren't read.
/// Words 11-15 aren't read.
pub type SetValues = [u16; 16];
/// The Camaro's values, for vehicles that come without any.
@@ -80,15 +80,15 @@ pub struct Spec {
pub turn: f32,
/// Pixels per frame per frame.
pub gravity: f32,
/// Tow hitch distance behind the car's centre, world units.
pub hitch: f32,
/// Weight in collisions between cars. Not from the original data.
pub mass: f32,
/// Half the car's length and width, world units. The tow hitch is
/// `half_length` behind the centre.
pub half_length: f32,
pub half_width: f32,
}
impl Spec {
/// Decodes the values as the executable does (fixed-point shifts).
pub fn from_set(v: &SetValues, mass: f32) -> Spec {
pub fn from_set(v: &SetValues) -> Spec {
let f = |k: usize, shift: u32| v[k] as i16 as f32 / (1u32 << shift) as f32;
Spec {
player: Handling {
@@ -103,8 +103,8 @@ impl Spec {
},
turn: f(2, 4),
gravity: f(4, 3),
hitch: f(6, 5) * super::track::CELL,
mass,
half_length: f(6, 5) * super::track::CELL,
half_width: f(7, 5) * super::track::CELL,
}
}
}
@@ -153,20 +153,6 @@ impl Spec {
}
}
/// Collision weight. The original's car-to-car physics isn't decoded, so
/// these are guesses.
pub fn default_mass(id: &str) -> f32 {
match id {
"f1" => 0.9,
"mini" => 0.8,
"midget" => 0.85,
"truck" => 1.8,
"police" => 1.2,
"cow" => 1.4,
_ => 1.0,
}
}
pub struct Vehicle {
pub id: String,
pub name: String,
@@ -212,7 +198,7 @@ impl Vehicle {
trailer: id == "caravan",
set: DEFAULT_SET,
menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, default_mass(&id)),
spec: Spec::from_set(&DEFAULT_SET),
id,
frames,
}))
@@ -235,7 +221,7 @@ impl Vehicle {
for (i, v) in self.set.iter_mut().enumerate() {
*v = crate::formats::be16(body, i * 2);
}
self.spec = Spec::from_set(&self.set, self.spec.mass);
self.spec = Spec::from_set(&self.set);
let pic = &body[32..32 + 512];
let mut px = vec![0u8; SPR_W * SPR_H];
for (i, p) in px.iter_mut().enumerate() {
@@ -293,7 +279,7 @@ impl Vehicle {
trailer: id == "caravan",
set: DEFAULT_SET,
menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, default_mass(id)),
spec: Spec::from_set(&DEFAULT_SET),
frames,
};
if let Some(txt) = txt {
@@ -329,12 +315,11 @@ impl Vehicle {
# original game does. Counting from 1:\n\
# 1 2 6 computer drivers: drag, grip, power\n\
# 9 10 11 human drivers: drag, grip, power\n\
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused.\n\
# 3 steering, 5 gravity, 7 and 8 half the car's length and width\n\
# (for collisions; the tow hitch is at the back). The rest are unused.\n\
# Top speed = power * drag / (64 - drag) / 32 world units per frame,\n\
# 50 frames a second (drag below 64). Higher grip, less sliding.\n\
handling {set}\n\
# weight when cars collide (not from the original data)\n\
mass {mass}\n",
handling {set}\n",
id = self.id,
name = self.name,
driver = self.driver,
@@ -345,7 +330,6 @@ impl Vehicle {
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
mass = self.spec.mass,
)
}
@@ -375,18 +359,12 @@ impl Vehicle {
self.set = [0; 16];
self.set[..vals.len()].copy_from_slice(&vals);
}
"mass" => {
self.spec.mass = val
.parse::<f32>()
.map_err(|_| bad(&format!("`{val}` is not a number")))?
.max(0.05)
}
// Settings from before the original handling was decoded.
"top" | "accel" | "brake" | "grip" | "turn" | "offroad" => {}
"top" | "accel" | "brake" | "grip" | "turn" | "offroad" | "mass" => {}
_ => return Err(bad(&format!("unknown setting `{key}`"))),
}
}
self.spec = Spec::from_set(&self.set, self.spec.mass);
self.spec = Spec::from_set(&self.set);
Ok(())
}
@@ -451,13 +429,13 @@ mod tests {
trailer: false,
set: DEFAULT_SET,
menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, 1.0),
spec: Spec::from_set(&DEFAULT_SET),
frames,
};
let mut v = blank(vec![0; TILTS * HEADINGS * SPR_W * SPR_H]);
v.driver = "SIMON".into();
v.set = [60, 71, 30, 0, 6, 16, 24, 12, 58, 42, 29, 0, 0, 0, 0, 0];
v.spec = Spec::from_set(&v.set, 0.9);
v.spec = Spec::from_set(&v.set);
v.name = "FORMULA 1".into();
let txt = v.settings_text();
let mut w = blank(Vec::new());
@@ -466,7 +444,7 @@ mod tests {
assert_eq!(w.name, "FORMULA 1");
assert_eq!(w.driver, "SIMON");
assert_eq!(w.set, v.set);
assert!(w.apply_settings("mass heavy").is_err());
assert!(w.apply_settings("handling 1 x").is_err());
// Older files with reconstructed handling still load.
assert!(w.apply_settings("original 1 2 3\ntop 200").is_ok());
assert_eq!(&w.set[..4], &[1, 2, 3, 0]);
@@ -474,11 +452,11 @@ mod tests {
#[test]
fn decodes_set_values() {
let s = Spec::from_set(&DEFAULT_SET, 1.0);
let s = Spec::from_set(&DEFAULT_SET);
assert_eq!(s.player.drag, 49.0 / 64.0);
assert_eq!(s.computer.power, 22.0 / 512.0);
assert_eq!(s.turn, 2.0);
assert_eq!(s.hitch, 11.0);
assert_eq!((s.half_length, s.half_width), (11.0, 8.0));
let p = s.with_preset(1);
let top = p.player.power * p.player.drag / (1.0 - p.player.drag);
assert!((top - 26.0 / 128.0).abs() < 1e-5);
+172 -67
View File
@@ -1,81 +1,165 @@
//! Computer drivers.
//! Computer drivers, as decoded from the executable's move routine.
//!
//! As in the executable, a computer car steers like a joystick: full lock
//! towards a target point ahead, or straight on when it is dead ahead, and
//! drives flat out. The cars' computer handling (`vehicle::Handling`) is
//! what makes them take bends at that. The target is a point a little way
//! along the road (`Track::edges`) at the driver's own line across it. A
//! driver that stops moving backs out.
//! At set-up the game lays a path of nodes along the middle of the road,
//! 2 game units (32 world units) apart. Each node also has a point on
//! either side, 1 game unit in from the wall. Each frame a computer
//! driver:
//!
//! 1. predicts where it will be in four frames, and finds the first node
//! that point hasn't reached;
//! 2. aims at the middle of the road four nodes on, at full throttle;
//! 3. but if the predicted point is beyond the line joining the side
//! points of that node and the one before (running wide on one side),
//! aims at the other side's point two nodes on, at 70% throttle;
//! 4. steers like a joystick: full lock towards the target, from where it
//! is now.
//!
//! The cars' computer handling (`vehicle::Handling`) is what lets them take
//! the bends at that. On top of the original, a driver that stops moving
//! backs out.
use super::car::{angle_diff, Car, Controls};
use crate::data::track::Track;
use super::car::{angle_diff, Car, Controls, DT};
use crate::data::track::Gate;
/// Heading error, in radians, below which the driver doesn't steer.
const DEAD_AHEAD: f32 = 0.06;
/// Nodes ahead for the usual target, and for the target across the road.
const AHEAD: usize = 4;
const ACROSS_AHEAD: usize = 2;
/// Throttle while cutting across the road.
const ACROSS_THROTTLE: f32 = 0.7;
/// Distance between nodes, and from each wall to the side points.
const NODE_SPACING: f32 = 32.0;
const SIDE_INSET: f32 = 16.0;
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct Node {
pub centre: (f32, f32),
/// The side points, near edge `a` and near edge `b` of the road.
pub side_a: (f32, f32),
pub side_b: (f32, f32),
}
/// The computer drivers' path round the road.
#[derive(Clone, Debug, Default)]
pub struct Path {
pub nodes: Vec<Node>,
/// For each road cross-section (`Track::edges`), the first node at or
/// after it.
first: Vec<usize>,
}
impl Path {
pub fn new(edges: &[Gate]) -> Path {
let m = edges.len();
let mut path = Path::default();
if m < 3 {
return path;
}
let mut first = vec![usize::MAX; m];
// Distance along the middle of the road to the next node.
let mut to_next = 0.0f32;
for k in 0..m {
let (g, h) = (edges[k], edges[(k + 1) % m]);
let (p, q) = (g.mid(), h.mid());
let len = (q.0 - p.0).hypot(q.1 - p.1);
let mut at = to_next;
while at < len {
let f = at / len.max(1e-3);
let lerp =
|u: (f32, f32), v: (f32, f32)| (u.0 + (v.0 - u.0) * f, u.1 + (v.1 - u.1) * f);
let (a, b) = (lerp(g.a, h.a), lerp(g.b, h.b));
let width = (b.0 - a.0).hypot(b.1 - a.1).max(1e-3);
let inset = SIDE_INSET.min(width * 0.5);
let (ux, uy) = ((b.0 - a.0) / width, (b.1 - a.1) / width);
if first[k] == usize::MAX {
first[k] = path.nodes.len();
}
path.nodes.push(Node {
centre: lerp(p, q),
side_a: (a.0 + ux * inset, a.1 + uy * inset),
side_b: (b.0 - ux * inset, b.1 - uy * inset),
});
at += NODE_SPACING;
}
to_next = at - len;
}
// Sections without a node of their own point at the next one.
let mut next = 0;
for k in (0..m).rev() {
if first[k] == usize::MAX {
first[k] = next;
} else {
next = first[k];
}
}
path.first = first;
path
}
fn node(&self, i: usize) -> Node {
self.nodes[i % self.nodes.len()]
}
/// Whether `p` has reached node `i`: it is past the line across the
/// road there.
fn reached(&self, i: usize, p: (f32, f32)) -> bool {
let (here, next) = (self.node(i), self.node(i + 1));
let (fx, fy) = (next.centre.0 - here.centre.0, next.centre.1 - here.centre.1);
(p.0 - here.centre.0) * fx + (p.1 - here.centre.1) * fy > 0.0
}
}
#[derive(Clone, Debug, Default)]
pub struct Ai {
/// Where across the road this driver aims (0 = one edge, 1 = the other).
pub line: f32,
/// Fraction of full throttle this driver uses.
pub pace: f32,
stuck_for: f32,
reversing: f32,
}
impl Ai {
pub fn new(line: f32, pace: f32) -> Ai {
Ai {
line,
pace,
stuck_for: 0.0,
reversing: 0.0,
}
pub fn new() -> Ai {
Ai::default()
}
/// The point `dist` along the road from the car's cross-section, at
/// this driver's line.
pub fn target(&self, car: &Car, track: &Track, dist: f32) -> (f32, f32) {
let e = &track.edges;
let m = e.len();
let at = |k: usize| {
let g = e[k % m];
(
g.a.0 + (g.b.0 - g.a.0) * self.line,
g.a.1 + (g.b.1 - g.a.1) * self.line,
)
};
let mut k = car.section + 1;
let mut p = at(k);
let mut left = dist - (p.0 - car.x).hypot(p.1 - car.y);
while left > 0.0 && k < car.section + m {
let q = at(k + 1);
let len = (q.0 - p.0).hypot(q.1 - p.1);
if len >= left {
let f = left / len;
return (p.0 + (q.0 - p.0) * f, p.1 + (q.1 - p.1) * f);
/// Where the driver aims, and how much throttle it uses.
pub fn target(&self, car: &Car, path: &Path) -> ((f32, f32), f32) {
let p = (car.x + 4.0 * car.vx * DT, car.y + 4.0 * car.vy * DT);
let n = path.nodes.len();
// Start from the car's own place on the road (offset by a lap so
// stepping back can't underflow), then find the first node the
// predicted point hasn't reached.
let mut i = path.first.get(car.section).copied().unwrap_or(0) + n;
for _ in 0..2 {
if path.reached(i - 1, p) {
break;
}
left -= len;
p = q;
k += 1;
i -= 1;
}
p
for _ in 0..8 {
if !path.reached(i, p) {
break;
}
i += 1;
}
let (prev, here) = (path.node(i - 1), path.node(i));
let across = path.node(i + ACROSS_AHEAD);
let mut aim = (path.node(i + AHEAD).centre, 1.0);
if beyond(prev.side_a, here.side_a, here.side_b, p) {
aim = (across.side_b, ACROSS_THROTTLE);
}
if beyond(prev.side_b, here.side_b, here.side_a, p) {
aim = (across.side_a, ACROSS_THROTTLE);
}
aim
}
pub fn drive(&mut self, car: &Car, track: &Track, dt: f32, racing: bool) -> Controls {
if !racing || track.edges.len() < 3 {
pub fn drive(&mut self, car: &Car, path: &Path, dt: f32, racing: bool) -> Controls {
if !racing || path.nodes.len() < AHEAD + 2 {
return Controls::default();
}
let speed = car.speed();
let look = 30.0 + speed * 0.2;
let (tx, ty) = self.target(car, track, look);
let ((tx, ty), throttle) = self.target(car, path);
let diff = angle_diff((ty - car.y).atan2(tx - car.x), car.heading);
let steer = if diff.abs() < DEAD_AHEAD {
0.0
} else {
diff.signum()
};
let steer = if diff == 0.0 { 0.0 } else { diff.signum() };
let speed = car.speed();
if self.reversing > 0.0 {
self.reversing -= dt;
return Controls {
@@ -93,18 +177,39 @@ impl Ai {
} else {
self.stuck_for = 0.0;
}
if diff.abs() > 2.3 && speed < 35.0 {
// Facing the wrong way: reverse-turn round.
return Controls {
throttle: 0.0,
brake: 1.0,
steer: -steer,
};
}
Controls {
throttle: self.pace,
throttle,
brake: 0.0,
steer,
}
}
}
/// Whether `p` is on the far side of the line `a`-`b` from `inside`.
fn beyond(a: (f32, f32), b: (f32, f32), inside: (f32, f32), p: (f32, f32)) -> bool {
let side = |q: (f32, f32)| (b.0 - a.0) * (q.1 - a.1) - (b.1 - a.1) * (q.0 - a.0);
side(p) * side(inside) < 0.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_nodes_are_evenly_spaced_and_inset() {
// A straight road 40 units wide along x, as a loop of sections.
let edges: Vec<Gate> = (0..40)
.map(|i| Gate {
a: (i as f32 * 10.0, 0.0),
b: (i as f32 * 10.0, 40.0),
})
.collect();
let path = Path::new(&edges[..20]);
let n = &path.nodes;
assert!(n.len() >= 5);
let d = (n[1].centre.0 - n[0].centre.0).hypot(n[1].centre.1 - n[0].centre.1);
assert!((d - NODE_SPACING).abs() < 1e-3);
assert_eq!(n[0].side_a, (0.0, 16.0));
assert_eq!(n[0].side_b, (0.0, 24.0));
}
}
+87 -33
View File
@@ -17,16 +17,16 @@
//! - **Walls:** the road edges (`Track::edges`) are walls. A car that
//! reaches one loses the part of its velocity into the wall and bounces
//! back with half of it.
//! - **Other cars:** see `bump`.
//!
//! Human and computer drivers use separate handling from the vehicle's
//! `.set` values.
use super::DT;
pub use super::DT;
use crate::data::track::{self, Gate, Track, CELL};
use crate::data::vehicle::{self, Handling, Spec};
use std::f32::consts::{PI, TAU};
pub const RADIUS: f32 = 11.0;
/// How close the car's centre may come to a wall, in world units.
const WALL_MARGIN: f32 = 4.0;
/// The executable samples the slope 0.2 cells from the car.
@@ -335,41 +335,95 @@ pub fn nearest_section(e: &[Gate], p: (f32, f32)) -> usize {
}
}
/// Pushes overlapping cars apart and exchanges momentum.
pub fn collide(a: &mut Car, b: &mut Car) {
let (dx, dy) = (b.x - a.x, b.y - a.y);
let d2 = dx * dx + dy * dy;
let min = RADIUS * 2.0;
if d2 >= min * min || d2 < 1e-6 {
return;
}
let d = d2.sqrt();
let (nx, ny) = (dx / d, dy / d);
let overlap = min - d;
let (ia, ib) = (1.0 / a.spec.mass, 1.0 / b.spec.mass);
let push = overlap / (ia + ib);
a.x -= nx * push * ia;
a.y -= ny * push * ia;
b.x += nx * push * ib;
b.y += ny * push * ib;
/// Something a car can run into: another car or a caravan.
#[derive(Clone, Copy, Debug)]
pub struct Body {
pub x: f32,
pub y: f32,
pub z: f32,
pub vx: f32,
pub vy: f32,
pub heading: f32,
}
let vn = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny;
if vn < 0.0 {
let j = -(1.0 + 0.45) * vn / (ia + ib);
a.vx -= j * ia * nx;
a.vy -= j * ia * ny;
b.vx += j * ib * nx;
b.vy += j * ib * ny;
let hit = (-vn / 150.0).min(1.0);
a.bump = a.bump.max(hit);
b.bump = b.bump.max(hit);
// A glancing knock spins the cars a little.
let spin = (nx * a.forward().1 - ny * a.forward().0) * hit * 0.25;
a.heading = (a.heading + spin).rem_euclid(TAU);
b.heading = (b.heading - spin).rem_euclid(TAU);
impl Car {
pub fn body(&self) -> Body {
Body {
x: self.x,
y: self.y,
z: self.z,
vx: self.vx,
vy: self.vy,
heading: self.heading,
}
}
}
/// Half-width and half-length of the box that `bump` tests points against,
/// world units. The executable uses the same box for every vehicle.
const TARGET: (f32, f32) = (6.0, 12.0);
/// The executable's collision (`$25e64`) between car `a` and `b`.
///
/// Six points on `a` are tested against `b`'s box: its four corners and a
/// point on each side, 0.58 of the way out. Each point inside the box
/// pushes the two apart along that point's offset from `a`'s centre, by
/// k = 64 |relative speed|^2 + 20 (game units); the side points push 2.5
/// times as hard. Each corner inside also spins `a` so that corner swings
/// away. Returns the velocity change for `b` (world units per second);
/// `a` has already received the opposite.
pub fn bump(a: &mut Car, b: &Body) -> Option<(f32, f32)> {
// Cars more than two cells apart, or at different heights (on and
// under a bridge), can't touch.
if (a.x - b.x).abs() > 2.0 * CELL || (a.y - b.y).abs() > 2.0 * CELL || (a.z - b.z).abs() >= 17.0
{
return None;
}
let (dvx, dvy) = ((a.vx - b.vx) * DT / CELL, (a.vy - b.vy) * DT / CELL);
let k = 64.0 * (dvx * dvx + dvy * dvy) + 20.0;
let (fx, fy) = a.forward();
let (ux, uy) = (-fy, fx);
let (bfx, bfy) = (b.heading.cos(), b.heading.sin());
let (l, w) = (a.spec.half_length, a.spec.half_width);
let side = w * 0.58;
// (along, across, push factor)
let points = [
(l, w, 1.0),
(l, -w, 1.0),
(-l, -w, 1.0),
(-l, w, 1.0),
(0.0, side, 2.5),
(0.0, -side, 2.5),
];
let (mut ix, mut iy, mut turn, mut hit) = (0.0, 0.0, 0.0, false);
for (along, across, factor) in points {
let (px, py) = (fx * along + ux * across, fy * along + uy * across);
let (rx, ry) = (a.x + px - b.x, a.y + py - b.y);
let b_along = rx * bfx + ry * bfy;
let b_across = ry * bfx - rx * bfy;
if b_across.abs() >= TARGET.0 || b_along.abs() >= TARGET.1 {
continue;
}
hit = true;
let push = k * factor / 256.0 / DT;
ix += px * push;
iy += py * push;
if factor == 1.0 {
turn -= (along * across).signum() * k / 16.0;
}
}
if !hit {
return None;
}
a.vx -= ix;
a.vy -= iy;
a.spin += turn;
a.heading = (a.heading + 2.0 * turn * TAU / 64.0).rem_euclid(TAU);
let knock = (dvx.hypot(dvy) * CELL / DT / 150.0).clamp(0.2, 1.0);
a.bump = a.bump.max(knock);
Some((ix, iy))
}
pub fn angle_diff(to: f32, from: f32) -> f32 {
(to - from + PI).rem_euclid(TAU) - PI
}
+122 -56
View File
@@ -5,8 +5,8 @@ pub mod car;
use crate::data::track::{Gate, Track};
use crate::data::vehicle::Vehicle;
use ai::Ai;
use car::{Car, Controls};
use ai::{Ai, Path};
use car::{Body, Car, Controls};
pub const DT: f32 = 1.0 / 50.0;
const COUNTDOWN: f32 = 3.0;
@@ -43,13 +43,18 @@ pub struct Trailer {
pub vehicle: usize,
pub x: f32,
pub y: f32,
/// World units per second, from how far it was dragged this frame.
pub vx: f32,
pub vy: f32,
/// Pointing at the hitch.
pub heading: f32,
}
/// Tow bar length from the car's hitch (`Spec::hitch`) to the trailer's
/// centre, in world units.
const TOW_BAR: f32 = 20.0;
const TRAILER_RADIUS: f32 = 10.0;
/// From the car's hitch (at the back of the car, `Spec::half_length`
/// behind its centre) to the trailer's centre: 0.656 game units.
const TOW_BAR: f32 = 10.5;
/// The most a trailer can swing round from its car before both bend.
const JACKKNIFE: f32 = std::f32::consts::FRAC_PI_2;
/// Seconds without crossing a gate before a computer car is rescued.
const RESCUE_AFTER: f32 = 8.0;
@@ -65,7 +70,7 @@ fn rescue(e: &mut Entrant, track: &Track) {
c.heading = heading.rem_euclid(std::f32::consts::TAU);
c.place(track);
if let Some(t) = &mut e.trailer {
let back = c.spec.hitch + TOW_BAR;
let back = c.spec.half_length + TOW_BAR;
t.x = from.0 - heading.cos() * back;
t.y = from.1 - heading.sin() * back;
t.heading = heading;
@@ -73,16 +78,40 @@ fn rescue(e: &mut Entrant, track: &Track) {
}
impl Trailer {
/// Drags the trailer after its car: it keeps its distance from the
/// hitch and swings to point at it.
fn follow(&mut self, car: &Car) {
/// Drags the trailer after its car, as the executable does: it turns
/// to point at the hitch and sits a tow bar's length behind it. If it
/// ends up more than 90 degrees from the car, both are bent back to 45
/// degrees either side of the angle between them.
fn follow(&mut self, car: &mut Car) {
let (fx, fy) = car.forward();
let (hx, hy) = (car.x - fx * car.spec.hitch, car.y - fy * car.spec.hitch);
let (dx, dy) = (self.x - hx, self.y - hy);
let d = dx.hypot(dy).max(1e-3);
self.x = hx + dx / d * TOW_BAR;
self.y = hy + dy / d * TOW_BAR;
let (hx, hy) = (
car.x - fx * car.spec.half_length,
car.y - fy * car.spec.half_length,
);
self.heading = (hy - self.y).atan2(hx - self.x);
let d = car::angle_diff(self.heading, car.heading);
if d.abs() > JACKKNIFE {
let mid = car.heading + d * 0.5;
let half = JACKKNIFE * 0.5 * d.signum();
self.heading = mid + half;
car.heading = (mid - half).rem_euclid(std::f32::consts::TAU);
}
let (ox, oy) = (self.x, self.y);
self.x = hx - self.heading.cos() * TOW_BAR;
self.y = hy - self.heading.sin() * TOW_BAR;
self.vx = (self.x - ox) / DT;
self.vy = (self.y - oy) / DT;
}
fn body(&self, track: &Track) -> Body {
Body {
x: self.x,
y: self.y,
z: track.ground(self.x, self.y),
vx: self.vx,
vy: self.vy,
heading: self.heading,
}
}
}
@@ -101,6 +130,8 @@ pub enum Phase {
pub struct Race {
pub track: Track,
/// The computer drivers' path.
pub path: Path,
pub entrants: Vec<Entrant>,
pub laps: usize,
pub time: f32,
@@ -146,7 +177,7 @@ impl Race {
let start_gate = 0;
let path: Vec<(f32, f32)> = gates.iter().map(Gate::mid).collect();
let mut rng = seed | 1;
let rng = seed | 1;
let mut entrants = Vec::new();
for (i, s) in setup.iter().enumerate() {
// Two-wide grid, rows 30 units apart, following the road back
@@ -156,7 +187,7 @@ impl Race {
// Leave room for a caravan between rows when towing.
let spec = vehicles[s.vehicle].spec.with_preset(s.preset);
let spacing = if s.trailer.is_some() {
30.0 + spec.hitch + TOW_BAR + TRAILER_RADIUS
30.0 + spec.half_length + TOW_BAR + 10.0
} else {
30.0
};
@@ -166,12 +197,10 @@ impl Race {
let mut car = Car::new(s.vehicle, spec, s.team, x, y, heading);
car.computer = s.driver == Driver::Cpu;
car.place(&track);
let line = 0.3 + 0.4 * rand01(&mut rng);
let pace = 0.86 + 0.12 * rand01(&mut rng);
entrants.push(Entrant {
car,
driver: s.driver,
ai: Ai::new(line, pace),
ai: Ai::new(),
name: s.name.clone(),
passed: 0,
next_gate: start_gate,
@@ -182,11 +211,13 @@ impl Race {
progress_at: 0.0,
trailer: s.trailer.map(|t| {
let (fx, fy) = (heading.cos(), heading.sin());
let back = spec.hitch + TOW_BAR;
let back = spec.half_length + TOW_BAR;
Trailer {
vehicle: t,
x: x - fx * back,
y: y - fy * back,
vx: 0.0,
vy: 0.0,
heading,
}
}),
@@ -194,6 +225,7 @@ impl Race {
}
let ground = track.rgb();
Race {
path: Path::new(&track.edges),
track,
entrants,
laps,
@@ -235,10 +267,10 @@ impl Race {
for e in self.entrants.iter_mut() {
let controls = match (e.driver, e.finished) {
(_, Some(_)) => e.ai.drive(&e.car, &self.track, DT, racing),
(_, Some(_)) => e.ai.drive(&e.car, &self.path, DT, racing),
(Driver::Human(p), None) if racing => human.get(p).copied().unwrap_or_default(),
(Driver::Human(_), None) => Controls::default(),
(Driver::Cpu, None) => e.ai.drive(&e.car, &self.track, DT, racing),
(Driver::Cpu, None) => e.ai.drive(&e.car, &self.path, DT, racing),
};
e.car.controls = controls;
// Finished cars cruise round on autopilot at reduced pace.
@@ -246,24 +278,34 @@ impl Race {
e.car.step(&self.track, pace);
}
for i in 0..self.entrants.len() {
for j in i + 1..self.entrants.len() {
let (a, b) = self.entrants.split_at_mut(j);
car::collide(&mut a[i].car, &mut b[0].car);
for e in self.entrants.iter_mut() {
if let Some(t) = &mut e.trailer {
t.follow(&mut e.car);
}
}
// Caravans follow their cars and shove other cars out of the way.
// As in the executable, only the human players' cars collide: each
// is tested against every other car and caravan but its own.
// Computer cars pass through each other and through caravans.
for i in 0..self.entrants.len() {
let Some(mut t) = self.entrants[i].trailer else {
if !matches!(self.entrants[i].driver, Driver::Human(_)) {
continue;
};
t.follow(&self.entrants[i].car);
for (j, other) in self.entrants.iter_mut().enumerate() {
if j != i {
push_off(&mut other.car, t.x, t.y);
}
for j in 0..self.entrants.len() {
if j == i {
continue;
}
let other = self.entrants[j].car.body();
if let Some((dx, dy)) = car::bump(&mut self.entrants[i].car, &other) {
let knock = self.entrants[i].car.bump;
let b = &mut self.entrants[j].car;
b.vx += dx;
b.vy += dy;
b.bump = b.bump.max(knock);
}
if let Some(t) = self.entrants[j].trailer {
car::bump(&mut self.entrants[i].car, &t.body(&self.track));
}
}
self.entrants[i].trailer = Some(t);
}
for (e, &(px, py)) in self.entrants.iter_mut().zip(&starts) {
@@ -385,26 +427,6 @@ impl Car {
}
}
/// Moves a car out of a caravan it has run into, killing the speed it had
/// into it.
fn push_off(c: &mut Car, tx: f32, ty: f32) {
let (dx, dy) = (c.x - tx, c.y - ty);
let d = dx.hypot(dy);
let min = car::RADIUS + TRAILER_RADIUS;
if d >= min || d < 1e-3 {
return;
}
let (nx, ny) = (dx / d, dy / d);
c.x += nx * (min - d);
c.y += ny * (min - d);
let vn = c.vx * nx + c.vy * ny;
if vn < 0.0 {
c.vx -= vn * nx * 1.3;
c.vy -= vn * ny * 1.3;
c.bump = c.bump.max((-vn / 150.0).min(1.0));
}
}
fn burn(ground: &mut [u32], sx: f32, sy: f32, strength: f32) {
use crate::data::track::{H, W};
let (x, y) = (sx as isize, sy as isize);
@@ -511,7 +533,7 @@ mod tests {
start: (0.0, 0.0),
flags: 0,
};
let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET, 1.0);
let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET);
let mut c = Car::new(0, spec, 0, 512.0 + 180.0, 512.0, 0.0);
c.place(&track);
// Flat out in a straight line: the outer wall turns the car back.
@@ -522,4 +544,48 @@ mod tests {
assert!(r > 140.0 && r < 215.0, "left the road at radius {r}");
}
}
fn test_car(x: f32, y: f32, heading: f32) -> Car {
let spec = crate::data::vehicle::Spec::from_set(&crate::data::vehicle::DEFAULT_SET);
Car::new(0, spec, 0, x, y, heading)
}
#[test]
fn bump_pushes_cars_apart() {
// Side by side, overlapping: b is pushed away along +y, a along -y.
let mut a = test_car(100.0, 100.0, 0.0);
let b = test_car(100.0, 110.0, 0.0);
let (dx, dy) = car::bump(&mut a, &b.body()).expect("they overlap");
assert!(dy > 0.0 && dx.abs() < 1e-3);
assert!(a.vy < 0.0);
// Too far apart, or one on a bridge above the other.
let mut a = test_car(100.0, 100.0, 0.0);
assert!(car::bump(&mut a, &test_car(100.0, 140.0, 0.0).body()).is_none());
let mut high = test_car(100.0, 110.0, 0.0);
high.z = 20.0;
assert!(car::bump(&mut a, &high.body()).is_none());
}
#[test]
fn trailer_jackknife_bends_both() {
let mut c = test_car(100.0, 100.0, 0.0);
// A trailer straight out to the side of the car.
let mut t = Trailer {
vehicle: 0,
x: 89.0,
y: 130.0,
vx: 0.0,
vy: 0.0,
heading: 0.0,
};
t.follow(&mut c);
let d = car::angle_diff(t.heading, c.heading).abs();
assert!((d - JACKKNIFE).abs() < 1e-4, "{d}");
// It sits a tow bar's length from the hitch.
let (hx, hy) = (
c.x - c.heading.cos() * c.spec.half_length,
c.y - c.heading.sin() * c.spec.half_length,
);
assert!(((t.x - hx).hypot(t.y - hy) - TOW_BAR).abs() < 1e-3);
}
}
+1 -1
View File
@@ -411,7 +411,7 @@ fn render_shot(data: GameData, args: &Args) -> Result<(), String> {
.entrants
.iter_mut()
.filter(|e| matches!(e.driver, game::Driver::Human(_)))
.map(|e| e.ai.drive(&e.car, &race.track, game::DT, true))
.map(|e| e.ai.drive(&e.car, &race.path, game::DT, true))
.collect();
race.step(&controls);
app::draw_race(&mut frame, &app.data, &race, &mut cams, players);