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
+8 -5
View File
@@ -44,7 +44,8 @@ give either to also gets the game data.
- The original title picture. - The original title picture.
- 1 or 2 players (split screen) plus up to 7 computer drivers. - 1 or 2 players (split screen) plus up to 7 computer drivers.
- Power-slides, jumps, hills that slow you going up and speed you up coming - Power-slides, jumps, hills that slow you going up and speed you up coming
down, car-to-car bumping, and skid marks burnt into the track. down, spinning bumps against other cars, and skid marks burnt into the
track.
- Lap and position tracking, lap times, and a results table. - Lap and position tracking, lap times, and a results table.
- Engine note pitched with revs, tyre screech, crashes, backfires, a horn - Engine note pitched with revs, tyre screech, crashes, backfires, a horn
and start beeps, all from the original samples. and start beeps, all from the original samples.
@@ -57,12 +58,14 @@ were decoded from its machine code and work the same way here:
- the meaning of every `.set` value, and the handling presets - the meaning of every `.set` value, and the handling presets
- the walls along the road edges - the walls along the road edges
- how the world is projected onto the screen, including height - how the world is projected onto the screen, including height
- the computer drivers' path and how they choose their target and throttle
- collisions between cars. As in the original, only human players' cars
collide: computer cars pass through each other and through caravans.
- towing, including caravans jack-knifing
The rest is new code written to feel like the original: The rest is new code written to feel like the original:
- how computer drivers choose their line (they steer and drive as the - lap counting
original's do) - the rescue of stuck cars (the original warps them back instead)
- car-to-car collisions
- lap counting and the rescue of stuck cars
[docs/FORMATS.md](docs/FORMATS.md) covers everything reverse engineered so [docs/FORMATS.md](docs/FORMATS.md) covers everything reverse engineered so
far. far.
+5 -6
View File
@@ -103,7 +103,6 @@ The display name, driver and handling:
| `driver` | driver's name, shown next to it | | `driver` | driver's name, shown next to it |
| `trailer` | `yes` for something towed behind cars (the caravan) rather than driven | | `trailer` | `yes` for something towed behind cars (the caravan) rather than driven |
| `handling`| the 16 values from the original `.set` file, used as the original game uses them (below) | | `handling`| the 16 values from the original `.set` file, used as the original game uses them (below) |
| `mass` | weight when cars collide; not from the original data |
The `handling` values, counting from 1: The `handling` values, counting from 1:
@@ -113,8 +112,8 @@ The `handling` values, counting from 1:
| 9, 10, 11 | human drivers: drag, grip, power | | 9, 10, 11 | human drivers: drag, grip, power |
| 3 | steering: 16 = 1/64 of a turn per frame at full lock | | 3 | steering: 16 = 1/64 of a turn per frame at full lock |
| 5 | gravity: 8 = 1 pixel per frame² | | 5 | gravity: 8 = 1 pixel per frame² |
| 7 | tow hitch distance behind the car: 2 = 1 world unit | | 7, 8 | half the car's length and width, for collisions: 2 = 1 world unit. The tow hitch is at the back. |
| 4, 8, 12–16 | unused | | 4, 12–16 | unused |
What the driving values do: What the driving values do:
- **Drag** (below 64): how much of the engine's speed survives each frame. - **Drag** (below 64): how much of the engine's speed survives each frame.
@@ -128,9 +127,9 @@ What the driving values do:
The menu's **Handling** option can replace every car's values with one of The menu's **Handling** option can replace every car's values with one of
the original's presets. the original's presets.
Older files with `original`, `top`, `accel`, `brake`, `grip`, `turn` and Older files with `original`, `top`, `accel`, `brake`, `grip`, `turn`,
`offroad` lines still load: `original` is read as `handling`, and the `offroad` and `mass` lines still load: `original` is read as `handling`,
others are ignored. To add a car, add a new `<id>.png` / `<id>.txt` pair. and the others are ignored. To add a car, add a new `<id>.png` / `<id>.txt` pair.
## Sounds: `sounds/*.wav` ## Sounds: `sounds/*.wav`
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 54 35 32 8 7 22 22 16 49 14 53 0 0 0 0 0 handling 54 35 32 8 7 22 22 16 49 14 53 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1
+2 -3
View File
@@ -9,9 +9,8 @@ trailer yes
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 2 65532 65532 1 12 10 16 14 0 0 0 0 0 0 0 0 handling 2 65532 65532 1 12 10 16 14 0 0 0 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 15 14 34 8 4 598 15 12 13 4 2003 0 0 0 0 0 handling 15 14 34 8 4 598 15 12 13 4 2003 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1.4
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 60 71 30 0 6 16 24 12 58 42 29 0 0 0 0 0 handling 60 71 30 0 6 16 24 12 58 42 29 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 0.9
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 51 21 30 16 3 45 16 13 44 13 93 0 0 0 0 0 handling 51 21 30 16 3 45 16 13 44 13 93 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 0.85
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 56 24 30 12 3 24 20 12 46 10 100 0 0 0 0 0 handling 56 24 30 12 3 24 20 12 46 10 100 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 0.8
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 56 34 22 6 2 15 19 11 49 14 44 0 0 0 0 0 handling 56 34 22 6 2 15 19 11 49 14 44 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1.2
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 56 34 22 6 2 15 21 12 49 14 44 0 0 0 0 0 handling 56 34 22 6 2 15 21 12 49 14 44 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 55 26 28 18 2 29 20 17 54 19 35 0 0 0 0 0 handling 55 26 28 18 2 29 20 17 54 19 35 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1.8
+2 -3
View File
@@ -9,9 +9,8 @@ trailer no
# original game does. Counting from 1: # original game does. Counting from 1:
# 1 2 6 computer drivers: drag, grip, power # 1 2 6 computer drivers: drag, grip, power
# 9 10 11 human drivers: drag, grip, power # 9 10 11 human drivers: drag, grip, power
# 3 steering, 5 gravity, 7 tow hitch distance; the rest are unused. # 3 steering, 5 gravity, 7 and 8 half the car's length and width
# (for collisions; the tow hitch is at the back). The rest are unused.
# Top speed = power * drag / (64 - drag) / 32 world units per frame, # Top speed = power * drag / (64 - drag) / 32 world units per frame,
# 50 frames a second (drag below 64). Higher grip, less sliding. # 50 frames a second (drag below 64). Higher grip, less sliding.
handling 57 28 32 12 3 29 20 14 51 15 63 0 0 0 0 0 handling 57 28 32 12 3 29 20 14 51 15 63 0 0 0 0 0
# weight when cars collide (not from the original data)
mass 1
+74 -11
View File
@@ -194,12 +194,13 @@ with these shifts. Word `k` is the `k`th `u16`, counting from 0:
| 3 | `$c0` | −w / 64 | stored but never read | | 3 | `$c0` | −w / 64 | stored but never read |
| 4 | `$106` | w / 8 | gravity, pixels per frame² | | 4 | `$106` | w / 8 | gravity, pixels per frame² |
| 5 | `$b8` | w / 512 | power (computer drivers) | | 5 | `$b8` | w / 512 | power (computer drivers) |
| 6 | (towing) | w / 32 | tow hitch distance behind the car, in game units | | 6 | (collisions, towing) | w / 32 | half the car's length, in game units; the tow hitch is at the back |
| 7 | (collisions) | w / 32 | half the car's width |
| 8 | `$bc` | w / 64 | drag (human drivers) | | 8 | `$bc` | w / 64 | drag (human drivers) |
| 9 | `$e2` | w / 512 | grip (human drivers) | | 9 | `$e2` | w / 512 | grip (human drivers) |
| 10 | `$b8` | w / 512 | power (human drivers) | | 10 | `$b8` | w / 512 | power (human drivers) |
Words 7 and 11–15 aren't read. [Game logic](#game-logic) shows how drag, Words 11–15 aren't read. [Game logic](#game-logic) shows how drag,
grip and power work together. In short, top speed is grip and power work together. In short, top speed is
`power × drag / (1 − drag)` game units per frame. For the Camaro that is `power × drag / (1 − drag)` game units per frame. For the Camaro that is
271 of this port's units a second for human drivers and 186 for computer 271 of this port's units a second for human drivers and 186 for computer
@@ -226,6 +227,7 @@ For a preset `(a, t, g)`:
- p = t/16 × (1/drag − 1), and power = p/8 - p = t/16 × (1/drag − 1), and power = p/8
- grip = g/256 / p / drag / 8 - grip = g/256 / p / drag / 8
- steering = byte 6 / 32, gravity = 0.5 - steering = byte 6 / 32, gravity = 0.5
- The car's size (words 6 and 7) still comes from its own settings.
So top speed is simply t/128 game units per frame. So top speed is simply t/128 game units per frame.
@@ -300,6 +302,7 @@ Each car is a 266-byte (`$10a`) record:
| `$a6` | engine speed | | `$a6` | engine speed |
| `$aa` | forward speed | | `$aa` | forward speed |
| `$b8`, `$bc`, `$e2`, `$e6`, `$106` | handling (above) | | `$b8`, `$bc`, `$e2`, `$e6`, `$106` | handling (above) |
| `$86` | active (negative) |
| `$d2`, `$d6` | links to the car towing this one, and the one it tows | | `$d2`, `$d6` | links to the car towing this one, and the one it tows |
| `$ea` | zone (half-gate) progress | | `$ea` | zone (half-gate) progress |
@@ -341,14 +344,77 @@ fits in one field. Then `$247ac` integrates every car.
A warping car (state 1) instead flies straight at a point on the track: A warping car (state 1) instead flies straight at a point on the track:
its velocity is the distance to it / 16. its velocity is the distance to it / 16.
Note that the heading vector points to the car's *rear*: pushing the
joystick forward gives a throttle of −1. This document flips that round
and talks about forward throttle as positive.
### Computer drivers ### Computer drivers
Computer cars use words 0, 1 and 5 for handling rather than 8, 9 and 10. Computer cars use words 0, 1 and 5 for handling rather than 8, 9 and 10.
They steer with the sign of the cross product between their heading and
the direction to a target point, as if holding a joystick hard over. The **The path.** At set-up (`$1ea82` onwards) the game walks the middle of
targets come from a table of points built along the road. The driver the road, the average of the two edge curves. It drops a node every 2
picks one side of the road or the other depending on which way the road game units, adjusting each step by bisection until the gap squared is
bends, and the throttle comes from fixed values. between 3.8 and 4.2. Each 36-byte node (array `-$7e68(a5)`) holds:
- the centre point
- the unit vector across the road
- a point 1 unit in from each wall
- its position along the spline
A second table maps each zone to its first node.
**Each frame:**
1. The driver predicts its position four frames ahead, `pos + 4 ×
velocity`. It finds the first node that point hasn't passed: only
nodes within −2 to +8 zones of the car count, and a node is passed
once the point is beyond the line across the road there.
2. Its default target is the centre of the node 4 on, at full throttle.
3. If the predicted point is beyond the line joining one side's inset
points at that node and the one before, it's running wide. It aims
at the other side's point 2 nodes on instead, at 0.7 throttle. The
constants are Motorola fast floating point values set at `$14886`:
1.0 (negated), and that times `$b333`.
4. Steering is the sign of the cross product of its heading with the
direction from where it is now to the target, as if holding a
joystick hard over.
### Collisions between cars
Routine `$1b324` tests only the human players' cars. Each is tested
against every other active car and caravan except the caravan it tows.
So computer cars pass through each other and through caravans.
For a pair (A = the player's car, B = the other):
1. **Proximity** (`$260ba`): skip unless |dx| ≤ 2 and |dy| ≤ 2 game units
and |dz| < 17 pixels. So cars on a bridge don't hit cars under it.
2. **Strength:** k = 64 × |velocity A − velocity B|² + 20, in game units
per frame.
3. **Points:** six points on A, rotated with A, come from a table built
for every heading out of words 6 and 7: the four corners, then one on
each side at 0.58 of the half-width. Each is tested against B's box,
which is the same for every vehicle: ±0.375 across and ±0.75 along
B's heading (`$25f3c`).
4. **Push:** for each point inside the box, B's velocity gains the
point's offset from A's centre × k / 256 and A's loses the same. The
side points use 2.5 k.
5. **Spin:** each corner inside the box also turns A by k / 16 (64ths of
a turn) on its turn rate, and twice that on its heading. The sign is
the one that swings that corner away from B.
6. **Sound:** any hit plays the crash sound for both cars.
### Towing
A caravan is a car record linked from its car (`$d6`), and it's dragged
in the per-car loop at `$1cf64`:
- **Hitch:** at the back of the car, half its length (word 6) behind the
centre. The game uses the car's position after this frame's move.
- **Swing:** the caravan turns to point at the hitch, and is placed 0.656
game units behind it, by setting its velocity to the gap.
- **Jack-knife:** if the caravan ends up more than 90° from the car, both
are set to 45° either side of the angle halfway between them. So the
caravan steers the car too.
- **Warps:** when the car warps, its caravan is parked off the map until
it lands.
## Executables ## Executables
@@ -360,10 +426,7 @@ bends, and the throttle comes from fixed values.
## Gaps ## Gaps
- **Game logic still to decode:** - **Game logic still to decode:**
- car-to-car collisions - the warp rules (when a car is warped back to the track)
- exactly how computer drivers pick target points and throttle
- the warp rules
- the towing physics, beyond the hitch distance
- the race rules and scoring - the race rules and scoring
- **Unknown data:** - **Unknown data:**
- the six per-track words after each track file's palette - the six per-track words after each track file's palette
+22 -44
View File
@@ -44,10 +44,10 @@ pub const SHEET_H: usize = SPR_H * TILTS;
/// | 2 | steering rate | /// | 2 | steering rate |
/// | 3 | read into the car but never used | /// | 3 | read into the car but never used |
/// | 4 | gravity | /// | 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 | /// | 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]; pub type SetValues = [u16; 16];
/// The Camaro's values, for vehicles that come without any. /// The Camaro's values, for vehicles that come without any.
@@ -80,15 +80,15 @@ pub struct Spec {
pub turn: f32, pub turn: f32,
/// Pixels per frame per frame. /// Pixels per frame per frame.
pub gravity: f32, pub gravity: f32,
/// Tow hitch distance behind the car's centre, world units. /// Half the car's length and width, world units. The tow hitch is
pub hitch: f32, /// `half_length` behind the centre.
/// Weight in collisions between cars. Not from the original data. pub half_length: f32,
pub mass: f32, pub half_width: f32,
} }
impl Spec { impl Spec {
/// Decodes the values as the executable does (fixed-point shifts). /// 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; let f = |k: usize, shift: u32| v[k] as i16 as f32 / (1u32 << shift) as f32;
Spec { Spec {
player: Handling { player: Handling {
@@ -103,8 +103,8 @@ impl Spec {
}, },
turn: f(2, 4), turn: f(2, 4),
gravity: f(4, 3), gravity: f(4, 3),
hitch: f(6, 5) * super::track::CELL, half_length: f(6, 5) * super::track::CELL,
mass, 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 struct Vehicle {
pub id: String, pub id: String,
pub name: String, pub name: String,
@@ -212,7 +198,7 @@ impl Vehicle {
trailer: id == "caravan", trailer: id == "caravan",
set: DEFAULT_SET, set: DEFAULT_SET,
menu_picture: None, menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, default_mass(&id)), spec: Spec::from_set(&DEFAULT_SET),
id, id,
frames, frames,
})) }))
@@ -235,7 +221,7 @@ impl Vehicle {
for (i, v) in self.set.iter_mut().enumerate() { for (i, v) in self.set.iter_mut().enumerate() {
*v = crate::formats::be16(body, i * 2); *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 pic = &body[32..32 + 512];
let mut px = vec![0u8; SPR_W * SPR_H]; let mut px = vec![0u8; SPR_W * SPR_H];
for (i, p) in px.iter_mut().enumerate() { for (i, p) in px.iter_mut().enumerate() {
@@ -293,7 +279,7 @@ impl Vehicle {
trailer: id == "caravan", trailer: id == "caravan",
set: DEFAULT_SET, set: DEFAULT_SET,
menu_picture: None, menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, default_mass(id)), spec: Spec::from_set(&DEFAULT_SET),
frames, frames,
}; };
if let Some(txt) = txt { if let Some(txt) = txt {
@@ -329,12 +315,11 @@ impl Vehicle {
# original game does. Counting from 1:\n\ # original game does. Counting from 1:\n\
# 1 2 6 computer drivers: drag, grip, power\n\ # 1 2 6 computer drivers: drag, grip, power\n\
# 9 10 11 human 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\ # Top speed = power * drag / (64 - drag) / 32 world units per frame,\n\
# 50 frames a second (drag below 64). Higher grip, less sliding.\n\ # 50 frames a second (drag below 64). Higher grip, less sliding.\n\
handling {set}\n\ handling {set}\n",
# weight when cars collide (not from the original data)\n\
mass {mass}\n",
id = self.id, id = self.id,
name = self.name, name = self.name,
driver = self.driver, driver = self.driver,
@@ -345,7 +330,6 @@ impl Vehicle {
.map(|v| v.to_string()) .map(|v| v.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "), .join(" "),
mass = self.spec.mass,
) )
} }
@@ -375,18 +359,12 @@ impl Vehicle {
self.set = [0; 16]; self.set = [0; 16];
self.set[..vals.len()].copy_from_slice(&vals); 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. // 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}`"))), _ => 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(()) Ok(())
} }
@@ -451,13 +429,13 @@ mod tests {
trailer: false, trailer: false,
set: DEFAULT_SET, set: DEFAULT_SET,
menu_picture: None, menu_picture: None,
spec: Spec::from_set(&DEFAULT_SET, 1.0), spec: Spec::from_set(&DEFAULT_SET),
frames, frames,
}; };
let mut v = blank(vec![0; TILTS * HEADINGS * SPR_W * SPR_H]); let mut v = blank(vec![0; TILTS * HEADINGS * SPR_W * SPR_H]);
v.driver = "SIMON".into(); v.driver = "SIMON".into();
v.set = [60, 71, 30, 0, 6, 16, 24, 12, 58, 42, 29, 0, 0, 0, 0, 0]; 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(); v.name = "FORMULA 1".into();
let txt = v.settings_text(); let txt = v.settings_text();
let mut w = blank(Vec::new()); let mut w = blank(Vec::new());
@@ -466,7 +444,7 @@ mod tests {
assert_eq!(w.name, "FORMULA 1"); assert_eq!(w.name, "FORMULA 1");
assert_eq!(w.driver, "SIMON"); assert_eq!(w.driver, "SIMON");
assert_eq!(w.set, v.set); 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. // Older files with reconstructed handling still load.
assert!(w.apply_settings("original 1 2 3\ntop 200").is_ok()); assert!(w.apply_settings("original 1 2 3\ntop 200").is_ok());
assert_eq!(&w.set[..4], &[1, 2, 3, 0]); assert_eq!(&w.set[..4], &[1, 2, 3, 0]);
@@ -474,11 +452,11 @@ mod tests {
#[test] #[test]
fn decodes_set_values() { 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.player.drag, 49.0 / 64.0);
assert_eq!(s.computer.power, 22.0 / 512.0); assert_eq!(s.computer.power, 22.0 / 512.0);
assert_eq!(s.turn, 2.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 p = s.with_preset(1);
let top = p.player.power * p.player.drag / (1.0 - p.player.drag); let top = p.player.power * p.player.drag / (1.0 - p.player.drag);
assert!((top - 26.0 / 128.0).abs() < 1e-5); 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 //! At set-up the game lays a path of nodes along the middle of the road,
//! towards a target point ahead, or straight on when it is dead ahead, and //! 2 game units (32 world units) apart. Each node also has a point on
//! drives flat out. The cars' computer handling (`vehicle::Handling`) is //! either side, 1 game unit in from the wall. Each frame a computer
//! what makes them take bends at that. The target is a point a little way //! driver:
//! along the road (`Track::edges`) at the driver's own line across it. A //!
//! driver that stops moving backs out. //! 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 super::car::{angle_diff, Car, Controls, DT};
use crate::data::track::Track; use crate::data::track::Gate;
/// Heading error, in radians, below which the driver doesn't steer. /// Nodes ahead for the usual target, and for the target across the road.
const DEAD_AHEAD: f32 = 0.06; 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 { 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, stuck_for: f32,
reversing: f32, reversing: f32,
} }
impl Ai { impl Ai {
pub fn new(line: f32, pace: f32) -> Ai { pub fn new() -> Ai {
Ai { Ai::default()
line,
pace,
stuck_for: 0.0,
reversing: 0.0,
}
} }
/// The point `dist` along the road from the car's cross-section, at /// Where the driver aims, and how much throttle it uses.
/// this driver's line. pub fn target(&self, car: &Car, path: &Path) -> ((f32, f32), f32) {
pub fn target(&self, car: &Car, track: &Track, dist: f32) -> (f32, f32) { let p = (car.x + 4.0 * car.vx * DT, car.y + 4.0 * car.vy * DT);
let e = &track.edges; let n = path.nodes.len();
let m = e.len(); // Start from the car's own place on the road (offset by a lap so
let at = |k: usize| { // stepping back can't underflow), then find the first node the
let g = e[k % m]; // predicted point hasn't reached.
( let mut i = path.first.get(car.section).copied().unwrap_or(0) + n;
g.a.0 + (g.b.0 - g.a.0) * self.line, for _ in 0..2 {
g.a.1 + (g.b.1 - g.a.1) * self.line, if path.reached(i - 1, p) {
) break;
};
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);
} }
left -= len; i -= 1;
p = q;
k += 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 { pub fn drive(&mut self, car: &Car, path: &Path, dt: f32, racing: bool) -> Controls {
if !racing || track.edges.len() < 3 { if !racing || path.nodes.len() < AHEAD + 2 {
return Controls::default(); return Controls::default();
} }
let speed = car.speed(); let ((tx, ty), throttle) = self.target(car, path);
let look = 30.0 + speed * 0.2;
let (tx, ty) = self.target(car, track, look);
let diff = angle_diff((ty - car.y).atan2(tx - car.x), car.heading); let diff = angle_diff((ty - car.y).atan2(tx - car.x), car.heading);
let steer = if diff.abs() < DEAD_AHEAD { let steer = if diff == 0.0 { 0.0 } else { diff.signum() };
0.0
} else {
diff.signum()
};
let speed = car.speed();
if self.reversing > 0.0 { if self.reversing > 0.0 {
self.reversing -= dt; self.reversing -= dt;
return Controls { return Controls {
@@ -93,18 +177,39 @@ impl Ai {
} else { } else {
self.stuck_for = 0.0; 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 { Controls {
throttle: self.pace, throttle,
brake: 0.0, brake: 0.0,
steer, 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));
}
}
+86 -32
View File
@@ -17,16 +17,16 @@
//! - **Walls:** the road edges (`Track::edges`) are walls. A car that //! - **Walls:** the road edges (`Track::edges`) are walls. A car that
//! reaches one loses the part of its velocity into the wall and bounces //! reaches one loses the part of its velocity into the wall and bounces
//! back with half of it. //! back with half of it.
//! - **Other cars:** see `bump`.
//! //!
//! Human and computer drivers use separate handling from the vehicle's //! Human and computer drivers use separate handling from the vehicle's
//! `.set` values. //! `.set` values.
use super::DT; pub use super::DT;
use crate::data::track::{self, Gate, Track, CELL}; use crate::data::track::{self, Gate, Track, CELL};
use crate::data::vehicle::{self, Handling, Spec}; use crate::data::vehicle::{self, Handling, Spec};
use std::f32::consts::{PI, TAU}; 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. /// How close the car's centre may come to a wall, in world units.
const WALL_MARGIN: f32 = 4.0; const WALL_MARGIN: f32 = 4.0;
/// The executable samples the slope 0.2 cells from the car. /// The executable samples the slope 0.2 cells from the car.
@@ -335,40 +335,94 @@ pub fn nearest_section(e: &[Gate], p: (f32, f32)) -> usize {
} }
} }
/// Pushes overlapping cars apart and exchanges momentum. /// Something a car can run into: another car or a caravan.
pub fn collide(a: &mut Car, b: &mut Car) { #[derive(Clone, Copy, Debug)]
let (dx, dy) = (b.x - a.x, b.y - a.y); pub struct Body {
let d2 = dx * dx + dy * dy; pub x: f32,
let min = RADIUS * 2.0; pub y: f32,
if d2 >= min * min || d2 < 1e-6 { pub z: f32,
return; pub vx: f32,
pub vy: f32,
pub heading: f32,
} }
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;
let vn = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny; impl Car {
if vn < 0.0 { pub fn body(&self) -> Body {
let j = -(1.0 + 0.45) * vn / (ia + ib); Body {
a.vx -= j * ia * nx; x: self.x,
a.vy -= j * ia * ny; y: self.y,
b.vx += j * ib * nx; z: self.z,
b.vy += j * ib * ny; vx: self.vx,
let hit = (-vn / 150.0).min(1.0); vy: self.vy,
a.bump = a.bump.max(hit); heading: self.heading,
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);
} }
} }
}
/// 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 { pub fn angle_diff(to: f32, from: f32) -> f32 {
(to - from + PI).rem_euclid(TAU) - PI (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::track::{Gate, Track};
use crate::data::vehicle::Vehicle; use crate::data::vehicle::Vehicle;
use ai::Ai; use ai::{Ai, Path};
use car::{Car, Controls}; use car::{Body, Car, Controls};
pub const DT: f32 = 1.0 / 50.0; pub const DT: f32 = 1.0 / 50.0;
const COUNTDOWN: f32 = 3.0; const COUNTDOWN: f32 = 3.0;
@@ -43,13 +43,18 @@ pub struct Trailer {
pub vehicle: usize, pub vehicle: usize,
pub x: f32, pub x: f32,
pub y: 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, pub heading: f32,
} }
/// Tow bar length from the car's hitch (`Spec::hitch`) to the trailer's /// From the car's hitch (at the back of the car, `Spec::half_length`
/// centre, in world units. /// behind its centre) to the trailer's centre: 0.656 game units.
const TOW_BAR: f32 = 20.0; const TOW_BAR: f32 = 10.5;
const TRAILER_RADIUS: f32 = 10.0; /// 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. /// Seconds without crossing a gate before a computer car is rescued.
const RESCUE_AFTER: f32 = 8.0; 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.heading = heading.rem_euclid(std::f32::consts::TAU);
c.place(track); c.place(track);
if let Some(t) = &mut e.trailer { 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.x = from.0 - heading.cos() * back;
t.y = from.1 - heading.sin() * back; t.y = from.1 - heading.sin() * back;
t.heading = heading; t.heading = heading;
@@ -73,16 +78,40 @@ fn rescue(e: &mut Entrant, track: &Track) {
} }
impl Trailer { impl Trailer {
/// Drags the trailer after its car: it keeps its distance from the /// Drags the trailer after its car, as the executable does: it turns
/// hitch and swings to point at it. /// to point at the hitch and sits a tow bar's length behind it. If it
fn follow(&mut self, car: &Car) { /// 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 (fx, fy) = car.forward();
let (hx, hy) = (car.x - fx * car.spec.hitch, car.y - fy * car.spec.hitch); let (hx, hy) = (
let (dx, dy) = (self.x - hx, self.y - hy); car.x - fx * car.spec.half_length,
let d = dx.hypot(dy).max(1e-3); car.y - fy * car.spec.half_length,
self.x = hx + dx / d * TOW_BAR; );
self.y = hy + dy / d * TOW_BAR;
self.heading = (hy - self.y).atan2(hx - self.x); 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 struct Race {
pub track: Track, pub track: Track,
/// The computer drivers' path.
pub path: Path,
pub entrants: Vec<Entrant>, pub entrants: Vec<Entrant>,
pub laps: usize, pub laps: usize,
pub time: f32, pub time: f32,
@@ -146,7 +177,7 @@ impl Race {
let start_gate = 0; let start_gate = 0;
let path: Vec<(f32, f32)> = gates.iter().map(Gate::mid).collect(); 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(); let mut entrants = Vec::new();
for (i, s) in setup.iter().enumerate() { for (i, s) in setup.iter().enumerate() {
// Two-wide grid, rows 30 units apart, following the road back // 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. // Leave room for a caravan between rows when towing.
let spec = vehicles[s.vehicle].spec.with_preset(s.preset); let spec = vehicles[s.vehicle].spec.with_preset(s.preset);
let spacing = if s.trailer.is_some() { 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 { } else {
30.0 30.0
}; };
@@ -166,12 +197,10 @@ impl Race {
let mut car = Car::new(s.vehicle, spec, s.team, x, y, heading); let mut car = Car::new(s.vehicle, spec, s.team, x, y, heading);
car.computer = s.driver == Driver::Cpu; car.computer = s.driver == Driver::Cpu;
car.place(&track); car.place(&track);
let line = 0.3 + 0.4 * rand01(&mut rng);
let pace = 0.86 + 0.12 * rand01(&mut rng);
entrants.push(Entrant { entrants.push(Entrant {
car, car,
driver: s.driver, driver: s.driver,
ai: Ai::new(line, pace), ai: Ai::new(),
name: s.name.clone(), name: s.name.clone(),
passed: 0, passed: 0,
next_gate: start_gate, next_gate: start_gate,
@@ -182,11 +211,13 @@ impl Race {
progress_at: 0.0, progress_at: 0.0,
trailer: s.trailer.map(|t| { trailer: s.trailer.map(|t| {
let (fx, fy) = (heading.cos(), heading.sin()); let (fx, fy) = (heading.cos(), heading.sin());
let back = spec.hitch + TOW_BAR; let back = spec.half_length + TOW_BAR;
Trailer { Trailer {
vehicle: t, vehicle: t,
x: x - fx * back, x: x - fx * back,
y: y - fy * back, y: y - fy * back,
vx: 0.0,
vy: 0.0,
heading, heading,
} }
}), }),
@@ -194,6 +225,7 @@ impl Race {
} }
let ground = track.rgb(); let ground = track.rgb();
Race { Race {
path: Path::new(&track.edges),
track, track,
entrants, entrants,
laps, laps,
@@ -235,10 +267,10 @@ impl Race {
for e in self.entrants.iter_mut() { for e in self.entrants.iter_mut() {
let controls = match (e.driver, e.finished) { 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(p), None) if racing => human.get(p).copied().unwrap_or_default(),
(Driver::Human(_), None) => Controls::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; e.car.controls = controls;
// Finished cars cruise round on autopilot at reduced pace. // Finished cars cruise round on autopilot at reduced pace.
@@ -246,24 +278,34 @@ impl Race {
e.car.step(&self.track, pace); e.car.step(&self.track, pace);
} }
for i in 0..self.entrants.len() { for e in self.entrants.iter_mut() {
for j in i + 1..self.entrants.len() { if let Some(t) = &mut e.trailer {
let (a, b) = self.entrants.split_at_mut(j); t.follow(&mut e.car);
car::collide(&mut a[i].car, &mut b[0].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() { 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; continue;
}; }
t.follow(&self.entrants[i].car); for j in 0..self.entrants.len() {
for (j, other) in self.entrants.iter_mut().enumerate() { if j == i {
if j != i { continue;
push_off(&mut other.car, t.x, t.y); }
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) { 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) { fn burn(ground: &mut [u32], sx: f32, sy: f32, strength: f32) {
use crate::data::track::{H, W}; use crate::data::track::{H, W};
let (x, y) = (sx as isize, sy as isize); let (x, y) = (sx as isize, sy as isize);
@@ -511,7 +533,7 @@ mod tests {
start: (0.0, 0.0), start: (0.0, 0.0),
flags: 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); let mut c = Car::new(0, spec, 0, 512.0 + 180.0, 512.0, 0.0);
c.place(&track); c.place(&track);
// Flat out in a straight line: the outer wall turns the car back. // 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}"); 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 .entrants
.iter_mut() .iter_mut()
.filter(|e| matches!(e.driver, game::Driver::Human(_))) .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(); .collect();
race.step(&controls); race.step(&controls);
app::draw_race(&mut frame, &app.data, &race, &mut cams, players); app::draw_race(&mut frame, &app.data, &race, &mut cams, players);