Go to file
dejvino 477195a7d6 Phase 3: regional control, sketch minimap, and fix the base hut
Three things, all tangled together enough to land at once.

Regions. A single directional front cuts the map into liberated, contested,
occupied and frontier. Heat accrual behind your own lines is exactly zero, not
merely small — a trickle would eventually fortify your own back garden, and
would mean the safe area quietly punished you for using it. Decay is judged by
whoever holds the road, not whoever holds the ground the car is standing on.
Modelled as one front rather than blobs of territory because the brief wants the
line to move in both directions, and Phase 4 can then just slide the offsets.

Territory is signalled by tone, never by the world announcing itself: crossing a
border eases the fog, light and palette between four looks. Phase 3's gate is
telling occupied from liberated without being told, so the HUD line naming the
band is debug scaffolding like the heat numbers.

Minimap. There was no way to tell where a target was. The map is drawn from
Intel, not from the world: the road network's shape is always faintly visible,
but only roads actually seen carry remembered heat, only ground actually driven
through is filled in, and the front line is dashed in only where both sides of
it have been explored. Adds a coarse fog-of-war grid to Intel, plus an
eight-point compass arrow in the HUD.

Base huts. The building was centred on the junction, so the starting hut sat
directly over the car and hid it. Huts now stand off the junction, in whichever
direction is furthest from tarmac, and are solid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:23:23 +02:00
.claude Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00
src Phase 3: regional control, sketch minimap, and fix the base hut 2026-08-07 10:23:23 +02:00
.gitignore Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00
index.html Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00
package-lock.json Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00
package.json Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00
README.md Phase 3: regional control, sketch minimap, and fix the base hut 2026-08-07 10:23:23 +02:00
tsconfig.json Phase 0: drivable car with persistent wear 2026-08-07 07:39:05 +02:00

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.
  • Phase 3 — regional control: behind your own lines, nobody is watching.

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
13 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.

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.

Regional control

The map is cut by a single front line into four bands, running from your own territory out past the edge of the known world:

Liberated → Contested → Occupied → Frontier

Band Heat accrual Heat decay
Liberated ×0 ×4
Contested ×0.55 ×1.4
Occupied ×1 ×1
Frontier ×1.3 ×0.8

Liberated accrual is exactly zero, not merely small. A trickle would eventually fortify your own back garden, which is nonsense, and it would mean the safe area quietly punished you for using it. Behind your own lines, nobody is writing down which roads you take.

Decay is judged by whoever holds the road, not whoever holds the ground the car is standing on — your own side dismantles checkpoints on its own roads whether or not you are there to watch.

It is modelled as one directional front rather than blobs of territory, deliberately: the brief wants the line to move, in both directions, and a scalar offset per boundary is something Phase 4 can slide either way without touching anything else.

You are never told which band you are in by the world itself. Crossing a border eases the fog, the light and the palette: your own ground is open and green, contested goes flat and hazy, occupied turns dusty and closes in, the frontier is cold and short-sighted. The HUD names the band for now, but that line is debug scaffolding for the same reason the heat numbers are — Phase 3's gate is telling them apart without being told.

The map

A sketch map, top right, drawn from Intel rather than from the world:

  • The road network's shape is always visible, as faint dashed lines. You are an operative with a map, not an amnesiac.
  • Roads you have actually seen are drawn solid and coloured by the heat you remember on them — which is what you last saw, not what is there now.
  • Ground you have driven through is filled in, tinted by who holds it. A survey mission fills in a whole area at once.
  • The front line is dashed in only where you have explored both sides of it. You know where the border is because you crossed it.

The HUD also carries an eight-point compass arrow to the active target, since the map alone does not tell you which way to point the car.

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, no engine imports:
                  world, roads, heat      — the map and its memory
                  regions                 — the front line and who holds what
                  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, sketch minimap
  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.
  • 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.

Phase 3: driving into an occupied region, you can tell it from a liberated one within a couple of minutes, from tone alone, without reading the HUD line that names it. If you cannot, the palettes in render/scene.ts are too close together — push them apart before trusting the band boundaries.

The static front is the whole of Phase 3. Phase 4 is what makes it move: mission completions nudging the boundaries, plus ambient drift that happens whether or not you are watching. Front.boundaries is already the only thing that would have to change.

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.