# 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. - **Phase 2** — bases, a quest board, and known-vs-new target selection. ## 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 | | `1`–`3` | accept a mission, when parked at a base | Drive to a green beacon (a base) and stop. Take a job, drive to the amber beacon, stop and hold position for three seconds, then return to **any** base. ```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. ## Missions and intel The board at each base offers a mix of **known targets** and **new** ones, and describes the route to each in words rather than numbers — "barricaded · last word 6 min ago", "no one has been down that way". The important part is *where those words come from*. The board never reads heat directly. It reads `sim/intel.ts`, which is a snapshot of what the player has actually observed and when. Drive a road and you write down what was on it; leave it alone and your note ages while the road keeps escalating without you. So a known route is genuinely knowable, genuinely stale, and never a guarantee — which is the tension the brief asks for. **Recon missions** are the tool for converting a new target into a known one: completing one surveys every road within 130 m, filling in the map without having to drive each one. Two mission types run end to end: **supply runs** and **surveys**. Both are travel → hold position at the target → return. Hand-in works at *any* base, not the issuing one, because the brief is explicit that a blocked road home must never strand the player. ### Rewards, and why repairs exist New targets pay `NOVELTY_BONUS` (1.8×) more than known ones. That multiplier is the phase's tuning dial: if you never take the new target it is too low, and if you never take the known one it is too high. Payment is in **parts**, which repair the car — the only thing in the game that pushes condition back up. Repairs go to the worst subsystem first and stop at its **ceiling**, which drops permanently with every bit of damage taken (`PERMANENT_SHARE`, currently 30%). So a car can be patched back to what it is still capable of, but never to what it was. The HUD bars show this directly: `█` is current condition, `·` is what repairs could still reach, `×` is gone. This is what gives the board's reward a reason to matter. Without an economy the choice between a safe route and a risky one has no stakes; with one, the risky job is what keeps the car alive a while longer. ## 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, no engine imports: world, roads, heat — the map and its memory intel, routing, quests — what the player knows and is asked to do bases, car — where missions come from, and decline physics/ Rapier world, raycast vehicle, input → wheel forces render/ three.js scene, roads, markers, chase camera core/ fixed-timestep loop, seeded RNG, keyboard ui/ debug HUD, quest board 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. - **Nothing is persisted** across reloads — condition, heat, intel and completed runs all reset. IndexedDB comes with the campaign layer, and `sim/intel.ts` plus `sim/heat.ts` are the two things that most need it. - **Repairs are automatic on hand-in.** Parts get spent the moment you report back. The brief wants repair to be a physical, on-foot act with scavenged parts; that is Phase 6. - **The board's route preview assumes the shortest path.** If you drive a different way, the intel you were shown described a route you did not take. `findRoute` already accepts a cost function, so a "safest route" preview is a small change when it is wanted. - **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 Two gates are open at once, and both need a person, not a test. **Phase 1:** you catch yourself avoiding a road because of its history, not its distance. If it fails, check `METRES_PER_HEAT` (escalation too slow to matter in one session), `DECAY_PER_SECOND` (nothing accumulates), or whether the props are actually inconvenient enough to route around. **Phase 2:** you take the new target a meaningful fraction of the time, for reasons other than curiosity — because the known one has genuinely got expensive. If the known option is always strictly better, raise `NOVELTY_BONUS`. If you never take a known target, lower it. Watch for one failure mode in particular: if you find yourself taking whichever job is *nearest* and ignoring the intel line entirely, the board is decorative and the phase has not landed, whatever the reward numbers say.