PROCITY/docs/LANES/LANE_B_STREETSCAPE.md
2026-07-14 10:46:40 +10:00

5.7 KiB
Raw Blame History

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() (14411515). 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.