drive-between-the-lines/README.md
dejvino 73e8ac68f1 Phase 2: bases, quest board, and known-vs-new targets
Four allied bases, placed by greedy farthest-point so no choke point can strand
the player, with the first at the spawn junction so the loop is available at
once. Missions hand in at any base, not the issuing one.

The board offers known and new targets side by side and describes each route in
words. Crucially it never reads heat directly — it reads sim/intel.ts, a record
of what the player has actually observed and when. Notes age while roads keep
escalating unwatched, so a known route is knowable, stale, and never a promise.
Recon missions survey everything within 130m, which is how a new target becomes
a known one without driving every road there.

New targets pay 1.8x. That multiplier is the phase's tuning dial: too low and
nobody takes the unknown, too high and nobody takes the known.

Payment is parts, which repair the car. To keep "decline, not reset" intact,
each subsystem gains a ceiling that falls permanently with damage — repairs
restore up to what the car is still capable of, never to what it was. Without an
economy the board's rewards had no stakes; with one, the risky job is what keeps
the car running a while longer.

Adds Dijkstra over the road graph, taking a cost function so a "safest route"
preview is a small change later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:06:32 +02:00

211 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
## 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 |
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.
```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 route corridor
Heat is credited to a road for anything within `ROUTE_CATCHMENT` (12 m) of its
centreline, not just the 9 m of tarmac. Otherwise the dominant strategy is to
drive *next to* every road and never accrue heat at all.
Two consequences follow from that, and both are deliberate:
- **The cutoff is hard, not a taper.** A taper leaves a gradient to optimise
along — sit at 80% of the catchment, 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.
12 m is chosen, not arbitrary: it puts ~7.5 m of verge beyond the kerb (about
two car widths, so hugging the shoulder gains nothing) while leaving 57% of the
map genuinely off-route. Widening it backfires — at 20 m two thirds of the map
counts as on-route, heat becomes unavoidable, and "take a different route" stops
being a choice at all.
### 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.
Two mission types run end to end: **supply runs** and **surveys**. Both are
travel → hold position at the target → return. 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.
### 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.
## Layout
The one rule worth keeping: **`src/sim/` imports neither three.js nor Rapier.**
Phases 14 (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
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
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.
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.