Roads are a jittered 7x7 grid with ~a quarter of the edges thinned out, kept connected. The loops are the point: "take a different road" is not a decision without alternative routes. Every metre driven adds heat to that segment; all roads shed it slowly. Crossing a threshold escalates Clear -> Patrol -> Barricade -> Turret, each of which puts real obstructions on the tarmac. Hysteresis stops a road on a boundary from rebuilding its barricade every few seconds. Level changes are deferred while the car is on the segment. Building a barricade around the player would spawn a static collider inside the chassis, and the player is meant to discover escalation by returning to a road, not by watching it assemble behind them. Checkpoint positions are seeded from the segment id, so a stretch of road always fortifies in the same place — recognising it is part of learning the map. Scenery now avoids the tarmac and the car spawns at a junction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.3 KiB
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
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 |
npm test # sim + headless physics
npm run build # typecheck + production bundle
Road heat
Every metre driven on 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.
Three things are deliberate:
- 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.tsandsim/car.tsare 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
applyWearonce 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.
- 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-compatinlines as base64. Switching to the non-compat@dimforge/rapier3dpackage 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.