Mission types. Four of them, differing in pacing and risk rather than in the noun on the board: a quick supply drop, a retrieval whose danger is all on the return leg, a long stationary intel transmission in the open, and a cheap survey whose payoff is the map. Retrieval carries cargo that impacts damage, and pays for what arrives rather than what you set off with, which makes the drive home a different problem from the drive out. Radio. Chatter framing the player as an undercover driver being talked at by people busy elsewhere. Every line is triggered by something actually happening in the sim — crossing a border, a road you personally made notorious, a car past saving, or the front genuinely drifting. The enemy-has-other-priorities line fires off measured drift, so the brief's ambient-drift-is-the-mechanism idea is said out loud without ever showing a number. Topics carry cooldowns and never repeat a sentence back to back; a test caught that they could. Field work. Past the line, with nothing in hand, a wounded stranger or a contact or a strippable wreck occasionally turns up. Deliberately not on a board: no route preview, no comparison, no shopping between them. Take the detour in front of you or drive on. They never appear behind your own lines, where a safe errand would defeat the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
389 lines
19 KiB
Markdown
389 lines
19 KiB
Markdown
# Drive Between the Lines — prototype
|
||
|
||
Endless driving survival game.
|
||
|
||
- **Phase 0** — a drivable car with persistent wear that is felt through the wheel.
|
||
- **Phase 1** — a road network whose roads remember being driven, and escalate.
|
||
- **Phase 2** — bases, a quest board, and known-vs-new target selection.
|
||
- **Phase 3** — regional control: behind your own lines, nobody is watching.
|
||
- **Phase 4** — the front line moves, from your work and on its own.
|
||
- **Phase 5** — four distinct jobs, radio texture, and work found in the field.
|
||
|
||
## Running
|
||
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
Then open http://localhost:5173. Append `?seed=anything` to regenerate the world —
|
||
seeds may be numbers or words.
|
||
|
||
| | |
|
||
|---|---|
|
||
| `W` / `S` | throttle / brake-reverse |
|
||
| `A` / `D` | steer |
|
||
| `Space` | handbrake (rear wheels only) |
|
||
| `R` | respawn the car — **does not** repair it |
|
||
| `1`–`3` | accept a mission, when parked at a base |
|
||
|
||
Progress saves itself every 20 seconds, and immediately whenever you take or
|
||
finish a job. Saves are per seed; `?seed=x` is a separate campaign.
|
||
|
||
Drive to a green beacon (a base) and stop. Take a job, drive to the amber
|
||
beacon, stop and hold position for three seconds, then return to **any** base —
|
||
the amber beacon goes out once the job at the target is done, since the target
|
||
is no longer anywhere you need to be.
|
||
|
||
```bash
|
||
npm test # sim + headless physics
|
||
npm run build # typecheck + production bundle
|
||
```
|
||
|
||
## Road heat
|
||
|
||
Every metre driven **on or alongside** a road adds heat to that segment; every
|
||
road everywhere sheds it slowly. Cross a threshold and the road escalates:
|
||
|
||
`Clear → Patrol → Barricade → Turret`
|
||
|
||
- **Patrol** parks a vehicle on the verge. The road narrows.
|
||
- **Barricade** puts concrete across it, leaving a gap you have to slow for.
|
||
- **Turret** adds a tower overlooking the checkpoint.
|
||
|
||
Roughly four traversals take a road from clear to turret; an untouched road
|
||
cools off in about four minutes. Both numbers are in `sim/heat.ts` and both are
|
||
guesses meant to be tuned.
|
||
|
||
### The road hierarchy
|
||
|
||
Roads are not all the same road:
|
||
|
||
| Class | Tarmac | Speed | Attention |
|
||
|---|---|---|---|
|
||
| Track (single car) | 4.5 m | ×0.55 | ×0.55 |
|
||
| Road (two lanes) | 9 m | ×1 | ×1 |
|
||
| Trunk (four lanes) | 15 m | ×1.35 | ×1.35 |
|
||
|
||
That trade is the network doing real work rather than being the floor you drive
|
||
on: a trunk is fast and direct and exactly where anyone looking for you would
|
||
look; a lane is slow and awkward and nobody is watching it. Trunks run the whole
|
||
width of the map and are never thinned away, so there is a shape to learn — get
|
||
onto the trunk, then work down the lanes — instead of a uniform mesh where every
|
||
road is as good as every other. Route previews are costed by **travel time**, not
|
||
distance, so the board's estimate matches the way a driver would actually go.
|
||
|
||
### The route corridor
|
||
|
||
Heat is credited to a road for anything within its corridor — half the tarmac
|
||
plus a 7.5 m verge, so it scales with the class — not just the tarmac itself.
|
||
Otherwise the dominant strategy is to drive *next to* every road and never
|
||
accrue heat at all.
|
||
|
||
Two consequences follow, and both are deliberate:
|
||
|
||
- **The cutoff is hard, not a taper.** A taper leaves a gradient to optimise
|
||
along — sit at 80% of the corridor, take 20% of the heat. A cliff edge means
|
||
you either use the route or you actually leave it.
|
||
- **Barricades span the full corridor**, well past the kerb. A checkpoint that
|
||
stopped at the tarmac would be free to round on the verge at speed.
|
||
|
||
The 7.5 m verge is about two car widths, so hugging the shoulder gains nothing,
|
||
while leaving most of the map genuinely off-route. Widening it backfires: make
|
||
the corridor too generous and heat becomes unavoidable everywhere, and "take a
|
||
different route" stops being a choice at all.
|
||
|
||
### Buildings, and why they are dense
|
||
|
||
Off-road used to be open country, which made every barricade optional — you just
|
||
swung wide around it. Buildings are now laid out on a jittered lattice (random
|
||
scatter cannot pack boxes and saturated at a third of the target), which takes
|
||
bypass lanes around a checkpoint from none obstructed to about two thirds. You
|
||
can still get around one; it means threading a maze at walking pace instead of a
|
||
free detour at speed.
|
||
|
||
### Other deliberate choices
|
||
|
||
- **No AI yet.** The props are stationary hazards. Escalation currently means the
|
||
road gets slower and more expensive to get wrong, which is enough to test the
|
||
phase's real question.
|
||
- **Changes never happen on the road you are on.** They are deferred until you
|
||
leave. Otherwise a barricade would spawn a static collider inside your car —
|
||
and you are supposed to discover escalation by *coming back*, not by watching
|
||
it assemble behind you.
|
||
- **A checkpoint's position is seeded from its segment**, so a given stretch of
|
||
road always fortifies in the same place. Recognising it is part of learning
|
||
the map.
|
||
|
||
The road network is a jittered grid with roughly a quarter of its edges removed,
|
||
keeping the whole thing connected. The loops are the point: "take a different
|
||
road this time" is not a decision unless alternative routes exist.
|
||
|
||
## Missions and intel
|
||
|
||
The board at each base offers a mix of **known targets** and **new** ones, and
|
||
describes the route to each in words rather than numbers — "barricaded · last
|
||
word 6 min ago", "no one has been down that way".
|
||
|
||
The important part is *where those words come from*. The board never reads heat
|
||
directly. It reads `sim/intel.ts`, which is a snapshot of what the player has
|
||
actually observed and when. Drive a road and you write down what was on it;
|
||
leave it alone and your note ages while the road keeps escalating without you.
|
||
So a known route is genuinely knowable, genuinely stale, and never a guarantee —
|
||
which is the tension the brief asks for.
|
||
|
||
**Recon missions** are the tool for converting a new target into a known one:
|
||
completing one surveys every road within 130 m, filling in the map without
|
||
having to drive each one.
|
||
|
||
Four jobs run end to end, and they differ in **pacing and risk**, not just in
|
||
the noun on the board — otherwise four mission types is one mission type with
|
||
four labels:
|
||
|
||
| Job | At the target | The risk |
|
||
|---|---|---|
|
||
| **Supply run** | 3s unloading | the drive out |
|
||
| **Retrieval** | 5s loading | the drive *home*, with goods that break |
|
||
| **Intel drop** | 9s transmitting | a long wait, stationary, in the open |
|
||
| **Survey** | 2s | cheapest on site; the payoff is the map |
|
||
|
||
Retrieval is the one that carries something home, and impacts damage it. It pays
|
||
the most, but it pays for what *arrives*, not what you set off with. Hand-in
|
||
works at *any* base, not the issuing one, because the brief is explicit that a
|
||
blocked road home must never strand the player.
|
||
|
||
### Work you find rather than work you are given
|
||
|
||
Past the line, and only when you have nothing in hand, someone occasionally
|
||
turns up: a wounded stranger, a contact flashing headlights, a wreck worth
|
||
stripping. They are deliberately **not** on a board — you cannot plan a route to
|
||
one, compare it against anything, or shop between them. You take the detour in
|
||
front of you or you drive on.
|
||
|
||
### Radio
|
||
|
||
Chatter that frames who you are — an undercover driver being talked at by people
|
||
busy elsewhere. Every line is triggered by something actually happening in the
|
||
sim: crossing a border, a road you have made notorious, a car past saving, or
|
||
the front genuinely drifting your way, which is the brief's "the enemy has other
|
||
priorities" said out loud without ever showing a number.
|
||
|
||
### Rewards, and why repairs exist
|
||
|
||
New targets pay `NOVELTY_BONUS` (1.8×) more than known ones. That multiplier is
|
||
the phase's tuning dial: if you never take the new target it is too low, and if
|
||
you never take the known one it is too high.
|
||
|
||
Payment is in **parts**, which repair the car — the only thing in the game that
|
||
pushes condition back up. Repairs go to the worst subsystem first and stop at
|
||
its **ceiling**, which drops permanently with every bit of damage taken
|
||
(`PERMANENT_SHARE`, currently 30%). So a car can be patched back to what it is
|
||
still capable of, but never to what it was. The HUD bars show this directly: `█`
|
||
is current condition, `·` is what repairs could still reach, `×` is gone.
|
||
|
||
This is what gives the board's reward a reason to matter. Without an economy the
|
||
choice between a safe route and a risky one has no stakes; with one, the risky
|
||
job is what keeps the car alive a while longer.
|
||
|
||
## Regional control
|
||
|
||
The map is cut by a single **front line** into four bands, running from your own
|
||
territory out past the edge of the known world:
|
||
|
||
`Liberated → Contested → Occupied → Frontier`
|
||
|
||
| Band | Heat accrual | Heat decay |
|
||
|---|---|---|
|
||
| Liberated | **×0** | ×4 |
|
||
| Contested | ×0.55 | ×1.4 |
|
||
| Occupied | ×1 | ×1 |
|
||
| Frontier | ×1.3 | ×0.8 |
|
||
|
||
Liberated accrual is exactly zero, not merely small. A trickle would eventually
|
||
fortify your own back garden, which is nonsense, and it would mean the safe area
|
||
quietly punished you for using it. Behind your own lines, nobody is writing down
|
||
which roads you take.
|
||
|
||
Decay is judged by whoever holds *the road*, not whoever holds the ground the
|
||
car is standing on — your own side dismantles checkpoints on its own roads
|
||
whether or not you are there to watch.
|
||
|
||
### The line moves
|
||
|
||
Two forces, and they are independent by design.
|
||
|
||
**Your work.** Completing a mission pushes the front forward, weighted by what
|
||
the job was worth — a long haul into unknown country moves the war more than a
|
||
local errand, and supply runs move it more than surveys. Ground won is not held
|
||
forever: the player's contribution decays over about seven minutes, so the enemy
|
||
presses back on its own.
|
||
|
||
**The war elsewhere.** The line also drifts with nobody's help, in both
|
||
directions. This is not background flavour: the brief is explicit that ambient
|
||
drift is what makes the setting a war rather than a level, and that it is the
|
||
*only* mechanism for the enemy's attention being pulled elsewhere — there is no
|
||
separate "baseline threat" system alongside it.
|
||
|
||
Drift is a sum of slow waves rather than a random walk. A walk either wanders
|
||
off the map or, once reined in, moves so little the war reads as static. Waves
|
||
at incommensurate periods are unpredictable in practice, always bounded, and
|
||
reproducible from the seed — so "the region changed while you were away" is
|
||
something a test can assert. Most of the movement is shared by every boundary,
|
||
because a front that advances advances as a whole; each band then wobbles a
|
||
little on its own so the map is not one rigid object sliding about.
|
||
|
||
In practice a boundary swings about ±45 m and a spot near a border changes hands
|
||
several times over a quarter of an hour.
|
||
|
||
**Nothing announces any of this.** There is no notification, no readout, no
|
||
number. The minimap shows who held each patch of ground *when you last stood in
|
||
it* — so after a while away, your map is simply wrong, and the only way to find
|
||
out is to go back and look.
|
||
|
||
**You are never told which band you are in by the world itself.** Crossing a
|
||
border eases the fog, the light and the palette: your own ground is open and
|
||
green, contested goes flat and hazy, occupied turns dusty and closes in, the
|
||
frontier is cold and short-sighted. The HUD names the band for now, but that
|
||
line is debug scaffolding for the same reason the heat numbers are — Phase 3's
|
||
gate is telling them apart *without* being told.
|
||
|
||
## The map
|
||
|
||
A sketch map, top right, drawn from `Intel` rather than from the world:
|
||
|
||
- **The road network's shape is always visible**, as faint dashed lines. You are
|
||
an operative with a map, not an amnesiac.
|
||
- **Roads you have actually seen** are drawn solid and coloured by the heat you
|
||
remember on them — which is what you last saw, not what is there now.
|
||
- **Ground you have driven through** is filled in, tinted by who held it *when
|
||
you were last there*. A survey mission fills in a whole area at once.
|
||
- **The border** is drawn between adjacent remembered cells that you recall as
|
||
belonging to different sides — never from the front's true position, which
|
||
would hand you a live readout of a line that is supposed to move behind your
|
||
back.
|
||
- **Old notes fade.** An observation drawn faint is one you should not trust.
|
||
|
||
Nothing on the map reads live world state. Heat is what you last saw on a road;
|
||
territory is who held it when you last stood there. Both keep changing after you
|
||
leave, and the map has no way of knowing.
|
||
|
||
The HUD also carries an eight-point compass arrow to the active target, since
|
||
the map alone does not tell you which way to point the car.
|
||
|
||
## Layout
|
||
|
||
The one rule worth keeping: **`src/sim/` imports neither three.js nor Rapier.**
|
||
Phases 1–4 (heat, regions, front lines, quests) are all simulation, and keeping
|
||
them engine-free is what makes them unit-testable and fast-forwardable — you can
|
||
run a hundred simulated days in milliseconds to tune an escalation curve without
|
||
ever opening a browser.
|
||
|
||
```
|
||
src/
|
||
sim/ pure model, no engine imports:
|
||
world, roads, heat — the map and its memory
|
||
radio, opportunities — texture: chatter and field work
|
||
save — the campaign as JSON
|
||
regions — the front line and who holds what
|
||
intel, routing, quests — what the player knows and is asked to do
|
||
bases, car — where missions come from, and decline
|
||
physics/ Rapier world, raycast vehicle, input → wheel forces
|
||
render/ three.js scene, roads, markers, chase camera
|
||
core/ fixed-timestep loop, seeded RNG, keyboard
|
||
ui/ debug HUD, quest board, sketch minimap
|
||
persistence.ts integration layer: saves in localStorage
|
||
carSpec.ts shared car dimensions, so body and mesh cannot drift apart
|
||
heatProps.ts integration layer: heat levels → colliders + meshes
|
||
```
|
||
|
||
`heatProps.ts` sits at the top level on purpose — it is the one module allowed to
|
||
touch both Rapier and three.js, because it owns objects that must exist in both
|
||
or neither. Its tests run headless: three.js scene graphs work fine in Node, so
|
||
"the barricade's collider was removed along with its mesh" is a unit test.
|
||
|
||
Data flows one way: `sim` → `physics` → `render`. Condition reaches the physics
|
||
layer already digested into a `Handling` by `deriveHandling`, so there is exactly
|
||
one place where "how broken the car is" turns into "how it drives".
|
||
|
||
## Notes on the state of it
|
||
|
||
- **The car handles like a placeholder.** Suspension, grip, and engine numbers in
|
||
`physics/physics.ts` and `sim/car.ts` are first guesses that pass a smoke test,
|
||
not something tuned by feel. That tuning *is* Phase 0's verification gate.
|
||
- **Wear rates are deliberately aggressive** so decline is visible in minutes
|
||
rather than hours. Turn them down in `applyWear` once the curve reads right.
|
||
- **No interpolation** between physics steps. Fine at 60 Hz; revisit if the step
|
||
rate changes.
|
||
- **Nothing is persisted** across reloads — condition, heat, intel and completed
|
||
runs all reset. IndexedDB comes with the campaign layer, and `sim/intel.ts`
|
||
plus `sim/heat.ts` are the two things that most need it.
|
||
- **Repairs are automatic on hand-in.** Parts get spent the moment you report
|
||
back. The brief wants repair to be a physical, on-foot act with scavenged
|
||
parts; that is Phase 6.
|
||
- **The board's route preview assumes the shortest path.** If you drive a
|
||
different way, the intel you were shown described a route you did not take.
|
||
`findRoute` already accepts a cost function, so a "safest route" preview is a
|
||
small change when it is wanted.
|
||
- **Junctions have no heat of their own.** A point near a junction is credited to
|
||
whichever segment is nearest, so a heavily used crossroads never fortifies as a
|
||
crossroads. Known gap, deferred on purpose.
|
||
- **Barricades are visually crude.** Blocking the whole 24 m corridor means two
|
||
long concrete slabs, which reads more like a wall than a checkpoint. The
|
||
gameplay shape is right; the presentation wants berms, wire, or wreckage on the
|
||
outer sections. They can also clip scenery placed on the verge — both are
|
||
static bodies, so physics is unaffected, but it looks wrong up close.
|
||
- **Heat has no diegetic signal at a distance.** You learn a road is hot by
|
||
arriving at the checkpoint. The brief wants it readable from patrol density and
|
||
wreckage before you commit — that needs the enemy presence Phase 5 brings.
|
||
- **The heat HUD lines are debug scaffolding.** The brief is explicit that no
|
||
numeric heat meter ships; they exist to tune the curve and should come out.
|
||
- **Bundle:** ~2.7 MB raw / ~945 KB gzipped, dominated by Rapier's WASM, which
|
||
`rapier3d-compat` inlines as base64. Switching to the non-compat `@dimforge/rapier3d`
|
||
package serves the WASM as a separate file (~570 KB gzipped, compiled in
|
||
parallel with the JS) at the cost of extra Vite plugin config. Worth doing
|
||
before anyone but you plays it; not worth doing now.
|
||
|
||
## Next
|
||
|
||
Two gates are open at once, and both need a person, not a test.
|
||
|
||
**Phase 1:** you catch yourself avoiding a road because of its history, not its
|
||
distance. If it fails, check `METRES_PER_HEAT` (escalation too slow to matter in
|
||
one session), `DECAY_PER_SECOND` (nothing accumulates), or whether the props are
|
||
actually inconvenient enough to route around.
|
||
|
||
**Phase 2:** you take the new target a meaningful fraction of the time, for
|
||
reasons other than curiosity — because the known one has genuinely got
|
||
expensive. If the known option is always strictly better, raise `NOVELTY_BONUS`.
|
||
If you never take a known target, lower it.
|
||
|
||
**Phase 3:** driving into an occupied region, you can tell it from a liberated
|
||
one within a couple of minutes, from tone alone, without reading the HUD line
|
||
that names it. If you cannot, the palettes in `render/scene.ts` are too close
|
||
together — push them apart before trusting the band boundaries.
|
||
|
||
**Phase 5:** someone coming to this cold can describe what kind of game it is
|
||
and who they are playing after one session, without the premise being explained
|
||
first. If the four jobs still feel like one job with four names, the dial is
|
||
`MISSION_SHAPE` — push the work times and the cargo risk further apart before
|
||
adding anything new.
|
||
|
||
**Phase 4:** returning to a region after time away, you are genuinely unsure
|
||
what you will find until you are back on the ground — and when it has changed,
|
||
it reads as the world having moved rather than as a notification you were given.
|
||
If the world instead feels arbitrary, the drift is too fast: lengthen the wave
|
||
periods in `sim/regions.ts` before shrinking their amplitude, since it is the
|
||
*rate* of change that reads as noise, not the size of it.
|
||
|
||
Phases 0–5 are all in. **Phase 6** is the deferred pile: a deeper repair economy
|
||
with scavenged parts and on-foot work, car identity and disguise feeding into
|
||
heat, and expanded combat. Nothing there should be pulled forward until the loop
|
||
below it is proven.
|
||
|
||
The most useful thing before any of that is taking the debug lines out of the
|
||
HUD. Several gates now depend on reading the world rather than a readout, and
|
||
the scaffolding actively works against you when you sit down to judge them.
|
||
|
||
Watch for one failure mode in particular: if you find yourself taking whichever
|
||
job is *nearest* and ignoring the intel line entirely, the board is decorative
|
||
and the phase has not landed, whatever the reward numbers say.
|