drive-between-the-lines/README.md
dejvino 506d07be5d Phase 1: road network and heat
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>
2026-08-07 07:47:19 +02:00

5.3 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 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 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.
  • 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.