The README is this project's design document, so the reasoning behind each change belongs in it rather than only in commit messages. Adds sections on what a crash costs and why the coefficients are anchored to a measured figure, killed in action, giving up a job, districts and landmarks, militia, and how a hunter drives. Also removes three notes that had gone stale - persistence not existing, wear rates awaiting tuning, and the heat lines wanting to come out of the HUD - and replaces the "take the debug lines out first" instruction under Next with three Phase 9 gates, since all of those are now done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
706 lines
36 KiB
Markdown
706 lines
36 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.
|
||
- **Phase 6** — an inhabited world: traffic, people, patrols, and a live war.
|
||
- **Phase 7** — sound, synthesised from the simulation.
|
||
- **Phase 8** — recognition: being noticed, being hunted, and getting away.
|
||
- **Phase 9** — stakes: a crash that costs something, a death that costs
|
||
something, a chase you can lose, and a town you can navigate.
|
||
|
||
## 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 |
|
||
| `X` | give up the mission in hand, when parked at a base |
|
||
| `M` | sound on/off |
|
||
| hold `R` | wipe the campaign and start over (5 seconds) |
|
||
|
||
Add `?debug=1` for the tuning readouts — heat, district standing, which band
|
||
you are in — and the debug panel. Without it the HUD tells you the car, the job
|
||
and your cover, and nothing else: several of the design gates below are about
|
||
reading the world rather than a number, and they cannot be judged with the
|
||
number on screen.
|
||
|
||
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** sends a car to work the road for a while, which cools it back down.
|
||
- **Barricade** sends an engineer to pour concrete across it, leaving a gap you
|
||
have to slow for.
|
||
- **Turret** sends another to put up a tower, and a gunner to man it.
|
||
|
||
None of that appears on its own — see [A world with people in it](#a-world-with-people-in-it).
|
||
|
||
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.
|
||
|
||
### District heat
|
||
|
||
Roads remember being used. Districts remember *you*.
|
||
|
||
Road heat alone rewards a strange kind of play: work one part of town hard but
|
||
take a different street every time, and nothing ever gets hot. So there is a
|
||
second, coarser pool. Every metre you cover inside a district raises its
|
||
standing — on-road or off, whether or not you touch a road at all — and that
|
||
bleeds into every road running through it, including ones you have never driven.
|
||
|
||
- It **accrues from being there**, so cutting across country is not a loophole.
|
||
- It **cannot fortify a road on its own** (`AREA_INFLUENCE` sits below the
|
||
barricade threshold). A hot district gets roads patrolled; concrete still
|
||
takes somebody actually using that road.
|
||
- It **outlasts road heat**. A road is re-secured by one patrol; a district's
|
||
reputation takes far longer to fade.
|
||
- A patrol working a road **does** settle the district around it, at about a
|
||
tenth of the rate it settles the road. Any faster and waiting out a patrol
|
||
would launder your reputation, and district heat is meant to be the part you
|
||
cannot patrol away.
|
||
|
||
### Speed is conspicuous
|
||
|
||
Heat scales with how fast you were going, not just how far. A car at walking
|
||
pace is traffic; a car doing ninety through a checkpointed district is the thing
|
||
people phone in. Roughly a 2.2× spread between crawling and flat out, and it
|
||
stops rewarding ever-higher speed past a point so the answer is never one exact
|
||
number on the clock.
|
||
|
||
Together with the road classes this is the real routing decision: the trunk fast
|
||
and noticed, or the lanes slow and ignored.
|
||
|
||
### 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.
|
||
|
||
### Tarmac versus everything else
|
||
|
||
Roads have to be worth using, or the hierarchy above is decoration. Off the
|
||
route the car makes about 40% of its road top speed, loses grip, and scrubs off
|
||
momentum in a fifth of the distance. It is a real cost, deliberately not a wall:
|
||
going around a checkpoint cross-country is a legitimate move, and an earlier
|
||
tuning that made off-road a 12 km/h crawl removed the choice rather than pricing
|
||
it.
|
||
|
||
### 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.
|
||
|
||
### Districts, and the buildings that ignore them
|
||
|
||
Every building used to be the same grey box between four and twelve metres tall.
|
||
From the air the world was one texture; from the driver's seat every junction
|
||
looked like the junction before it, and "learn the map" was not something the
|
||
map supported.
|
||
|
||
The world is cut into districts, and a district agrees with itself: its
|
||
buildings share a palette, a rough height and a silhouette, and vary only
|
||
slightly around them. That agreement is the point twice over — it makes crossing
|
||
into the next district a visible event, and it is the only reason the exceptions
|
||
read at all.
|
||
|
||
Because about one plot in fifty ignores its district completely: much taller,
|
||
lighter, a different shape. Those are the things you actually navigate by, and
|
||
they are floored at a height that clears every ordinary roofline on the map,
|
||
since a landmark you cannot see from outside its own district is no use in the
|
||
only place you need it.
|
||
|
||
Colour rides on the instance rather than the material, so a district costs no
|
||
extra draw calls. Colliders stay boxes whatever the silhouette says — both the
|
||
physics and the line-of-sight grid already assume a building is solid to its own
|
||
footprint.
|
||
|
||
### Other deliberate choices
|
||
|
||
- **Concrete never closes around the car standing in the gap.** A block spawning
|
||
on top of you would put a static collider inside the chassis, so the last slab
|
||
waits until you move.
|
||
- **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.
|
||
|
||
### Giving one up
|
||
|
||
Park at any base with a job you have not started bringing home and `X` takes it
|
||
off you, for 12 m of front line. A target can end up behind concrete there is no
|
||
way round, and the only exit used to be holding `R` — which does not drop the
|
||
job, it destroys the campaign. It cannot be free either, or the board is a
|
||
reroll button: park, dislike the route, drop it, take another. Dropped jobs are
|
||
counted apart from finished ones. They are not runs.
|
||
|
||
### 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.
|
||
|
||
### What a crash actually costs
|
||
|
||
The impact coefficients were originally written against an imagined scale.
|
||
Measured on the running game, a head-on into a building at 85 km/h comes back
|
||
from Rapier as a single contact event of about **2.5e6 N** — twenty-five units
|
||
of "impact" against the old 1e5 reference. Through the old coefficients that was
|
||
123% of the chassis and 37% of its ceiling, gone, in one step, in a game whose
|
||
missions pay about 0.3 parts. One crash ended the campaign, and the campaign had
|
||
no way to end.
|
||
|
||
Everything is now anchored to that measured figure, and damage is capped per
|
||
step — a crash is a pile of contacts across several steps plus whatever the car
|
||
scrapes on the way to a standstill, and that sum has no natural bound. A hard
|
||
crash costs about 15% of the chassis and 5% of its ceiling.
|
||
|
||
Baseline wear came down with it. Tyres shed 8e-5 per metre, so twelve clean
|
||
kilometres took them from new to ruined while a two-kilometre round trip paid
|
||
0.3 parts against 0.24 of condition spent. Driving perfectly barely broke even.
|
||
At a quarter of that, a typical mission spends about 0.06, so roughly two clean
|
||
runs pay for one bad crash — enough slack to gamble with, not enough to ignore.
|
||
|
||
## Killed in action
|
||
|
||
A car could be driven to zero in every subsystem and would keep going forever at
|
||
a wheezing thirty kilometres an hour. Nothing was at stake, so nothing was worth
|
||
protecting, so there was never a reason to take the quiet road. "Decline, not
|
||
reset" only reads as a stake if the decline can run out.
|
||
|
||
When the **chassis** is gone, so are you. Somebody pulls you out; you come round
|
||
at the nearest base; the job in hand is written off with the car; and 25 m of
|
||
front line goes back to the enemy, through the same path a finished mission uses
|
||
so the war moves for one reason only.
|
||
|
||
The replacement is a real car — patched to its ceilings, because one that still
|
||
needed the parts you do not have would just kill you again on the way out of the
|
||
yard — and every ceiling is 8% down on the one you wrote off. That is the
|
||
ratchet applied at the one moment repairs cannot reach.
|
||
|
||
It stops at a floor, and that is worth being honest about: strictly it breaks
|
||
"the ceiling only ever falls". Without one, enough deaths issue a car that is
|
||
written off the instant you are handed it, and the only way out of the campaign
|
||
is wiping it. A genuinely miserable car you can still drive is the interesting
|
||
version of that ending rather than the broken one.
|
||
|
||
Nothing about any of it is announced. The front moving against you is something
|
||
you find out by driving back out there.
|
||
|
||
## A world with people in it
|
||
|
||
Up to Phase 5 the world was scenery. Heat poured concrete on a road and that was
|
||
the whole of the enemy. `sim/units.ts` is the layer that makes it inhabited.
|
||
|
||
**Enemy ground is patrolled because it is enemy ground.** There are standing
|
||
patrols on the roads wherever you are past the line, and more of them the deeper
|
||
in you go — none in liberated territory, one in contested, three in occupied,
|
||
five on the frontier. Nobody summoned these; they are what hostile territory
|
||
looks like.
|
||
|
||
**On top of that, nothing appears — things are sent.** When a road turns
|
||
notorious a patrol is *dispatched*: it starts at a junction a few hundred metres
|
||
away, drives there, sweeps up and down for about a minute, and while it works the
|
||
road it brings the heat back down. Then it leaves. The enemy is not punishing
|
||
you; they are re-securing a route and going home.
|
||
|
||
**Everything on a road is built, including the concrete.** At `barricade` an
|
||
engineer is dispatched and the blocks go up over about twenty seconds of someone
|
||
standing there; at `turret` another is sent and the tower takes forty. Arrive
|
||
mid-build and you find a half-finished stump with nobody on it. Only once the
|
||
tower stands does a gunner man it, up on top so he shoots over his own barricade.
|
||
Let the road cool and the whole thing is abandoned.
|
||
|
||
**Traffic and people.** Civilian cars route across the network with somewhere to
|
||
be; people wander on foot near the buildings. Both scatter when shooting starts.
|
||
|
||
**Your own side is standing in your own streets.** Liberated territory read as
|
||
empty — no patrols by design, and nobody else either — so the safest part of the
|
||
map was also the deadest, and coming home felt like arriving nowhere rather than
|
||
arriving somewhere held. Militia mirror the ambient patrols the other way up:
|
||
fourteen at home, five in contested ground, none past that, because a friendly
|
||
face on the frontier would be a safety net the setting is not supposed to have.
|
||
Crossing the map reads 14, 14, 11, 5, 0 against enemies 0, 0, 3, 4, 4 — who is
|
||
standing in the street is a more legible signal of whose ground you are on than
|
||
the palette is.
|
||
|
||
They are placed by checking who holds the ground they would *stand* on rather
|
||
than the band the player is in, since near a border those are different answers,
|
||
and they leave when the line moves over them.
|
||
|
||
### The war you are driving through
|
||
|
||
Skirmishes break out between the two armies on contested and occupied ground,
|
||
whether or not you are anywhere near. They are not a set piece for the player —
|
||
you are not invited.
|
||
|
||
Rounds are **real objects in flight**, not instant hits resolved between two
|
||
combatants. That is the whole point: if shots resolved instantly, driving through
|
||
a battle would cost nothing, and "the only road to the target runs through a
|
||
firefight" would not be a decision. A stray round does not check whose war it is,
|
||
so it will hit a bystander, a civilian, or you.
|
||
|
||
Buildings stop bullets, which is what makes cover worth anything. Hits are routed
|
||
through the same wear model as a crash, so being shot costs you ceiling too — it
|
||
is permanent in the same way everything else is.
|
||
|
||
**The enemy does not shoot at you by default.** You are an undercover driver, not
|
||
a target. They open up only past the line *and* on a road they have already
|
||
checkpointed — that is, a road you personally made notorious.
|
||
|
||
## Being recognised
|
||
|
||
You are an undercover driver, not a combatant, and that only means anything if
|
||
cover is a state you can *lose*.
|
||
|
||
**Suspicion** builds while enemy eyes are on you — faster the closer they are,
|
||
squared with proximity, so one patrol beside the car matters more than a crowd
|
||
across the street. Line of sight is real: buildings block it. Behind your own
|
||
lines it cannot build at all. Some things skip the meter entirely and land in
|
||
full — ramming a vehicle, being shot at, and above all putting somebody under
|
||
the wheels.
|
||
|
||
At the top of the meter you are **hunted**. Nearby enemies drop what they were
|
||
doing and come after you, and they keep recruiting as you drive past fresh
|
||
patrols — a chase you could outrun by passing more of them would be no chase.
|
||
This is also the *only* thing that makes the enemy shoot at you: being a target
|
||
is a state, not a property of the road you are on.
|
||
|
||
**They drive differently when they are chasing.** A patrol on its rounds does
|
||
14 m/s and a healthy car does 29, so being hunted used to mean holding `W` until
|
||
the 95 m sight radius did the rest. Chasing cars wind up to 21 m/s, which sits
|
||
deliberately between the two: faster than anyone averages through a grid of
|
||
junctions and buildings, slower than the car's top end on a clear run. So a long
|
||
trunk straight is an escape and the lanes are not — which puts the road
|
||
hierarchy under real pressure for the first time. The fast conspicuous road is
|
||
the one you want when they are behind you, and it is the one they will be
|
||
looking on next time.
|
||
|
||
They also go **round** buildings rather than through them. Line of sight was
|
||
already blocked by them and movement was not, which made "break line of sight
|
||
and change direction" advice the world did not honour.
|
||
|
||
**Getting away** takes one of two things:
|
||
|
||
- **Break line of sight for fourteen seconds.** They drive to where they last
|
||
saw you, not to where you are, so turning a corner and changing direction is
|
||
the move. Any glimpse resets the clock.
|
||
- **Let your own side occupy them.** Hunters are patrols, not fanatics: an
|
||
insurgent squad on their doorstep is a better use of their time. Enough of
|
||
them peeling off ends the chase outright. This is why the militia matter
|
||
mechanically as well as atmospherically — running for your own lines works
|
||
because the closer you get to home the more likely they peel off. The rule
|
||
always existed; there was simply nobody around to trigger it.
|
||
|
||
Escaping does not hand your cover straight back. Suspicion drops to 0.6 and
|
||
decays slowly from there — they know there is a car worth looking for, and you
|
||
have to be dull for a while to be forgotten.
|
||
|
||
## Debug
|
||
|
||
Load with `?debug=1` for a `window.__dbtl` handle and a small panel in the
|
||
corner. The panel has `−` / `+` buttons for the car's condition, because
|
||
"drive a wrecked car for a minute" is something you want to do *while driving*
|
||
rather than while typing into a console.
|
||
|
||
The handle also steps the simulation by hand, renders on demand, and writes
|
||
frames to `shots/` via a dev-only endpoint — which is how this build gets looked
|
||
at at all when the tab is not compositing. `__dbtl.state` exposes the live sim.
|
||
|
||
## Sound
|
||
|
||
Everything is **synthesised** — there are no audio files. The build has to stay
|
||
self-contained, so the engine is detuned oscillators, the tyres and gunfire are
|
||
shaped noise, and the ambience is a filtered drone. That constraint turned out to
|
||
be an advantage: sound generated from the simulation can carry the simulation's
|
||
state rather than just decorating it.
|
||
|
||
- **The engine has gears**, so the note climbs and drops rather than rising
|
||
forever with the speedometer.
|
||
- **A worn engine sounds wrong**, not just slow: roughness rises and the top of
|
||
the rev range falls away as condition drops. The car's decline is audible.
|
||
- **Bald tyres squeal sooner** than good ones, off the same lateral slip the
|
||
physics actually reports.
|
||
- **Gunfire is positional** — it falls off with the square of distance and pans
|
||
as you turn, so a firefight two streets away is a direction, not just a noise.
|
||
- **Territory has a tone.** The drone drops and thickens the further past the
|
||
line you are, matching what the palette does visually.
|
||
|
||
The split is deliberate: `audio/mix.ts` holds every decision as plain numbers
|
||
and is unit-tested; `audio/audio.ts` is only Web Audio wiring, which cannot be
|
||
tested outside a browser. Browsers refuse to make noise before the user has
|
||
interacted, so the context is resumed on the first keypress or click.
|
||
|
||
## 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.
|
||
|
||
Your own ground is a pocket, not the map: roughly 15% liberated, 25% contested,
|
||
27% occupied, 33% frontier. The starting base sits at the friendly *edge* rather
|
||
than in the middle, because starting central put half the world harmlessly
|
||
behind your own line where nothing watches and nothing happens. Those splits are
|
||
measured, not guessed — the bands are strips across a square with the axis
|
||
running diagonally, so area is nowhere near linear in depth.
|
||
|
||
**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. On top of the light, the population: home
|
||
ground is thick with your own people and thins to nobody past the line. The HUD
|
||
names the band only under `?debug`, for the same reason the heat numbers are
|
||
behind it — Phase 3's gate is telling them apart *without* being told, and you
|
||
cannot judge that with the answer on screen.
|
||
|
||
## 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.
|
||
- **Your own ground starts filled in.** A fresh campaign knows the pocket its
|
||
side holds — the cells, the roads through them, and the junctions as places
|
||
you have been. You are a local with a car; the war is the part you would not
|
||
know. It also fixes the board, which splits its offers into targets you have
|
||
visited and targets you have not: with nothing visited every offer came up
|
||
"new", so the known-against-new comparison the mission design turns on was not
|
||
on screen until you had wandered around for a while. None of it survives the
|
||
line moving — the map still only remembers who held a place when you last
|
||
stood in it.
|
||
- **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, its districts and its memory
|
||
radio, opportunities — texture: chatter and field work
|
||
units, combat — traffic, patrols, soldiers, rounds
|
||
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
|
||
kia — what happens when the decline runs out
|
||
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
|
||
audio/ mix (pure: what it should sound like), audio (Web Audio wiring)
|
||
persistence.ts integration layer: saves in localStorage
|
||
debug.ts ?debug=1 handle: step, render and capture frames to shots/
|
||
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 tuned against the parts economy** rather than by feel: a
|
||
clean mission spends about a fifth of what it pays, and a hard crash costs
|
||
roughly two clean missions. Those ratios are the dial, not the raw numbers.
|
||
- **No interpolation** between physics steps. Fine at 60 Hz; revisit if the step
|
||
rate changes.
|
||
- **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.
|
||
- **Hunters steer round buildings by trial rather than by pathfinding.** They
|
||
try seven headings, widening either side of straight-on, and take the first
|
||
that is clear. Good enough that corners work; it will not solve a cul-de-sac.
|
||
- **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.
|
||
- **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.
|
||
|
||
**Phase 6:** the world should feel lived in before it feels dangerous — traffic
|
||
and people you have no reason to interact with, and a war that is obviously not
|
||
about you. If firefights read as encounters staged for the player, the spacing
|
||
and spawn rules in `sim/units.ts` are the dial.
|
||
|
||
**Phase 9:** three gates, and all of them are about whether anything is at
|
||
stake.
|
||
|
||
- You slow down for a corner you would previously have taken flat, because the
|
||
car is worth protecting. If you still drive like it is a rental, `IMPACT_REFERENCE`
|
||
is too forgiving; if a single mistake ends the run, it is too harsh.
|
||
- Being hunted makes you think about *where* rather than only about the
|
||
throttle. If you still escape by holding `W` on any road at all, raise
|
||
`HUNT_SPEED`; if the chase is unloseable, the answer is more militia near home
|
||
rather than slower hunters, since being saved by your own side is the more
|
||
interesting exit.
|
||
- You can say where you are without the HUD, from the roofline and from who is
|
||
standing in the street. Districts and militia are both aimed squarely at this;
|
||
if it still takes reading a line of text, push `PALETTES` apart before
|
||
touching anything else.
|
||
|
||
Still deferred: a deeper repair economy
|
||
with scavenged parts and on-foot work, car identity and disguise feeding into
|
||
heat, expanded combat, and something happening when the front line reaches a
|
||
landmark — missions move it and nothing ever acknowledges that it moved. Nothing there should be pulled forward until the loop
|
||
below it is proven.
|
||
|
||
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.
|