drive-between-the-lines/README.md
dejvino 475db8773c Heat: credit the verge to the road, and block the whole corridor
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>
2026-08-07 09:41:52 +02:00

6.9 KiB
Raw Blame History

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 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 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 — 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: simphysicsrender. 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.