Merge pull request #6 from andydixon/ai-collisions-towing
Use the original computer drivers, car collisions and towing
This commit is contained in:
@@ -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
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+87
-33
@@ -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,39 +335,93 @@ 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,
|
||||||
let d = d2.sqrt();
|
pub heading: f32,
|
||||||
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 {
|
||||||
|
|||||||
+122
-56
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user