PROCITY/B-progress.md
m3ultra 4514cb94b5 Lane B R38 (v8 WAVE 1, INHABITATION): all four runtime items — the whole round costs +1 walked draw
The town is inhabited: 23 towns finally differ, 181 back yards have a Hills Hoist with washing on it,
a magpie owns one street for six weeks of the year, and six point sources put a life off-screen.

MEASURED, walked, not bookmarked — two port-isolated no-store servers (:8472 = `git archive HEAD`,
:8471 = treatment, assets symlinked so E's mid-round wave is identical on both sides), 102 8-metre
spine steps at yaw 0, each frame fully streamed, through the shell's own composer:

  worst draws  224 -> 225   (+1)
  worst tris   89,502 -> 93,298   (+3,796 of ~75k spare)
  sum draws over 102 frames  16,676 -> 16,663   (-13)

The citizen sim and the tram are hidden identically on both sides, and that IS the measurement: with
them live the same step read control 346 vs treatment 232, because peds spawn on wall-clock time.
`?yards=0` reproduces HEAD exactly (224 / 89,502 / 16,676). Isolated at a fixed pose: washing +1 draw
/ +512 tris, magpie +1 draw / +182 tris, every yard box prop +0 draws.

§1.2 THE CHARACTER VECTOR — +0 draws, +0 tris, classic byte-identical BY CONSTRUCTION.
`townCharacter(null)` returns the frozen v1 literals and a null tint keeps the SAME material cache key,
so classic gets the same object, not a copy of it: default-boot ground hashes identical to HEAD
mesh-for-mesh, `?classic=1` hashes unmoved from R37. There is no `if (classic)` in the ground path.
The sky feeds the BASE skyName, never `setSky` (a one-way latch that would have killed R32's dawn dome
on all 23 towns). The vacuous arm is per-town: `newtown_godverse` / `redhill_godverse` carry `state: ''`,
so a per-corpus arm would have dropped exactly those two silently. 21 distinct palettes across 23 towns.
`gravel` and `reddust` were not unwired but UNWIREABLE — their `use` values named no slot ground.js had;
making the slot addressable fixed it at the root, and both are selected now. E's three new grounds
landed mid-round and the upgrade was a name change in one table, which is what procedural-first is for.

§1.4 BACKYARD AUSTRALIA — inside boxMats, +0 draws. Houses inset off their boundaries so there is a
yard; 36 `yard` lots that had rendered as blank shopfront sheds since v1 are now fenced blocks with a
hoist, a shed and often a tank. Collision byte-identical (the lot rect is where the fence stands).

THE WASHING SHIPS, and I wrote the inverted weight rather than dropping it. The shipped top-weighted
mode moves a hem 0.10 cm calm / 0.67 cm gusty; `pegged` gives 4.5 cm / 31 cm — ~52x, and at the hem
instead of the pegs. The `canopy` string is byte-for-byte unchanged, so the gum-tree covenant argument
is untouched. It is ONE TOWN-WIDE InstancedMesh (1 draw at any N, cap 256, proximity-first off the R37
chunk seam) rather than per-chunk, which would have been +1 x 31 live chunks = 4x the whole margin.
Zero on every real town (no hoists ⇒ no mesh) and zero at night.

§1.3 THE MAGPIE — +1 draw on screen, exactly 0 otherwise; 182 tris of 900. Territory scales with the
road graph: 6 on the 6.78 km synthetic town, 157 on bendigo's 188.6 km / 2,593 edges, nearest 120 m
from spawn. The latch sets, the day roll clears it, localStorage gains no key and the save never
mentions it. `swoopPos()` is exported and pure. The hat stays cut.

MY OWN SEASON MECHANISM WAS VACUOUS AND THE SWEEP CAUGHT IT: keyed on citySeed alone it produced a
CONSTANT, because the town key is deliberately off the plan — 24 of 24 towns were 214 days from
swooping season, and it would have gated green because `?magpie=1` forces it. Re-keyed on
(citySeed, townKey): `newtown_real` is in season on arrival on the default seed, marrickville is 8 days
out, and 12.9% of 1,000 seeds put the synthetic default in season against a design ratio of 11.5%.

§1.1 THE AUDIO LAYER — six new mixer layers, zero draws, zero tris, seeded anchors, distance falloff,
AND A StereoPannerNode PER LAYER, so direction is real (measured spread -0.71..+0.85) instead of a
limitation written into the notes. Day-of-week derived locally. All six anchored on the default boot;
3 of 6 report `no-anchor` on real towns rather than being faked onto shopfronts. The falsifiability
control — one manifest key renamed away, served from a third port — makes that source report
`no-entry`, 0 plays, never `audible`, with the other five unchanged and zero console errors.

TWO DEFECTS MY OWN GREEN NUMBERS COULD NOT SEE, both caught by a screenshot: the washing shipped at 256
instances / +1 draw / cap respected and was INVISIBLE behind its own back fence (per-instance peg height,
and a fence taller than the hoist), and the magpie perched on THIN AIR over the road. R37's lesson
relearned at full price. Fence 1.60 / hoist 2.15 measured against the object; territories now snap to
furniture.js's own streetlight rule.

LANE E'S PROPS, ADJUDICATED: hoist + aerial permanently primitive (E's spike killed them, and primitive
is also the +0-draw mechanism). `paling_fence` at 1,121 tris x 905 runs = 1.01M triangles — no. The
`magpie` GLB is HELD FOR THE TINT: it reads as a crow, and a magpie seen for 1.2 s in peripheral vision
IS its white bar. My 182-tri bird already carries it, at a fifth of the triangles and no fetch.
furniture.js's "novelty_record is 26.5k tris" was stale by six epochs — corrected to 8,000.

Zero fetch delta: `?noassets=1` fetches the same 23 assets, same list, on HEAD and on R38.
0 console errors on 10 boots (default, classic, yards=0, washing=0&magpie=0&ambient=0, noassets,
magpie=1, darwin, hobart, bendigo, the patched-manifest control) beyond the known PointerLock ones.

Shots: docs/shots/laneB_r38/. Detail + every number: LANE_B_NOTES §38.
2026-08-03 18:53:26 +10:00

354 lines
29 KiB
Markdown
Raw 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.

# 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 38 (v8 WAVE 1 — INHABITATION) — all four runtime items, **+1 walked draw for the lot**
Full detail + every number in LANE_B_NOTES §38. Measured as a true A/B: two port-isolated no-store
servers (`:8472` = `git archive HEAD`, `:8471` = treatment, assets symlinked so E's mid-round wave is
identical on both), 102 walked 8 m spine steps at yaw 0, each frame fully streamed, rendered through
the shell's own composer. **The citizen sim + tram are hidden identically on both sides** — with them
live the same step read control 346 vs treatment 232, because peds spawn on wall-clock time.
| walked spine, sim quiet | HEAD | R38 | delta |
|---|---|---|---|
| worst draws | 224 | **225** | **+1** |
| worst tris | 89,502 | **93,298** | **+3,796** (of ~75k spare — the cheap resource, spent) |
| sum draws over 102 frames | 16,676 | **16,663** | **13** |
Isolated at a fixed pose: **washing +1 draw / +512 tris · magpie +1 draw / +182 tris · every yard box
prop +0 draws.** `?yards=0` reproduces HEAD **exactly** (224 / 89,502 / 16,676).
- **§1.2 the character vector** — `character.js`, 23 towns, ground palette + sky bias + ambience mix.
**+0 draws, +0 tris.** Classic is byte-identical **by construction, not by flag**: `townCharacter(null)`
returns the v1 literals, and a null tint keeps the *same material cache key*, so classic gets the same
object. Default-boot ground hashes identical to HEAD mesh-for-mesh; `?classic=1` hashes unmoved from
R37. Sky feeds the **base** `skyName`, never `setSky` (a one-way latch that would have killed R32's
dawn dome on all 23 towns). The vacuous arm is **per-town**`newtown_godverse`/`redhill_godverse`
carry `state: ''`, so a per-corpus arm would have dropped exactly those two silently. **21 distinct
palettes across 23 towns.** `gravel` and `reddust` were not unwired but *unwireable* — their `use`
values named no slot `ground.js` had; making the slot addressable fixed it at the root. E's three new
grounds landed mid-round and the upgrade was a name change in one table.
- **§1.4 backyard Australia** — inside `boxMats`, **+0 draws**. 145 houses inset off their boundaries so
there is a yard to put things in; **36 `yard` lots that had rendered as blank shopfront sheds since v1**
are now fenced blocks with a hoist, a shed and often a tank. Collision byte-identical.
- **§1.4's washing** — **ONE town-wide InstancedMesh, +1 draw at any N**, cap 256, proximity-first off
the R37 chunk seam. Per-chunk would have been +1 × 31 live chunks = 4× the whole margin. **0 on every
real town** (no hoists ⇒ no mesh) and **0 at night** (it comes in). I **wrote the inverted wind mode**
rather than dropping it: the shipped top-weighted weight moves a hem **0.10 cm calm / 0.67 cm gusty**;
`pegged` gives **4.5 cm / 31 cm**, a ~52× gain, in the right place. The `canopy` string is byte-for-byte
unchanged.
- **§1.3 the magpie** — **+1 draw on screen, exactly 0 otherwise; 182 tris of 900.** Territory scales
with the graph: **6 on 6.78 km synthetic → 157 on bendigo's 188.6 km / 2,593 edges, nearest 120 m.**
Latch is runtime-only: it sets, the day roll clears it, **localStorage gains no key** and the save file
never mentions it. `swoopPos()` is exported and pure. The hat stays cut.
- **§1.1 the audio layer** — **six new mixer layers, zero draws, zero tris**, seeded anchors, distance
falloff, and **a `StereoPannerNode` per layer** so direction is real (measured spread 0.71…+0.85)
instead of a limitation written down. Day-of-week derived locally. All six anchored on the default
boot; **3 of 6 report `no-anchor` on real towns** rather than being faked. The falsifiability control
— one manifest key renamed away on a third port — makes that source report **`no-entry`, 0 plays,
never `audible`**, five others unchanged.
**Two defects my own green numbers could not see, both caught by a screenshot.** The washing shipped at
256 instances / +1 draw / cap respected **and was invisible behind its own back fence** (per-instance peg
height + a fence taller than the hoist); the magpie perched **on thin air** over the road. R37's lesson,
relearned at full price. Territories now snap to `furniture.js`'s own streetlight rule.
**Lane E's props, adjudicated:** hoist + aerial **permanently primitive** (E's spike killed them
outright, and primitive is also the +0-draw mechanism) · `paling_fence` at 1,121 tris × **905 runs =
1.01 M triangles** — no · `magpie` GLB **held for the tint**: it currently reads as a crow, and a magpie
seen for 1.2 s in peripheral vision *is* its white bar. My 182-tri bird already carries it.
## Round 37 (v8 WAVE 0 §0.1 + §0.4) — THE KERB IS FOOTPATH, at +0 draws · commit `47db478`
John's ruling landed as geometry. `ground.js` narrows the road quad to the carriageway and extends
the **existing** merged `footGeos` class inward to meet it, consuming Lane A's `vergeBand(e)[0]`.
- **+0 draws, proven not asserted**: identical draw count at every one of 102 walked spine steps
(worst frame **276 both**, sum **20,081 both**) and on `katoomba_real` (worst 100 both).
**Tris went DOWN**8 synthetic, 288 katoomba. The charter's "~2k tris" was an estimate against
a class that already existed; extending a quad re-parameterises it. Honest number, stated loudly.
- **14 of 14 gig posters were standing in painted bitumen. 0 are now.** No poster moved; the ground did.
- Surfaces verified by **raycast**: `main`@24 road 05 / footpath 5.2515.25 · `side`@12 (72% of the
corpus) road 03 / footpath 3.259.5 · **`lane` degenerate `[4,4]` ⇒ pre-ruling geometry** ·
**`arcade` all footpath, no road quad, no kerbs** · **residential keeps its grass nature strip**.
The lane case branches on `outer <= inner`, never on a band literal (Fable's mid-round correction).
- **A defect my own numbers could not see:** the first cut painted every intersection shut (a 12.5 m
footpath crosses the perpendicular *carriageway*, where a 3.5 m one only clipped a corner). All
numbers were green; the screenshot was not. Fixed by putting the carriageway on top under the flag.
- **`?verge=0` ships permanently** and is EXACT: ground vertex+UV hashes identical to the HEAD build,
and the same detector goes red on the default boot.
- **`?classic=1` is FENCED, not amended** (R31 dig-flip pattern): identical ground geometry AND
identical ground-only rendered pixels at three poses. → Fable's call if classic should be amended.
- **§0.4** — the chunk seam was never connectable: on HEAD `!!chunks.ctx === false`, so a consumer
holding only `PROCITY.chunks` had nowhere to attach. Now `chunks.onChunkBuilt(fn) → unsubscribe()`
(many consumers) + `chunks.ctx` (the documented single slot). 136 built / 144 disposed over a walk,
payload + key shape verified, throwing subscriber survives, and **zero behaviour change with no
subscriber**: a 102-step per-frame walk hashes `ceb1ec66` on both builds. `sim.js` untouched.
## Round 30 (v7.0-alpha ledger #4) — THE COLLECTION UI + SLEEP surface · qa GREEN 6/6
The crate panel + game bar in `hud.js` (my DOM-overlay patterns), on F's §30.1 `PROCITY.game`
contract — and verified against F's REAL committed core (`207bcff` landed mid-round; my test
snapshot diffed byte-identical to it — **nothing stubbed**, unlike C's wave-1 run).
- **Game bar** (bottom-right): `day N · $cash` · `CRATE n` (count in the standing HUD; opens the
panel, `C` toggles) · `☾ SLEEP``game.sleep()`, enabled any time (alpha).
- **Crate panel**: cover thumb (pack-atlas CSS crop off the entry's sku — per-shop godverse base or
town-wide; parody = type glyph by design), title/artist, `paid $p · where · day d`, newest first,
header totals. 50-row rebuild 0.9 ms; 0 draws (DOM-only).
- **Construction-gated on the game object itself** (lazy, R19-selector style): `?classic=1`/`?game=0`
⇒ zero game DOM, zero listeners, help line byte-identical, 0 console errors (measured, clean tabs).
`?noassets` ⇒ 0 stock fetches, placeholders stand.
- **SLEEP proven end-to-end**: day 1→2, weekNight 0→1, wake DAWN seg 0, `procity-save/1` on disk,
next boot loads it and the UI reads it. **venue.js §30.4 filing fixed as filed** — compat
`venueShopId` is now a live weekNight-keyed getter (116 → 490 across a sleep, measured).
- Full measurement table + honest notes: LANE_B_NOTES §30. Verified on a port-isolated no-store
:8151 snapshot server (not :8130/:8144 — Lane F was mid-flight in the shared tree).
## 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.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 (~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-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.)