PROCITY/B-progress.md
m3ultra f7dd44f55a Lane B (Streetscape): round-11 audio — WebAudio engine (street side)
- web/js/world/audio.js: one AudioContext unlocked on first gesture; day/night ambience crossfade, rain (weather.intensity), footsteps, shop-door music spill (nearest open music-shop, distance-gain), tram rumble+bell, enterShop SFX. Consumes Lane E manifest.audio (3x retry). Self-ticks off window.PROCITY.
- House audio law verified live: silent pre-gesture (0 fetches), ?mute=1 silent surface (0 fetches), ?noassets=1 zero audio/manifest fetches, lazy-load (8.7MB<=25MB), no per-frame alloc.
- window.PROCITY.audio for Lane F: playInterior({musicKey,toneKey})/stopInterior (street beds duck while inside), playSfx, setMasterGain, mute, state diag. Confirms Lane C room.audio={musicKey,toneKey} contract.
- Small guarded [Lane B R11 audio] loader in index.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:00:31 +10:00

18 KiB
Raw Blame History

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, 113188 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 222px) + 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 → 13/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 122168 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 🔒 <name> — 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.py10 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 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 (~12 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 ~130210. 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-atlasdone (session 2): Lane B implemented it. Draw + material gates now green.
  3. Interiors hand-offdone: 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 8130http://localhost:8130 . (The app needs no server-side logic; the P-key shot harness uses browser downloads.)