Ledger #4. Verified fresh Chromium seed 20261990. No new rng, goldens untouched. - Town-wide pools (venue.js): extended the R13 instanced-pool pass from venue frontages to EVERY streetlight + bus shelter. New townPoolSpecs(plan) replicates furniture.js's lamp cadence off the plan street graph (lamp every 18m from s=8, alternating sides, off=halfRoad+0.7) and reuses the exported busShelterStops(plan) -- read-only, furniture.js untouched (imported one function). All pools ride the ONE existing InstancedMesh -> still ~1 draw: 381 instances / 1 draw / ~760 tris. Night-driven; fog:true now so distant pools fade into night fog (pools aren't chunk-streamed) instead of floating as dots. Gig-gated -> ?classic=1 is pool-free = v2 parity by construction (no always-on furniture change, the v3.0 blocker). Deterministic cadence, no seeded draws (release law). - Default-boot re-baseline: measured the default content (gigs + weather=rain + winmap + tram all live), settled. Busiest venue block (pub, NIGHT, rain) ~157 scene / ~170 total draws, 66k tris; venue_night 122, queue_night 121, district_posters/street_noon 185 (worst tour shot), market_square fresh 97/52k. ALL <=300 draws / <=200k tris. Flip adds ~3 draws (rain 1 + tram 2; winmap 0), pools add ~0 (instanced). Daytime dense-citizen ceiling (~275, Lane D, live-walk) unchanged (pools night-only). - Bookmark spot-check: all 19 resolve, un-letterboxed, 0 errors under the default boot; rain+tram+winmap compose cleanly in old frames (street_noon rainy 177/114k, nothing blocked). No pose fixes. Ran before F's flip landed (out of the stated order): pools are gig-gated so they work under ?gigs now; re-baseline uses explicit all-flags URLs = post-flip default content. F re-confirms at close. -> Lane F (B->F handshake): default-boot busiest block ~170 total draws / 66k tris; worst street view 185 / 131k; densest daytime 97 / 52k. All under budget. Town-wide pools ~1 draw. Pool sprite is procedural (E has nothing to do). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
646 lines
48 KiB
Markdown
646 lines
48 KiB
Markdown
# LANE B — NOTES (measured)
|
||
|
||
## Round 16 (Fable's ROUND16 → Lane B) — v3.1 THE FLIP: town-wide pools + default-boot re-baseline
|
||
|
||
Two deliverables. Verified fresh Chromium, seed 20261990. (Ran before F's flip landed — my pools are
|
||
gig-gated so they work under `?gigs=1` now, and the re-baseline uses **explicit** all-four-flags URLs =
|
||
the post-flip default-boot content; F re-confirms against the real default at close.)
|
||
|
||
### 1. Town-wide streetlamp pools (ledger #4) — `venue.js`
|
||
Extended the R13 instanced-pool pass from venue frontages to **every streetlight + bus shelter**. New
|
||
`townPoolSpecs(plan)` replicates furniture.js's exact lamp cadence off the plan's street graph (a lamp
|
||
every 18 m from `s=8`, alternating sides, at `off = halfRoad+0.7`) and reuses the exported
|
||
`busShelterStops(plan)` — **read-only, `furniture.js` untouched** (imported the one function). All pools
|
||
(venue + town) ride the **one existing InstancedMesh**, so town-wide is still **~1 draw**:
|
||
- **381 pool instances / 1 draw / ~760 tris** for Boolarra Heads (22 edges, 27 nodes). Night-driven via
|
||
the existing opacity ramp; `fog: true` now (was false) so distant pools fade into the night fog
|
||
instead of floating as dots over un-built chunks (pools aren't chunk-streamed). A/B'd `fog:false` —
|
||
no dramatic difference and a dot-risk on bigger towns, so `fog:true` is the keeper.
|
||
- **Gig-gated → classic parity by construction:** venue.js only exists under the gig layer, so
|
||
`?classic=1` (gigs off) is pool-free = v2 parity. No always-on `furniture.js` change (the v3.0 blocker).
|
||
- Pure function of the plan (deterministic cadence, no rng) → **no new seeded draws** (release law).
|
||
|
||
### 2. Default-boot budget re-baseline (ledger #4) — the number the budget law now means
|
||
Measured on the **default boot content** (gigs + `weather=rain` worst-case + winmap + tram all live),
|
||
settled (shoot-twice / clean-load to defeat the orphan-chunk teleport inflation):
|
||
|
||
| view (default boot, all flags) | draws (composer total) | tris | gate |
|
||
|---|---|---|---|
|
||
| **busiest venue block** (pub, NIGHT, rain, 3/4) | **~157 scene / ~170 total** | **66k** | ≤300 / ≤200k ✅ |
|
||
| `venue_night` | 122 | 53k | ✅ |
|
||
| `queue_night` | 121 | 61k | ✅ |
|
||
| `district_posters` / `street_noon` (wide main spine) | **185** | 131k | ✅ (worst tour shot) |
|
||
| `market_square` (densest daytime, fresh) | 97 | 52k | ✅ |
|
||
|
||
**No budget bust.** The flip adds ~3 draws (rain Points 1 + tram 2; winmap 0 — it's a shader on
|
||
existing windows), and the **town-wide pools add ~0** (1 instanced mesh). Worst default-boot street
|
||
view measured is **185 total draws / 131k tris** — ~115 draw / ~70k tri margin. (Pools are night-only,
|
||
so the pre-existing daytime dense-citizen ceiling — Lane D territory, ~275 total per R13's live-walk —
|
||
is unchanged by this round; winmap/rain add ≤3 there. F's live week-soak confirms the full-density number.)
|
||
|
||
### 3. Bookmark spot-check on the default boot
|
||
All **19 bookmarks resolve, un-letterboxed, 0 page errors** with all four flags on. Rain particles +
|
||
tram + winmap now appear in the old opt-in frames and compose cleanly — `street_noon` rainy (177 draws
|
||
/ 114k tris) frames the strip with rain + storm sky + wet road, nothing blocked. No pose fixes needed.
|
||
|
||
**→ Lane F (B→F handshake — the default worst-case numbers):** default boot, busiest venue block =
|
||
**~170 total draws / 66k tris**; worst street view (wide main spine) = **185 / 131k**; densest daytime
|
||
= 97 / 52k. All ≤300 / ≤200k. Town-wide pools are ~1 draw. Re-confirm against the real default once
|
||
your flip lands; the pool sprite is procedural (no asset, E has nothing to do).
|
||
|
||
---
|
||
|
||
## Round 15 (Fable's ROUND15 → Lane B) — verify the hero after A's poster flip (v3.0 release)
|
||
|
||
Task: re-verify `queue_night` after A's ledger-#1 flip (`35da83c`: `buildPosters` item 1 → `+sin,+cos`
|
||
street face, `ry = lot.ry + π`; gig golden re-pinned `0x1f636349 → 0x4f4a549d`, synthetic frozen).
|
||
|
||
**A's placement is correct — verified.** The pub's own-frontage poster moved from **15.9 m behind the
|
||
building** (R13/R14) to **0.09 m from the door on the +Z street face**, facing the reader (A's `ry+π`
|
||
composes with venue.js's `+π` render → the printed face points along the street outward normal, name
|
||
not mirrored). No pose change needed — my R14 prediction held.
|
||
|
||
**But a night-lighting gap surfaced (and John sanctioned the fix).** Posters are unlit
|
||
`MeshStandardMaterial` (emissive `000000`); nothing illuminates them at night (marquee/pools/glow are
|
||
emissive *materials*, not lights). Measured the frontage poster centre pixel: **DAY `[183,71,59]`
|
||
(vivid) → NIGHT `[4,0,1]` (pure black)** — so `queue_night` (seg 5) still framed no readable bill, the
|
||
very thing A's golden move was for; `district_posters` was dark for the same reason. **Fix (venue.js,
|
||
John-sanctioned exception to the R14 freeze):** posters get `emissiveMap = <the poster art>` +
|
||
`emissive = white`, `emissiveIntensity` **ramped in `update()` by night** (0 by day → 0.55 at night,
|
||
same lerp as the glow/pools). Self-lights the bill after dark; **daylight stays byte-identical**
|
||
(emissive 0 → verified the midday SCREAMING UTES spine poster is pixel-unchanged, no glow/wash). No new
|
||
rng, no plan change, goldens untouched (`0x3fa36874` / `0x4f4a549d` both frozen). `?noassets` fallback
|
||
poster self-lights too (its canvas exists, zero fetch).
|
||
|
||
Verified fresh Chromium, seed 20261990:
|
||
- **`queue_night`** (seg 5, live queue spawned): marquee + window glow + streetlamp pools + **readable
|
||
red poster by the door** + queue punters + dark CLOSED neighbours. 90 draws / 29k tris.
|
||
- **`district_posters`** (seg 5): a venue poster now reads on the spine (was black). 180 draws / 138k tris.
|
||
- **Daylight** spine poster: byte-identical to R13/R14 (day emissive ramps to ~0). `venue_night` reads.
|
||
- Spine posters face the street everywhere (A's flip only moved item-1 frontage posters; item-2 pole
|
||
posters unchanged, still readable).
|
||
|
||
**→ Lane F (B→F handshake): HERO VERIFIED.** `queue_night` frames queue + marquee + A's now-visible
|
||
poster; drive the queue live in `tour_shots.py` (the rAF is throttled in automation — I spawn D's
|
||
`VenueQueue` manually to preview). Shoot at **seg 5 (NIGHT)** — the poster now reads there (fix above),
|
||
and night is required for the pools + window glow. Poster night-readability is on venue.js's `update()`
|
||
ramp, so the shell's per-frame `venuePresentation.update(gigState.byVenue)` already drives it.
|
||
|
||
---
|
||
|
||
## Round 14 (Fable's ROUND14 → Lane B) — v3.0 RELEASE: alias retirement + the tour audit
|
||
|
||
Release round — no new systems. Three B files touched (`audio.js`, `venue.js`, `dbg.js`); no world
|
||
geometry, no rng, goldens frozen (`0x3fa36874` synthetic, `0x1f636349` gig — neither moved). Verified
|
||
fresh Chromium, seed 20261990, `?gigs=1`.
|
||
|
||
### 1. Alpha-alias readers dropped (debt #1 — B first; **F unblocked**)
|
||
The R12 `window.PROCITY.gigs` scalar alias (`.state/.on/.venueShopId/.cover/.paid/…`) is retired this
|
||
round. B's readers removed:
|
||
- **`audio.js`** `venueGigState()` — dropped the alias hop (`g.venueShopId === shopId → g.state`).
|
||
Now byVenue-only + the clock fallback: `g.byVenue[id]` → else `plan.gigs` night-0 vs the segment.
|
||
- **`venue.js`** `stateGetter()` — dropped the bare-string arm; reads `gigStates.byVenue || gigStates`
|
||
(F passes `gigState.byVenue`, confirmed at `index.html:379`). Seed call is now `update(null)`.
|
||
|
||
Verified live: F's `gigState.byVenue` reads `{116:'on', 404:'on', 490:'quiet'}` at NIGHT (pub + RSL
|
||
playing, band_room dark); passing it to `venue.update()` lights the marquee with **no string arm**, and
|
||
`audio.js` resolves each venue's `gig-<genreKey>` with **no alias hop**. Audio engine loads `live`.
|
||
|
||
**→ Lane F (you can now delete the alias):** an alias-surface sweep (all of `web/js/**`, `index.html`,
|
||
`tools/**`) confirms **CROSS-LANE CLEAN** — no A/C/D/E file reads the scalar alias (interior_mode.js
|
||
uses `onOf()/gigOf()`; sim.js uses shell-driven `setGig`). The only remaining alias readers are **your
|
||
own 20 lines** in `tools/flags_check.py` + `tools/qa/gig_shot.py` (`.venueShopId`×12 → `venueShopIds[0]`
|
||
or an explicit id; `.state`×5 → `stateOf(id)`; `.cover`×2 → `coverOf(id)`; `.bandName`×3 →
|
||
`bandNameOf(id)`; `gig_shot.py:81` already prefers `stateOf(id)`). Delete the alias getters at
|
||
`gig_state.js:141–151` + migrate those tool lines in the same commit; `!!P.gigs` existence checks keep
|
||
working (the per-venue machine still exists). CITY_SPEC §v3 runtime block loses the alias line (with A).
|
||
|
||
### 2. Bookmark / tour audit under `?gigs=1` — **all clean, no pose fixes**
|
||
All 19 bookmarks (16 v2 + `venue_night` + `district_posters` + new `queue_night`) resolve, frame
|
||
**un-letterboxed**, **0 page errors**, ≤300 draws. The district's in-place venue conversions hijacked
|
||
**no** bookmark: `night_neon`→"Video Regal" (openLate is never converted), `dig_real`→"Retro Groove"
|
||
(a real record shop survives — multiple per type). Framing eyeballed clean on the tour spread:
|
||
`street_noon`, `venue_night`, `queue_night`, `tram_stop`, `district_posters`, `night_neon`. **No dbg.js
|
||
pose changes were needed** beyond adding `queue_night`.
|
||
- **Measurement gotcha (re-confirmed):** `DBG.shot` teleports, so shooting many bookmarks in sequence
|
||
leaves orphan chunks that inflate tris on later shots (saw 238k on `patronage_door`; **42k** on a
|
||
fresh load / shoot-twice-to-settle). Shoot from a clean load, or twice, for the true per-frame count.
|
||
- **Observation → Lane D / Fable (not a Lane B fix):** `tram_stop` is a stable **~293k tris** — real,
|
||
from close-range full-detail GLB ped rigs on the footpath (framing is fine). It's not a venue block
|
||
(F's tri gate measures at the venue block, ~64k), so it likely doesn't trip the gate, but it exceeds
|
||
the 200k reference for a tour shot; worth a look if a close ped shot lands in the release set.
|
||
|
||
### 3. `queue_night` — John's release hero (`dbg.js`)
|
||
New bookmark `queue_night` (seg 5 / NIGHT / 'on'): a close 3/4 on the pub door, biased to the venue's
|
||
`+right` (the queue side). New `poseForVenueQueue()` recomputes venue.js's frontage geometry from the
|
||
pub lot: `DIST 7 m` out on the street, `SIDE = mw/2 + 2.2 m` to the queue side, eye 2.2 m, looking at
|
||
the door lifted to y2.7 (catches the marquee). Verified: frames marquee + window glow + streetlamp
|
||
pools + the queue. **The queue is spawned by the shell's live rAF loop** (`index.html:383` builds D's
|
||
`VenueQueue` on `openOf(id)`), so F's `gig_shot.py` must drive it **live** (the automation rAF is
|
||
throttled — I confirmed the pose + D's line by manually spawning `VenueQueue` at the pub queueZone: 2
|
||
punters seat at the door, facing it).
|
||
- **→ Lane A (blocks the hero's poster):** `queue_night` frames door + marquee + queue, but **no poster
|
||
lands in the close frame** — the pub's own-frontage poster is still on A's local **−Z** (the BACK
|
||
wall, `gigs.js buildPosters` item 1 `-sin,-cos`; measured 15.9 m from the +Z street facade), and the
|
||
nearest spine poster is 53 m away. Per Fable's "fix poses, don't touch world code," B did **not**
|
||
work around it. **Flip item 1 to `+sin,+cos`** and the frontage poster seats by the door — in the
|
||
existing `queue_night` frame with no pose change. (Re-flagged from R13; now it gates John's hero shot.)
|
||
|
||
### 4. Poster-repetition call (debt #4) — **FINE, no ask to E**
|
||
Tonight's 14 posters carry **2 distinct designs** (Screaming Utes / screenprint ×7, The Dead Ringers /
|
||
xerox ×7 — different skins, different bands). Variety = number of venues playing tonight (one seeded
|
||
skin per gigId), so a spine run reads as two real band bills, not a repeat. Only a town where a single
|
||
gig plays everywhere would look monotonous (rare, and reads as "a residency"). No new templates needed.
|
||
|
||
### 5. Town-wide streetlamp pools — v3.1 candidate (Fable ruling, no code)
|
||
Fable ruled the night pools **stay venue-scoped for v3.0** (town-wide needs always-on `furniture.js`
|
||
changes that fight flags-off parity in release week; venue blocks read fine — `venue_night` proves it).
|
||
Parked here as a **v3.1 candidate** alongside wind sway and bench-sit loiter. No R14 code.
|
||
|
||
---
|
||
|
||
## Round 13 (Fable's ROUND13 → Lane B) — the district reads by night (`?gigs=1`, v3.0-beta)
|
||
|
||
Four B pieces, all gig-gated (flags-off byte-identical, verified). Files: `venue.js` (rewrite),
|
||
`audio.js` (spill), `dbg.js` (bookmarks). Verified in a **fresh browser context** (the stale-module
|
||
trap bit me once here — the pre-warmed tab served R12 `venue.js`; a `cache:'reload'` + hard nav fixed
|
||
it, proof = the returned object's key set flipped from R12 `{frontage,venueShopId}` to R13
|
||
`{venues,venueShopIds,queueZones,…}`). Seed 20261990 = **Boolarra Heads, 3 venues**: The Exchange
|
||
Hotel (pub/pubrock, id116), Wangaratta RSL (rsl/covers, id404), The Vinyl Shed (band_room/grunge,
|
||
id490); 14 gigs/week, tonight **Screaming Utes @ pub $10** + **The Dead Ringers @ RSL free**.
|
||
|
||
### 1. `venue.js` generalised to N venues — `createVenuePresentation(plan, skins, scene)`
|
||
Now renders **every** `plan.shops` with `s.venue`, not just the pub. Returns
|
||
`{ group, venues[], update(gigStates), dispose, venueShopId (primary/alpha compat), venueShopIds[],
|
||
queueZones{}, queueZoneFor(id), frontage (primary, compat), posterCount, posterDraws }`.
|
||
- **Frontage per venue** — warm marquee slab + emissive bulb row over the door, on B's **+Z facade
|
||
side** (`sin ry, cos ry`; buildings.js canon), plus a **window-light spill** wash (additive, faint
|
||
night baseline + gig glow). Ramped per-venue by `update()`.
|
||
- **Streetlamp pools (the night-readability pass)** — one procedural warm radial sprite, **one
|
||
InstancedMesh for the whole district** (door pool + 2 footpath pools per venue). Night-driven (on at
|
||
NIGHT, off by day — streetlamps don't care about gigs), so venue blocks read on the Friday-night walk.
|
||
Scoped to venue blocks; **town-wide deferred on purpose** (would need always-on `furniture.js`
|
||
changes → breaks flags-off parity).
|
||
- **Posters, town-wide, instanced per gig.** A given gig's poster is identical wherever pasted, so B
|
||
groups `plan.posters` by `gigId`, picks ONE seeded skin per gig, and renders each group as a single
|
||
`InstancedMesh` → **14 posters tonight = 2 draws / 2 canvas textures** (was 1 mesh + 1 canvas each).
|
||
Band name overprinted in the skin's `nameZone`; flat printed-poster fallback on 404 / `?noassets`.
|
||
- **`?noassets` fetch fix (latent R12 bug).** R12 `posterTexture` fetched `poster-*.jpg` even under
|
||
`?noassets`. R13 gates the fetch behind `!noassets` (read from the URL, like buildings.js `WINMAP`).
|
||
Measured `?noassets=1&gigs=1`: **0 poster fetch, 0 audio/manifest fetch** — the gig layer adds no
|
||
network over the documented 22-JPEG facade/ground baseline. Venue still builds (flat posters), no crash.
|
||
|
||
### 2. `audio.js` muffled spill → per-genre, per-venue (debt #1 closed on B's side)
|
||
The R12 spill hard-coded `pubrock-live`; E's rename made that a **silent bed** (band mimes). Now B
|
||
`import { gigKeyFor } from '../citygen/gigs.js'` (the ONE genre→bed map) and spills each venue's
|
||
`gig-<genreKey>`. Walks the venue list, the **nearest venue at doors/on within 26 m wins**, so
|
||
pub→band_room→RSL **crossfades the genre** through the same 470 Hz lowpass. Verified beds resolve to
|
||
E's manifest: pub→`gig-pubrock`, RSL→`gig-covers`, band_room→`gig-grunge` (all present). Audio engine
|
||
loads `live` with the new citygen import; flags-off it loads fine and the spill loop is skipped
|
||
(no `s.venue` shops). **F: delete the `GIG_BED` bridge — B no longer needs it.**
|
||
|
||
### 3. Queue-zone marker (for Lane D) — `venue.queueZone = { x, z, ry, len }`
|
||
Each venue publishes a queue zone: a subtle chalk-pale ground strip **beside the door** (past the
|
||
marquee edge, out on the footpath), running **along the frontage**. `queueZones` = `{ [venueShopId]:
|
||
zone }`; also `queueZoneFor(id)`. **Contract for D:** `(x,z)` = the head (front of queue, at the door
|
||
end); the line **extends** in `(sin ry, cos ry)` for `len` metres; a queuer **faces the door** =
|
||
`(−sin ry, −cos ry)`. D places poses at `head + (sin ry, cos ry)·s` for `s ∈ [0,len]`. Verified 3
|
||
zones seat correctly (pub head (−30.9, 390.0) beside the door). D's fallback ("straight line from the
|
||
door") stays valid if the zone is absent.
|
||
|
||
### 4. `dbg.js` bookmarks — the street money shots (for `tools/qa/gig_shot.py`)
|
||
- **`venue_night`** (seg 5): stands 13 m in front of the **pub** frontage — lit marquee + streetlamp
|
||
pools + window spill + queue + posters, dark CLOSED neighbours behind. The street money shot.
|
||
- **`district_posters`** (seg 5): the spine at night, poster poles + lit frontages receding.
|
||
- New `poseForVenue(kind, dist)` resolves from the live plan (falls back to `streetViewPose` with no
|
||
gig layer, so flags-off shots don't crash).
|
||
|
||
### `update(gigStates)` — the shape to agree with F (F drives it every street frame)
|
||
`update` is **polymorphic**: pass `{ [venueShopId]: 'quiet'|'doors'|'on'|'done' }` (a plain object or
|
||
Map, or `{ byVenue: {…} }`) for **per-venue** frontage lighting; a **bare state string** is alpha
|
||
compat and lights **only the primary pub** (`plan.gigs[0].venueShopId`). **F R13 ask:** build
|
||
`window.PROCITY.gigs.byVenue = { [venueShopId]: { state, cover, paid, … } }` (keep the alpha single
|
||
object as a compat alias) and call `venuePresentation.update(PROCITY.gigs.byVenue)`. **audio.js reads
|
||
`PROCITY.gigs.byVenue[id].state` too** (falls back to the alpha alias, then a clock fallback from
|
||
`plan.gigs` night 0), so the spill works across the transition. Until F wires the map, only the pub
|
||
frontage marquee lights from the alpha string — **pools + baseline window glow (night-driven) light
|
||
all venue blocks regardless**, so the district still reads at night.
|
||
|
||
### Budget (measured, fresh Chromium, gigs-on, NIGHT, all frontages lit, busiest = pub block)
|
||
| view | scene draws | tris | gate |
|
||
|---|---|---|---|
|
||
| `venue_night` (composer total) | **96–113** | 19–32k | ≤300 / ≤200k ✅ |
|
||
| pub block 3/4 street (worst of 4 poses) | **154 scene (~167 total)** | **64k** | ✅ ~130 draw margin |
|
||
| `district_posters` (composer total) | 142 | — | ✅ |
|
||
Gig-night layer cost is tiny: pools **1 draw**, posters **≤2 draws** (instanced per gig), frontage
|
||
~3 draws/venue-in-frame. `?noassets=1&gigs=1` worst 176–198 (post passes vary), still ≤300. **0
|
||
console errors** in every mode (gigs / gigs+noassets / flags-off).
|
||
|
||
### → Lane A (a finding for you — cross-lane, please flip one sign)
|
||
`buildPosters` **item 1** seats each venue's own frontage poster at `lot + (−sin ry, −cos ry)·(d/2)`
|
||
— the lot's **−Z** side. But B's facades/doors are on **+Z** (buildings.js canon since round 1, and
|
||
the shipped town the player faces), so the venue-frontage poster lands on the **back wall**: measured
|
||
for the pub it's **15.9 m from the street facade, 0.07 m from the back**. The CITY_SPEC "front = −Z"
|
||
poster convention disagrees with the building convention — a street-side sibling of debt #4's RY
|
||
clash. **Fix:** item 1 → `+sin, +cos` (the spine-run posters, which use the pole outward-normal
|
||
derivation, render correctly and face the footpath — verified "SCREAMING UTES" readable, not
|
||
mirrored). B renders A's `(x,z,ry)` verbatim (mesh oriented `ry+π` to face A's −Z normal), so once A
|
||
flips, the pub gets its poster with no B change. Not blocking the money shot (marquee + pools carry it).
|
||
|
||
---
|
||
|
||
## Round 9 (Fable's ROUND9 → Lane B) — v2 tour bookmarks + the tram (`?tram=1`)
|
||
|
||
Two deliverables, both v2-law clean (default boot untouched, golden hash frozen, `qa.sh --strict` GREEN):
|
||
|
||
### 1. v2 tour bookmark poses (`dbg.js`) — F shoots these
|
||
Added **8 bookmarks** to `js/world/dbg.js` (16 total now), all measured un-letterboxed at 113–188 draws:
|
||
`rain_verandah` (the money shot — open VIDEO shop w/ winmap parallax, CLOSED dark neighbours, night rain),
|
||
`patronage_door`, `dig_real`, `katoomba_main`, `tram_stop`, `rain_street`, `night_crowd`, `shopfront_detail`.
|
||
Best combo to shoot the hero: `?winmap=1&weather=rain&dbg=1` → `DBG.shot('rain_verandah')` at night seg 5.
|
||
`tram_stop` now picks the **nearest-origin** shelter stop (most open framing — the spine-end stop was
|
||
boxed in by big buildings); frames the `bus_shelter` cleanly at midday.
|
||
|
||
### 2. Tram (`?tram=1`) — new file `js/world/tram.js`, NON-BLOCKING for the tag
|
||
One small bus/tram, out-and-back loop along the **main-street spine**, door-dwell (3.5 s) at each
|
||
`busShelterStops` stop, ping-pong reverse at the ends. **Deterministic** (position is a pure function of
|
||
elapsed time — verified byte-identical across fresh instances). **Exactly 2 draws** (textured body +
|
||
merged headlight pair — verified via `renderer.info` delta). Composes with weather/night: headlights
|
||
`emissiveIntensity` jumps 0.15→1.6 when `lighting.isNight()`. "TOWN LOOP" canvas livery. `ring()` is a
|
||
door-bell stub (audio still parked). Flag-off is byte-identical (shell never constructs it).
|
||
|
||
#### → Fable (F, shell wiring — I don't touch `index.html`)
|
||
```js
|
||
import { createTram } from './js/world/tram.js';
|
||
const tram = params.get('tram') && params.get('tram') !== '0'
|
||
? createTram({ scene, plan, camera, lighting })
|
||
: null; // default-off; needs lighting.isNight() for night headlights
|
||
// in the street branch of the main loop: if (tram) tram.update(dt);
|
||
```
|
||
Flags-table row: `?tram=1` — owner B — landed, default-off, self-contained module (F wires the 2 lines
|
||
above). Smoke: boot `?tram=1`, assert `scene.getObjectByName('tram')` present + tram-group render delta
|
||
≤2 draws. No collision in v0 (player walks through — noted for v3). `lighting.isNight()` is assumed on
|
||
the lighting object; if F's lighting exposes night differently, pass a predicate — tram degrades to the
|
||
day headlight value if `isNight` is absent (guarded).
|
||
|
||
## Round 8 (Fable's ROUND8 → Lane B) — weather (`?weather=1`), carried from R7
|
||
|
||
New file `js/world/weather.js` + a one-line `setSky()` on `lighting.js`. Seeded, deterministic, cheap.
|
||
|
||
- **Weather state** — `weatherFor(citySeed, day=0)` → `{ state, intensity }`, weights ~57% clear /
|
||
24% overcast / 19% rain (AU coastal-town plausible; verified over 400 seeds). `day=0` today (one
|
||
day cycle); a future day-counter rolls new weather. **Seed 20261990 (default town) rolls `clear`**,
|
||
so `?weather=1` there is v1-identical — use **`?weather=rain`** (or `overcast`) to force a state for
|
||
demos/smokes, or a rainy seed (7, 424242).
|
||
- **Rain** — ONE draw: a **camera-following `THREE.Points`** layer with a procedural streak sprite and
|
||
a fall+wind vertex shader (no CPU per-particle). Plus **wet ground** (darken + drop roughness on the
|
||
shared ground materials, restored on dispose) and a **matching rainy sky dome** (`lighting.setSky` →
|
||
summer-storm/monsoon/cold-front). Overcast: greyer dome, no particles. Fully procedural — 0 depot/GLB
|
||
fetch (the sky swap is a skin JPEG, same class as the town's own).
|
||
- **Budget/composition/determinism**: worst view **141 draws (day rain) / 126 (night)** — ≤300.
|
||
Composes with `?winmap` + night — **rain outside a lit interior-mapped window is the money shot**
|
||
(`docs/shots/laneB/weather_rain_night_winmap.png`). Deterministic per seed. `?noassets` compatible.
|
||
**0 console errors** in all states. **Flag-off is byte-identical** (the shell simply never constructs
|
||
weather). `qa.sh --strict` GREEN. *(Deferred, "if cheap": vertex-shader wind sway on trees/awnings —
|
||
needs touching furniture's tree material + skins' awning material; noted, not done this round.)*
|
||
|
||
### `window.PROCITY.weather` contract (Lane D consumes this — decision #1)
|
||
Shape: **`{ state: 'clear' | 'overcast' | 'rain', intensity: 0..1 }`**, stable for the session.
|
||
`intensity` is 0 for clear, ~0.3–0.6 overcast, ~0.45–1.0 rain. Set by `createWeather` when the flag is
|
||
on; **F sets `{ state:'clear', intensity:0 }` when off** so D always reads a valid value (below). D:
|
||
read it, don't import weather.js.
|
||
|
||
### → Fable (F, shell wiring — I don't touch `index.html`)
|
||
```js
|
||
import { createWeather } from './js/world/weather.js';
|
||
const wp = params.get('weather');
|
||
const weather = (wp && wp !== '0')
|
||
? createWeather({ scene, plan, skins, lighting, camera, force: /^(clear|overcast|rain)$/.test(wp) ? wp : null })
|
||
: null; // seeded when ?weather=1; forced when ?weather=rain|overcast|clear
|
||
window.PROCITY.weather = weather ? weather.state : { state: 'clear', intensity: 0 };
|
||
// in the street branch of the main loop: if (weather) weather.update(dt);
|
||
```
|
||
Flags-table row: `?weather=1` — owner B — landed, default-off, self-contained module (F wires the 3
|
||
lines above). Smoke: boot `?weather=rain`, assert rain Points present + `PROCITY.weather.state==='rain'`,
|
||
0 errors.
|
||
|
||
---
|
||
|
||
## Round 6 (Fable's ROUND6 → Lane B) — place the orphaned street props
|
||
|
||
Placed the two published-but-unconsumed street GLBs (`procity_street_bin_01`, `procity_street_bus_shelter_01`)
|
||
via the existing use-if-ready furniture GLB path (fail-soft to primitives; `?noassets` → primitives,
|
||
**0 network** verified). All in `furniture.js` (+ a 3-line collider merge in `chunks.js`).
|
||
|
||
- **Bins** — genuinely sparse now (one loop, ~1 per block: `s += 78 m`, seeded side per edge), a country
|
||
town not a mall. Decoupled from benches. GLB (3.0k tris) on its 0.57×0.68 footprint, primitive fallback.
|
||
- **Bus shelters — on the street graph, and this is the future tram/bus-stop contract.** Deterministic
|
||
rule: **the midpoint of every MAIN edge whose `hashEdge(id) % 3 === 0`, on the road-side footpath by the
|
||
kerb, long axis along the edge, open front to the road** — yields **1–3 per town** (2 in seed 20261990).
|
||
The GLB (2.33×2.4×1.41, 8.0k tris) gets a **solid oriented-rect collider** so the player rounds it
|
||
(furniture now returns `colliders`; `chunks.js` merges them into `getColliders`). Collision-verified: a
|
||
straight walk into it is blocked.
|
||
- **→ whoever builds the V2 tram/bus loop:** call **`busShelterStops(plan)`** (exported from
|
||
`furniture.js`) → `[{ edgeId, x, z, yaw }]`. It re-derives the stops from the exact same rule the
|
||
shelters are placed by — pure function of the plan, zero side effects. That is the stop list; don't
|
||
re-invent it. To move/space stops, change the one rule + `busShelterStops` together.
|
||
- **Perf (measured, worst view WITH the rig-fleet citizens, midday):** **94k tris · 148 draws** — both
|
||
well under the ≤200k / ≤300 gates (~100k tri and ~150 draw margin). Determinism holds (seed → same
|
||
props). All-flags-on (`?winmap=1&dig=1&roster=stream`) composes clean, 0 console errors. Shot:
|
||
`docs/shots/laneB/bus_shelter.png`. `qa.sh --strict` GREEN.
|
||
- **→ Lane E (decimation candidate, not blocking):** the `street_bin` GLB is **3.0k tris** — heavy for
|
||
a prop placed ~15×; I bounded it with the sparse cadence, but a decimate to ~500 tris would give back
|
||
tri headroom if the combined-lane budget tightens. (Same class as the deferred `novelty_record`.)
|
||
|
||
---
|
||
|
||
## Round 5 (Fable's ROUND5 → Lane B) — v2: the Vuntra window trick, behind `?winmap=1`
|
||
|
||
- **Parallax interior-mapping glass** (V2_IDEAS "window trick"). Behind **`?winmap=1`, default-off.**
|
||
Single-cube interior mapping in one shared `ShaderMaterial` on the existing window geometry: each
|
||
glass fragment casts the view ray into a virtual room box and shades the surface it hits (back wall
|
||
+ seeded shelf silhouettes with product blocks, side walls, floor/ceiling) — a plausible room with
|
||
real parallax, **zero extra geometry, still 1 window draw/chunk**. Per-shop variation rides
|
||
attributes (`aTangent` = window right vector, `aTint` = seeded room colour, `aSeed` = shelf
|
||
variation), **not per-shop materials**. Verified live: rooms are visible through the glass with
|
||
parallax day + night.
|
||
- **§3.5 respected** via the same `uHour/uNight` + `aHours` the closed-facade system uses: at night an
|
||
**open shop's room glows warm** ("Video Regal" lit), a **closed shop's room is dark** ("Second Time"
|
||
dark) — screenshots in `docs/shots/laneB/winmap_*.png`.
|
||
- **v2 prime law held.** `WINMAP` is read straight from the URL in **buildings.js** — **no shell seam,
|
||
so F only needs a flags-table row, no `index.html` wiring.** Flag-OFF path is byte-identical to v1:
|
||
`windowQuad` collapses to the v1 `hoursQuad` (no extra attributes) and the finalize uses the v1
|
||
`windowMaterial()` — verified: flag-off windows are `MeshStandardMaterial` with no `aTangent`;
|
||
flag-on are the interior `ShaderMaterial` with `aTangent`. Seeded from `shop.seed` (deterministic).
|
||
- **Budget + fetches:** flag-on worst continuous-walk view with citizens **122–168 draws** (≤300);
|
||
the shader is **100% procedural** — measured **0 fetch delta** vs the `?noassets` baseline (both 22
|
||
= the pre-existing facade/ground skin JPEGs). `?noassets=1&winmap=1` adds no network. **0 console
|
||
errors** in all modes. `qa.sh --strict` GREEN (4/4); `GOLDEN.hash` untouched (no plan change).
|
||
|
||
**→ Fable (F2 flags table):** `?winmap=1` — owner Lane B — state landed, default-off — needs **no
|
||
index.html wiring** (self-read in buildings.js). Smoke: boot `?winmap=1`, walk a block, glass shows
|
||
rooms; night open=warm / closed=dark.
|
||
|
||
---
|
||
|
||
## Round 4 (Fable's ROUND4 → Lane B) — done
|
||
|
||
- **B1 — closed-facade visuals (§3.5, the headline).** Closed shops now render **dark windows + a
|
||
red CLOSED plate**; open shops (esp. the one `openLate` **video rental** at night) render **lit
|
||
windows**; the door tooltip reads `🔒 <name> — CLOSED · opens HH:00` on aim. **Batched — the facade
|
||
atlas is untouched:** per-shop open/closed rides on a per-vertex `aHours` (vec2 = the shop's
|
||
`[open,close]`) feeding TWO shared shaders (window emissive gated by *open AND night*; plate alpha
|
||
gated by *closed*), driven by ONE pair of shared uniforms (`uHour/uNight`) updated **once per
|
||
day-segment change** — no per-shop materials, no per-frame poll. lighting.js dispatches a
|
||
`procity:segment` event (numeric hour matching Lane F's `currentHour()`); buildings.js listens and
|
||
calls `setFacadeHours`. Verified live: **22:00 → only "Video Regal" lit, every other facade CLOSED**;
|
||
midday → all open, no plates; tooltip correct. **Worst view with citizens: 200 total draws** (≤300).
|
||
- **B2 — shot harness (gate-6).** (a) **Letterbox "fixed":** the bars were **not** a composer bug (I
|
||
proved the composer/renderer/camera are correctly 1280×720 in headless at every stage) — they were
|
||
`street_noon`/`arcade`/`warehouse_fringe` poses **jamming the camera against a big building wall**.
|
||
Reframed those bookmarks to look *down a street* (`streetViewPose`), so all 10 tour shots now fill
|
||
the frame. Also added a defensive `composer.setSize` sync in `DBG.render` per F's request. (b) Added
|
||
the **3 missing poses** F listed (`crossroads_busy`, `residential_collar`, `warehouse_fringe`) —
|
||
resolved from the live plan (busiest node / house-or-backstreets lot / outer-ring street), always a
|
||
valid full-frame pose. `night_neon` now frames the open video shop glowing amid its CLOSED
|
||
neighbours (the §3.5 money shot). `tools/shots.py` → 10 un-letterboxed shots, **0 console errors**.
|
||
- **B3 — street-furniture GLB upgrade + `novelty_record`.** furniture.js now upgrades primitives to
|
||
depot GLBs via a **synchronous use-if-ready** check at chunk-build (no async write into a live
|
||
chunk; `?noassets` → primitives, **0 network** verified). Placed the **novelty_record** landmark on
|
||
the footpath in front of every record store (and a food_cart by market stalls / milk bars).
|
||
**⚠ measured GLB tris:** bench **1.9k** ✓, food_cart **5.0k** ✓, streetlight **5.0k** ✓, but
|
||
**novelty_record is 26.5k** (5× the ≤5k `glb_law`) — × many instances it spiked the busiest view to
|
||
**574k tris**. So novelty_record stays on its **low-poly primitive** (giant vinyl disc, budget-safe)
|
||
until Lane E decimates the GLB — **same treatment the ped rigs need (→ Lane E)**. With that, the
|
||
bench GLB verified sitting on its exact footprint (0.44×1.21), tris back to ~25k.
|
||
|
||
`bash tools/qa.sh --strict` **GREEN** (4/4) after these changes. Round-4 files: `dbg.js`, `buildings.js`,
|
||
`lighting.js`, `hud.js`, `furniture.js` (+ these docs, + `docs/shots/laneB_r4/`).
|
||
|
||
---
|
||
|
||
|
||
Streetscape + game shell. Files: `web/index.html`, `web/js/world/*`, `web/js/world/fixture_plan.js`.
|
||
All numbers measured in-browser (Chromium, M-series) via `renderer.info`, 1280×720-ish viewport.
|
||
`renderer.info.autoReset` is off and reset once/frame, so **draws/tris are the true per-frame
|
||
totals across all passes** (scene + bloom + output). "scene draws" below excludes the ~11
|
||
post-processing passes; "total" adds them.
|
||
|
||
## Perf — fixture (`fixture_plan.js`, "Yarraville Junction", 26 shops, radius 3, shadows on)
|
||
|
||
| metric | measured | gate | status |
|
||
|---|---|---|---|
|
||
| draw calls | **148 scene (~159 total)** | ≤ 300 | ✅ |
|
||
| triangles | **~5.8k** | ≤ 200k | ✅ |
|
||
| skin materials (city-wide, shared) | **21** | ≤ 25 | ✅ |
|
||
| live chunks (radius 3) | 12 | — | — |
|
||
| chunk build | ~1–8 ms | 4 ms/frame budget | drained 1/frame |
|
||
|
||
Walked the fixture end-to-end (~310 m of main street + the backstreets): contiguous, no seams,
|
||
chunks stream in/out silently (live count 8↔12 as you move; build queue never starved), day/night
|
||
cycles through 6 segments, signs legible, door tooltips + `procity:enterShop` fire, collision
|
||
stops the player at wall−radius (measured 10.62 m against an 11 m frontage, radius 0.38). Runs with
|
||
`web/assets/` renamed away — no crashes, flat-colour fallback town, canvas signs still legible.
|
||
|
||
## Perf — Lane A integration (`generatePlan`, "Boolarra Heads", ~493 shops, 1 km², radius 2 auto, shadows off)
|
||
|
||
Lane A's `generatePlan` landed mid-build; `index.html` prefers it over the fixture (guarded import),
|
||
so this is the shipped default now. It is a full city with content in nearly every chunk — far
|
||
denser than the fixture the ≤300 gate was sized against.
|
||
|
||
Streetscape numbers below are **buildings + ground + furniture only** (what Lane B owns), measured by
|
||
a scripted continuous walk (real streaming + disposal, so ≤25 live chunks — no orphan-chunk inflation).
|
||
|
||
**Round-3 re-measure WITH Lane D citizens (pop 140, the shipped default).** Driving the real loop
|
||
(`chunks.update` + `citizens.update`) along the dense corridors via `window.DBG`, up to **125 active
|
||
peds** at midday: worst continuous-walk view **263 scene / 275 total** at **MIDDAY** and **275 total
|
||
at NIGHT** — **≤300 everywhere**, ~25 draw margin. Peds are a batched impostor layer (~1–2 draws for
|
||
the whole near tier), so they cost far less than their count. Radius-2 auto is already the spawn
|
||
default for a big city; no config change was needed. (Fable's independent integrated read is ~191 on a
|
||
typical main-street view.)
|
||
|
||
| metric | measured | gate | status |
|
||
|---|---|---|---|
|
||
| draw calls (worst gameplay view, continuous walk) | **~261 total (249 scene)** | ≤ 300 | ✅ ~40 draw margin |
|
||
| draw calls (typical street view) | **~130–210 total** | ≤ 300 | ✅ |
|
||
| triangles | 12k–25k | ≤ 200k | ✅ (huge headroom) |
|
||
| skin materials (shared, city-wide) | **5** | ≤ 25 | ✅ (facade atlas collapsed ~22 facade mats → 1) |
|
||
| live chunks (radius 2, walking) | ≤ 25 | — | — |
|
||
| chunk build | min ~1 / median ~8 / max ~22 ms | 4 ms/frame | ~1 heavy chunk/frame |
|
||
|
||
Integration verified: Lane B streams and renders Lane A's 493-shop city correctly — facades, signs,
|
||
doors, ground, furniture, day/night all work. Collision holds for the arbitrary lot orientations
|
||
`generatePlan` emits (e.g. `ry = −1.32 rad`); see the collision fix below.
|
||
|
||
### Adaptive settings (index.html)
|
||
`BIG_CITY = plan.shops.length > 120`. Big city → **radius 2 + no sun-shadow pass**; fixture → radius
|
||
3 + shadows. Override with `?r=N` / `?shadows=0|1`. Dropping the shadow pass ≈ halves draw calls in
|
||
dense areas (the shadow map re-renders every caster) for little visual loss in a flat-lit low-poly
|
||
town — LANE_B doc explicitly allows "none".
|
||
|
||
### Draw-budget close-out (this session) — the gate now holds with margin
|
||
Fable's first integrated smoke test read **~334 draws at spawn** (that was radius 3, pre-adaptive) and
|
||
the deep districts appeared to peak ~445. Two things closed the gap:
|
||
|
||
1. **Facade texture atlas** (`skins.js` + `buildings.js`). Every facade skin packs into one 2048²
|
||
atlas (6×6 slots, each pre-painted with the skin's flat fallback colour, then the JPEG drawn over
|
||
it — house law preserved), so a chunk's facades merge into **one** mesh (was ~5–8 per-skin meshes)
|
||
and ~22 facade materials collapse to **1** (this also fixes the "≤25 materials was at the cap /
|
||
over for some seeds" concern — now **5** city-wide). UV convention mirrors the sign atlas.
|
||
2. **Instanced-mesh merges.** Awnings were one InstancedMesh *per skin* (red/green/blue) per chunk;
|
||
now **one per chunk** with per-instance colour (the stripe JPEG lived on the awning's unseen top
|
||
face, so flat colour reads the same under the verandah). Shells + parapets + posts merged into
|
||
**one** InstancedMesh (per-instance colour) instead of two.
|
||
|
||
Measured effect (worst dense frustum): **393 → 265 scene draws**; worst *continuous-walk* gameplay
|
||
view **~261 total**. Note the earlier "445" figures were partly a **measurement artifact** — teleport-
|
||
jumping between distant spots left orphan chunks the real streamer disposes; a proper walk never
|
||
exceeds the radius-2 set. Verified day + night, facades still textured, awning colours still varied.
|
||
|
||
## Architecture decisions worth knowing (for Lane F)
|
||
|
||
- **Ground is built once, not streamed.** The street graph is a handful of edges; merged flat quads
|
||
are a few draws total and can't seam. If Lane A ships a many-hundred-edge network, revisit.
|
||
- **Sky/lighting are global**, re-centred on the player each frame (dome radius 650, sun follows).
|
||
Linear fog at `(radius+0.4)·64 m` doubles as the chunk pop-in mask.
|
||
- **Per-chunk streaming = buildings + furniture only.** `ChunkManager` keeps chunks within Chebyshev
|
||
radius R, disposes past R+1, drains a nearest-first build queue under a 4 ms/frame budget. A single
|
||
dense chunk can exceed 4 ms (median 8), so it builds ~1/frame → an occasional ~8–22 ms build frame
|
||
entering a dense district (fog-masked). Splitting a chunk build across frames is the next step if
|
||
that hitch matters.
|
||
- **Night lighting is faked** (Vuntra-style): window + streetlamp **emissive** toggled by a night
|
||
flag, plus bloom — no per-streetlight real lights. `ChunkManager.setNight()` re-applies to chunks
|
||
streamed in while already night.
|
||
- **`scene.environment` is set** (round 3, `lighting.attachRenderer`): a neutral PMREM'd sky→ground
|
||
gradient (no asset). Lane D's rig-fleet peds bake their impostor atlas from `scene.environment`
|
||
(`sim.js`) and go dark if it's null; set here *before* `CitizenSim` constructs. One env for all
|
||
segments (the atlas bakes once; the mostly-non-metal streetscape is barely affected). Verified set
|
||
in street/night/interior + after interior-return (`interior_mode` only swaps `scene.background`).
|
||
- **Shared materials, city-wide** (`skins.js`). Facades share **one atlas material** (all skins in a
|
||
2048² atlas); shells + parapets + posts are **one** InstancedMesh/chunk (per-instance colour); all
|
||
awning/canopy skins are **one** InstancedMesh/chunk (per-instance colour). Signs are a per-chunk
|
||
canvas atlas sized to the chunk's actual sign count, mipmaps off. Net: ~1 draw per kind per chunk.
|
||
- **Orientation law** (obeyed by `buildings.toWorld`, the InstancedMesh shells, and `pushOutRect`):
|
||
a lot at `ry=0` faces `+Z`; `local→world = RotY(ry)`. Collider and geometry share this convention.
|
||
|
||
## Fixes applied after an adversarial multi-agent review (this session)
|
||
|
||
- **Collision for arbitrary `ry`** (`planutil.pushOutRect`): world→local used the wrong rotation sign;
|
||
correct only for axis-aligned `ry`. The fixture is axis-aligned so it passed, but Lane A's angled
|
||
lots would let you walk through walls. Fixed to the true inverse of `toWorld`. **(Critical for Lane A.)**
|
||
- **House doors** were merged into the interactive door mesh with no `doorRects` → raycasting a house
|
||
door mis-resolved to the nearest shop. Now a separate non-picked mesh.
|
||
- **Awning skin index** used a signed shift (`seed >> 3`) → negative index → undefined skin for ~⅓ of
|
||
shops. Now `>>> 3`.
|
||
- **Sign atlas** was a fixed 2048² (~16 MB canvas + ~22 MB GPU each) per shop-chunk regardless of
|
||
sign count. Now sized to the actual sign count, mipmaps off — critical at Lane A's density.
|
||
- **Furniture edge hashing** assumed string edge ids; `generatePlan` uses numeric ids → every edge
|
||
got the same seed. Coerced with `String(id)`.
|
||
- **Furniture drop:** `chunkIndex` rasterised only the edge centreline, so furniture at a perpendicular
|
||
offset could land in an unbuilt chunk and vanish. Rasterisation now covers the full road+verge band.
|
||
- **`takeShots()`** never restored the pre-shot mode (P in map mode dumped you into street).
|
||
|
||
Reviewers' non-issues (intentional): `getColliders` returns a shared scratch array (per-frame
|
||
alloc avoidance); per-frame `{clock,chunks}` HUD object (negligible).
|
||
|
||
## The `shot()` harness
|
||
|
||
Press **P** → renders 3 fixed cameras and downloads PNGs. Reference stills captured to
|
||
`docs/shots/laneB/` (main-street day, plaza overview, night strip). Note: at Lane A's city the fixed
|
||
camera poses are tuned for the fixture; re-aim if used against the generated city.
|
||
|
||
## `window.DBG` — the QA harness hook (this session, for Lane F's shots.py/soak.py)
|
||
|
||
Loaded only with **`?dbg=1`** (`js/world/dbg.js`, installed by the shell). Answers LANE_F_NOTES §4.
|
||
All poses are derived from the **live plan** so bookmarks work on any seed; the day cycle pauses on
|
||
the first scripted segment change for deterministic captures.
|
||
|
||
- `DBG.ready` — true once the first chunks are built + the queue is drained (textures may still decode).
|
||
- `DBG.shot(name)` — snaps to a named bookmark, sets its time-of-day, settles bloom, returns stats.
|
||
Bookmarks: `street_noon`, `arcade` (dept anchor), `market_square` (stalls), `milkbar_dusk`, `night_neon`.
|
||
- `DBG.teleport(x, z, ry)` / `DBG.setSegment(seg 0–5)` — drive the soak walk + time of day.
|
||
- `DBG.enterShop(shopId)` / `DBG.exitShop()` — scripted interior visits (drives Lane F's bridge →
|
||
Lane C `buildInterior`; verified end-to-end: `enterShop` → mode `interior`, `exitShop` → back to street).
|
||
- `DBG.info()` → `{ drawCalls, tris, fps, heapMB, geometries, textures, chunk, mode }` (budget/soak read this).
|
||
|
||
Every bookmark stays under budget (busiest, the dept anchor, ~269 draws). `fps` reads the HUD's
|
||
smoothed value (0 under a throttled/background tab; populates in a foreground Playwright run).
|
||
|
||
## `ChunkManager` lifecycle hooks (optional, LANE_F_NOTES §8)
|
||
|
||
`createChunkManager` now fires `ctx.onChunkBuilt(key, {cx, cz, buildings, furniture, data})` and
|
||
`ctx.onChunkDisposed(key, {cx, cz})` **if a consumer sets them on `ctx`** — a clean per-chunk seam
|
||
for spawning/ambient/LOD. Inert by default (the shell doesn't set them; **Lane D's citizens drive off
|
||
`plan.streets` instead of chunk lifecycle**, so this is a spare hook, not a dependency).
|
||
|
||
## WebAudio engine — `audio.js` (round 11, the audio round)
|
||
|
||
`web/js/world/audio.js` — one `AudioContext`, unlocked on the first user gesture. Self-contained: it
|
||
reads live state off `window.PROCITY` and self-ticks (its own rAF), so the shell only calls
|
||
`createAudioEngine(window.PROCITY, { noassets })` once (after `window.PROCITY` is assigned) and stores
|
||
it at `window.PROCITY.audio`. Consumes Lane E's `manifest.audio` (self-fetched, **3× retry** so a
|
||
dropped boot-burst fetch doesn't mute the session).
|
||
|
||
Layers, all crossfaded off systems that already exist (gains ramp; the update path allocates nothing):
|
||
- **Ambience** — `street-day` / `street-night` beds crossfade on `lighting.getClock().night`; **rain**
|
||
bed gain follows `PROCITY.weather.intensity` (silent unless it's actually raining, i.e. `?weather`).
|
||
- **Footsteps** — distance-accumulated from `player.position` while pointer-locked (a step per stride).
|
||
- **Shop-door spill** — nearest *open* music shop (record/milkbar/video/arcade/dept) within ~9 m leaks
|
||
its interior music bed at low distance-gain (scan throttled to 5 Hz over live door meshes; honours
|
||
`PROCITY.isOpen`). Verified: 6 m from "Video Barn" → `video-synth` at gain ~0.08.
|
||
- **Tram** — rumble gain by camera↔tram distance (`scene.getObjectByName('tram')`); bell rings once as
|
||
it settles at a nearby stop. Conditional on `?tram`.
|
||
- **SFX** — `procity:enterShop` → doorbell + door-open; `playSfx(key)` for the rest (till/riffle/toast).
|
||
|
||
**House audio law — all verified in-browser:** silent-and-happy (missing/failed audio → silence, no
|
||
errors); **nothing plays or fetches before the first gesture**; `?mute=1` → a silent surface (exists
|
||
for smokes, never unlocks, 0 fetches); `?noassets=1` → live but **0 audio/manifest fetches**; beds
|
||
lazy-load (≤25 MB pack, 8.7 MB shipped); spill/music selection is a pure function of shop type.
|
||
|
||
### Surface (`window.PROCITY.audio`, for Lane F's smokes + interior wiring)
|
||
- `setMasterGain(0..1)`, `mute()`, `unmute()`, `get ready`, `get muted`.
|
||
- `playSfx(key, {gain})`, `footstep(surface)`.
|
||
- **`playInterior({ musicKey, toneKey })` / `stopInterior()`** — Lane F calls these from
|
||
`interior_mode` on enter/exit, passing **Lane C's `room.audio` contract** (a `musicKey` into
|
||
`manifest.music`, a `toneKey` into `manifest.ambience`). The street beds auto-duck to 0 while
|
||
`mode !== 'street'`. Confirmed working: `playInterior` fades the room's music+tone in, `stopInterior`
|
||
fades them out. **→ Lane C: `room.audio = { musicKey, toneKey }` on `buildInterior` is the right shape.**
|
||
- `state` — `{ ready, muted, mode, manifest, loaded, layers, nearestSpill }` diagnostics for smokes.
|
||
|
||
## Venue / gigs — the pub from the street (round 12, v3.0-alpha, `?gigs=1`)
|
||
|
||
Two B-owned pieces for the one-pub vertical slice. **Prime flag law:** both are inert/absent without
|
||
the gig layer, so flags-off boot is byte-identical (goldens untouched — no citygen edits).
|
||
|
||
- **`web/js/world/venue.js`** (new, like weather.js/tram.js — the **shell/Lane F constructs it under
|
||
`?gigs`**). `createVenuePresentation(plan, skins, scene) → { group, frontage, update(gigState),
|
||
dispose, venueShopId, posterCount }`.
|
||
- **Posters** — renders E's `skins.poster.{grunge,retro,screenprint,xerox}` at every `plan.posters`
|
||
position `(x,z,ry)` with the gig's `bandName` overprinted in the skin's `nameZone` (canvas
|
||
composite, like a shop sign). Seeded template per poster. Flat printed-poster fallback if the JPEG
|
||
404s (house law). Verified: "THE WOMBATTS" over the band-photo art. *(Note to A: main-street poster
|
||
positions land mid-road on intersection nodes — a verge/pole offset would seat them better.)*
|
||
- **Lit frontage** — a warm marquee slab + emissive bulb row over the venue door; `update(gigState)`
|
||
ramps the glow (`'on'`→full, `'doors'`→warm-up, else dark). F drives it from the gig state machine.
|
||
- **Muffled-gig spill** (in `audio.js`, already shell-wired) — extends the R11 door-spill: near the
|
||
venue (≤26 m) while a gig is at `doors`/`on`, the `pubrock-live` bed plays through a **470 Hz
|
||
lowpass** (thump through the bricks) at low distance-gain. Gig state = `window.PROCITY.gigs` (Lane F)
|
||
with a clock+`plan.gigs` fallback so it works before F lands. Inert without the gig layer.
|
||
|
||
### For Lane F (wiring this into the shell)
|
||
```js
|
||
// under ?gigs (plan already has gigs/posters/venue via generatePlanFor(seed, src, {gigs:true, customBands})):
|
||
const venue = createVenuePresentation(plan, skins, scene); // posters + frontage
|
||
// in the street loop, drive the frontage from your state machine:
|
||
venue.update(PROCITY.gigs.state); // 'quiet'|'doors'|'on'|'done'
|
||
```
|
||
The muffled spill needs **no wiring** — `audio.js` reads `plan.gigs` + `window.PROCITY.gigs` itself.
|
||
Set `window.PROCITY.gigs = { state }` (or `{ on: bool }`) and B's spill + fallback both follow it.
|
||
|
||
**Stretch deferred:** streetlamp-pool night-readability on the venue block (touches shared furniture
|
||
rendering + wants a budget-safe scoped decal) — flagged as the next polish, not landed this round.
|
||
|
||
## Controls
|
||
WASD move · shift run · mouse look · click a door · `[` `]` step time-of-day · `T` pause clock ·
|
||
`M` map · `P` screenshot · Esc release pointer.
|