John looked at R41's proof shots and named three faults. All three are fixed, at zero draw cost, and every lane corrected somebody's numbers on the way — including its own. E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief, quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library called colour was baked AO. So the fix was a palette imposed on colour already present, in place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints and origins provably did not move. (A first-pass bbox check ignored node rotations and "found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s office teal and went burgundy; the beer umbrella was white and went brewery green. THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench = 1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl, red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43, not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage). E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the R41 deny-list's failure class through a different door: an innocent name from another pipeline. Only rendering all 24 and LOOKING found it. Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran 65..99 bones; named characters carry facial/hair bones). THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo, Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real. B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then measured: the atlas canvases are already byte-identical across boots, the game ships NO webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600, ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks 2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default). THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached 0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed otherwise — the side term was already right, which is why it survived 41 rounds of reading. Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth. C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control. E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every booth part sat 25mm sunk into its own worktop. D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex is in the identity signature and the impostor atlas; removal would renumber the town). Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a fresh default boot fetches 32 ped GLBs, none retired. THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only 3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently dropping six tracks per clip since it shipped. Testing the thing being thrown away found it. Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s race against a live sim), not deterministically red. F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law (0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified; CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict. R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there — the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with everything else frozen was the last thing moving at 53-62 px. ?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed. Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647, 0x5f76e76 unmoved. No tag — John's playtest rules the epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54 KiB
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(+ optionalflag,lane). Three rules carry it:licence:"unverified"is legal and a guess is not;attributionis the exact string, printed verbatim,nullwhen nothing is owed;requiredis the licence question, not an editorial one — Mixamo/Rokoko arefalsebecause their licences ask for nothing, CC BY is alwaystrue.evidencemust 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.jsonis 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
knownblock, keyed by TOWN (I re-derived the seed-invariance: 23 caches × 3 seeds, shop ids / edge ids / lot geometry all identical; synthetic alone keys@<seed>). 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=0map 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; selfcheck0x5f76e76either 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 (R13RY_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 triswith the bird on screen,+0off;?noassetsstays 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 withoutrotateY(π)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'sdump_bird.mjs(module-scopeloadGLBunder node); gated onIN_BROWSERand re-run green. Two stale154 triscomments 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=1hashes unmoved from R37. Sky feeds the baseskyName, neversetSky(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_godversecarrystate: '', so a per-corpus arm would have dropped exactly those two silently. 21 distinct palettes across 23 towns.gravelandreddustwere not unwired but unwireable — theirusevalues named no slotground.jshad; 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; 36yardlots 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;
peggedgives 4.5 cm / 31 cm, a ~52× gain, in the right place. Thecanopystring 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
StereoPannerNodeper 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 reportno-anchoron real towns rather than being faked. The falsifiability control — one manifest key renamed away on a third port — makes that source reportno-entry, 0 plays, neveraudible, 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 ·lanedegenerate[4,4]⇒ pre-ruling geometry ·arcadeall footpath, no road quad, no kerbs · residential keeps its grass nature strip. The lane case branches onouter <= 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=0ships permanently and is EXACT: ground vertex+UV hashes identical to the HEAD build, and the same detector goes red on the default boot.?classic=1is 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 onlyPROCITY.chunkshad nowhere to attach. Nowchunks.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 hashesceb1ec66on both builds.sim.jsuntouched.
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,Ctoggles) ·☾ 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/1on disk, next boot loads it and the UI reads it. venue.js §30.4 filing fixed as filed — compatvenueShopIdis 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_stoppicks the nearest-origin stop (open framing; spine-end stop was walled in). F shoots these — best hero combo?winmap=1&weather=rain&dbg=1seg 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 eachbusShelterStopsstop, ping-pong at the ends. Deterministic (verified byte-identical across fresh instances). Exactly 2 draws (verified viarenderer.infodelta). Composes w/ weather+night (headlights 0.15→1.6 vialighting.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.weathercontract 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=1seeded (default town rolls clear);?weather=rain|overcast|clearforces 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 →chunksmerges intogetColliders); walk-into-it is blocked. ExposedbusShelterStops(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_binGLB 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=1default-off. One sharedShaderMaterialon 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 fromshop.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?noassetsbaseline; 0 console errors;GOLDEN.hashuntouched.?winmapis 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
openLatevideo rental at night) → lit windows; door tooltip🔒 <name> — CLOSED · opens HH:00. Batched with a per-vertexaHours+ two shared shaders + oneuHour/uNightuniform pair, updated once on aprocity:segmentevent (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_fringeposes jammed the camera into a wall. Reframed them to look down a street (newstreetViewPose), added the 3 missing bookmarks F asked for, pointednight_neonat the glowing video shop amid CLOSED neighbours, and added a defensive composer resize.tools/shots.py→ 10 un-letterboxed shots, 0 console errors (indocs/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.
- food_cart (5.0k) GLBs are fine and sit on their footprints. → Lane E: decimate novelty_record
(same as the ped rigs). File:
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 eachplan.posters(x,z,ry) with the gig'sbandNameoverprinted innameZone(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 gigdoors/on, playspubrock-livethrough a 470 Hz lowpass at low distance-gain. Readswindow.PROCITY.gigs(F) with a clock+plan.gigsfallback. 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.gigsabsent → 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 — verifiedvideo-synthat ~0.08 by Video Barn's door), tram rumble+bell (?tram), SFX (doorbell/door-open onprocity: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.audioexposesplayInterior({musicKey,toneKey})/stopInterior()(street beds auto-duck while inside),playSfx,setMasterGain,mute, and astatediag surface. - For Lane C: confirmed
room.audio = { musicKey, toneKey }onbuildInterioris the right shape (musicKey →manifest.music, toneKey →manifest.ambience). - Gates green (manifest validator 0 errors incl. audio 23; selfcheck 1727/1727).
qa.sh --stricthangs on a headless Playwright gate — ran the static gates directly. Files:web/js/world/audio.js(new) + a small[Lane B R11 audio]loader inweb/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 viawindow.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 inlighting.attachRenderer(beforeCitizenSimconstructs; no asset). Verified set in street / night / interior / after interior-return (interior_modeonly swapsscene.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.DBGharness exposed (?dbg=1,js/world/dbg.js) — answers LANE_F_NOTES §4 so Fable'sshots.py/soak.pycan drive the game:ready,shot(name),teleport,setSegment,enterShop/exitShop(drives the real B→F→C interior chain),info().ChunkManagerlifecycle 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, newdbg.js, + a small guarded DBG loader inindex.html. Uncommitted (per the lane treaty; and Fable is concurrently wiringindex.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
generatePlanwhen 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 indocs/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:
- Collision was wrong for non-axis-aligned
ry(pushOutRectused the wrong rotation sign). Latent on my axis-aligned fixture; would let you walk through Lane A's angled buildings. Fixed — verifiedkeptOut:trueagainst ary=−1.32lot. - House doors were raycastable and mis-resolved to the nearest shop → clicking a house "entered" a random shop. Fixed (separate non-picked mesh).
- Awning skin used a signed shift → undefined skin for ~⅓ of shops. Fixed (
>>>). - 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).
- Furniture: numeric edge ids broke the seed hash (all edges identical); offset furniture could land in an unbuilt chunk and vanish. Both fixed.
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.htmlprefersgeneratePlan(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(facadeMatnow strips a.jpg/facade-prefix). Lane A storesshop.facadeSkinas 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.blockare strings in the fixture, numbers from Lane A;lot.frontEdgeis'N'/'S'in the fixture, a number from Lane A;shop.facadeSkinis a bare key in the fixture, a filename from Lane A;lot.ryis 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)
- Freeze the CityPlan schema in CITY_SPEC (the drift list above) — unblocks C/D/E. Still open.
Decide the facade-atlas— done (session 2): Lane B implemented it. Draw + material gates now green.Interiors hand-off— done: Fable wiredenterShop → interiorMode → buildInteriorin the shell; verified viaDBG.enterShop. Lane B's seam (procity:enterShop/procity:exitShop) is intact.- Run Lane F's
shots.py/soak.pyagainstwindow.DBGnow that it exists (bookmarks are seed-derived). - Optional: capture fresh
docs/shots/laneB/stills of the generated city viaDBG.shot(...); wiremapmode into Lane A'smap.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.)