Adds the harsher crash calibration, the overhaul, the death cam, the currency, the traffic rules and why each of them is narrower than it looks, crewed vehicles, allied convoys, and the building-grid bug that was letting bullets, line of sight and people through three quarters of every wall. Two new gates under Next: whether the crash cost reads as punishing or as a reason to slow down, and whether the overhaul becomes something you save for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
810 lines
43 KiB
Markdown
810 lines
43 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.
|
||
- **Phase 10** — a world that behaves: traffic with lanes, crews you can read,
|
||
walls that are actually there, and money you can spend.
|
||
|
||
## 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 |
|
||
| `C` | overhaul the car, 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, repairs, and the overhaul
|
||
|
||
New targets pay `NOVELTY_BONUS` (1.8×) more than known ones. The currency sign
|
||
is `¤` — Unicode's generic one, which looks like money and is nobody's actual
|
||
money, for a place whose banks are somebody else's problem. 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 money, and money repairs the car. 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.
|
||
|
||
Repairs happen automatically on hand-in, and they stop at the ceiling. Pressing
|
||
`C` at a base does the other thing: an **overhaul**, which buys the *ceiling*
|
||
itself back at 0.12 a point per ¤1, worst subsystem first. It deliberately does
|
||
not touch the current level — it raises what the car could be, and ordinary
|
||
repair still has to fill it in.
|
||
|
||
The ceiling used to fall and nothing else: the only number in the game that
|
||
moved one way, with no counterplay at any price. That reads less as a design
|
||
pillar than as a punishment you cannot answer. Now decline is something you can
|
||
fight, and what keeps it meaningful is the exchange rate rather than the
|
||
impossibility — putting a car back to factory after one bad crash is about a
|
||
dozen missions. Early on there is never a surplus to spend, so the option is
|
||
invisible; it only becomes the thing to save for once the ceiling is what is
|
||
actually holding you back.
|
||
|
||
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 ¤30. 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.
|
||
|
||
What it costs is deliberately brutal. That reference head-on leaves the chassis
|
||
at **23%**, the tyres at 61% and the engine at 75%, ordered by what a collision
|
||
actually ruins. One is a disaster you drive home from; two in a row is the end
|
||
of the car. An earlier calibration left it at 65% and perfectly driveable, which
|
||
made hitting a building at ninety an inconvenience rather than a decision.
|
||
|
||
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
|
||
¤30 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 money 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.
|
||
|
||
### The five seconds before you wake up
|
||
|
||
The run does not end the instant the car does. Control goes, the camera leaves
|
||
the bumper and climbs away to sixty metres — turning slowly — until the street,
|
||
whatever you hit and whoever is standing around watching are all in frame. The
|
||
point of the shot is not the car; by then the car is a fact. It is *where* this
|
||
happened, which is the thing you have to drive back out to.
|
||
|
||
The world keeps running underneath it, because a death cam over a frozen world
|
||
is a screenshot rather than a moment. What stops is the driver: no throttle, no
|
||
board, no overhaul, and no quietly finishing the job by virtue of having stopped
|
||
moving on top of the target. Anyone shooting stops a quarter of the way in —
|
||
the burst that killed you should finish, but standing over a wreck emptying
|
||
magazines into it reads as the enemy beating a dead horse.
|
||
|
||
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.
|
||
|
||
**And they drive like traffic.** Cars hold a line parallel to the centreline,
|
||
offset right by a share of the road's own half-width — a track gets a nudge, a
|
||
trunk gets a lane — so two meeting head-on are on opposite sides of the road
|
||
instead of in the same metre of tarmac. Steering is rate-limited, so a
|
||
right-angle junction is a turn rather than one frame of instant pivot. They lift
|
||
off for the car in front, and overlapping vehicles are pushed apart, because
|
||
following distance does not cover a merge: two cars converging on a junction are
|
||
more than ninety degrees apart right up until they both turn onto the same
|
||
heading and are already touching.
|
||
|
||
Each of those rules is narrower than it first looks, and the narrowness is the
|
||
point. Only civilians defer, or a patrol queues politely behind a bus and never
|
||
reaches the road it was dispatched to. Only for cars going roughly the same way,
|
||
or two meeting on a single track each wait forever for the other. And separation
|
||
is same-direction only, since oncoming cars are already a lane apart — which on
|
||
a narrow road is less than the separation radius, so pushing them apart undid
|
||
the lane discipline that put them there.
|
||
|
||
**Armed vehicles carry a visible crew.** A patrol car used to be an empty box
|
||
that occasionally emitted tracers. There is now a man standing up out of it, and
|
||
his weapon comes up only once they have decided about you: across his lap
|
||
otherwise, levelled and tracking when the car is hunting or when something of
|
||
the other army is in range. That difference is the point — being recognised was
|
||
a meter filling up in the corner of the screen, and it is now something you can
|
||
watch happen across a street.
|
||
|
||
**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.
|
||
|
||
**Your own side has vehicles too.** Convoys route the network like traffic, so
|
||
they inherit lane discipline and following distance for nothing, but only
|
||
between junctions your side holds: 4 at home, 2 in contested ground, none past
|
||
it. Fewer than the men on foot, because a truck is scarcer than a rifle and
|
||
because friendly traffic outnumbering the civilian kind would turn every road
|
||
into a convoy. They carry the same crew the enemy cars do.
|
||
|
||
### 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.
|
||
|
||
**Nothing shoots or walks through a wall.** Rounds always died against
|
||
buildings, but nothing checked before pulling a trigger, so units emptied
|
||
magazines into the concrete between them and a target they could not see — what
|
||
you saw from the car was tracers coming out of a solid wall. Firing now needs
|
||
the same line of sight the round does. Free movement wrote straight into x and z
|
||
too, so pedestrians, militia, fighters and fleeing civilians all walked through
|
||
buildings; they now take the same detour the hunters do, and spawns retry rather
|
||
than dropping somebody inside a wall on the first frame.
|
||
|
||
Underneath both was one bug worth recording. The building lookup grid stepped
|
||
the world coordinate by the cell size from `x-reach` to `x+reach`, but a
|
||
building's reach is about 7 m and a cell is 24 m — so the loop always took
|
||
exactly one step, every building registered a single cell, and it silently
|
||
vanished from the other three it straddled. Points in those cells read as open
|
||
ground for bullets, for line of sight and for people. Six units in ninety were
|
||
standing inside buildings; now none are.
|
||
|
||
**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 panel also has `−¤` / `+¤`, so the workshop and the board can be exercised
|
||
without first driving eight missions to afford looking at them.
|
||
|
||
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 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.** Money gets spent the moment you report
|
||
back; the overhaul under `C` is the only deliberate spend. The brief wants
|
||
repair to be a physical, on-foot act with scavenged parts; that is still
|
||
deferred.
|
||
- **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 10:** the world should hold up to being *looked at* rather than only
|
||
driven through. Park somewhere busy and watch for a minute: traffic should
|
||
queue and pass rather than interpenetrate, crews should be relaxed until they
|
||
are not, and nothing should be standing in a wall. If any of that fails it is
|
||
visible immediately, which is the point of putting it in the list.
|
||
|
||
**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. One serious crash should be a disaster you drive home
|
||
from and two should end the car; if that reads as too punishing, the dial is
|
||
the coefficients in `applyWear` rather than `IMPACT_REFERENCE`, which is
|
||
measured rather than chosen.
|
||
- Money accumulates faster than you can spend it on repairs, and the overhaul
|
||
becomes the thing you save for. If you never press `C`, `CEILING_PER_FUND` is
|
||
too mean; if the ceiling stops mattering, it is too generous.
|
||
- 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.
|