5.7 KiB
LANE B — STREETSCAPE (chunked 3D town + game shell)
Prompt for Opus 4.8. You are working in
/Users/jing/Documents/PROCITY. Readdocs/CITY_SPEC.mdanddocs/RESEARCH.mdfirst, then skim the two source patterns you are generalizing:90sDJsim/web/world/index.htmlbuildStrip()(~line 740) andthriftgod/web/index.htmlbuildStreet()(1441–1515). You ownweb/index.htmlandweb/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 viacore/prng.js. Commit in small, described steps onmain.
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 fromchunkIndex(plan)data. Build budget 4ms/frame — queue chunk builds, one per frame or viarequestIdleCallback. 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
textPlaneproportions 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-*.jpgskins.
web/js/world/ground.js— roads/footpaths/kerbs as merged strips along street edges (ground-asphalt*,ground-footpath*,ground-grassfor yards,ground-brickpavefor 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-*.jpgpicked by seed, BackSide sphere), 6-segment day cycle ported from 90sDJsimapplyLighting: 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 viaprocity: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
web/index.html+ allweb/js/world/*modules above.web/js/world/fixture_plan.js(works standalone before Lane A).shot()bookmark harness: pressP→ downloads canvas PNG from 3 fixed cameras; save references intodocs/shots/laneB/.- A
docs/LANES/LANE_B_NOTES.mdwith 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=Nonce Lane A'sgeneratePlanis 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.