minimap.js drew every lot in the plan from the first frame (493 shops on the synthetic, one M press, against 5,330 m / 4.8 game-days of walking to earn it). It now draws what has been walked past AND FRAMES ITSELF TO IT — a shop lot goes 0.83x1.11 px -> 29.4x39.2 px on bowral_real, 35.3x. ZERO DRAWS, measured as an A/B: two port-isolated no-store servers, HEAD vs treatment, the same 15 stations x 4 yaws compared as JSON before either sweep runs. Draw calls IDENTICAL on 60/60 samples at noon and at night; with the gig layer off both sides (the venue queue's async rigs were the only noise) NIGHT is identical in draws AND triangles. Map mode cannot cost a draw structurally: the frame loop never calls composer.render() in the map branch. - discovery.js (new): a 25 m PROXIMITY PROBE, not onChunkBuilt (R=2 => the outermost live chunk corner is 181 m out), on a plan-derived spatial hash — 1.09-1.37 us a probe, every 6th street frame = every 0.46 m at WALK. Plus a side test: you must be in FRONT of the shopfront. 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. - save.js: optional `known` block under the delta law, keyed by TOWN (re-derived: 23 caches x 3 seeds, shop/edge ids and lot geometry all identical; the synthetic alone keys @<seed>). Bounded by the plan (max 493 shops / 2,593 edges) at 8,192, 64 towns FIFO, ints only. Whole corpus fully learned = 137,547 B (2.6% of quota); 482 B after a 500 m outing. Eight reject arms demonstrated. - classic BY CONSTRUCTION: createMinimap(plan, null) is the pre-R39 map and the file has no flag test at all. ?classic=1 / ?fog=0 / ?game=0 map canvases are hash-identical to HEAD's. - Lane A's address layer consumed (getTownCache + createAddresses): street names along walked streets, deduped; the synthetic gets district labels from the same line. No town-type branch. FILED, NOT FIXED: index.html:384 (Lane D patronage door points) and :450 (the R32 spawn) use (-sin ry, -cos ry) — the BACK of the building. buildings.js:410/583-597, dbg.js:53 and minimap.js all use local +Z. Measured against each lot's own frontEdge the +sin point is 0.00 m from the kerb and the shipped one 15.25 m the wrong side, 493/493 and 72/72 shops. At the shipped spawn 0 of the shops within 25 m are in front of you on all four towns tested; at the corrected one, all of them. Two characters, but it moves the default boot's opening pose mid-round — Fable's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
32 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 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.
- FILED, NOT FIXED:
index.html's front normal is inverted at:384(Lane D patronage door points) and:450(the R32 spawn).buildings.js,dbg.jsandminimap.jsall use local +Z; measured, the shell's point sits 15.25 m behind the facade and 0 of the shops within 25 m of the shipped spawn are in front of the player on all four towns tested. Two characters, but it moves the default boot's opening pose mid-round — Fable's call.
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.)