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

121 lines
5.3 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.
## 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 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: `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.
- **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.