# PROCITY-B — progress (Lane B: Streetscape + game shell) **Status: complete and verified.** Lane B turns a CityPlan into a walkable, chunk-streamed 3D town plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's `generatePlan` (which landed mid-session). Everything below was verified live in a browser. ## Round 9 (Fable's ROUND9 → Lane B) — v2 tour bookmarks + tram (`?tram=1`) · qa GREEN Ship-v2.0 round. Two deliverables, both v2-law clean, `qa.sh --strict` GREEN, flags-off byte-identical. - **8 v2 tour bookmarks** in `js/world/dbg.js` (16 total): `rain_verandah` (hero — open VIDEO shop w/ winmap parallax rooms, CLOSED dark neighbours, night rain), `patronage_door`, `dig_real`, `katoomba_main`, `tram_stop`, `rain_street`, `night_crowd`, `shopfront_detail`. All un-letterboxed, 113–188 draws, 0 errors. `tram_stop` picks the **nearest-origin** stop (open framing; spine-end stop was walled in). F shoots these — best hero combo `?winmap=1&weather=rain&dbg=1` seg 5. - **Tram** (`js/world/tram.js`, `?tram=1`, NON-BLOCKING for the tag): one small bus, out-and-back loop on the main-street spine, 3.5 s door-dwell at each `busShelterStops` stop, ping-pong at the ends. **Deterministic** (verified byte-identical across fresh instances). **Exactly 2 draws** (verified via `renderer.info` delta). Composes w/ weather+night (headlights 0.15→1.6 via `lighting.isNight()`). "TOWN LOOP" canvas livery reads in daylight; `ring()` door-bell stub (audio parked). Shell-wiring (2 lines) documented for F in LANE_B_NOTES; no collision in v0 (noted for v3). - **Rain polish** (weather.js, quick R9 debt): capped `gl_PointSize` (clamp 2–22px) + lower base — near drops are now thin streaks, not big blobs. Re-verified: rain smoke still GREEN, money-shot re-shot. - Still deferred (cut line): wind sway on trees/awnings (touches other lanes' materials). ## Round 8 (Fable's ROUND8 → Lane B) — weather (`?weather=1`), carried from R7 · qa GREEN New `js/world/weather.js` + `lighting.setSky()`. Seeded per citySeed → clear/overcast/rain (~57/24/19% over 400 seeds, deterministic). **Rain = ONE draw** (camera-following `Points` + streak shader) + wet ground (roughness/darken on shared ground mats, restored on dispose) + matching rainy sky (`lighting.setSky`). Fully procedural (0 depot fetch). Worst view **141 draws** (≤300); composes with `?winmap`+night — **rain outside a lit interior-mapped window** shot saved. Deterministic, 0 console errors, **flag-off byte-identical** (shell never constructs it). - **Exposed the `window.PROCITY.weather` contract for Lane D** (decision #1): `{ state, intensity }`, documented in LANE_B_NOTES with the exact shell-wiring 3 lines for F (I don't touch index.html). `?weather=1` seeded (default town rolls clear); `?weather=rain|overcast|clear` forces a state. - Deferred (cut line): vertex-shader wind sway on trees/awnings — noted, not done (touches other lanes' materials); everything else in the R7 brief landed. Tram stretch not attempted. ## Round 6 (Fable's ROUND6 → Lane B) — placed the orphaned street props · qa GREEN Wired the two published-but-unconsumed street GLBs (`street_bin_01`, `bus_shelter_01`) via the use-if-ready furniture path (fail-soft; `?noassets` → primitives, 0 network). Files: `furniture.js`, `chunks.js` (collider merge). - **Bins**: sparse (~1/block, `s += 78 m`, seeded), decoupled from benches — country town not a mall. - **Bus shelters on the street graph** (future tram/bus stops): deterministic rule = midpoint of every MAIN edge with `hashEdge(id)%3===0`, road-side footpath, long axis along the edge, open front to road → **1–3/town** (2 in seed 20261990). **Solid collider** (furniture now returns colliders → `chunks` merges into `getColliders`); walk-into-it is blocked. **Exposed `busShelterStops(plan)` → the stop list contract** for whoever builds the vehicle loop. - **Perf**: worst view WITH rig-fleet citizens **94k tris / 148 draws** (≤200k / ≤300, big margin); deterministic; all-flags-on composes clean (0 errors). Flagged the 3k-tri `street_bin` GLB to Lane E as a decimation candidate (bounded by the sparse cadence for now). Shot: `docs/shots/laneB/bus_shelter.png`. ## Round 5 (Fable's ROUND5 → Lane B) — v2 window trick, behind `?winmap=1` · qa GREEN (4/4) - **Parallax interior-mapping window glass** (V2_IDEAS "Vuntra window trick"), behind **`?winmap=1` default-off.** One shared `ShaderMaterial` on the existing window geometry does single-cube interior mapping — each fragment rays into a virtual room box and shades back wall + seeded shelves + side walls + floor/ceiling. **Zero extra geometry, 1 window draw/chunk, per-shop variation via attributes (aTangent/aTint/aSeed), not per-shop materials.** Seeded from `shop.seed`. - **§3.5 held:** night → open shop's room glows warm, closed shop's room dark (reuses the closed-facade `uHour/uNight/aHours`). Screenshots: `docs/shots/laneB/winmap_{day,night_open_vs_closed,night_closed}.png`. - **v2 prime law held & proven:** flag-OFF byte-identical to v1 (windows `MeshStandardMaterial`, no winmap attrs); flag-ON worst view **122–168 draws** (≤300); shader **100% procedural — 0 fetch delta** vs `?noassets` baseline; 0 console errors; `GOLDEN.hash` untouched. `?winmap` is self-read in buildings.js → **no index.html seam for F** (just a flags-table row). File: `buildings.js`. ## Round 4 (Fable's ROUND4 → Lane B) — done · qa.sh --strict GREEN (4/4) - **B1 — closed-facade visuals (§3.5, headline).** Closed shops → **dark windows + red CLOSED plate**; open shops (esp. the `openLate` video rental at night) → **lit windows**; door tooltip `🔒 — CLOSED · opens HH:00`. Batched with a per-vertex `aHours` + two shared shaders + one `uHour/uNight` uniform pair, updated once on a `procity:segment` event (lighting.js → buildings.js) — **facade atlas untouched, no per-shop materials, no per-frame poll.** Verified live: **22:00 → only "Video Regal" lit, all others CLOSED**; midday → all open; worst-view **200 draws with citizens** (≤300). Files: `buildings.js`, `lighting.js`, `hud.js`. - **B2 — shot harness (gate-6 critical path).** The "letterbox" was **not** a composer bug (proved the composer is correctly sized headless) — the `street_noon`/`arcade`/`warehouse_fringe` **poses jammed the camera into a wall**. Reframed them to look down a street (new `streetViewPose`), added the **3 missing bookmarks** F asked for, pointed `night_neon` at the glowing video shop amid CLOSED neighbours, and added a defensive composer resize. `tools/shots.py` → **10 un-letterboxed shots, 0 console errors** (in `docs/shots/laneB_r4/`). File: `dbg.js`. - **B3 — furniture GLB upgrade + novelty_record.** furniture.js now use-if-ready-upgrades primitives to depot GLBs (sync check, `?noassets` → 0 network verified). Placed **novelty_record** at every record store + food_cart by stalls/milk bars. **Finding:** the **novelty_record GLB is 26.5k tris** (5× `glb_law`), spiking a busy view to **574k** → it stays on its low-poly primitive; bench (1.9k) + food_cart (5.0k) GLBs are fine and sit on their footprints. **→ Lane E: decimate novelty_record (same as the ped rigs).** File: `furniture.js`. ## Round 12 (v3.0-alpha, VENUES & GIGS, `?gigs=1`) — Lane B street-side — done Built the pub's street presence for the one-pub vertical slice. Dependencies (A venue+`plan.gigs`, C watchPoints, E posters+gig audio, D band+crowd) all landed; F wires it last. - **`web/js/world/venue.js`** (new, B-owned; shell/F constructs under `?gigs`) — `createVenuePresentation(plan, skins, scene) → {group, update(gigState), dispose, venueShopId}`. **Posters:** E's 4 poster skins at each `plan.posters` (x,z,ry) with the gig's `bandName` overprinted in `nameZone` (canvas composite; seeded template; flat fallback if JPEG missing). Verified "THE WOMBATTS" over the band-photo art. **Lit frontage:** warm marquee + emissive bulb row over the venue door, `update(gigState)` ramps the glow. Verified: "The Thornbury Hotel" lit at night. - **Muffled-gig spill** (extended `audio.js`): near the venue (≤26 m) while gig `doors`/`on`, plays `pubrock-live` through a 470 Hz **lowpass** at low distance-gain. Reads `window.PROCITY.gigs` (F) with a clock+`plan.gigs` fallback. Verified: gain ~0.14 muffled at night, fades off far/day. - **Prime flag law held:** flags-off = no gigs/posters/venue (goldens frozen, no citygen edits), audio gig layer created but inert (`plan.gigs` absent → skipped). Byte-identical boot. - **Stretch deferred:** streetlamp-pool night-readability on the venue block (touches shared furniture rendering) — flagged as next polish. **Note to A:** main-street poster positions land mid-road on intersection nodes — a verge/pole offset would seat them better. - Static gates green (I didn't touch citygen; manifest/selfcheck unaffected). Files: `web/js/world/ venue.js` (new) + `web/js/world/audio.js` (spill extension) + docs. **F wiring documented in LANE_B_NOTES.** ## Round 11 (AUDIO) — Lane B WebAudio engine — done Built **`web/js/world/audio.js`** (B-owned) + a guarded create-call in the shell. One `AudioContext` unlocked on the first gesture; self-contained (reads `window.PROCITY`, self-ticks); consumes Lane E's `manifest.audio` (23 assets, 8.7 MB, ≤25 MB) with a 3× fetch retry. - **Layers** (crossfaded, no per-frame alloc): day/night ambience (lighting segment), rain (`weather.intensity`), footsteps (player movement), **shop-door music spill** (nearest open music-shop, distance-gain — verified `video-synth` at ~0.08 by Video Barn's door), tram rumble+bell (`?tram`), SFX (doorbell/door-open on `procity:enterShop`). - **House audio law — all verified live:** boots silent (nothing plays/fetches pre-gesture, 0 console errors); `?mute=1` → silent surface (exists for smokes, 0 fetches); `?noassets=1` → 0 audio/manifest fetches; beds lazy-load; spill is a pure function of shop type. - **For Lane F:** `window.PROCITY.audio` exposes `playInterior({musicKey,toneKey})`/`stopInterior()` (street beds auto-duck while inside), `playSfx`, `setMasterGain`, `mute`, and a `state` diag surface. - **For Lane C:** confirmed `room.audio = { musicKey, toneKey }` on `buildInterior` is the right shape (musicKey → `manifest.music`, toneKey → `manifest.ambience`). - Gates green (manifest validator 0 errors incl. audio 23; selfcheck 1727/1727). `qa.sh --strict` hangs on a headless Playwright gate — ran the static gates directly. Files: `web/js/world/audio.js` (new) + a small `[Lane B R11 audio]` loader in `web/index.html` + these docs. ## Round 3 (Fable's ROUND3_INSTRUCTIONS → Lane B) — done - **Task 1 (draw budget, re-measured WITH citizens).** Radius-2 auto is already the spawn default; no config change needed. Drove the real loop (`chunks.update` + `citizens.update`, pop 140, up to **125 active peds**) along the dense corridors via `window.DBG`: worst continuous-walk view **263 scene / 275 total at MIDDAY**, **275 total at NIGHT** — **≤300 everywhere**, ~25 margin. Peds are a batched impostor layer (~1–2 draws). Documented in LANE_B_NOTES. - **Task 2 (`scene.environment`).** Was **null** → Lane D's rig-fleet peds bake their impostor atlas from it (`sim.js`) and would render dark. Set a neutral PMREM'd sky→ground gradient in `lighting.attachRenderer` (before `CitizenSim` constructs; no asset). Verified **set in street / night / interior / after interior-return** (`interior_mode` only swaps `scene.background`). - **Task 3 (furniture GLB footprints)** — **standby**: Fable's §3.4 (manifest GLB upgrade + `?noassets`) hasn't landed (furniture.js still primitives); will verify placement/collision when it does. - **Task 4 (optional polish)** — deferred (fresh generated-city stills / map.html wiring). - Only Lane B file changed this round: `web/js/world/lighting.js` (+ these docs). Committed by exact pathspec (Lanes C/E/F have concurrent uncommitted work in the shared tree — untouched). ## Session 2 (continuation) — closed the two open Lane-B items Fable flagged - **Draw budget now holds with margin.** Facade texture atlas + awning-merge + shell/trim-merge: worst dense frustum **393 → 265 scene draws**, worst continuous-walk gameplay view **~261 total** (≤300 gate, ~40 margin). Bonus: **skin materials 25 → 5** (the atlas is one shared facade material), fixing the "≤25 was at the cap" concern too. (The old "~445" peak was largely a measurement artifact — orphan chunks from teleport-jumping that the real streamer disposes.) - **`window.DBG` harness exposed** (`?dbg=1`, `js/world/dbg.js`) — answers LANE_F_NOTES §4 so Fable's `shots.py`/`soak.py` can drive the game: `ready`, `shot(name)`, `teleport`, `setSegment`, `enterShop`/`exitShop` (drives the real B→F→C interior chain), `info()`. - **`ChunkManager` lifecycle hooks** (`onChunkBuilt`/`onChunkDisposed`, optional/inert) — answers §8. - Verified: fixture ("Yarraville Junction", 26 shops) still builds clean with all 3 awning colours; shadow path (`?shadows=1`) renders from the merged meshes; day + night + door tooltip + interiors all work; **zero console errors**. Files touched: `skins.js`, `buildings.js`, `chunks.js`, `hud.js`, new `dbg.js`, + a small guarded DBG loader in `index.html`. **Uncommitted** (per the lane treaty; and Fable is concurrently wiring `index.html` — our edits coexist cleanly on stable anchors). Owned/created (only `web/index.html` + `web/js/world/*`, per the ownership treaty): | file | role | |---|---| | `web/index.html` | shell: importmap→vendor, renderer + ACES + bloom, MODE state machine (`map`/`street`/`interior`), main loop, adaptive perf, guarded `generatePlan` import, shot harness | | `web/js/world/fixture_plan.js` | hand-authored CityPlan (schema v1): 4 N + 2 S terraced blocks (26 shops), cross-street houses, 2 market stalls, streets graph — spans ~6 chunks so streaming is exercised | | `web/js/world/planutil.js` | chunk math (64 m), `chunkIndex`, node/lot indices, oriented-rect collider + `pushOutRect` | | `web/js/world/skins.js` | city-wide shared material registry (one material per skin), seeded flat-colour fallbacks | | `web/js/world/buildings.js` | facade kit: instanced shells/parapets/posts/awnings, merged per-skin facades, per-chunk sign atlas, doors/windows | | `web/js/world/ground.js` | roads/footpaths/kerbs/plaza/base plane, merged strips, UV-baked tiling | | `web/js/world/furniture.js` | instanced streetlights (emissive at night), benches, bins, gum trees, bus stops — all with primitive fallbacks | | `web/js/world/lighting.js` | seeded sky dome + 6-segment day/night cycle, one following sun/shadow, fog pop-in mask | | `web/js/world/player.js` | PointerLock WASD+run, eye 1.7 m, push-out collision vs nearby-chunk colliders | | `web/js/world/chunks.js` | the streamer: radius-R build / R+1 dispose, nearest-first queue, 4 ms/frame budget, night propagation | | `web/js/world/hud.js` | crosshair, throttled door-raycast tooltip, live draws/tris/fps/seed/clock readout, click→`procity:enterShop` | | `web/js/world/minimap.js` | `map` mode: 2D top-down town directory (streets + lots by type + player dot) | ## Acceptance — all met - ✅ Walk the fixture 5 min: contiguous, no seams, chunks stream silently, day/night cycles, signs legible, tooltips work, budgets green (148 scene draws, 21 skin materials, 5.8k tris). - ✅ Works with `generatePlan` when present (guarded import; falls back to fixture if absent). - ✅ Runs with `web/assets/` renamed away — no crashes, flat-colour fallback, canvas signs still show. - ✅ Instancing from day one, per-chunk 2048-wide sign atlas, canvas text (no fonts), seeded-everything. - ✅ 3 reference stills in `docs/shots/laneB/`; measured numbers in `docs/LANES/LANE_B_NOTES.md`. ## Verified live (in-browser) Streaming (chunks build/dispose as you walk, queue never starves), 6-segment day→night with glowing shopfront windows + streetlamp bloom at night, door raycast → correct shop → `procity:enterShop` + toast, pixel-accurate collision (stops at wall − player radius), all-fallback mode, and Lane A's full 493-shop city rendering + streaming + arbitrary-angle collision through Lane B. ## Perf (measured — full table in `docs/LANES/LANE_B_NOTES.md`) - **Fixture** (radius 3, shadows on): ~148 scene draws, low tris, well under all gates. - **Lane A city** (~493 shops, radius 2 auto, shadows off): worst continuous-walk gameplay view **~261 total draws** (≤300 gate, ~40 margin); typical ~130–210. Skin materials **5** (≤25). Streetscape-only; Fable's integrated measure with citizens (pop 140) is ~191, also under gate. ## Adversarial multi-agent review → fixes landed I ran a 4-dimension review (correctness / memory-perf / spec / robustness) with adversarial verification. Confirmed real bugs, all fixed and re-verified: 1. **Collision was wrong for non-axis-aligned `ry`** (`pushOutRect` used the wrong rotation sign). Latent on my axis-aligned fixture; would let you walk through Lane A's angled buildings. **Fixed** — verified `keptOut:true` against a `ry=−1.32` lot. 2. **House doors** were raycastable and mis-resolved to the nearest shop → clicking a house "entered" a random shop. **Fixed** (separate non-picked mesh). 3. **Awning skin** used a signed shift → undefined skin for ~⅓ of shops. **Fixed** (`>>>`). 4. **Sign atlas** was a fixed 2048² per shop-chunk regardless of sign count → texture-memory blowup at Lane A density. **Fixed** (sized to actual signs, mipmaps off). 5. **Furniture**: numeric edge ids broke the seed hash (all edges identical); offset furniture could land in an unbuilt chunk and vanish. **Both fixed**. 6. **`takeShots()`** didn't restore the pre-shot mode. **Fixed**. ## ⚠️ Things Fable should know (cross-lane) - **Lane A landed and is now the shipped default.** `index.html` prefers `generatePlan(seed)` over the fixture. Seed 20261990 → "Boolarra Heads", ~493 shops. The fixture remains the fallback + schema ref. - **A "Lane F integration fix" already appeared in `skins.js`** (`facadeMat` now strips a `.jpg`/`facade-` prefix). Lane A stores `shop.facadeSkin` as a **full filename** (`"facade-stucco-pink.jpg"`), not the bare skin key the CITY_SPEC table implies. That normalize is what makes Lane A's facades load. Kept. - **Schema drift between fixture and `generatePlan`** (Lane B tolerates all of it, but CITY_SPEC should pick one and say so): `lot.id`/`lot.block` are strings in the fixture, **numbers** from Lane A; `lot.frontEdge` is `'N'/'S'` in the fixture, a **number** from Lane A; `shop.facadeSkin` is a bare key in the fixture, a **filename** from Lane A; `lot.ry` is axis-aligned in the fixture, **arbitrary radians** from Lane A. Recommend Fable freeze these in CITY_SPEC so Lane C/D/E don't each guess. - **≤25 skin-materials gate — RESOLVED (session 2).** Was at the cap (some seeds 27); the facade atlas collapses ~22 facade materials into 1 shared atlas material, so it's now **5** city-wide. - **Perf gate on the full city — RESOLVED (session 2).** The facade texture atlas landed (collapses every chunk's facades → 1 draw and ~22 facade materials → 1) plus awning + shell/trim instanced merges. Worst gameplay view is now ~261 total (≤300). The flat-colour fallback is preserved (each atlas slot is pre-painted with its skin's fallback colour before the JPEG loads). See LANE_B_NOTES. ## Suggested next instructions (your call) 1. Freeze the CityPlan schema in CITY_SPEC (the drift list above) — unblocks C/D/E. **Still open.** 2. ~~Decide the facade-atlas~~ — **done (session 2): Lane B implemented it.** Draw + material gates now green. 3. ~~Interiors hand-off~~ — **done: Fable wired `enterShop → interiorMode → buildInterior` in the shell;** verified via `DBG.enterShop`. Lane B's seam (`procity:enterShop`/`procity:exitShop`) is intact. 4. Run Lane F's `shots.py`/`soak.py` against `window.DBG` now that it exists (bookmarks are seed-derived). 5. Optional: capture fresh `docs/shots/laneB/` stills of the generated city via `DBG.shot(...)`; wire `map` mode into Lane A's `map.html` (currently my lightweight minimap). ## Run `cd web && python3 -m http.server 8130` → http://localhost:8130 . (The app needs no server-side logic; the P-key shot harness uses browser downloads.)