Single source of truth for what exists and what is next in Worldloop — a next-gen procedural city simulation built in Three.js. Each cycle picks the next ⏳ PLANNED item from a phase below and drives it to ✅ DONE (one commit per feature). Status legend: ✅ DONE · 🟢 mostly done (small leftovers) · 🟠 partial · ⏳ PLANNED (next work).
| Component | File(s) | State |
|---|---|---|
| Entry / system wiring / HUD | src/main.js |
✅ done |
| Scene + renderer setup | src/scene.js |
✅ done |
| Procedural geometry + material cache | src/world.js |
✅ done |
| Planetary surface model (biomes, water, cities) | src/planet.js |
✅ done (Phase 16, PLANET-01) |
| Chunked infinite-city streaming | src/chunk_manager.js |
✅ done |
| Traffic (cars obey lights, avoid obstacles) | src/traffic.js |
✅ done |
| Traffic lights | src/traffic_lights.js |
✅ done |
| Dynamic street parking | src/parking.js |
✅ done |
| Smart pedestrians (crosswalks, avoidance) | src/pedestrians.js |
✅ done |
| Player controller / driving | src/player.js |
✅ done |
| Car models | src/car_models.js |
✅ done |
| Weather + 365-day seasons | src/weather.js |
✅ done |
| Volumetric clouds (instanced, infinite scroll) | src/weather.js |
✅ done |
| Airplanes | src/airplanes.js |
✅ done |
| Crash deformation physics | src/deformation.js |
✅ done |
| Visual effects | src/effects.js |
✅ done |
| Simplex noise | src/noise.js |
✅ done |
| Browser entry | index.html + style.css |
✅ done |
| Agent verification | check_world.mjs |
✅ done |
| Accepted design decisions | docs/adr/0001..0009 |
✅ done |
| Agent loop | tools/pi-loop/ |
✅ done |
| 2026 test suite (EPIC) | tools/test/ + tools/ci/ (runner, property, snapshot, visual, perf, mutation, pipeline, nightly) |
✅ DONE (Phase 15, AAA-25..31) |
- Version reconciled (
v6.4.2in README/HUD). ✅ DONE - README + screenshots reflect current city. ✅ DONE
- ADR index (
docs/adr/README.md) lists all 9 accepted decisions. ✅ DONE
Modern Three.js city sims don't draw every building as its own mesh. The ADRs
(geometry merging 0006, object pooling 0007) are accepted; land them fully.
- Geometry merging for buildings/road tiles ✅ DONE (ADR 0006)
- Object pooling for cars/pedestrians ✅ DONE (ADR 0007)
- ✅ DONE (Round 3 / Gap A) Instanced buildings — buildings are authored
once as a shared
1x1x1unitBoxGeometryand drawn as oneInstancedMeshper material (ADR 0011); per-instance matrices replace duplicated merged vertex buffers. - ✅ DONE (Round 4 / Gap B) Render-distance LOD — far city chunks build
low-poly instanced silhouettes (coarse concrete boxes, no foliage/lights,
no population); near chunks keep full detail. ChunkManager upgrades/downgrades
a chunk when it crosses
lodDistance(ADR 0012). - ✅ DONE (Round 5 / ADR 0013) Frame-budget telemetry — shared
src/telemetry.jsreports FPS + draw-call count as machine-readable JSON incheck_world.mjs(scene-graph draw calls + synthetic achievable FPS), and the browser path records realrenderer.infoper frame, exposeswindow.frameBudget, and shows afps N | dc NHUD line.
- Traffic + traffic lights ✅ DONE
- Street parking ✅ DONE
- Smart pedestrians (crosswalks, obstacle avoidance) ✅ DONE
- ✅ DONE (Round 6) Road-network graph —
src/road_graph.jsbuilds a deterministic implicit directed graph over the chunk grid (nodes at road cells, directed edges along shared road axes, two-way). Cars turn at intersections onto perpendicular roads via the graph instead of pure lane-following. Exposed tocheck_world.mjs(connectivity + shortest-path + turn checks) and wired intomain.js/TrafficSystem. - Crosswalk timing integration — pedestrians and traffic lights share one clock so cars yield at crosswalks with a pedestrian present. ✅ DONE (ADR 0010)
- 365-day seasonal cycle ✅ DONE (ADR 0001)
- Automatic probabilistic weather transitions ✅ DONE (ADR 0002)
- Fixed 10-minute day ✅ DONE (ADR 0003)
- Manual weather controls removed ✅ DONE (ADR 0004)
- Sync start date with real world ✅ DONE (ADR 0005)
- Unreachable celestial bodies ✅ DONE (ADR 0008)
- Cloud visual improvements (instanced, 3 sizes, infinite scroll) ✅ DONE (ADR 0009)
- Reflections ✅ DONE
- Tone mapping / ACES ✅ DONE
- ✅ DONE (Round 8) Post-processing (bloom for neon districts, rain droplets) —
added
PostProcessingPipelinetosrc/effects.js: EffectComposer with RenderPass → UnrealBloomPass (neon-district glow) → droplet ShaderPass (rain/snow overlay) → OutputPass. Browser renders through the composer (main.js); Node path keepsupdate()deterministic (enabled=false) socheck_world.mjsasserts bloom/droplet targets without a WebGL context. Added apostFxflag + 6 checks tocheck_world.mjs. ADR 0015.
- Driving (enter/exit car, pointer-lock) ✅ DONE
- Crash deformation ✅ DONE
- Taxi, giga tower, trees ✅ DONE
- ✅ DONE (Round 9) Minimap / district label HUD — small canvas map of
streamed chunks + current district name, so the player can navigate.
src/minimap.jsMinimap: deterministic district labels via SimplexNoise over chunk coords (same world pos -> same district), draws a 160x160 canvas of the biome grid around the player, machine-readable incheck_world.mjs(ADR 0016). - ✅ DONE (Round 10) Weather-reactive driving — snow/rain reduce tire grip
in the player car (longer stopping distance, lower top speed/acceleration) via
a deterministic
Player.getDrivingModifiers()model; exposed as a machine-readableweatherfield (current+drivingmods) incheck_world.mjsoutput. ADR 0017. - ✅ DONE (Round 7 / Gap D) Dynamic city events — emergency response &
sirens. Added
src/emergency.jsEmergencySystem(pooled ambulance / fire / police fleet), new EMS vehicle builders incar_models.js, siren + rotating-light effects viaeffects.js, crash hook inplayer.js, and lane-clearing priority intraffic.jsso ordinary cars yield ahead. Exposed aneventscounter +EmergencySystemchecks incheck_world.mjs(53/53 green). ADR 0014.
check_world.mjsstructured verification ✅ DONEdocs/adr/accepted-decision log ✅ DONEtools/pi-loop/agent loop ✅ DONE- ✅ DONE (Round 11) CI smoke —
node check_world.mjs --ciruns the structured checks PLUS a headless browser render assertion (dependency-free CDP drivertools/ci/headless_cdp.mjs, self-describingwindow.__worldloopready signal insrc/main.js). ADR 0018. It caught a browser-only ordering bug (emergencySystemwired beforePlayerexisted).
- Gap A — instanced buildings ✅ RESOLVED (ADR 0011): building geometry
is authored once (shared unit
BoxGeometry) and rendered as per-materialInstancedMesh, cutting retained vertex/memory cost before LOD work. Gap B — render-distance LOD ✅ RESOLVED (ADR 0012, Round 4): far city chunks build low-poly instanced silhouettes (coarse concrete boxes, no foliage/lights/population); ChunkManager rebuilds a chunk when it crosseslodDistance. Next: Frame-budget telemetry (Phase 1). - Gap B — road-network graph: needed for realistic traffic; keep it a pure
data structure (
src/road_graph.js) socheck_world.mjscan assert connectivity without a browser. - Gap C — crosswalk timing ✅ resolved (ADR 0010): small, self-contained;
done as Round 2 — cars yield at crosswalks via
pedestrianNearCrosswalk. - Gap D — emergency events ✅ DONE (Round 7): gameplay-depth win built on
the already-done crash/deformation + pooled-traffic systems. Added an EMS
vehicle type, an
eventstelemetry field, and a siren/light effect stage. ADR 0014. - Resolved ✅:
check_world.mjsrebuilt as a real structured verification harness. It runs the ACTUAL src systems in Node against the realthreepackage (installed as a dev dep viapackage.json), instantiates every system, calls their update() loops, asserts API behavior (traffic speeds, weather day, chunk builders, player wiring, deformation, noise), verifiesmain.jswiring, and emits machine-readable JSON with per-check pass/fail + summary.npm install(three@0.160.0) +node check_world.mjs→ 23/23 green, exit 0. This is the agent loop's deterministic surface/feature verification gate. - Every feature must keep both paths green: browser render +
check_world.mjs.
- New
src/construction.jsConstructionSystem— animates tower cranes at active construction sites. world.jscreateCityChunknow deterministically turns some corner lots into construction sites: a partially-built concrete core, dark-metal scaffolding frame, and a tower crane (mast + rotating jib + counterweight + hoisted hook block + cable). The origin chunk always carries one crane so verification is deterministic.- Cranes are regular (non-instanced) meshes so
ConstructionSystem.update(delta)can swing the jib (phase advances bydelta * SPEED) and hoist the hook block; the animation is reproducible. - Wired into
ChunkManager.loadChunk/unloadChunk(viachunkData.construction) andmain.js(constructionSystem.update(delta)in the animate loop). - Verified in
check_world.mjs: origin chunk carries a crane,craneCount()matches loaded sites, jib rotation advances bydelta * SPEED, andmain.jswiresConstructionSystem. - ADR-0019 documents the decision (deterministic construction placement + animated crane system).
- Night-time window illumination: buildings get emissive "lit window" panels on their road-facing facades, driven by the day/night cycle. At night windows glow warm yellow; by day they dim to near-invisible dark panels.
world.js:matWindow(transparent warm MeshBasicMaterial) added to the shared material cache aswindow;addWindowLightslays a deterministic grid of lit windows (SimplexNoise over position, so the same building always shows the same lit windows), merged into a single InstancedMesh.weather.js: completes the previously-danglingmaterials.windowreference —updateTimeCyclenow modulates window color AND opacity (0.08 day → 1.0 night).- Verified in
check_world.mjs:world.materials.windowpresent, window panels merged as an InstancedMesh sharingworld.materials.window, and weather lights windows at night / dims by day. - ADR-0020 documents the decision (night-time window illumination via shared emissive material).
A feature is ✅ DONE only when: it ships as one commit; the browser renders it
with no console errors; node check_world.mjs passes; roadmap.md status is
updated; and a docs/adr/ entry exists if the decision is non-obvious.
Driving plan: docs/aaa-migration-plan.md. Decisions: docs/adr/ADR-0021..0024.
Each item ships green: check_world.mjs passes, browser render clean, one commit per feature.
- ✅ DONE (AAA-01) Adopt a local build (Vite/esbuild); move three to a pinned dev-dep; replace the CDN importmap in
index.htmlwith a bundled entry. (ADR 0021) M — esbuild bundlessrc/main.js→dist/worldloop.js;npm run devserves offline; CI smoke builds then renders. - ✅ DONE (AAA-02) Fix the loop: exactly one
update()per frame, explicitdt, clamp, frame budget.src/loop.js,scene.js,main.js. (ADR 0025) S - ✅ DONE (AAA-03, Round 3) Seeded deterministic RNG (xorshift) replacing
Math.random()innoise.js(road_graph.js already deterministic).src/core/rng.js,setSeed(DEFAULT_SEED)inmain.js, seeded-noise checks incheck_world.mjs. (ADR 0022) S - ✅ DONE (AAA-04): Fixed-timestep
core/time.js— accumulator,update(fixedDt)/render()split, frame-budget clamp, pause/resume (P key).src/core/time.js; wired into main.js; telemetry.recordClock; check_world.mjs asserts determinism/clamp/pause/wiring.
- ✅ DONE (AAA-05)
core/app.jsstate machine — boot/loading/menu/playing/paused/gameover; owns the pause hook (frozen states pause the sim clock, playing resumes); deterministic transition log +snapshot(); P key routes throughapp.togglePause().src/core/app.js, wired into main.js, check_world.mjs asserts flow/clock/pause. (ADR 0023) M - ✅ DONE (AAA-06)
src/input/logical ActionMap + keyboard/mouse/touch/gamepad adapters + InputManager (held actions + one-shot edge-triggered events).player.jsconsumes the InputManager and owns no raw DOM key/mouse input bindings; click-to-play overlay + pause key moved into the input module; main.js wires pause->App.togglePause and lock->Player. (ADR 0023) M - ✅ DONE (AAA-07)
core/settings.js: quality tiers (low/medium/high/ultra→ pixel-ratio cap, draw-distance scale, postFx flag), control remap persisted + rehydrated into the InputManager ActionMap, audio prefs; JSON persistence via a Node-safe localStorage shim;main.jswires it as the single source of truth (renderer cap, chunk radius, input remap). (ADR 0027) M - ✅ DONE (AAA-08)
core/save.js: serialize seed + player state + progress; replay-safe.SaveGamecaptures{ version, seed, player, simClock, progress, meta }with no wall-clock timestamp;serialize/fromJSON,save/load(Node-safe shim), deterministicfingerprint()(replay-safety), andrestore()resume are verified incheck_world.mjs+save_sim.mjs. Wired intomain.jsaswindow.saveGame/window.__worldloop.saveGamewith an init autosave. (ADR 0022/0023, ADR 0028) M
- ✅ DONE (AAA-09) Spatial broadphase (uniform grid) replacing per-frame
getColliders()concat inchunk_manager.js.SpatialGridbuckets static chunk colliders once on stream (insert on load, remove on unload); the per-frame hot path usesgetCollidersNear(x,z,r)— a local cell subset instead of the full concat. Dynamic colliders (parking/traffic) are appended distance-culled. (ADR-0029) M - ✅ DONE (AAA-10) Chunk LOD/pooling + streaming budget; remove the double
update()inmain.js.ChunkManagernow exposes a per-framestreamBudget+pendingLoadsqueue: missing active chunks are enqueued and drained at moststreamBudgetperupdate(), and LOD rebuilds count against the same budget. Unloaded chunkData is stashed inchunkPool(keyed by chunk id) and reused on re-stream (no geometry rebuild). The redundant init-timechunkManager.update()inmain.jsis removed — the first per-frame sim update streams the initial world. (ADR-0030) L - AAA-11 Renderer resilience: capability tiers, pixel-ratio cap, context-loss recovery, shadow budget. (ADR 0023) M
- AAA-12
render/assets.js: GLTF/GLB + KTX2/Basis loader, streaming, preload manifest. (ADR 0024) L - AAA-13 WebAudio bus: master/music/sfx buses, pooling, mute/pause. (ADR 0024) M
- AAA-14 Convert hero props (cranes, cars, EMS) to authored GLB + textures with LOD. (ADR 0024) M
- AAA-15 Real UI layer: HUD, menu, pause, settings, loading, dialog; accessibility. (ADR 0023) L
- AAA-16 Post-FX quality tiers from settings (bloom/droplets on/off by tier). (ADR 0023) M
- AAA-17 Node unit-test runner seeded from
core/rng.js; fixtures aroundcheck_world.mjssystems. (ADR 0024) M - AAA-18 Headless visual-regression screenshots (seeded determinism). (ADR 0022/0024) M
- AAA-19 Perf-regression thresholds over
telemetry.jsframe budget. (ADR 0024) M - AAA-20 GitHub Actions CI/CD: build → unit → smoke → visual → perf → deploy. (ADR 0021/0024) M
- AAA-21 Save/continue + session persistence UI (title, resume, settings). (ADR 0023) L
- AAA-22 Gameplay loop: objectives/score/checkpoint/progression + telemetry events. (ADR 0023) L
- AAA-23 Audio/music integration (EMS siren, weather, UI). (ADR 0024) S
- AAA-24 Authoring + polish pass: hero assets, lighting, post-FX, accessibility, README/docs accuracy. (ADR 0021..0024) L
A next-gen game-development test suite, 2026-style: deterministic seeded runs,
property-based fuzzing, golden/snapshot fixtures, headless visual-regression,
perf-regression budgets, mutation testing, and a CI/CD gate — layered on and
generalizing the AAA-17..20 verification spine. One commit per item; every item
keeps check_world.mjs green and the browser render clean.
STATUS: DONE — AAA-25..31 landed (see git log). npm run test:ci gates every
commit (build → unit → property → snapshot → visual → perf → mutation → deploy)
and npm run test:nightly runs the seeded marathon with flake-tracking.
- ✅ AAA-25 Test-runner core
tools/test/runner.mjs: per-test seeded RNG isolation fromcore/rng.js, parallel-safe, TAP/JUnit + machine-readable JSON output; supersedes the ad-hoc check harness as the structured surface. (ADR 0022/0024) M - ✅ AAA-26 Property-based testing (fast-check style, seeded) over the sim: traffic routing, road-graph invariants, chunk LOD transitions, weather state machine — invariant assertions under seeded fuzz, reproducible per seed. (ADR 0022) M
- ✅ AAA-27 Snapshot / golden-file tests: deterministic fixture snapshots of telemetry JSON, road-graph topology, seeded city-chunk matrices; diff-gate on commit so any determinism drift fails the build. (ADR 0022) L
- ✅ AAA-28 Headless visual-regression screenshots (seeded determinism) at fixed quality tiers: pixel-diff baselines across the settings tier × postFX matrix (extends AAA-18 beyond a single tier). (ADR 0022/0024) M
- ✅ AAA-29 Perf-regression thresholds over
telemetry.jsframe budget: - ✅ AAA-29b Minimum-frame-rate governor (
src/performance.js): best-effort 60 FPS. Tracks a frame-time EMA and walks a costliest-first ladder (post-fx/bloom → 1x pixels → shadow map → chunk radius) when throughput drops below target, recovering one step at a time once the budget is comfortably met. Post-fx is gated by the active quality tier (bloom was previously always-on regardless of tier — the single biggest GPU cost). Directional shadow frustum now covers only the streamed city (±80 from ±200), cutting the shadow-pass area ~6×. Ladder logic is pure, so the Node harness asserts degradation/recovery deterministically. fps / draw-call / chunk-budget limits per quality tier, fail on regression. (ADR 0024) M - ✅ AAA-30 Mutation testing: mutate seeded sim paths (RNG, road graph, weather, chunk LOD) and assert the suite still catches them — guards the verification harness itself against becoming a tautology. (ADR 0022) L
- ✅ AAA-31 CI/CD pipeline (AAA-20) extended end-to-end: build → unit → property → snapshot → visual → perf → deploy, plus a nightly seeded marathon with flake-tracking so the suite stays green over long deterministic runs. (ADR 0021/0024) M
check_world.mjsgreen at every phase; headless smoke passes in CI.- Bundled local build (no CDN); deterministic seeded runs; fixed timestep.
- Game states (boot/loading/menu/playing/paused) with save/resume.
- Input abstraction (kbd/mouse/touch/gamepad) + remappable controls.
- Settings (quality tiers) persisted; renderer resilient (tiers, context-loss).
- Spatial broadphase; streaming chunks; authored GLB/KTX2 assets; audio bus.
- Unit + visual + perf regression in GitHub Actions; deployable artifact.
- Docs accurate (README, roadmap, ADRs) — no drift.
- ✅
tools/test/runner.mjsis the single structured surface; per-test seeded RNG isolation. - ✅ Property-based (seeded fuzz), snapshot/golden, headless visual-regression, perf-budget, and mutation gates all green and wired into CI (build → unit → property → snapshot → visual → perf → deploy) + nightly seeded marathon.
- ✅ Every item ships green:
check_world.mjspasses, browser render clean, one commit each.
Goal: stop being "one city at the origin plus a grey wasteland" and become a single seeded planet you can drive forever — real biomes, oceans, lakes, rivers, forests, deserts and polar caps, with procedurally named settlements of different characters scattered across it and highways connecting them.
Driver: src/planet.js is the only source of truth for "what is this place?".
Every downstream system (chunk streaming, renderer, population, minimap, HUD)
reads the same classification object.
- ✅ DONE (PLANET-01)
src/planet.js— seeded planetary surface model. Elevation (continents + hills + ridged ranges), moisture, temperature, river carving; 16-biome Whittaker classification with a per-biome ecology/render table (BIOME_TABLE); lakes that actually break the waterline, sea/lake split by land-ring test, rivers carved in low country; jittered chunk-snapped city lattice with 10 city kinds (CITY_KINDS), procedural names, populations, and L-shaped inter-city road legs;groundHeight()grades city cores and road corridors flat. Verified by 21 newcheck_world.mjschecks (biome variety, ocean/lake/river presence, water fraction, cross-instance determinism, seed sensitivity, city profiles, road alignment, flat city pads). (ADR 0031) L - ✅ DONE (PLANET-02) Stream the planet.
ChunkManagerclassifies every chunk throughplanet.classifyChunk()(thedist < 6 → city,|cx|≤5 → highway, else-wasteland rules are gone, andcreateWastelandChunkis deleted with them);world.jsshares its tree archetypes (addTreeToGeomsnow takesrand/y/scale/foliage-key options) and a vegetation registry; the newsrc/terrain.jsbuilds the wilds: a 7×7 vertex-coloured height grid with boundaries shared with the neighbour chunk, per-cell water quads at the waterline (sea/lake/river tinted separately), biome-species trees with snow-dressed cold foliage, and instanced rocks/bushes/tufts/flowers/reeds/cacti/mushrooms/ferns/bones/ driftwood; far-LOD wilderness keeps the ground grid and drops the ecology.RoadGraphandMinimapnow read the planet too (lazy node growth, real settlement/region names, city pins, nearest-settlement line), and the browser surface exposes__worldloop.planet / placeAt / teleport / weather. Also fixes a latent renderer bug: indexed trunk/cone prototypes mademergeGeometries()reject a whole foliage batch, silently deleting tree species from every chunk (the prototypes are now non-indexed). 13 net-new checks (197/197 in Node, 201/201 with the headless-browser smoke, zero console errors/exceptions) plus a real-pixel check of the live browser: 112 trees + 676 props streamed in a forest chunk, water meshes in wet chunks, 116 distinct colour buckets in a frame. (ADR 0032) L - ⏳ PLANET-03 City personalities in geometry: per-kind height profile, style mix, palette, landmarks (clocktower/silo/refinery/lighthouse/pyramid), farms, docks — so a harbour town does not look like a neon district.
- ⏳ PLANET-03 City personalities in geometry: per-kind height profile, style mix, palette, landmarks (clocktower/silo/refinery/lighthouse/pyramid), farms, docks — so a harbour town does not look like a neon district.
- ⏳ PLANET-04 Relief + hydro: render real terrain height, waterline shader pass, bridges where roads cross rivers, ground-height sampling for player, cars and pedestrians.
- ⏳ PLANET-05 Wildlife + sky: flocks, herds, fish; planet sky gradient with sun/moon so a drive reads as a world, not a level.
- ⏳ PLANET-06 Navigation: minimap city pins + region names, "nearest settlement" HUD, and a seed field so a planet can be shared.
- Lakes had to be built by pulling the surface down (
mix(e, -0.16, lake)); subtracting a basin term from a positive elevation measured 0 lake cells. Do not "rebalance" that back. - Raw fbm needs the contrast multipliers — with them at 1 the planet is one beige biome and the variety checks fail.
- Cities are chunk-snapped on purpose. Keep city
cx/czintegers; if a future change makes roads diagonal, traffic/parking/pedestrians all need lane-axis support first. Planetcaches per cell/chunk; callreset()(or a fresh instance) after changing any tuning field, otherwise tuning looks like it had no effect.- Headless screenshots are worthless until the click-to-play overlay (z-index
1000,
rgba(0,0,0,0.8)) is hidden — a naive capture is 80% black and looks like a rendering bug. The dev screenshot driver hides it first. - Screenshot pixel checks need
--headless=new; old headless composites WebGL unreliably, andPage.captureScreenshotthen lies about brightness. RoadGraph.classifyis injected (planet-backed) with the historic origin rules kept as the fallback, otherwise the golden topology snapshot moves.