Driving alongside a road accrued no heat at all, so the dominant strategy was to never actually use one. Heat is now credited to any road within 12m of the centreline, tarmac or verge. The cutoff is hard rather than tapered: a taper leaves a gradient to optimise along, where sitting at 80% of the catchment costs 20% of the heat. A cliff edge means you either use the route or genuinely leave it. 12m is a balance, not a guess. It puts ~7.5m of verge past the kerb — two car widths, so hugging the shoulder gains nothing — while leaving 57% of the map off-route. At 20m two thirds of the map counts as on-route, heat becomes unavoidable everywhere, and choosing a different route stops meaning anything. Barricades now span the full corridor for the same reason. Stopping at the kerb left them free to round on the verge at speed. Junction heat is still deferred; a point near a junction is credited to the nearest segment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
6.9 KiB
Markdown
149 lines
6.9 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.
|
||
|
||
## 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 |
|
||
|
||
```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.
|
||
|
||
## 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 — roads, heat, world generation, condition → handling
|
||
physics/ Rapier world, raycast vehicle, input → wheel forces
|
||
render/ three.js scene, roads, chase camera
|
||
core/ fixed-timestep loop, seeded RNG, keyboard
|
||
ui/ debug HUD
|
||
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.
|
||
- **Condition and heat are not yet persisted** across reloads. IndexedDB comes
|
||
with the campaign layer.
|
||
- **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
|
||
|
||
Phase 1's gate: **you catch yourself avoiding a road because of its history, not
|
||
its distance.** That cannot be checked by a test — it needs you driving the same
|
||
routes for a while and noticing what you start doing.
|
||
|
||
If it fails, the likely culprits, in order: escalation is too slow to matter
|
||
within a session (`METRES_PER_HEAT`), decay is so fast that nothing accumulates
|
||
(`DECAY_PER_SECOND`), or the props are not actually inconvenient enough to route
|
||
around. Tune before building Phase 2 on top.
|