# LANE B — STREETSCAPE (chunked 3D town + game shell) > **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read > `docs/CITY_SPEC.md` and `docs/RESEARCH.md` first, then skim the two source patterns you are > generalizing: `90sDJsim/web/world/index.html` `buildStrip()` (~line 740) and > `thriftgod/web/index.html` `buildStreet()` (1441–1515). You own `web/index.html` and > `web/js/world/*`. Do not touch other lanes' files. Plain ES modules, vendored three.js via > importmap (`web/vendor/` — copy the importmap style from 90sDJsim's index.html), no npm, > no build. All randomness via `core/prng.js`. Commit in small, described steps on `main`. ## Mission Turn a CityPlan into a walkable, contiguous, chunk-streamed 3D town at 60fps, plus the game shell (mode state machine, controls, day/night, HUD). This is the lane where PROCITY stops being two separate street-strip games and becomes a *city*. **Don't wait for Lane A**: start by hand-writing `web/js/world/fixture_plan.js` — a small hard-coded CityPlan (2 blocks of main street, one cross street, ~14 shops, valid per the CITY_SPEC schema). Build everything against the schema; swap in `generatePlan()` when Lane A lands. If the schema needs a field Lane A didn't foresee, propose it in CITY_SPEC in the same commit. ## Architecture - `web/index.html` — shell only: importmap, canvas, HUD divs, mode state machine (`map | street | interior`), main loop. Keep it thin; logic lives in modules. - `web/js/world/chunks.js` — the streamer. `ChunkManager(plan, scene)`: on player move, ensure chunks within radius R (default 3) exist, dispose beyond R+1. Each chunk = one Group built from `chunkIndex(plan)` data. **Build budget 4ms/frame** — queue chunk builds, one per frame or via `requestIdleCallback`. Dispose = remove + `geometry.dispose()` for non-shared geometry + return instanced slots. - `web/js/world/buildings.js` — the facade kit: - Shells: **InstancedMesh per chunk** (one box geometry, per-instance matrix + color). Parapet/verandah posts/awning boxes likewise instanced. - Facade planes: shops sharing a skin share a material — group facade planes per skin into merged geometry (`BufferGeometryUtils.mergeGeometries`, vendored) per chunk. ≤25 skin materials city-wide. - Sign band: per-chunk **2048px canvas atlas** — draw every sign text once, UV-map planes into it. One material per chunk for all signage. Steal `textPlane` proportions from the parents but do NOT create per-sign canvases. - Doors: plane + `userData.place = {kind:'door', shopId}` for the raycaster. Windows: dark tinted planes, emissive warm at night. - Verandahs/awnings over footpath on main street (the classic AU posted verandah — boxes + posts, instanced), `tex-awning-*.jpg` skins. - `web/js/world/ground.js` — roads/footpaths/kerbs as merged strips along street edges (`ground-asphalt*`, `ground-footpath*`, `ground-grass` for yards, `ground-brickpave` for the arcade + market square). UV along edge length. Intersections: simple quad fill, don't over-engineer curb geometry. - `web/js/world/furniture.js` — instanced streetlights, benches, bins, gum trees (cross-plane billboard trees are fine), bus stops; placed seeded along footpaths. GLB props may load from the 3GOD depot (promise-cached, placeholder box fallback — pattern in RESEARCH) but every furniture type MUST have a primitive fallback. - `web/js/world/lighting.js` — sky dome (`sky-*.jpg` picked by seed, BackSide sphere), 6-segment day cycle ported from 90sDJsim `applyLighting`: sun direction/intensity, ambient, streetlights + window emissives at night. ONE directional shadow map (2048) following the player, or none — measure. ACES tonemapping, bloom optional at threshold 0.9 (vendored postprocessing exists). - `web/js/world/player.js` — PointerLockControls walk (WASD + shift-run), eye 1.7m. Collision: push-out against building AABBs of the current + neighbour chunks and a "stay off private yards" rule; streets/footpaths/square all freely walkable. No navmesh — rectangles are enough. - `web/js/world/hud.js` — crosshair, shop-name tooltip via throttled raycast (every 6th frame, far=6), time-of-day widget, FPS/draw-call debug readout (`renderer.info`), seed display. - Door interaction: raycast hit + click → dispatch `window.dispatchEvent(new CustomEvent( 'procity:enterShop', {detail:{shopId}}))` and pause street mode. Lane C listens for it; until Lane C lands, show a "🚪 {name} — interiors coming" toast and keep walking. Interior returns via `procity:exitShop`. ## Performance gates (hard) - ≤300 draw calls, ≤200k tris in street view (HUD shows both, live). - No per-frame allocation storms; reuse vectors/matrices. - Chunk in/out causes no visible hitch (verify with the HUD frame-time graph). - `setPixelRatio(min(dpr,2))`; textures ≤1024 (skins already are). ## Deliverables 1. `web/index.html` + all `web/js/world/*` modules above. 2. `web/js/world/fixture_plan.js` (works standalone before Lane A). 3. `shot()` bookmark harness: press `P` → downloads canvas PNG from 3 fixed cameras; save references into `docs/shots/laneB/`. 4. A `docs/LANES/LANE_B_NOTES.md` with the measured numbers (draw calls, tris, chunk build ms). ## Acceptance - `cd web && python3 -m http.server 8130` → walk the fixture town for 5 minutes: contiguous, no seams, chunks stream silently, day/night cycles, signs legible, tooltips work, budget numbers green on an M-series MacBook. - Works with `?seed=N` once Lane A's `generatePlan` is importable (guard the import so the page still runs on fixture data if citygen is absent). - The page loads and runs with `web/assets/` renamed away (all-fallback mode) — no crashes.