1337 lines
97 KiB
Markdown
1337 lines
97 KiB
Markdown
# LANE B — NOTES (measured)
|
||
|
||
## Round 30 (v7.0-alpha ledger #4) — THE COLLECTION UI + the SLEEP surface: **shipped, verified against F's REAL core (nothing stubbed)**
|
||
|
||
**The crate is on screen: cover thumb · title/artist · price paid · where + day found; count, day and
|
||
cash in the standing HUD; SLEEP saves and wakes tomorrow. All of it built on `PROCITY.game` exactly as
|
||
§30.1 publishes it — and by verify time none of it was a stub: F committed the save core (`207bcff`)
|
||
mid-round, and every game-layer file my test server carried is byte-identical to that HEAD (diffed,
|
||
not assumed). Lane C stubbed because F hadn't landed; B landed after F did — the wave order worked.**
|
||
|
||
### What ships (`web/js/world/hud.js` — my file, my DOM-overlay patterns; +8 lines in `venue.js`, F's §30.4 filing)
|
||
- **The game bar** (bottom-right): `day N · $cash` readout + `CRATE n` (the standing-HUD count, opens
|
||
the panel; `C` toggles it on the street) + `☾ SLEEP` → `game.sleep()` — enabled any time (alpha;
|
||
home-base gating is beta). The shell's `onDay` hook owns the wake toast; the bar just re-reads the
|
||
contract getters.
|
||
- **The crate panel**: newest find first; each row = cover thumb, title/artist (C's §30.1 fallback
|
||
chain title → sku → slotId), `paid $p · <where> · day d`. Header totals finds + $ spent. *Where* =
|
||
the shop's name off the plan when the entry's `townKey` is this boot's, else the town part of its
|
||
townKey (`redhill godverse`) — honest, never guessed.
|
||
- **Cover thumbs = the pack atlas, cropped in CSS.** The entry's `sku` resolves against its pack index
|
||
(per-shop `assets/stock_godverse/<godverseShopId>/` when set, else the town-wide `assets/models/`),
|
||
and the row's 44 px thumb is `background-image` + `background-size: 100/du% × 100/dv%` +
|
||
`background-position: u0/(1−du)` off the item's `uv` rect (origin top-left = CSS's origin, no flip —
|
||
the WebGL V-flip in stockpack.js is exactly the transform NOT to apply here). Zero canvas, zero
|
||
texture decode beyond the browser's own; B keeps its own tiny index-only fetch cache (`base|type`,
|
||
read-only GETs — can't fight stockpack's texture cache). Parody finds (slotId, no sku) wear a type
|
||
glyph by design (§30.4); so does any missing pack / unknown sku / remote (`depot:`/http) atlas.
|
||
- **Construction-gated like the R19 selector, but on the game itself:** the HUD builds NOTHING at
|
||
`createHUD` — a lazy check (every 30th frame) builds the surface on first sight of a truthy
|
||
`window.PROCITY.game` (the shell constructs the game after the HUD). `?classic=1` / `?game=0` ⇒
|
||
`game` is null forever ⇒ **none of this DOM, the KeyC listener, or the help-line suffix ever
|
||
exists** — the gate is the game object, not a re-derivation of the flags.
|
||
- **`venue.js` — F's §30.4 filing, fixed as filed:** the compat `venueShopId` ("tonight's venue") was
|
||
frozen at night-0's gig; now a live getter keyed to `PROCITY.gigs.weekNight` (0 pre-game/day-1 ⇒
|
||
exact pre-v7 value). No consumer of the field exists in-tree today (grepped), so the getter is pure
|
||
forward-correctness for whoever reads "tonight" next.
|
||
|
||
### Measured (fresh contexts, port-isolated no-store server :8151 — NOT :8130/:8144 — serving a code
|
||
### snapshot + assets symlink, so F's mid-flight edits couldn't shift under the test; snapshot later
|
||
### diffed clean against F's landed commits)
|
||
| check | result |
|
||
|---|---|
|
||
| `?classic=1` / `?game=0` (clean tab) | `game: null` · game DOM **0/5 ids** · help line byte-identical · **0 console errors** |
|
||
| default boot | bar lazily built, `day 1 · $191` (wallet-backed start), CRATE 0 |
|
||
| 3 finds via the REAL `game.recordFind` (godverse sku · town-wide sku · parody slotId) | CRATE 3 · head `3 finds · $92 spent` · thumbs from **both** pack bases (`stock_godverse/158/`, `models/`) — real cover art on screen (screenshotted), parody = ♪ · exactly 4 fetches (2 index + 2 atlas) |
|
||
| foreign-town entry via the REAL `game.import()` | row reads `paid $25 · redhill godverse · day 1` (townKey fallback) · missing sku ⇒ placeholder, 0 errors |
|
||
| sell seam (`game.removeFind`) | 4 → 3: bar + open panel both follow within one poll (≤6 frames) |
|
||
| **SLEEP** (button) | day 1→2 · weekNight 0→1 · wake DAWN 06:30 seg 0 · `procity-save/1` on disk with day 2 + 3 entries · shell toast `🌅 day 2` · cash untouched · **`venueShopId` re-keyed 116 → 490** (the §30.4 fix, proven behaviourally) |
|
||
| next boot (same profile) | the saved game LOADS and the bar/panel read it — persistence proven from the UI side too |
|
||
| `?noassets` + a real-sku find | **0** stock/atlas fetches (asset law) · placeholder stands · 0 errors |
|
||
| cost | 50-row full rebuild **0.9 ms** · first 3-row open 2.1 ms · DOM-only = 0 draws (C proved the class; unchanged) |
|
||
|
||
**Honest notes:** (1) the four `THREE.PointerLockControls` console errors seen mid-drive are the
|
||
automation browser refusing the Pointer Lock API on my splash click — reproduced click-free at zero,
|
||
absent on every clean-tab boot; environment, not code. (2) F's splash ("walk into town") was new in
|
||
the tree mid-round and sat over my first screenshot — noted, F's surface, untouched. (3) Beta polish,
|
||
not done: foreign-town rows could carry real display names via E's towns index (the selector already
|
||
fetches it) — one map at build time, when the money-shot pass comes.
|
||
|
||
---
|
||
|
||
## Round 28 (v5.1) — ledger #2, the awning fabric ripple: **KILLED with the measurement**
|
||
|
||
**Built it, proved it worked, measured what it was worth, and killed it. A 10×-over-driven billow —
|
||
1.4 m of flap on a 0.14 m canopy — cannot move 1% of the frame. It is not the gain; it is the
|
||
geometry. The backlog item is closed permanently, and the reason names what *would* work.**
|
||
|
||
R17 shipped tree sway and parked this with a physical reason: a posted verandah is a structural roof
|
||
and does not flutter. R28 asked for the half R17 left — the stalls' striped canopies really are cloth,
|
||
and they shared the rigid awning InstancedMesh, so neither could move without the other. That part was
|
||
all true, and the split worked. The cloth just turned out to be invisible.
|
||
|
||
### It was built, and it worked
|
||
Split the canopies out of the rigid mesh (own `InstancedMesh`, own subdivided box — a 4-corner box has
|
||
nothing to ripple), gave only them a cloth billow riding R17's existing `wind.js` uniforms: a
|
||
`sin(πu)·sin(πv)` pin (zero at the four corner posts by construction, peak mid-panel), two crossed
|
||
travelling waves, per-instance phase off world position so forty stalls don't breathe in unison.
|
||
Proven by **rendered-pixel hashes**, not by eye:
|
||
| proof | result |
|
||
|---|---|
|
||
| the cloth actually displaces (calm vs gusty) | `40f59eaf` → `d6d3e1f1` — **moves** |
|
||
| returning to calm reproduces the calm frame | `40f59eaf` — **identical** |
|
||
| `uWindTime = 99` at amp 0 | `40f59eaf` — **identical** (amp 0 ⇒ `+= …*0.0`, the R17 pattern) |
|
||
| **`?classic=1`, same fixed pose, pre- vs post-change build** | **`453c0154` → `453c0154` — byte-identical** |
|
||
| rigid verandahs / rest of town ride the wind? | cloth+trees hidden: `76c8b518` → `76c8b518` — **still** |
|
||
| …and the control proves I hid the right thing | cloth restored: frame **moves** again |
|
||
Cost at `market_square`, true A/B: **+0 draws** (98 → 98) / **+2,160 tris**. +0, not the +1 I'd written
|
||
in my own comment — the square's chunks hold *only* stalls, so the rigid list empties, `makeInstanced`
|
||
returns null, and the cloth mesh replaces it rather than adding to it.
|
||
|
||
### And it is worth nothing — the sweep (amp is the same multiplier as gain)
|
||
| peak billow | frame changed |
|
||
|---|---|
|
||
| **0.02 m — CALM, the default weather amp** | **0.05%** |
|
||
| **0.14 m — STORM, the most any player ever sees** | **0.21%** |
|
||
| 0.35 m (2.5× over-driven) | 0.33% |
|
||
| 0.70 m (5×, absurd on a 0.14 m slab) | 0.49% |
|
||
| **1.40 m (10×, a canopy flapping a body-length)** | **0.77%** |
|
||
At the player's actual browsing pose, storm-force: **0.00%, max channel delta 1**. Not one pixel.
|
||
|
||
**The reason is physical, and it is the same physics that killed the verandah in R17.** A canopy is a
|
||
horizontal sheet at y 2.2 viewed by a 1.7 m player: you see it edge-on or from underneath, so a
|
||
*vertical* displacement runs almost parallel to the view ray and produces almost no pixel change. You
|
||
cannot see a flat horizontal sheet flex vertically while standing under it. No gain fixes that — 10×
|
||
buys 0.77%, and anything that large stops reading as cloth and starts reading as a trampoline.
|
||
|
||
**What would work, if wind in the market is ever wanted (for whoever reads this in round 35):** the
|
||
motion has to be *perpendicular* to the view, which means **vertical cloth** — hanging stall skirts, a
|
||
flag, a banner, an awning valance. That is an **asset** (Lane E), not a shader. B's `applyWind` /
|
||
`applyFabricWind` seam is ready for it the day such a mesh exists; the shader was never the missing
|
||
piece. **Ledger #2 leaves the backlog forever.**
|
||
|
||
### Kept from the round
|
||
Nothing shipped: `buildings.js` and `wind.js` are byte-identical to HEAD. Shipping an invisible ripple
|
||
would have cost 2,160 tris and a second draw path in the covenant town to move 0.05% of a calm frame.
|
||
*The R17 rule was "ships or dies". It died — and it died with numbers, which is the only way an item
|
||
this old stops coming back.*
|
||
|
||
**Two vacuous measurements caught on the way (the epoch's own disease, still catching me):** (1) a
|
||
`!!material.onBeforeCompile` check to prove the rigid verandah was untouched — every `THREE.Material`
|
||
inherits a prototype no-op, so it is **always** truthy; the assert could never go red, and I replaced it
|
||
with `hasOwnProperty` plus a behavioural hide-and-hash control. (2) a gain sweep that reported the
|
||
identical 0.21% at gains 0.4→4.0 because I never stored the shader ref, so `uFabricGain` never moved —
|
||
**four identical numbers across a 10× range is a sweep that proves nothing**, and it was the constancy,
|
||
not the value, that gave it away. Both would have passed as green.
|
||
|
||
---
|
||
|
||
## Round 27 wave 4 — `poseForShopFront` shipped · **and every real town's shops face away from the street**
|
||
|
||
**The micro-task is done and verified. In doing it I found why F photographed empty road: it is not
|
||
the pose. Every shopfront in every OSM town has faced away from its street since R18, and the tours
|
||
have been photographing the backs of buildings for nine rounds. Root cause is a CITY_SPEC that
|
||
contradicts itself. The fix is one line in A's `plan_osm.js` — proven, then reverted, because it
|
||
isn't mine and it needs a ruling.**
|
||
|
||
### 1. The deliverable — `DBG.poseForShopFront(sel, dist = 8)`
|
||
Poses at a NAMED SHOP's frontage: door centred, sign band + R26 crate mark in shot, under the verandah.
|
||
`clusterPose()` answers "where is the town's HEART", which is the wrong question for a named shop —
|
||
Red Hill's heart doesn't contain Monster Robot, which is exactly how F got three frames of empty road.
|
||
|
||
Keyed through **one shop selector**: F's R22/R24 resolver lived inside `enterShop()`, so I extracted it
|
||
rather than grow a second key scheme beside it — a selector bug resolves the wrong shop *and looks like
|
||
it worked* (F's own first cut proved that), so the id-namespace law now has exactly one home.
|
||
Verified live, and this is the case that matters:
|
||
| call | resolves to | |
|
||
|---|---|---|
|
||
| `poseForShopFront(31)` | **Wholefood Cafe** (plan id 31) | + `ambiguous:` "31 is also the godverseShopId of Silky Oak Furnature — use 'g:31'" |
|
||
| `poseForShopFront('g:31')` | **Silky Oak Furnature** (godverse id 31) | the prefix IS the fence |
|
||
| `poseForShopFront('g:999999')` | `{error}` | never throws, never guesses |
|
||
|
||
Framing proven by **projection**, not by eye — `doorCentreNdcX = 0.0000` (exactly), door / sign / mark
|
||
all inside the frustum. Derived from buildings.js rather than hardcoded: door is centred on the lot
|
||
front (so `off = 0`, unlike `poseForShop`'s `off = 1.0`); sign band is `min(h,FACADE_H)−0.35 ± 0.25` ⇒
|
||
**y 2.40–2.90**; the mark is **y 2.45–2.52**; `camY = 2.0` sits below the `AWNING_Y = 2.95` verandah, so
|
||
a ray rising to a target ≤ 2.9 can never cross it — the sign is clear *by geometry*, at any distance.
|
||
|
||
### 2. Correcting my own R27 intel to F — I gave F two wrong numbers
|
||
My R27 note said the mark "rides high (y ≈ 3.3)" and the vinyl landmark blocks only 4% of it. **Both
|
||
wrong.** The mark is at y 2.45–2.52; I had ray-scanned y 3.0–3.6, which is empty awning air *above the
|
||
sign* — that is why it read ~0. Re-measured on the true band: the `novelty_record` disc (top y **2.50**)
|
||
reaches **into** the mark band and clips **~15% of the strip from every offset** — moving the camera only
|
||
changes *which* 15%, so there is nothing to dodge. The recommendation (**shoot head-on**) survives, but
|
||
for the opposite reason to the one I gave: not because the vinyl is clear of the mark, but because it
|
||
never isn't, and the repeating spines read through it anyway.
|
||
|
||
### 3. THE FINDING — real-town shopfronts face the paddock, not the street
|
||
Standing on Musgrave Road (Monster Robot's own `frontEdge: 1316`) and looking at the shop returns a
|
||
**blank grey back wall**: no sign, no door, no windows, no mark. Measured, not eyeballed:
|
||
| town | shop lots w/ frontEdge | facing their street | **180° backwards** |
|
||
|---|---|---|---|
|
||
| `redhill_godverse` | 35 | **0** | **35** (dot = −1.000) |
|
||
| `katoomba_real` | 72 | **0** | **72** (dot = −1.000) |
|
||
| synthetic (default) | — | — | none — looks right, always has |
|
||
|
||
dot = **−1.000 exactly**, every lot, both towns: a convention, not a defect. Katoomba's `street_noon` —
|
||
the bookmark the release tours have shot for nine rounds — is **46 draws / 19,680 tris of bare road**.
|
||
|
||
**Root cause: CITY_SPEC contradicts itself, and the two generators each implement one half.**
|
||
- **CITY_SPEC:71** (the lots contract): *"ry: … a GLB modelled facing −Z ends up with its facade's OUTWARD
|
||
normal pointing at frontEdge. **World facing = (−sin ry, −cos ry)**"* → **−Z is the facade.**
|
||
- **CITY_SPEC:335** (posters, R15): *"seated on the **+Z street facade** (**B's canon**: buildings.js facade +
|
||
venue.js door/queue/camera are all +Z; `lot + (sin ry, cos ry)·(d/2+ε)`)"* → **+Z is the facade.**
|
||
|
||
`plan_osm.js:443` obeys the first (`ry = atan2(nx, nz)`, its comment says *"facade (−Z) faces road"*).
|
||
`plan.js` (synthetic), `buildings.js`, `venue.js` and the R15 posters obey the second. Synthetic — the
|
||
town every golden and every one of us tests — follows B's canon and looks correct, which is precisely
|
||
why nine rounds of real-town work never caught it. **This is the epoch's own signature species: a rule
|
||
meeting a case written after it.** The +Z canon is the de-facto standard (v1→v5, every synthetic golden,
|
||
posters R15 ledger #1); `plan_osm.js` is the lone dissenter.
|
||
|
||
**Proven, then reverted.** One line in A's file — `ry = Math.atan2(-nx, -nz)` — flips katoomba from
|
||
**0/72 facing → 72/72 facing**, and turns B's `poseForShopFront` camera from **26 m out in a paddock into
|
||
4 m from Musgrave Road**: the money shot F specified, shopfront + mark + kerb + a citizen at the door.
|
||
I did not commit it. It is A's file, it moves every real-town plan (⇒ real-town goldens re-pin under
|
||
amendment law), and a spec that contradicts itself is Fable's to rule on, not mine to pick a side of.
|
||
|
||
**This also re-reads my own R22 diagnosis.** I recorded "chunk-buildings **348 tris** (0.2%)" in katoomba's
|
||
busiest venue block and credited the atlas/instancing work. 348 tris is ~29 boxes: that was not efficiency,
|
||
it was the buildings being **absent from the frame**, and I read the evidence as a win. The number was
|
||
right; my reading of it was wrong, for five rounds.
|
||
|
||
Files: `web/js/world/dbg.js` (the helper + the extracted selector). No other lane's file touched.
|
||
|
||
---
|
||
|
||
## Round 27 (on-call) — the R26 handshake **verified against G's real manifest**; intel for F's #5 money shot
|
||
|
||
R27 owes B nothing (*"B/D: notes current; nothing else owed"*, *"B/D on-call"*). This is on-call work: F's
|
||
tour (#5) is specified as *Monster Robot Party's frontage, **B's mark visible*** — a claim my R26 verification
|
||
could not actually test, because G's manifest didn't exist yet and I proved the treatment against a **temp
|
||
manifest I scaffolded and deleted**. G has since shipped the real one (`36d10ca`). Claim now measured, not
|
||
assumed.
|
||
|
||
**Monster Robot Party is in `redhill_godverse`, not `newtown_godverse`** — the only town B ever tested the
|
||
mark in. F's money shot is posed in a town this treatment had never been verified against. It works.
|
||
|
||
### The R26 handshake held, unmodified
|
||
G's real emission is `{version:1, shops:[{godverseShopId, types, sourcing}]}` — 15 entries, lands inside B's
|
||
tolerant reader (`.shops`, objects carrying `godverseShopId`) with **zero coordination**, exactly as the R26
|
||
note promised. **The `tier` → `sourcing` rename (Fable's waves-1-2 ruling) did not touch B**: the mark
|
||
deliberately never reads that field, so a rename of it cannot break the footpath. *The "don't leak tier"
|
||
call paid a dividend it wasn't designed for — a cue that reads no field cannot break when the field moves.*
|
||
|
||
### Both arms proven live — the discriminating arm for the first time
|
||
R26's temp manifest marked **all 18** keyed shops, so "manifest is the authority, not the key" was never
|
||
actually exercised. G's real manifest makes it load-bearing: redhill has **9 keyed, only 4 stocked**.
|
||
Measured at Monster Robot's frontage (live chunk set), ground truth vs render:
|
||
| shop | keyed | in manifest | rendered |
|
||
|---|---|---|---|
|
||
| **Monster Robot Party #3962749** (`real`) | ✓ | ✓ | **marked** ✓ |
|
||
| Silky Oak Furnature #31 | ✓ | ✗ | **bare** ✓ |
|
||
| Thrift Shop and Eternity Boutique #908 | ✓ | ✗ | **bare** ✓ |
|
||
|
||
Two keyed shops stand metres from a stocked one wearing no mark, because they have no crate. **The street
|
||
advertises exactly the crates that exist.** (#31 is the same shop as Fable's R25 id-namespace bug — its
|
||
godverse id collides with a plan id; the mark reads `godverseShopId` only, so it is unaffected.)
|
||
|
||
Frontage: **135 draws / 74,354 tris**, 0 page errors, manifest prefetched at boot.
|
||
|
||
### Intel for F's #5 (no action needed from B)
|
||
The **`novelty_record` landmark** (B's furniture — a giant 0x1a1a1a vinyl disc on a post, marking record
|
||
shops) stands 2.5 m off Monster Robot's frontage and is **prominent in the head-on frame**. Measured
|
||
occlusion by ray-grid, so F can frame without guessing:
|
||
| band | head-on (8 m) | off-axis (+4 m) |
|
||
|---|---|---|
|
||
| **mark + sign** | **4%** blocked | 0% |
|
||
| window glass | 22% blocked | **44%** blocked |
|
||
|
||
**Shoot head-on.** The vinyl sits low (y≈1.3) and the mark rides high (y≈3.3), so it never meaningfully
|
||
occludes the mark; stepping off-axis *doubles* the window occlusion instead of helping. A giant vinyl record
|
||
in the frame of a record-shop money shot is a feature, not a blemish. *(I first read the black disc as a
|
||
render bug — it isn't; it's the landmark doing its job. Recorded so the next reader doesn't re-open it.)*
|
||
|
||
No code changed this round; notes-only.
|
||
|
||
---
|
||
|
||
## Round 26 (Fable's ROUND26 → Lane B) — #5 the polish pick: **the stocked shopfront reads from the footpath**
|
||
|
||
**Shipped. A stocked godverse shop now tells you it's diggable before you reach the door — a crate of
|
||
spines under the sign, and a warm-lamp-over-crates room behind the glass. Cost: +0 draws / +0 tris.**
|
||
|
||
Pick rationale: of the three offered (signage/window treatment · awning ripple · tram-stop tri diet), this
|
||
one is the beta's own theme — EVERY CRATE DIFFERENT is worth nothing if the street doesn't say which shops
|
||
*have* a crate. (The tri diet is likely moot after E's R22 decimation; the ripple is a vibe with no beta
|
||
dependency. Both stay on the list.)
|
||
|
||
### The authority question — the manifest, not the key
|
||
The mark keys off **`stock_godverse/index.json` membership**, deliberately *not* off `godverseShopId`.
|
||
Keyed only means "this shop has a stock identity"; the manifest means "a crate is actually here". Marking
|
||
keyed-but-unstocked shops would advertise a crate the shop hasn't got — **the street would lie, the one
|
||
thing this epoch keeps refusing to do**. This is ledger #6's manifest (G emits, F consumes) doing a second
|
||
job for free: it is already the truth of who carries an atlas.
|
||
|
||
The mark **does not leak tier**. "You can dig here" is equally true of `real` and `mint`; provenance
|
||
belongs on C's price card and in E's gates, not shouted from the awning.
|
||
|
||
### What it is (both halves free)
|
||
| cue | how | cost |
|
||
|---|---|---|
|
||
| **crate mark** — a row of seeded spines under the shop name | painted into the chunk's **existing** sign-atlas cell (`drawSign` → `drawCrateMark`), 7-colour palette, `shop.seed` LCG so a shop's crate is stable | **0 draws / 0 tris** (same 2048² canvas, same 1 draw/chunk) |
|
||
| **window cue** — warm lamp over crates, not a generic 90s wall | rides the **existing** per-shop `aTint` attribute (`roomTint` → `STOCKED_ROOM_TINT`), `?winmap` parallax path | **0 draws / 0 tris** (no new attribute, no new material) |
|
||
|
||
### Measured (newtown_godverse, 18 keyed = 18 stocked under a temp manifest, since deleted)
|
||
**True A/B at one pose — manifest 200 vs manifest 404:**
|
||
| | control (404) | treatment (200) |
|
||
|---|---|---|
|
||
| spine-marked sign cells | **0** | **3** (18,768 exact-palette px) |
|
||
| stocked window tint verts | **0** | **36** |
|
||
| `street_noon` | 65 draws / 115,502 tris | **65 draws / 115,502 tris** |
|
||
|
||
**+0 draws / +0 tris** — with the treatment *proven on*, not merely assumed. Stocked shopfront (Goulds
|
||
Books, footpath pose): **85 draws / 144,730 tris**, inside street law (≤300 / ≤200k).
|
||
|
||
Consistency check against ground truth: 3 stocked shops in the live chunk set (Dorrigo Hotel, Old Wares,
|
||
Goulds Books) → **3 marked cells, 9 stocked window quads** (3 shops × ~2.6 windows). Cues agree.
|
||
|
||
### Covenants (re-proved — the prefetch is new code on the boot path)
|
||
| boot | keyed | `stock_godverse` fetches | errors |
|
||
|---|---|---|---|
|
||
| `?classic=1` (zero-fetch covenant) | 0 | **0** | 0 |
|
||
| default synthetic (`?dbg=1`) | 0 | **0** | 0 |
|
||
| `newtown_godverse`, manifest 404 | 18 | 1 (404, eaten) | **0** |
|
||
|
||
Tested with the manifest **present on disk** — the harshest form: a stray fetch would have returned 200
|
||
and been unmissable.
|
||
|
||
### Three traps this round (all self-caught, all recorded)
|
||
1. **The module-load fetch would have broken the classic covenant.** First draft fetched the manifest at
|
||
module load → fires on *every* boot → a console 404 on the synthetic default and a **fetch delta to
|
||
`?classic=1`**, whose covenant is *zero fetch*. Restructured to lazy `ensureStockIndex()` +
|
||
plan-driven `prefetchStockIndex(plan)`, called from `createChunkManager` — **keyed plans only**.
|
||
2. **The lazy trigger alone raced the first chunk build.** With only the per-chunk trigger, boot showed
|
||
`manifestRequested: []` (spawn chunks had no keyed shops); the manifest only fetched after walking to a
|
||
keyed shop — meaning **the first stocked shop you approach would build unmarked**. Fixed by prefetching
|
||
at chunk-manager construction, before any chunk builds. Verified: manifest now fetched at boot.
|
||
3. **A vacuous "zero cost" and a junk detector.** First measurement read +0/+0 — because the manifest was
|
||
never fetched (stale `chunks.js` + a cached 404), so *nothing had been applied*. Numbers identical
|
||
because the feature was off. Then the pixel detector reported **60 "marked" cells with no treatment at
|
||
all** — it was counting ordinary busy sign cells. Replaced with an exact-palette signature (the spine
|
||
colours no registry sign uses): control **0**, treatment **3**. *Vacuous-gate law applies to a lane's
|
||
own verification, not just to gates — a green that can't go red proves nothing.*
|
||
Corollary recorded: **`force-cache` serves a deleted file** (`transferSize: 0, status: 200` while disk
|
||
404s) — an A/B against disk state is not an A/B. Poison the cache (`cache:'reload'`) or you are testing
|
||
your own memory.
|
||
|
||
### Handshake for G / F / C (manifest shape)
|
||
`buildings.js` reads the manifest **fail-soft and shape-tolerantly**: a root array, or `.shops` /
|
||
`.entries` / `.atlases`, of bare ids or of objects carrying `godverseShopId` | `id`. Any other shape, a
|
||
404, or `?noassets` ⇒ **no treatment, no error, no console noise**. G's real emission (ledger #6) needs no
|
||
coordination with B — if it lands in any of those shapes, the footpath lights up on its own. **B consumes
|
||
`index.json` read-only and emits nothing into `stock_godverse/**`** — the temp manifest used to prove this
|
||
round was deleted; the directory is G's.
|
||
|
||
Files: `web/js/world/buildings.js` (the two cues + the lazy fail-soft reader), `web/js/world/chunks.js`
|
||
(one line: the keyed-plan prefetch).
|
||
|
||
---
|
||
|
||
## Round 22 (Fable's ROUND22 → Lane B) — #1a the `venue_night` tri diagnosis → **OWNER: LANE E**
|
||
|
||
**Verdict: the driver is the four GLB street-furniture props. 149,161 of 163,015 tris = 91.5% of the
|
||
stress view. Not posters, not buildings, not pools, not the density widening per se. Owner = E (asset
|
||
tris) per the ledger's own rule. Decimation clears the gate with ~120k to spare.**
|
||
|
||
Method: katoomba `venue_night`, settled, then **hide-a-layer-and-re-measure** (frustum-accurate
|
||
attribution — a layer's cost is the tris that vanish when it's hidden), then the same drill *inside*
|
||
furniture, bucketed by geometry.
|
||
|
||
### Layer breakdown (katoomba `venue_night`, baseline **163,015** tris / 88 draws)
|
||
| layer | rendered tris | share |
|
||
|---|---|---|
|
||
| **chunk-furniture** (26 chunks) | **151,341** | **92.8%** |
|
||
| ground | 9,932 | 6.1% |
|
||
| venue-presentation (frontage+posters+pools) | 770 | 0.5% |
|
||
| lighting (sky dome) | 624 | 0.4% |
|
||
| **chunk-buildings** (26 chunks) | **348** | 0.2% |
|
||
| venue-posters | 222 | 0.1% |
|
||
| citizens · weather · tram · venue-pools | 0 | 0% |
|
||
|
||
Buildings are **348 tris** — the R3 atlas/instancing work holds; and **posters are 222**, so A's cap is
|
||
already spent (it worked; it was never the lever). Pools cost **0**.
|
||
|
||
### Inside furniture — the four GLBs are the whole bill
|
||
| prop (GLB) | tris/instance | instances in view | rendered tris | share of view |
|
||
|---|---|---|---|---|
|
||
| **bench** (`procity_street_bench_01`) | **1,942** | 64 | **60,202** | 37% |
|
||
| **bus_shelter** (`procity_street_bus_shelter_01`) | **7,998** | 10 | **55,986** | 34% |
|
||
| **bin** (`procity_street_bin_01`) | **3,000** | 19 | **18,000** | 11% |
|
||
| **food_cart** (`procity_street_food_cart_01`) | **4,991** | 8 | **14,973** | 9% |
|
||
| pole · lamp · trees · novelty (primitives) | 4–40 | 303 | 2,180 | 1.3% |
|
||
|
||
**GLB total 149,161 (91.5%). Primitives 2,180 (1.3%).** Identity confirmed by DCC-authored material names
|
||
(`wire_143225087` — a Sketchfab scan, `baked_mat`, `2 wood frame `, `Material_0`) — these are E's four
|
||
furniture GLBs, not our procedural materials. Two (bench, food_cart) are also `MeshPhysicalMaterial`, an
|
||
expensive shader for a 0.4 m street prop.
|
||
|
||
**Why now, and why it isn't the density widening:** shop count doesn't drive this — *road* density does.
|
||
Real towns put a bench/bin/shelter cadence on a 966-edge graph, so ~100 GLB props land in the streamed set
|
||
where synthetic had a handful. The props were always over-tessellated (a **1.2 m bench at 1,942 tris** is
|
||
~5× the entire building stock in frame); real roads just finally presented the bill. **This is my own R6
|
||
note coming due** — filed then: *"the street_bin GLB is 3.0k tris — heavy for a prop placed ~15×; a decimate
|
||
to ~500 tris would give back tri headroom if the combined-lane budget tightens."* It tightened.
|
||
|
||
### → Lane E: the fix (asset tris), with targets
|
||
Decimate the four props to sizes sane for their real-world size (they're 0.4–2.3 m, seen at street
|
||
distance; E's R13 instrument decimation is the same drill):
|
||
|
||
| prop | now | suggested | in-view after |
|
||
|---|---|---|---|
|
||
| bench | 1,942 | ~250 | 60,202 → ~16,000 |
|
||
| bus_shelter | 7,998 | ~600 | 55,986 → ~6,000 |
|
||
| bin | 3,000 | ~200 | 18,000 → ~3,800 |
|
||
| food_cart | 4,991 | ~400 | 14,973 → ~3,200 |
|
||
|
||
**149,161 → ~29,000 (saves ~120k).** Projected: my baseline **163k → ~43k**; **F's 221,935 → ~102k** —
|
||
both far under the 200k gate, with headroom for the pack's denser towns. (Cheap extra: drop the two
|
||
`MeshPhysicalMaterial`s to `MeshStandardMaterial`.)
|
||
|
||
### → Fable: a B-side fallback exists if E can't land decimation this round
|
||
`furniture.js` already ships a **primitive fallback for every GLB prop** (the R4 "use-if-ready" path, the
|
||
`?noassets` route). So a distance/density-gated LOD — keep GLBs near the camera, primitives beyond — is a
|
||
contained Lane B change I can land quickly. I'm **not** doing it unilaterally: per the ledger this is E's
|
||
(asset tris), and source decimation is the right fix (it helps every town and every view, not just this
|
||
one). Say the word if you want the B-side gate as belt-and-braces.
|
||
|
||
## Round 22 — #3 per-town bookmarks (Lane G's filed bug) — FIXED
|
||
|
||
**Root cause:** every "nearest" resolver in `dbg.js` sorted by distance from the **world origin**. That's
|
||
the town centre only on a *synthetic* plan. A real OSM town is projected about its cache's bbox centre
|
||
while its shops cluster hundreds of metres away — so bookmarks teleported into arbitrary far-flung
|
||
geometry, exactly as G reported. Measured origin-pick distance from the retail heart: **katoomba 408 m ·
|
||
newtown 626 m · fremantle 371 m**.
|
||
|
||
**Fix:** resolve every bookmark about the town's **densest shop cluster** (grid-bin the shop lots at 120 m,
|
||
heaviest bin's centroid = the retail heart) instead of the origin — `byDist`, `lotsByDist`,
|
||
`streetViewPose`, `stopPose`, and `crossroadsPose`'s tie-break.
|
||
|
||
**Gated on the REAL-ROADS signal (`edges > 200`)** — the same primitive the R20 tram fence uses — so
|
||
**synthetic and the marched fixtures keep their exact origin-centred behaviour** and F's release-tour
|
||
control shot cannot move. This gate is *load-bearing, not cosmetic*: synthetic's anchor is only 117 m out
|
||
but it **does** re-pick (Anglibarn Op Shop → Dusty Pages), so an unconditional switch would have silently
|
||
moved the control. Same reason `crossroadsPose`'s new tie-break is gated — a degree tie would otherwise
|
||
resolve differently on synthetic.
|
||
|
||
| town | edges | ref used |
|
||
|---|---|---|
|
||
| synthetic · melbourne · katoomba-fixture · silverton | 22 · 15 · 15 · 1 | **origin — unchanged** ✅ |
|
||
| **newtown_godverse** (G's town) | 1,626 | anchor (98, −629) ✅ |
|
||
| katoomba_real | 993 | anchor (−227, −625) ✅ |
|
||
|
||
**Result on katoomba** — shopfront bookmarks went from ~408 m adrift to **≤42 m** of the retail heart:
|
||
`arcade` 7 m · `milkbar_dusk`/`patronage_door`/`shopfront_detail` 24 m · `market_square` 26 m · `dig_real`
|
||
42 m; strip views (`street_noon`, `katoomba_main`, `district_posters`) ~195 m — on the high street;
|
||
`venue_night`/`queue_night` ~345 m (that's A's venue placement, not the bookmark). All 19 resolve, 0
|
||
errors, synthetic 19/19 unchanged.
|
||
- `warehouse_fringe` sits **3,074 m** out — **correct by design**: it's the `far:true` fringe shot, and on
|
||
a 5 km town the fringe genuinely is 3 km away. Left alone deliberately.
|
||
|
||
**→ Lane F (ledger #4, the shared posing primitive):** published on the harness as
|
||
**`DBG.townAnchor`** (`{x, z, n, anchored, ref}` — the retail heart + whether it's in use) and
|
||
**`DBG.clusterPose(kinds)`** (stand on the road at the densest cluster, look along it). That's your
|
||
cluster-pose finding as a `dbg.js`-consumable — the tour and the bookmarks now resolve through the same
|
||
primitive, so a tour shot and its bookmark can't drift apart.
|
||
|
||
### Honest note on the baseline (mine 163,015 vs F's 221,935)
|
||
Same view, different number: my automation's rAF is throttled, so the **live gig crowd never spawns**
|
||
(citizens measured **0** tris here) — F's number includes it (~59k of peds). This doesn't move the verdict:
|
||
furniture is 92.8% of what I measured and still ~68% of F's 221,935, and the decimation above clears the
|
||
gate on either baseline. Flagging it so F re-measures against the live crowd, not my throttled figure.
|
||
|
||
---
|
||
|
||
## Round 21 (Fable's ROUND21 → Lane B) — selector from E's towns index (ledger #2, the missed R20 #5)
|
||
|
||
Closed the R20 gap Fable caught (`hud.js:11` still hardcoded `REAL_TOWNS`). The real-town list now comes
|
||
from **E's `web/assets/towns/index.json`** (`procity-towns-index/1`), so it grows with the town pack instead
|
||
of being edited in `hud.js`. Verified fresh Chromium. Nothing else touched — the tram/ground work is done.
|
||
|
||
### How it works (fail-soft by construction)
|
||
`createHUD` is synchronous, so the picker is **built immediately from the fallback literal** and then
|
||
**upgraded in place** when the index resolves. That ordering *is* the fail-soft: if the index never arrives,
|
||
the working list is already on screen.
|
||
- **Index present** → real names, states and shop counts: `Katoomba, NSW · 80 shops`, `Fremantle, WA · 80
|
||
shops`, `Newtown, NSW · 72 shops`, `Bendigo, VIC · 36 shops`, `Castlemaine, VIC · 24 shops`. (`roads:false`
|
||
towns would read `· marched`; none today.)
|
||
- **`?noassets`** → the fetch is **skipped entirely** (asset law — the shell doesn't load town caches there
|
||
either, so reading the index would be pointless as well as illegal). Measured: **0 index fetches**,
|
||
fallback list, 0 errors.
|
||
- **Anything wrong with the index** → the fallback simply stands. Exercised the exact chain against every
|
||
nasty case; **none throws, none rebuilds from junk**:
|
||
|
||
| index case | result |
|
||
|---|---|
|
||
| 404 / missing · network error · malformed JSON | fallback stands ✅ |
|
||
| `towns: []` · `towns` not an array · junk entries (no `key`) | fallback stands ✅ |
|
||
| good index | rebuilds from index ✅ |
|
||
|
||
`REAL_TOWNS_FALLBACK` is retained **only** as that fallback (comment says so) — it is no longer the source
|
||
of truth. Verified live: booting `katoomba_real` shows `Katoomba, NSW · 80 shops` pre-selected; the picker
|
||
fits (193 px of its 230 px max); 0 page errors; navigation unchanged (`onchange` is set once on the
|
||
`<select>`, so rebuilding the options never clears it).
|
||
|
||
**→ Lane F:** unchanged from R19 — **no shell seam**, the selector is entirely `hud.js` (already built by
|
||
`createHUD`), and it lives inside `#pc-hud` so `setVisible(false)` hides it for the beta money shot. Your
|
||
R19 gating call (always-in-HUD vs `?dbg`) is still yours; one line either way.
|
||
|
||
---
|
||
|
||
## Round 20 (Fable's ROUND20 → Lane B) — the tram ruling + ground polish (ledger #3/#4, v4.0-beta)
|
||
|
||
### 1. The tram ruling (ledger #3) — route by SHOP ADJACENCY, fence per-town
|
||
`tram.js` `spinePolyline` now walks from each dead-end of the main subgraph, **greedily taking the
|
||
unused main edge that fronts the most shops** (`lot.frontEdge`), and keeps the best-scoring path (ties →
|
||
longer). Component logic isn't needed — a walk can't leave its component.
|
||
|
||
**Synthetic + fixtures are byte-identical, by construction and by measurement.** A synthetic main node has
|
||
≤2 mains so there's never a choice, and its two dead-ends tie exactly ⇒ strict `>` keeps the pre-R20 start
|
||
⇒ the same route. Verified by running the old and new logic against the same plan: **identical polylines**
|
||
(Boolarra Heads: 7 points, 6 edges, 810 m, 172 shops fronted, 2 stops — unchanged).
|
||
|
||
| town | shops | shops fronting mains | OLD route | NEW route | stops | tram |
|
||
|---|---|---|---|---|---|---|
|
||
| synthetic | 493 | 172 | 172 | **172** (810 m) | 2 | RUNS (unchanged) |
|
||
| fixture melbourne / katoomba | 95 / 19 | 0 / 0 | 0 | 0 (378 m) | 3 | RUNS (unchanged) |
|
||
| fixture silverton | 12 | — (0 mains) | — | — | 0 | no-op (unchanged) |
|
||
| **REAL newtown** | 64 | 32 | **0** | **8** (1924 m) | 16 | **RUNS** |
|
||
| **REAL castlemaine** | 22 | 11 | 1 | **6** (2184 m) | 10 | **RUNS** |
|
||
| REAL katoomba | 69 | 7 | **0** | 3 (3784 m) | — | **FENCED** |
|
||
| REAL fremantle | 79 | 4 | **0** | 2 (3054 m) | — | **FENCED** |
|
||
| REAL bendigo | 35 | 7 | **0** | 1 (4376 m) | — | **FENCED** |
|
||
|
||
The alpha tram fronted **0 shops on 4 of 5 real towns** — it really was a highway bus. Shop-adjacency
|
||
improves every town, but on katoomba/fremantle/bendigo the best main chain still fronts <5 shops, so per
|
||
Fable's fallback the tram is **fenced per-town**.
|
||
|
||
**The fence keys off the REAL-ROADS signal (`edges > 200`), never shop count alone** — because the marched
|
||
fixtures legitimately front **0 shops on their mains** (the marched spine is bare; the avenues carry the
|
||
shops), so a naive shop-count fence would have killed their v2 tram. Synthetic (22 edges) and fixtures (15)
|
||
are an order of magnitude under the threshold and can never be fenced. Threshold: `shopsFronted < 5`.
|
||
|
||
### 2. Bug found + fixed en route: phantom tram stops on real graphs
|
||
The stop projection mapped **every shelter in the town** onto the route with no distance gate — harmless on
|
||
synthetic (2 shelters, both on the spine), nonsense on a real graph: **Newtown produced 149 stops** (~9 min
|
||
of pure dwell at points the tram never passes). Added a **30 m** gate (a shelter sits `halfRoad+1.7` ≈ ≤16 m
|
||
off its own centreline, so every genuine stop survives): **newtown 149 → 16, castlemaine 37 → 10**;
|
||
synthetic **2** and fixtures **3** unchanged.
|
||
|
||
### 3. Ground polish at real intersections (ledger #4) — measured, NO fix needed
|
||
Real Katoomba: **325 degree-3+ junctions, 28 under 30°, worst 4.3°** (near-parallel). Viewed at grazing eye
|
||
level (the worst case for z-fighting) the worst junction reads **clean — no z-fight, no gaps**: continuous
|
||
tarmac, correct footpath/kerb. Why it holds: the layers are already y-separated (road `0.0` / footpath
|
||
`0.02` / kerb `0.06`) so nothing co-plane-fights across layers, and road-on-road overlap at junctions is
|
||
**invisible by design** — `OVERLAP = 1.5` makes strips overrun their nodes on purpose, and they share one
|
||
skin, one y and one merged draw, so a UV mismatch can't read on uniform asphalt.
|
||
- *Cosmetic only (v4.0 candidate, not a defect):* at ultra-acute (<10°) junctions the footpath/kerb strips
|
||
of the two near-parallel roads double up on the sliver between them. Reads busy, not broken.
|
||
|
||
### 4. Selector polish (ledger #5) — BLOCKED on E
|
||
E's `web/assets/towns/index.json` hasn't landed (`build_towns.py` still WIP), so `REAL_TOWNS` stays
|
||
hardcoded this round. It's a one-line swap to `fetch('assets/towns/index.json')` when E lands.
|
||
|
||
**→ Lane F (B→F tram verdict + selector):** tram **runs** on synthetic, all fixtures, **newtown** and
|
||
**castlemaine**; **fenced** on **katoomba, fremantle, bendigo** (`routeInfo.fenced` + `reason` are exposed
|
||
for your smoke). Selector gating (always-on HUD vs `?dbg`) is your call — one line either way.
|
||
|
||
**→ Lane A (finding, with measurements):** only a small slice of shops front a **main** edge on real towns —
|
||
katoomba **7/69**, fremantle **4/79**, bendigo **7/35**, newtown 32/64, castlemaine 11/22. The real high
|
||
street is coming out classified **`side`**, which is *why* the tram must be fenced on three towns. If your
|
||
main/side classifier promoted shop-dense ways to `main`, the fence would lift and the tram would find the
|
||
actual high street. Worth a look beyond the tram: **`onMain` also gates `gigs.js pickVenues` pub placement**.
|
||
|
||
---
|
||
|
||
## Round 19 (Fable's ROUND19 → Lane B) — the town selector (ledger #4, beta pull-forward, non-blocking)
|
||
|
||
Shipped the in-game town picker; verified fresh Chromium. **No `index.html` seam** — it lives in `hud.js`,
|
||
which already owns its DOM overlay.
|
||
|
||
### `hud.js` town selector
|
||
A compact `<select>` in the HUD overlay (top-left, styled to match, `pointer-events:auto` so it's usable
|
||
when the pointer is free — Esc first). **9 options**: `▸ Synthetic — procedural` + the 3 OSM fixtures
|
||
(`osmTownKeys()` minus `_real`: Melbourne/Katoomba/Silverton) + the 5 real-road caches (Katoomba, Newtown,
|
||
Fremantle, Bendigo, Castlemaine — hardcoded `REAL_TOWNS`, since there's no towns manifest yet; beta derives
|
||
it from an index). Selecting navigates: synthetic → clears `plansrc`/`town`; a town → `?plansrc=osm&town=<key>`,
|
||
**preserving all other params** (seed, flags). Current town pre-selected; an unknown current town is added
|
||
so it still shows.
|
||
- **Verified end-to-end**: picked `katoomba_real` through the dropdown → it navigated + booted real Katoomba
|
||
(966 edges / 323 intersections / 3 venues), selector then reads "Katoomba · real roads". 0 page errors.
|
||
- Lives **inside `#pc-hud`**, so `hud.setVisible(false)` hides it for F's money shots (no extra work).
|
||
|
||
**→ Lane F (B→F "selector seam"): there is NO seam** — the selector is self-contained in `hud.js` (already
|
||
constructed by `createHUD`). Nothing to wire in `index.html`. If you want it gated (e.g. `?dbg` only) that's
|
||
a one-line `setVisible`/guard call, your choice; I left it always-in-HUD as a modest debug-grade feature.
|
||
|
||
### Corner-strips spot-check (R18 failure-item #3) — ground fills real intersections OK
|
||
Overhead look at a degree-4 real intersection (58 exist on the current graph): the merged road strips
|
||
**overlap-fill the junction** (carriageway continuous, lane markings + zebra crossing render, no gross gap).
|
||
Acceptable for the alpha; a finer acute-angle / z-fight pass is a beta detail. `ground.js` is road-agnostic
|
||
enough for real topology.
|
||
|
||
### Budget note → F (measured against A's UNCOMMITTED WIP — caveat, not a final finding)
|
||
F's `BIG_CITY` one-liner (`e6fcb7b`) is live and working — real Katoomba now streams big-city (radius 2 +
|
||
shadows off), chunks **50 → 26**. **But** against the current working tree the venue block is still **~237k
|
||
tris** at night — and that tree carries **A's uncommitted R19 finalization WIP** (`plan_osm.js` modified;
|
||
the graph grew 799 → 966 edges / 323 intersections = junction-joining in, but the **far-island CULL not yet
|
||
applied**). Fable's ruling calls the cull "half of B's tris problem", so **the ≤200k gate needs F's fix AND
|
||
A's committed cull together** — re-measure the busiest real block after A pins (that's the deferred
|
||
"tris post-cull" re-measure; it genuinely waits on A's commit, per the R18/C lesson that the dev server
|
||
serves other lanes' uncommitted edits). Not filing 237k as a defect — it's mid-finalization.
|
||
|
||
### The uncommitted `docs/shots/v4_alpha/katoomba_mainstreet.png`
|
||
Present in the tree since before this R19 session (not a deliberate save of mine — my audit screenshots are
|
||
browser-tool captures, uncommitted). Likely a prior B-audit capture. **F should reshoot deterministically
|
||
via the tour harness** for the official money shot regardless; treat its provenance as unverified.
|
||
|
||
---
|
||
|
||
## Round 18 (Fable's ROUND18 → Lane B) — irregular-geometry audit (ledger #4, v4.0-alpha REAL ROADS)
|
||
|
||
**The deliverable is the failure list.** Audited `katoomba_real` on E's real roads (schema v2, **484 OSM
|
||
ways → a 799-edge / 872-node graph, 126 real intersections** (99 deg-3, 27 deg-4), edges **3.5 m–1303 m
|
||
(28 short <12 m)**, span **~3945 × 4944 m**). E's roads cache landed mid-audit; verified fresh Chromium,
|
||
cache-busted (the shell force-caches town JSON).
|
||
|
||
### Verdict: Lane B's streetscape is ROAD-AGNOSTIC — it consumes the CityPlan graph generically and survives
|
||
Real Katoomba boots with **0 page errors**; every B system builds and renders on the irregular graph —
|
||
ground strips, furniture (39 chunks / 148 trees), town-wide pools, tram, chunk streaming (`chunks.count`
|
||
scales), buildings, wind. The real main street reads well (road + footpath + streetlamps + a working gig
|
||
poster + citizens; `docs/shots` — the Screaming Utes bill renders on real bones). Like C's interiors, the
|
||
streetscape needed **no code change** to eat real geometry. The real risks are elsewhere (below).
|
||
|
||
### FINDING → F (gate-blocking, proven one-line fix): the `BIG_CITY` adaptive heuristic misfires on real roads
|
||
`index.html` sets `BIG_CITY = plan.shops.length > 120` → radius + shadow-pass. Real-roads towns invert
|
||
the assumption: Katoomba has **20 shops (< 120)** but a **799-edge / 5 km** graph, so it stays small-town
|
||
mode (**radius 3 + sun-shadow ON**) and streams **50–52 chunks** → the busiest venue block hits
|
||
**~280k tris (clean load, settled) — BREACHES the 200k gate** (draws 150, still < 300).
|
||
**Proven fix** — boot with `?r=2&shadows=0` (= what BIG_CITY mode would give): venue block drops to
|
||
**84k tris day / 104k night, 20 chunks, 72 draws** — comfortably under. **Recommend**: extend the
|
||
heuristic to the graph, e.g. `BIG_CITY = plan.shops.length > 120 || (plan.streets?.edges?.length||0) > 200`
|
||
(catches Katoomba's 799; leaves synthetic Boolarra's 22 untouched). One line, index.html; the concept is
|
||
B's adaptive-streaming (LANE_B_NOTES R3) extended from shop-count to edge-count. This is the charter's
|
||
mega-strip/density risk in Lane B's budget.
|
||
|
||
### Tram (spine assumption — Fable's flagged risk): degrades gracefully, no crash — fence-ready, not fenced
|
||
`spinePolyline` walks one main chain from a degree-1 node. On real Katoomba (232 main edges) it runs a
|
||
**3527 m / 49-edge chain = 21% of mains** (the longest walkable chain — plausibly the real main road);
|
||
the other mains form other chains with no tram. **No crash**, deterministic, ≤2 draws as ever. Arbitrary-
|
||
but-functional for the alpha. A smarter route (pick the shop-adjacent chain) or a hard fence (tram off on
|
||
real-roads towns) is a **v4.0-beta** call — no clean `plan.mode` marker is exposed to key a fence off, so
|
||
I did **not** add untested fencing this round (alpha philosophy: surface + document). Fence-ready via the
|
||
same `edges.length > 200` signal if beta wants it.
|
||
|
||
### Furniture / pools scale to real geometry (no break)
|
||
Town-wide pools = **~4989 instances** on Katoomba (all in the ONE InstancedMesh → still **~1 draw**;
|
||
built upfront, fogged beyond the streaming radius). Lamp cadence = ~4906 town-wide. **28 short edges
|
||
(<12 m) get 0 lamps** (the `for(s=8; s<len-4; …)` loop — graceful sparsity, not a crash). No fix needed.
|
||
|
||
### Correction — the "disconnected graph" I first saw was a SYNTHETIC-FIXTURE artifact, NOT real Katoomba
|
||
Stress-testing A's `selfcheck` roads fixture first (before E's real cache landed) produced a **fully
|
||
disconnected graph** (4 edges / 8 nodes / all deg-1 / 0 intersections) — Douglas–Peucker flattened its
|
||
near-collinear coordinates and dropped the junction vertices. **Real Katoomba does NOT have this**
|
||
(126 intersections form correctly). So it's a fixture-degeneracy note, not a real-town finding — but
|
||
worth flagging to A that DP can drop junctions on near-straight ways (charter risk #4, fidelity).
|
||
|
||
### For F's alpha gate (assertion list, all green on real Katoomba except the budget finding)
|
||
boots 0-error · graph has real intersections (deg ≥ 3 present) · my modules all build · budget **needs
|
||
the BIG_CITY fix** (else 280k > 200k at the venue block; with it, 104k) · tram no-crash · pools ~1 draw.
|
||
|
||
---
|
||
|
||
## Round 17 (Fable's ROUND17 → Lane B) — wind sway SHIPS (ledger #5, parked since R7)
|
||
|
||
**Verdict: SHIPPED, not killed.** The R7 park reason ("touches shared materials") was tested against
|
||
R16's measurement-over-brief standard and is a **non-blocker**: the gum-tree billboard material lives
|
||
in `furniture.js` (Lane B), so wind sway is **entirely Lane B — no C/E material seam**. New file
|
||
`web/js/world/wind.js` + two small hookups (`furniture.js` applies it, `weather.js` drives it).
|
||
|
||
### What ships
|
||
- **`wind.js`** — one shared uniform set (`uWindTime`, `uWindAmp`) + `applyWind(material)` (an
|
||
`onBeforeCompile` vertex patch: top-weighted horizontal sway, per-instance phase from world position
|
||
so neighbours desync, `#ifdef USE_INSTANCING`-guarded) + `updateWind(dt, intensity)`.
|
||
- **Trees only.** Gum-tree canopies sway. **Awnings are excluded — a *measured* call, not a punt:**
|
||
they're rigid posted-verandah box slabs (`buildings.js`), and a structural verandah roof doesn't
|
||
flutter. (Fabric-canopy ripple would be a separate v3.3 item — the striped canopies share the rigid
|
||
awning InstancedMesh today, so splitting them is its own task. Not needed for "wind sway ships.")
|
||
- **Weather-driven:** `updateWind` is called only from `weather.update(dt)` → base breeze on any
|
||
weather-on boot (`amp 0.05`, clear included) scaling to rain gusts (`0.05 + intensity·0.30 ≈ 0.35`).
|
||
|
||
### Byte-identical when weather is off (the covenant)
|
||
`updateWind` runs **only** inside `weather.update`, and the weather *system* is not constructed under
|
||
`?weather=0` or `?classic=1` — so `uWindAmp` stays at its init **0**, and the injected displacement is
|
||
`sin(...)·0.0·h == 0.0` → `transformed += 0.0` → identical vertices → identical pixels. Verified:
|
||
- `amp=0`, two different `uWindTime` values → **0 / 180,000 region pixels differ**.
|
||
- `?weather=0` boot → `uWindAmp 0`, advancing time → **0 tree-motion pixels**.
|
||
- `?classic=1` boot → `uWindAmp 0`, trees render, **0 console errors** (shader patched but inert).
|
||
- No plan change, no fetch, no rng (time-based, not seeded) — F's classic gate (`0x3fa36874` + fetch
|
||
delta) is untouched; the sway is pure render.
|
||
|
||
### Sway works + budget
|
||
- Realistic **rain amp 0.305**: advancing 0.8 s moves **~14,100 / 180,000** region pixels (clear
|
||
motion); exaggerated amp 2.5 moves 101k. Trees render normally with the shader (verified the
|
||
side-street verge — natural canopies, no glitch/explosion).
|
||
- **~0 draws** (a handful of ALU ops in the tree InstancedMesh's existing vertex program — no new
|
||
geometry, no new draw). **0 console errors** (GLSL compiles clean).
|
||
|
||
**→ Lane F (B→F handshake): SWAY SHIPPED.** It's in old frames now under the default boot (weather-on).
|
||
`?classic=1` / `?weather=0` are sway-free and byte-identical (proven above). README `?weather` note, if
|
||
you grow one, can mention "trees sway with the wind." No new asset (procedural), so E has nothing to do.
|
||
|
||
---
|
||
|
||
## 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.
|