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

90 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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