# 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 42 (§42.5) — the bench turns to face the road, and the font race turns out to have been the crowd Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §42. Everything below: today's tree, headless fresh contexts, port-isolated no-store servers, pin method of record (stepwalk 2 m · yaw 0 · laps 2 · 802 stations), A/B against a control tree — a HEAD copy of `web/` with `assets/ models/ vendor/` **and every `js/` subtree that is not Lane B's** symlinked to the live tree, so another lane's mid-round wave is identical on both arms. **1. FIX 1 — THE BENCH (and the streetlight) FACED THE WRONG WAY. D's filing is correct on both.** Verified before changing anything, off the live InstancedMesh matrices, each instance matched to the edge whose **perpendicular offset equals the placement rule** (position only, so the yaw verdict is not circular; `unmatched: 0`). | live scene, seed 20261990 | BEFORE | AFTER | |---|---|---| | bench front vs the direction to the road | **90.0° — min = med = max, 60/60** | **0.0° — min = med = max, 60/60** | | lamp head gains … toward the road | **0.00 m — 0 of 89 reach over it** | **+1.05 m — 89 of 89** (the whole arm) | The code and its comments had disagreed for 41 rounds: `:203` said *"facing the road"*, `:197` said *"arm reaches over the road"*, and both used `atan2(ux, uz)` — the yaw that points local +Z **down the street**. Fixed with one named convention rather than two more literals: `faceYaw = atan2(-uz, ux)` (= `alongYaw − π/2`), road-facing yaw = `faceYaw + (side > 0 ? π : 0)` — **the `side` term was already there and already right**, which is why the defect survived reading. Also found and fixed in the same path: **9 of the town's 60 benches are the depot GLB** (this box reaches the CDN), and that GLB is **0.437 × 0.845 × 1.215 m with all 318 backrest verts in a slab at x ∈ [0.151, 0.219]** — plank along **Z**, sitter facing **−X**, i.e. 90° out from the house "+Z is the front" law. Normalised by rotating the **geometry** once at load (`+π/2`), not the instance, so a station's yaw means one thing whichever geometry draws. Zero draws, zero tris. **2. FIX 2 — `benchStops(plan)` ships, and the RULE now has one copy.** `benchStationsOnEdge(e)` is what `buildChunkFurniture` places from **and** what the export publishes — a second copy inside the same file would have been D's defect at a shorter distance. Signature: `benchStops(plan) → [{ edgeId, kind, s, side, x, z, yaw, seatLen }]`, pure function of the plan (no THREE/scene/DOM), plan-edge order then ascending `s`, whole town. `side` is on the **furniture** perpendicular so D's `side === −forward` test is unchanged; `yaw` is the instance rotation with **+Z the bench front, now pointing at the road** (a sitter is still `yaw + π`); `seatLen` 1.6 m so D's ±0.38 m two-up offset stops being a copy of B's geometry. **Gated, not asserted** (D's own arm-6 method, pointed at the export): **254 stations town-wide, 60 instances in the window, 60/60 within 2 cm and 60/60 yaw-identical to <1e-6 rad; CONTROL at 2 m offset matches 0 of 65 on both counters.** **3. FIX 3 — `await document.fonts.ready` IS SHIPPED AND IT FIXES NOTHING. The cause is the crowd.** Measured, not assumed: the sign-atlas canvases are **byte-identical across three boots** (41–49 canvases per bookmark, same sha1 list every time); `document.fonts.size === **0**` at document start and at ready, because the API tracks **font faces** and this game ships none (house law — canvas text); and on **6 of 6 boots** `fonts.ready` resolves at 38–51 ms while the **first `fillText` in the process** is at 89–109 ms, i.e. **+49 to +60 ms later**, with `fonts.status === 'loaded'` already. There is no window in which a sign is painted early. It is shipped anyway at the only free site — a module-level await at the top of `buildings.js`, so every `buildChunkBuildings` call is downstream of it and the builder **stays synchronous** (the 4 ms/frame streamer budget is untouched). **Measured added boot latency, 6 boots/arm: first sign paint 102.28 → 102.62 ms (+0.34 ms on the mean) against a control whose own spread is 93.7–110.6 ms; `DBG.ready` 3,822 → 3,802 ms.** Noise, both directions. What actually moves the frame is the **citizen sim on wall-clock time** — and **the freeze recipe every lane quotes does not freeze anything**: `?roster=v1&pop=0` boots **140 citizens** (96 active, measured) because `index.html:454` is `(parseInt(params.get('pop'),10) || 140)` and **0 is falsy**. Two fresh contexts of `street_noon` differ by **983 px of 921,600, all of them in rows 361–418 — the pedestrian band** — and by **0 px** with the crowd genuinely out. So `dbg.js` gains **`DBG.freeze()`** (`?dbg=1` only): `setPopulation(0)` for the fixed roster **plus** hiding `citizens.group` for the chunk-streamed default (whose active set is per-chunk density and ignores `target`), returning `{draws:{before,after}, peds, stillLive:{weather,tram,magpie,washing,gigs}}` so a harness asserts. **THE 3-BOOT HASH TABLE** — F's method, fresh context per (bookmark, boot), `?classic=1`, 2.5 s settle: | bookmark | BEFORE (control) | AFTER (fonts fix in) | AFTER + `DBG.freeze()` | BEFORE + same freeze | |---|---|---|---|---| | `street_noon` | **3 of 3** | **3 of 3** | **1 of 3** | **1 of 3** | | `shopfront_detail` | **3 of 3** | **3 of 3** | **1 of 3** | **1 of 3** | | `crossroads_busy` | **3 of 3** | **3 of 3** | **1 of 3** | **1 of 3** | | `market_square` | 1 of 3 | 1 of 3 | 1 of 3 | 1 of 3 | | `night_neon` | 1 of 3 | 1 of 3 | 1 of 3 | 1 of 3 | | **stable** | 2/5 | **2/5 — unchanged** | **5/5** | **5/5** | **→ LANE F, EXPLICITLY: YES, byte-hash goldens become possible and the R41 ruling can be reversed — but not for the reason it was made.** The condition is not "the await landed", it is "the frame holds nothing that runs on wall-clock time". A golden boot is `?classic=1&dbg=1&weather=0&tram=0&magpie=0&washing=0` **+ `DBG.freeze()` before the shot**; `?pop=0` is not a substitute and never was. The last column is the proof it is the freeze and not any R42 code — the untouched HEAD tree goes 5/5 the same way. Two asks in your files: fix `index.html:454` (`Number.isFinite`, not `||`), and if a gate pins a hash, assert `freeze().stillLive` is all-false in the same run or the golden rots the first time someone shoots without a flag. (On `market_square`: R41 has B calling it stable and F calling it 3-of-3; on this box today it is stable in all four arms.) **→ LANE D, A BREAKING CHANGE, AND IT IS WHY THE EXPORT EXISTS.** `sim.js benchStationsFor()` is now **90° wrong**. Positions are untouched (your 14/14 position arm stays green); your **yaw arm goes red**, and `_seatPose()` will put sitters up to 0.38 m off the plank facing along the street — that is `bench_seated_after.jpg`, shipped un-retouched on purpose. `bench_seated_after_dwired.jpg` is the same frame with the sitter re-seated off the **real bench transform**: that is what importing `benchStops` in 42.4 delivers. **4. ZERO DRAWS, measured on both arms** — pin method, 802 stations, control vs treatment: | synthetic, pin | CONTROL | R42 | delta | |---|---|---|---| | default noon (seg 2) | 282 / 100,346 | **282 / 100,346** | **0 / 0** | | default **NIGHT** (seg 5) | 291 / 122,183 | **291 / 122,183** | **0 / 0** — ruling 4's nine draws intact | | `?classic=1` NIGHT | 269 / 90,580 | **269 / 90,580** | **0 / 0** — byte-exact to the R38/R40/R41 pin | | `?classic=1` noon | 269 / 90,592 | **269 / 90,592** | **0 / 0** | 0 console errors on all 8 runs. `night_neon` frame **98 draws / 14,454 tris on both**; crowd-frozen bench pose **130 draws on both**. (Night tris read 122,183 against R41's 120,093 — the +2,090 is on the **control** too, arriving through the symlinked `models/`/`assets/` from another lane's in-flight R42 work. Recorded so nobody bills it to this round.) **5. CLASSIC.** `furniture.js` and the new `buildings.js` await carry **no classic flag test at all**, so classic and default are identical to each other by construction and the fix lands on both. Fetch surface **127 URLs on both arms, added `[]`, removed `[]`** (blob: object URLs excluded — random UUID per boot); HUD DOM **2355:8 identical**; help line unchanged; `night_neon` **0/0 delta**; classic pin **269 / 90,580 byte-exact**; **0 page errors and 0 console errors** on `?dbg=1`, `?classic=1`, `?noassets=1` and `?roster=v1&pop=0`. The only probe difference anywhere is `DBG.freeze` existing, and `DBG` exists only under `?dbg=1`. Goldens: `node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76` unmoved**. **6. FILED, NOT FIXED.** The **bus shelter has the bench's bug** — `syaw = atan2(-uz, ux)` is `faceYaw` **without** the `side` term and the shelter always stands on `+perp`, so its open front reads *away* from the road on the primitive. Left alone deliberately: `busShelterStops()`'s yaw is consumed by `tram.js` and the shipped shelter is a depot GLB whose axis is **unmeasured**. Measure the GLB, then flip both together. Also to E: a depot prop disagreeing with the house axis is a **class** of bug, not an instance — the bench was one, the shelter is the next one waiting. Shots: `docs/shots/laneB_r42/` — `bench_seated_{before,after,after_dwired}.jpg` (same station, same pose, R10 human-sized line off the sitter's own bone bbox), `bench_geometry_ab.jpg` and `streetlight_night_ab.jpg` (crowd frozen, 130 draws both arms). Files: `web/js/world/furniture.js`, `web/js/world/buildings.js`, `web/js/world/dbg.js`, `docs/shots/laneB_r42/`, these docs. **No git commands run** (lane treaty; Fable commits). ## Round 41 (§41.5) — the game learns to say where it came from, and 81 pubs stop wearing op-shop paint Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §41. Everything below: today's tree, headless fresh contexts, port-isolated no-store servers, pin method (stepwalk 2 m · yaw 0 · laps 2 · 802 stations), A/B against a HEAD control tree whose `assets/` `models/` `vendor/` are symlinked to the live tree so another lane's mid-round wave is identical on both arms. **1. THE CREDITS SURFACE — the schema is the product; the panel is 260 lines that never decide a licence.** `web/assets/credits.json` (schema `procity-credits/1`) is the file **every lane appends to**; `web/js/world/credits.js` renders it; `hud.js setCredits()` puts the way in; `index.html` gets one `[Lane B R41]` dynamic-import block. Reachable on **F2** or the HUD link, bottom-left. - **Schema, documented for C/D/E/G in LANE_B_NOTES §41** (with a paste-ready ACCAD row for R42): `id · name · kind · licence · attribution · required · source · used_for · count · files · evidence` (+ optional `flag`, `lane`). Three rules carry it: **`licence:"unverified"` is legal and a guess is not**; **`attribution` is the exact string**, printed verbatim, `null` when nothing is owed; **`required` is the licence question, not an editorial one** — Mixamo/Rokoko are `false` because their licences ask for nothing, CC BY is always `true`. `evidence` must name a file in this repo. - **Seeded with 14 entries read off the repo, nothing invented.** Two carry a **live obligation** the game had never shown: **OpenStreetMap ODbL 1.0** (23 town caches — `SOURCES.md`'s own words are *"keep the © OpenStreetMap contributors credit visible"*) and **three.js r175 MIT**. Plus Mixamo (4 clips), Rokoko (4 dance clips), 103 skins, 29 audio assets, the fitting/furniture fleet split by provenance, the GODVERSE stock art, the generation stack, and "no font files ship". **THE ATTRIBUTION RIDES THE LINK:** on an OSM boot it reads `© OpenStreetMap contributors · credits (F2)`; on the synthetic town, where no OSM data is on screen, claiming it would be the opposite of attribution, so it reads plainly. - **Two findings filed to E, both shipped as `unverified` + amber flag rather than a guess:** the **base ped roster (19 GLBs) has no per-asset licence record anywhere in this repo** (CITY_SPEC's declared zone includes CC-BY and CGTrader RF — both can owe attribution, so a credit may be owed *today*); and the **18 house-library fittings still carry AUDIT.md's own 38-round-old blanket caveat** ("record a one-line source/licence per asset before publish"), having been published since R3. AUDIT.md documents props in forensic detail and the people not at all. - **Cost:** zero draws (DOM the composer never renders), **zero boot fetches** — `credits.json` is fetched on FIRST OPEN, asserted 0 before and exactly 1 after — and **zero fetches under `?noassets=1`**, where the panel says so honestly instead of shipping a second copy of the data. **2. A's D3 — 81 venue lots, re-measured and fixed.** `SHOP_REGISTRY` carries 9 types and never carried `pub`/`band_room`/`rsl`, so `SHOP_REGISTRY[type] || SHOP_REGISTRY.opshop` dressed every gig venue in op-shop paint. Re-measured over the whole shipped corpus at the golden seed: **27 towns · 1,838 shops · 81 venue lots · 81 of 81 taking the fallback — exactly 3 per town, in every town.** A's R40 number reproduces to the digit. (The bite has drifted `:344`→`:465`→**`:483`**; the line moves, the defect didn't — hence a named resolver, not another literal at a line number.) `buildings.js signStyleFor()`: B's nine shipped palettes → **A's `core/registry.js`** (gilt/stencil/ club, carried since R13, and the source for any type A adds next) → op-shop, now genuinely last-resort. Venue treatment = accent frame + top fascia band, **venue-gated by construction** (`accent` exists only on registry-sourced palettes). **Not done, deliberately:** repainting the other nine onto A's palettes — that is an art call for John (record would go muted-teal → hot-pink neon), not a bug fix, and R41 is the round F photographs. Filed. The 28-round-stale `fixture_plan.js` header A clocked is rewritten to say what that table honestly is. **3. ZERO DRAWS, measured not claimed** — frozen town on both arms (`roster=v1&pop=0&weather=0&tram=0& magpie=0&washing=0`; every one of those moves on wall-clock time), station lists compared as JSON before either sweep runs: | frozen, 136 station×yaw samples/arm | HEAD | R41 | delta | |---|---|---|---| | synthetic **NIGHT** (worst class) | 283 draws / 116,681 tris | **283 / 116,681** | **0 draws AND 0 tris on 136/136** | | synthetic noon | 273 / 96,396 | **273 / 96,396** | **0 and 0 on 136/136** | | `katoomba_real` NIGHT | 167 / 109,208 | **167 / 109,208** | **0 and 0 on 136/136** | | `?classic=1` NIGHT | 264 / 90,578 | **264 / 90,578** | **0 and 0 on 136/136** | **544 of 544 samples identical in both counters, 0 console errors on every arm.** Pin method on the treatment tree reproduces R40 exactly: **default 282 noon / 291 NIGHT, classic 269 / 90,580 byte-exact** — **ruling 4's nine-draw night margin is intact and Lane B spent none of it.** The new `core/registry.js` import in `buildings.js` is free: `citygen/index.js → plan.js` already pulls it on every boot, and the classic fetch surface is **121 URLs on both arms, `added []`, `removed []`**. **4. CLASSIC IS BYTE-IDENTICAL.** A classic plan carries **0 venue-typed shops of 493**, so the resolver's registry arm is *structurally* unreachable there; the credits module is never fetched (null-provider idiom, no flag test in either file) so there is no `#pc-creditlink`, no overlay, no injected CSS, no F2 listener, no `PROCITY.credits`, and the help line is unchanged. HUD DOM **2354:8 on both arms**. Draw/tri delta **0 on 136/136**. → **F, a finding, not this round's defect:** pixel-hashing five classic bookmarks gives 2/5 matches — and **HEAD-vs-HEAD gives the same 2/5**. `street_noon`/`shopfront_detail`/`crossroads_busy` are not byte-reproducible across boots even at HEAD (likely the sign atlas painting `bold Npx Arial` before the font resolves, in race-dependent chunk order). A screenshot golden on those three cannot be a byte-hash. Goldens **157,647/157,647, `0x5f76e76` unmoved** (no plan field is touched — signage is a canvas texture, the credits surface is DOM). `tools/qa/r40_lane_b.py`: **all assertions green**; `tools/flags_check.py`: **GREEN, 0 fails / 0 warns** (no new POST_V2 exception needed — classic never requests either credits file). Shots: `docs/shots/laneB_r41/venue_signage_ab.png` is the contact sheet — **1,088 / 9,645 / 14,160 changed pixels on the three pairs and every one of them inside the sign band, nothing else in the town moves** — plus the full frames and the panel. Honest caveat: the pub is the *least* visible of the three (op-shop `#2c2820` and gilt `#3a2416` are near-neighbour browns), so its read comes mostly from the `#c0392b` accent frame; the band room and RSL are unmistakable. Files: new `web/assets/credits.json`, new `web/js/world/credits.js`, `hud.js`, `buildings.js`, `fixture_plan.js`, `web/index.html` (`[Lane B R41]` marks), `docs/shots/laneB_r41/`, these docs. **No git commands run** (lane treaty; Fable commits). ## Round 40 (§40.3) — the ?r= breach GATED at its true number, classic's selector ratified, the fog signed — plus §40.4's arcade kit (Fable-routed) Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §40. Everything below: today's tree, headless fresh contexts, no-store servers, pin method (stepwalk 2 m · yaw 0 · laps 2 · 802 stations). **1. THE ?r=3 BREACH — re-measured, then GATED (option b), because the carried 307 was fiction.** | mode | before (carried) | re-measured (pin method, final tree) | law now | |---|---|---|---| | default noon / night | 282 / 292 (R38 pin) | **282 / 291** ✅ pin reproduced | ≤300 (unchanged) | | `?classic=1` night | 269 | **269 / 90,580 tris — byte-exact** | ≤300 (unchanged) | | `?r=3` noon / night | "307 / 317" (stale bookmark) | **382 / 391** at 45–48 live chunks | **≤420, its OWN declared law** | Decision: **gate, not shave** — the 91-draw excess is the R+1 live-chunk window (48 vs 31) on a per-chunk cost already collapsed to ~1 draw/kind/chunk; a shave that size is a content cut to the DEFAULT town to legalise a diagnostic. So: `?r=` above auto now declares `PROCITY.budget = {draws: 420, tris: 200k, diagnostic: true}`, console-warns its law, the HUD warn threshold reads it, `DBG.info().budget` carries it, and **NEW `tools/qa/r40_lane_b.py`** asserts it with both arms non-vacuous (measures 386 ≤ 420 at 8 m stations AND 386 > 300, so the exemption is load-bearing; ≤300 someday ⇒ "retire the exemption", printed not failed). Ceiling 420 = 391 + ~7% jitter margin; growth past it goes red and is re-pinned consciously. → F: one qa.sh line; README `?r=N` row still says 307. **2. ?classic=1 TOWN SELECTOR (Fable ruled yes) — verified true by construction, made deliberate.** Classic boots with the selector present + visible, **all 27 options** (3 fixtures + 23 real/godverse towns), one towns-fetch = exactly the named POST_V2_EXCEPTION (`assets/towns/index.json`), picks FROM classic stay classic (query string preserved), zero draws (DOM chrome), 0 errors. Ruling written into the selector block (hud.js) and asserted in r40_lane_b.py. → F: the flags_check POST_V2_EXCEPTIONS label still calls this a debt-ledger question; it's ruled now. **3. FOG SIGNAGE — three surfaces that said nothing now consume A's layer** (`street || label`, null handled once, no town-type branch): **HUD `street` row** ("Lurline Street" on katoomba_real, supplier `ways`; "the south end of the main street" on the synthetic) · **door tooltip** ("🚪 Little Paris Cafe · Katoomba Street — click to enter") · **fog-map caption** ("… 2 streets walked · Lurline Street · press M to close"). New `createStreetLocator(plan)` in discovery.js (the probe's hash, read-only; signage ≠ discovery, so not fog-keyed); rides the existing frame%6 throttles; zero draws. **Classic-gated by construction**: locator built only `!CLASSIC`, so `#pc-street` doesn't exist in classic's DOM and the tooltip is the pre-R40 byte — A/B'd on the same door, asserted both arms in r40_lane_b.py. **4. §40.4 ARCADE KIT (routed mid-round by Fable, E's spec applied verbatim):** roof spans (depth 2.5 = half-lane, slabs meet flush), **a spanned roof has no posts**; `ctx.arcadeBlocks` keyed on `district.kind` never `edge.kind`; explicit `ARCADEKIT` gate (classic forces off — mandatory; `?arcadekit=0` = falsifiability control). Measured: **−34 post instances exactly, lane draws 120 → 120 (+0)**, shot pair `docs/shots/laneB_r40/`, classic pin cell byte-exact after the change. Goldens **157,647/157,647, 0x5f76e76 unmoved** (selfcheck after every wave). r40_lane_b gate: **all assertions green**. Files: `web/index.html` (`[Lane B R40]` marks), `hud.js`, `minimap.js`, `discovery.js`, `dbg.js`, `chunks.js`, `buildings.js`, new `tools/qa/r40_lane_b.py`, `docs/shots/laneB_r40/`, these docs. **No git commands run** (lane treaty; Fable commits). ## Round 39 (v9 Layer 2 — THE FOG, item 39.2) — **zero draws, and the map is finally readable** Full detail + every number in LANE_B_NOTES §39. `minimap.js` drew all 493 synthetic shops from the first frame; it now draws what has been walked past and **frames itself to it**. New `web/js/world/discovery.js`, `minimap.js` rewritten, `save.js` gains the ledger, the shell wires it. - **ZERO DRAWS, measured as an A/B** (two port-isolated no-store servers, HEAD vs treatment, same 15 stations × 4 yaws computed from the plan and compared before either sweep runs, sim + tram hidden identically): **draw calls identical on 60 of 60 samples at noon and at night.** And map mode cannot cost a draw *structurally* — the frame loop never calls `composer.render()` in the map branch, so the counter reads the same number open (131) as closed (131). - **Legibility.** A shop lot goes from **0.83×1.11 px to 29.4×39.2 px on bowral_real — 35.3×** (launceston 27.6×, castlemaine 21.3×, katoomba 20.8×, adelaide 16.9×, fitzroy 11.1×, synthetic 4.0× at 21×41). Reproduces Fable's binding table to the digit. The frame is **the shops you have stood in front of + where you stand** — framing on walked *edges* was my own first cut and it blew the frame to 340 m at spawn, because one synthetic edge is 400 m long. - **The rule: 25 m of the shopfront AND in front of it**, probed off a plan-derived spatial hash every 6th street frame (**0.46 m of travel at WALK**), never off `onChunkBuilt` (R=2 ⇒ 128 m). **1.09–1.37 µs a probe** — 0.0014% of a frame. Falsifiability control with a red arm: walking the synthetic's two service laneways, radius-only reveals 93 shops, the shipped probe 59 — **34 refused through the back wall**; on the arcade it refuses 0 of 17. - **The save.** Optional `known` block, keyed by TOWN (I re-derived the seed-invariance: 23 caches × 3 seeds, shop ids / edge ids / lot geometry all identical; synthetic alone keys `@`). Bounded by the plan (max 493 shops, 2,593 edges) with a 8,192 cap, 64 towns FIFO, ints only. **Worst case 160 KB for the entire corpus fully learned; ~300 B after a 500 m outing.** Eight reject arms demonstrated, live state untouched on reject, foreign-town fog does not leak. - **Classic by construction:** `createMinimap(plan, null)` is the pre-R39 map and the file has **no flag test**. `?classic=1` / `?fog=0` / `?game=0` map canvases are **hash-identical to HEAD's**. - **Lane A's address layer consumed** — street names along walked streets, deduped by name; the synthetic gets district labels from the same line. No town-type branch anywhere. - **THE SIGN, landed on Fable's ruling (`a982f3c`)** — `index.html:428` (Lane D patronage door points) and `:500` (the R32 spawn) computed the shop front as `(−sin ry, −cos ry)`, the **back** of the building. Patronage door points **on the footpath: 0/493 → 427/493 synthetic, 0/72 → 67/72 katoomba, 0/139 → 139/139 fitzroy, 0/30 → 28/30 bowral**. At spawn, shops within 25 m that are in front of you: **0 → 4 on both towns tested.** Katoomba's shipped boot opened *standing in a paddock facing a blank back wall* — the R31 playtest's own complaint, which R32 was written to fix and never did. **No golden moves** (runtime pose; selfcheck `0x5f76e76` either side). **F: the R32 spawn gate and the R35 first-five-minutes smoke photograph this pose and need re-baselining.** Written up as the **FOURTH instance of the −Z/+Z confusion** (R13 `RY_FLIP` · R15 the poster on the back wall · R27 every real-town shopfront facing the paddock · R39 these two) with the lineage table in LANE_B_NOTES §39. - **THE MAGPIE, on Fable's second ruling** — E's 894-tri GLB wired as `furniture.js`'s **use-if-ready** path (`+1 draw / +894 tris` with the bird on screen, `+0` off; `?noassets` stays on the 182-tri procedural bird). **Two things measured before wiring, both would have shipped wrong:** E's bird faces **+Z** and this file flies **−Z** (proved by slicing the GLB along Z — head end mean y 0.31, tail end 0.028), so without `rotateY(π)` it swoops **tail-first** — *the fifth −Z/+Z instance, and the first caught before it shipped*; and its origin is at the **feet**, dropped 0.20 m onto the lamp arm. The fallback's white is re-placed (+0 tris, verified by E's own dump: still **182 tris**) and **measured**: pale area 180 → 399 px but true white 33 → 28, because a downward-facing surface gets no sun — the GLB reads 755. My edit also broke E's `dump_bird.mjs` (module-scope `loadGLB` under node); gated on `IN_BROWSER` and re-run green. Two stale `154 tris` comments corrected to **182**. - **FILED:** the arcade's awning posts land **±0.50 m from the centreline — two rows 1.00 m apart, 34 posts down a 5.00 m lane** (my own measurement, confirming E's). One clamp fixes it, but it belongs with the scheduled arcade treatment, not ahead of it. ## 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 0–5 / footpath 5.25–15.25 · `side`@12 (72% of the corpus) road 0–3 / footpath 3.25–9.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, 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.)