John looked at R41's proof shots and named three faults. All three are fixed, at zero draw cost, and every lane corrected somebody's numbers on the way — including its own. E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief, quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library called colour was baked AO. So the fix was a palette imposed on colour already present, in place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints and origins provably did not move. (A first-pass bbox check ignored node rotations and "found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s office teal and went burgundy; the beer umbrella was white and went brewery green. THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench = 1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl, red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43, not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage). E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the R41 deny-list's failure class through a different door: an innocent name from another pipeline. Only rendering all 24 and LOOKING found it. Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran 65..99 bones; named characters carry facial/hair bones). THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo, Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real. B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then measured: the atlas canvases are already byte-identical across boots, the game ships NO webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600, ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks 2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default). THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached 0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed otherwise — the side term was already right, which is why it survived 41 rounds of reading. Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth. C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control. E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every booth part sat 25mm sunk into its own worktop. D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex is in the identity signature and the impostor atlas; removal would renumber the town). Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a fresh default boot fetches 32 ped GLBs, none retired. THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only 3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently dropping six tracks per clip since it shipped. Testing the thing being thrown away found it. Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s race against a live sim), not deterministically red. F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law (0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified; CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict. R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there — the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with everything else frozen was the last thing moving at 53-62 px. ?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed. Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647, 0x5f76e76 unmoved. No tag — John's playtest rules the epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2764 lines
198 KiB
Markdown
2764 lines
198 KiB
Markdown
# LANE B — NOTES (measured)
|
||
|
||
## Round 42 (§42.5) — THE BENCH TURNS TO FACE THE ROAD · `benchStops()` · and the font race that never existed
|
||
|
||
All numbers this section: today's tree, headless Chromium, **fresh contexts** (never a reused tab),
|
||
port-isolated **no-store** servers, 1280×720, seed 20261990. Budget cells use the **pin method of
|
||
record** (`budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the
|
||
main spine at yaw 0). ROUND42 **ruling 4 is binding** — the street is 291/300 at night, nine draws —
|
||
so every claim below is an **A/B against a control tree**: a HEAD copy of `web/` with `assets/`,
|
||
`models/`, `vendor/` **and every `js/` subtree that is not Lane B's** symlinked to the live tree, so
|
||
another lane's mid-round wave lands identically on both arms. (It did: the night tri count moved
|
||
+2,090 against R41's pin on **both** arms — E's in-flight §42.1/§42.3 work — and 0 between them.)
|
||
|
||
---
|
||
|
||
### §42.5-A — THE BENCH FACED THE WRONG WAY. So did the streetlight. (Lane D's filing, verified)
|
||
|
||
D filed it as "observed while mirroring, not fixed (your file)". **Verified before changing anything**,
|
||
by reading the InstancedMesh matrices out of a live scene and matching each instance to the edge whose
|
||
**perpendicular offset equals the placement rule** (position only — so the yaw verdict is not
|
||
circular). `unmatched: 0` on both arms.
|
||
|
||
| live scene, seed 20261990 | BEFORE (control tree) | AFTER (this round) |
|
||
|---|---|---|
|
||
| bench front (local +Z) vs the direction to the road | **90.0° — min = median = max, 60/60** | **0.0° — min = median = max, 60/60** |
|
||
| bench front vs the street's own direction | 0.0°, 60/60 | 90.0°, 60/60 |
|
||
| lamp head (local +Z, 1.05 m out) gains … toward the road | **0.00 m — 0 of 89 lamps reach over it** | **+1.05 m — 89 of 89** (the whole arm) |
|
||
| lamp arm vs the street's own direction | 0.0°, 89/89 | 90.0°, 89/89 |
|
||
|
||
**The code and its own comments had disagreed for 41 rounds.** `furniture.js:203` said *"facing the
|
||
road"*, `:197` said *"arm reaches over the road"*, and both used `yaw = atan2(ux, uz)` — the yaw that
|
||
points local +Z **down the street**. D is right on both counts.
|
||
|
||
**The fix is one named convention, not two more literals.** There are exactly two useful yaws on a
|
||
street edge and the file only ever had one:
|
||
|
||
```
|
||
alongYaw = atan2( ux, uz) → local +Z runs DOWN THE STREET (what bench + lamp used)
|
||
faceYaw = atan2(-uz, ux) → local +Z runs ACROSS it, along +perp (= alongYaw − π/2)
|
||
```
|
||
|
||
`perp` is `(−uz, ux)`; an item at offset `side · off` on it has the road at `−side · perp`, so the
|
||
road-facing yaw is `faceYaw + (side > 0 ? π : 0)`. **The `side` term was already there and already
|
||
correct** — the base was wrong, which is exactly why the defect survived reading. The shelter's
|
||
`atan2(-uz, ux)` was already `faceYaw`; it is left byte-identical (see the filing below).
|
||
|
||
**THE DEPOT BENCH IS A DIFFERENT BENCH, and it was 90° out from the primitive.** This box reaches the
|
||
CDN, so a default boot draws **9 of its 60 benches from `procity_street_bench_01.glb`** and 51 from
|
||
the primitive. Measured off the GLB's own vertex data: it is **0.437 (X) × 0.845 (Y) × 1.215 (Z)** and
|
||
**all 318 of its backrest verts (y > 0.55) lie in a slab at x ∈ [0.151, 0.219]** — its plank runs
|
||
along **Z** and its sitter faces **−X**, where the house law is "+Z is the front". Every GLB bench in
|
||
the town therefore stood square to every primitive one. Fixed by rotating the **geometry** once at
|
||
load (`GLB_FIX_YAW.bench = +π/2`, maps local −X → +Z), **not** the instance — so a station's yaw means
|
||
the same thing whichever geometry is drawn, `benchStops()` stays one truth, and D's sitters are right
|
||
on both. Confirmed in the scene: the bench-shaped instanced mesh goes from a 1.216 m Z-extent to a
|
||
1.216 m X-extent, same 949 verts, same 9 instances, **0 draws / 0 tris**.
|
||
|
||
**Proof shots** — same bench station (`edge 3, s = 14, side −1`), same camera pose, both arms, R10
|
||
human-sized line off the sitter's own bone bbox:
|
||
`docs/shots/laneB_r42/bench_seated_{before,after,after_dwired}.jpg`, plus the crowd-frozen geometry
|
||
pair `bench_geometry_ab.jpg` and the night `streetlight_night_ab.jpg` (130 draws on both arms).
|
||
|
||
> **→ LANE D, THIS IS A BREAKING CHANGE FOR YOUR MIRROR, AND IT IS WHY THE EXPORT EXISTS.**
|
||
> `sim.js benchStationsFor()` computes `yaw = atan2(ux, uz) + (side > 0 ? π : 0)`. That is now **90°
|
||
> wrong**. Positions are untouched (your 14/14 position arm stays green); your **yaw arm goes red**,
|
||
> and `_seatPose()` will put sitters up to 0.38 m off the plank and facing along the street — visible
|
||
> in `bench_seated_after.jpg`, which ships deliberately un-retouched. `bench_seated_after_dwired.jpg`
|
||
> is the same frame with the sitter re-seated off the **real bench transform**: that is what 42.4
|
||
> delivers. **Import `benchStops(plan)` in your wave and delete the mirror** (§42.5-B).
|
||
|
||
---
|
||
|
||
### §42.5-B — `benchStops(plan)` — the export that retires D's mirror
|
||
|
||
D asked for it "next to the existing `busShelterStops(plan)` (same shape, same reason it exists)".
|
||
Shipped, and the *rule itself* now lives in one function that **both** readers call, so the two cannot
|
||
drift: `benchStationsOnEdge(e)` is what `buildChunkFurniture` places from and what `benchStops(plan)`
|
||
publishes. A second copy inside the same file would have been the same defect at a shorter distance.
|
||
|
||
```js
|
||
import { benchStops } from '../world/furniture.js';
|
||
|
||
benchStops(plan) → [{ edgeId, kind, s, side, x, z, yaw, seatLen }]
|
||
```
|
||
|
||
Pure function of the plan — no THREE, no scene, no DOM, no side effects, safe in a worker or in Node.
|
||
Returns **every** station in the town (not just streamed ones), in `resolveEdges(plan)` order then
|
||
ascending `s`.
|
||
|
||
| field | type | meaning |
|
||
|---|---|---|
|
||
| `edgeId` | number | the plan edge id, in `resolveEdges(plan)` order |
|
||
| `kind` | `'main'\|'side'\|'lane'` | the edge's class |
|
||
| `s` | number, m | distance along the edge from its **A** node: 14, 40, 66, … step 26 |
|
||
| `side` | `+1 \| −1` | which footpath, on the **furniture** perpendicular `(−uz, ux)`. D's own lane perpendicular is `(uz, −ux)` = **minus** this, so D's own-footpath test stays `side === −forward`, unchanged |
|
||
| `x`, `z` | number, m | the bench origin = the instance translation. **Unchanged by R42** |
|
||
| `yaw` | number, rad | the instance's Y rotation. Local **+Z is the bench FRONT** and now points **at the road**. A rig front is local −Z after the R13 facing-normalise, so a sitter is still **`yaw + π`** |
|
||
| `seatLen` | number, m | **1.6** — the plank, so a caller's ±0.38 m two-up offset is not a second copy of B's geometry. (A depot-GLB bench is a shorter **1.216 m** plank; ±0.38 still fits.) |
|
||
|
||
**Gated, not asserted** — D's own arm-6 method, pointed at the export instead of the mirror:
|
||
**254 stations town-wide; 60 bench instances standing in the streamed window; 60/60 matched by a
|
||
station within 2 cm, and 60/60 with the yaw identical to < 1e-6 rad.** **CONTROL:** the same stations
|
||
offset 2 m match **0 of 65** on both counters — so "coincides" is a measurement, not a hit on a
|
||
neighbouring prop.
|
||
|
||
---
|
||
|
||
### §42.5-C — `await document.fonts.ready`: shipped, and it fixes NOTHING. Here is what does.
|
||
|
||
F's R41 ruling: no gate may pin a screenshot byte-hash while *"the sign atlas paints `bold Npx Arial`
|
||
before the font resolves"*. That diagnosis was mine originally (R41 §41.5-D) and F reproduced the
|
||
symptom independently. **The symptom is real. The cause is not the font.**
|
||
|
||
**1. The atlas is already deterministic.** Three boots per bookmark, hashing every live sign-atlas
|
||
canvas: **41–49 canvases per bookmark and the identical sha1 list on all three boots**, every
|
||
bookmark. At shot time `document.fonts.status === 'loaded'`, `check('bold 74px Arial, sans-serif')` is
|
||
true, and a fixed probe string measures **859.2744140625 px to the last digit on every boot**.
|
||
|
||
**2. The API cannot even see this game's text.** `document.fonts.size === **0**` at document start and
|
||
at `ready` — the CSS Font Loading API tracks **font faces**, and PROCITY ships none (house law: all
|
||
text is canvas, `Arial` is resolved by the platform). An await on `document.fonts.ready` is an await
|
||
on an empty set.
|
||
|
||
**3. And it already resolved long before the first sign was painted.** 6 boots (3 default, 3 classic),
|
||
instrumented from an init script that hooks `CanvasRenderingContext2D.fillText`:
|
||
|
||
| | `document.fonts.ready` | first `fillText` in the process | order |
|
||
|---|---|---|---|
|
||
| default × 3 | 42.5 / 49.3 / 50.1 ms | 102.3 / 109.2 / 105.1 ms | ready first by **+55 to +60 ms** |
|
||
| classic × 3 | 38.6 / 40.5 / 50.3 ms | 93.4 / 89.5 / 101.3 ms | ready first by **+49 to +55 ms** |
|
||
|
||
**6 of 6 boots.** There is no window in which a sign could be painted early.
|
||
|
||
**IT IS SHIPPED ANYWAY, at the only site where it is free** — a module-level `await` at the top of
|
||
`buildings.js`. Every importer (`chunks.js` → the shell → every `buildChunkBuildings` call) is
|
||
downstream of it, so no atlas can be painted early, and **`buildChunkBuildings` stays synchronous**:
|
||
the streamer's 4 ms/frame budget is untouched and no chunk build becomes async. **Measured added boot
|
||
latency, 6 boots per arm, control vs treatment:** first sign paint **102.28 ms → 102.62 ms (+0.34 ms
|
||
on the mean)** against a control arm whose own spread is **93.7–110.6 ms**; `DBG.ready` **3,822 →
|
||
3,802 ms**. It is inside the noise in both directions. It is kept because it costs nothing and because
|
||
it is the invariant a future webfont would silently break — not because it closed anything.
|
||
|
||
**WHAT ACTUALLY MOVES THE FRAME: THE CROWD.** The citizen sim advances on **wall-clock** time, so two
|
||
boots settled for the same 2.5 s are two different streets. Two fresh contexts, `?classic=1`,
|
||
`street_noon`, diffed pixel-for-pixel:
|
||
|
||
| | changed pixels of 921,600 | bounding box |
|
||
|---|---|---|
|
||
| as everyone has been booting it | **983** | rows **361–418** — the pedestrian band, nothing else |
|
||
| with the crowd genuinely out | **0** | *none* |
|
||
|
||
**AND THE FREEZE RECIPE EVERY LANE QUOTES DOES NOT FREEZE ANYTHING.** `?roster=v1&pop=0` boots
|
||
**140 citizens** (96 active in the window, measured), because `index.html:454` reads
|
||
`(parseInt(params.get('pop'), 10) || 140)` and **`0` is falsy**. Every "town frozen" measurement in
|
||
this project since the flag was added has had a live crowd in it.
|
||
|
||
**THE KNOB: `DBG.freeze()`** (`dbg.js`, `?dbg=1` only, no game state a player can reach). Two
|
||
mechanisms because the roster has two modes — `setPopulation(0)` empties the fixed roster, and hiding
|
||
`citizens.group` covers the chunk-streamed default whose active set is per-chunk density and ignores
|
||
`target` entirely. Returns `{ draws: {before, after}, peds: {active, hidden}, stillLive: {weather,
|
||
tram, magpie, washing, gigs} }` — the draw delta is the proof the crowd left the frame, and
|
||
`stillLive` names the other wall-clock movers so a harness asserts instead of assuming.
|
||
|
||
#### The 3-boot hash table — F's method, four arms
|
||
|
||
`?classic=1`, `dbg=1&roster=v1&pop=0&weather=0&tram=0&magpie=0&washing=0`, **a fresh context per
|
||
(bookmark, boot)**, 2.5 s settle, sha256 of the PNG.
|
||
|
||
| bookmark | BEFORE (control tree) | AFTER (fonts fix in) | AFTER + `DBG.freeze()` | BEFORE + the same freeze |
|
||
|---|---|---|---|---|
|
||
| `street_noon` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ |
|
||
| `shopfront_detail` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ |
|
||
| `crossroads_busy` | **3 of 3** | **3 of 3** | **1 of 3** ✅ | **1 of 3** ✅ |
|
||
| `market_square` | 1 of 3 | 1 of 3 | 1 of 3 ✅ | 1 of 3 ✅ |
|
||
| `night_neon` | 1 of 3 | 1 of 3 | 1 of 3 ✅ | 1 of 3 ✅ |
|
||
| **stable** | **2/5** | **2/5 — the fix changed nothing** | **5/5** | **5/5** |
|
||
|
||
The fourth column is the control that matters: the **untouched HEAD tree** also goes 5/5 under the
|
||
same freeze, so the thing that made the bookmarks reproducible is the freeze, not any R42 code.
|
||
|
||
**On `market_square`:** R41 has B calling it rock-solid and F calling it 3-of-3. On this box today it
|
||
is **stable in all four arms**. Both readings were honest; the bookmark simply depends on whether a
|
||
citizen happens to be in that frame.
|
||
|
||
> **→ LANE F, THE ANSWER TO YOUR QUESTION: YES — byte-hash screenshot goldens become possible, and
|
||
> the R41 ruling can be reversed, but not for the reason it was made.** The condition is not "the
|
||
> font await landed"; it is **"the frame contains nothing that runs on wall-clock time"**. A golden
|
||
> boot is `?classic=1&dbg=1&weather=0&tram=0&magpie=0&washing=0` **+ `DBG.freeze()` before the
|
||
> shot** — `?pop=0` is not a substitute and never was. All five bookmarks pin at 1 of 3 that way, on
|
||
> both trees. Two further asks, both in your files: **(a) fix `index.html:454`** (`Number.isFinite`,
|
||
> not `||`) so `?pop=0` means zero, and **(b)** if a gate pins a hash, pin it on a `DBG.freeze()`
|
||
> boot and assert `freeze().stillLive` is all-false in the same run, or the golden will rot the first
|
||
> time someone shoots without a flag.
|
||
|
||
---
|
||
|
||
### §42.5-D — THE BUDGET: nothing was spent (ruling 4)
|
||
|
||
Pin method of record, 802 stations, control tree vs treatment tree, same box, same run:
|
||
|
||
| synthetic, pin (802 stations, yaw 0, laps 2) | CONTROL | R42 | delta | law |
|
||
|---|---|---|---|---|
|
||
| default, noon (seg 2) | 282 / 100,346 | **282 / 100,346** | **0 / 0** | ≤300 ✅ |
|
||
| default, **NIGHT** (seg 5) | 291 / 122,183 | **291 / 122,183** | **0 / 0** | ≤300 ✅ — **the nine-draw margin is intact** |
|
||
| `?classic=1`, NIGHT | 269 / 90,580 | **269 / 90,580** | **0 / 0** | ≤300 ✅ **byte-exact to the R38/R40/R41 pin** |
|
||
| `?classic=1`, noon | 269 / 90,592 | **269 / 90,592** | **0 / 0** | ≤300 ✅ |
|
||
|
||
**0 console errors on all eight runs.** Night draws are still **291/300 — Lane B spent 0 of the 9.**
|
||
Structurally it cannot: a yaw is a rotation in an instance matrix that already existed, and the GLB
|
||
normalisation is one `rotateY` on a geometry that was already merged. The bookmark frames confirm it
|
||
frame-by-frame: `night_neon` **98 draws / 14,454 tris on both arms**, and the crowd-frozen bench pose
|
||
**130 draws on both arms**.
|
||
|
||
**On the night tri number:** R41 pinned 120,093 and today both arms read **122,183**. The +2,090 is on
|
||
the **control** too — it arrived with another lane's in-flight R42 work through the symlinked
|
||
`models/`/`assets/`. Recorded so nobody attributes it to this round.
|
||
|
||
### §42.5-E — CLASSIC, and the goldens
|
||
|
||
`furniture.js` and the new `buildings.js` await contain **no classic flag test at all**, so classic and
|
||
default are identical to each other by construction (the `ground.js` idiom) and the fix lands on both.
|
||
What must not move, did not:
|
||
|
||
| classic evidence, control vs treatment | result |
|
||
|---|---|
|
||
| fetch surface (blob: object URLs excluded — they carry a random UUID per boot) | **127 URLs on both arms · added `[]` · removed `[]`** |
|
||
| HUD DOM (`innerHTML` length : body children) | **2355 : 8 on both — identical** |
|
||
| help line · `#pc-creditlink` · `PROCITY.budget` | unchanged / absent / `{draws:300, tris:200000, radius:2, auto:2, diagnostic:false}` on both |
|
||
| `night_neon` frame | **98 draws / 14,454 tris on both — delta 0 / 0** |
|
||
| pin, NIGHT | **269 / 90,580 — byte-exact** |
|
||
| page errors · console errors | **0 · 0**, on `?dbg=1`, `?classic=1`, `?noassets=1`, `?roster=v1&pop=0` |
|
||
|
||
The only difference the probe finds anywhere is `DBG.freeze` existing — and `DBG` exists only under
|
||
`?dbg=1`, which is not a player boot.
|
||
|
||
`node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76`
|
||
UNMOVED**. No plan golden can move: furniture is placed from the plan and writes nothing back to it.
|
||
|
||
### §42.5-F — filed, not fixed (each with its measurement)
|
||
|
||
1. **→ F: `index.html:454` `?pop=0` is a no-op** (`|| 140`). Above.
|
||
2. **→ D: retire `benchStationsFor()` this wave.** Above. Your gate's yaw arm is red until you do.
|
||
3. **→ a future round: the bus shelter has the same defect the bench had.** `syaw = atan2(-uz, ux)`
|
||
is `faceYaw` **without** the `side` term, and the shelter always stands on `+perp`, so its open
|
||
front (+Z) reads **away** from the road on the primitive — exactly the bench's bug. **Deliberately
|
||
not touched:** `busShelterStops()`'s yaw is consumed by `tram.js`, and the shipped shelter is a
|
||
depot GLB whose own axis convention is **unmeasured**. Measure the GLB, then flip both together.
|
||
4. **→ E, an asset-contract note:** the depot bench is a **1.216 m** plank where B's primitive is
|
||
**1.6 m**, and it was authored front-to-−X. B normalises the rotation at its own load seam
|
||
(above), but a depot prop that disagrees with the house axis is a class of bug, not an instance —
|
||
the shelter is the next one waiting.
|
||
|
||
**Files:** `web/js/world/furniture.js` · `web/js/world/buildings.js` · `web/js/world/dbg.js` ·
|
||
`docs/shots/laneB_r42/` · these docs. **No git commands run** (lane treaty; Fable commits).
|
||
|
||
---
|
||
|
||
## Round 41 (§41.5) — THE CREDITS SURFACE, and A's D3: 81 venue lots stop wearing op-shop paint
|
||
|
||
All numbers this section: today's tree, headless Chromium, **fresh contexts**, port-isolated
|
||
**no-store** servers, 1280×720, seed 20261990. Budget cells use the **pin method of record**
|
||
(`budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the main
|
||
spine at yaw 0, the worst bearing). ROUND41 ruling 4 is binding — the street budget is spent
|
||
(291/300 at night, nine draws) — so both items are **zero-draw, measured as an A/B against a control
|
||
tree**, not asserted.
|
||
|
||
---
|
||
|
||
### §41.5-A — THE CREDITS SURFACE (`credits.js` + `assets/credits.json`)
|
||
|
||
PROCITY had **no attribution surface at all**, and two of the obligations were already live:
|
||
|
||
- **OpenStreetMap, ODbL 1.0.** `web/assets/towns/SOURCES.md` states the condition in its own words —
|
||
*"if a rendered/public build surfaces this data, keep the © OpenStreetMap contributors credit
|
||
visible"* — and 23 town caches ship. Nothing in the running game showed it.
|
||
- **three.js r175, MIT.** The notice lives in the vendored file header, which no player can reach.
|
||
|
||
And it is the gate in front of **4,472 more licence-clean motion clips** on the fleet that R41 could
|
||
not spend for exactly this reason (ruling 2): **100STYLE** (1,620 BVH, CC BY 4.0 — requires crediting
|
||
Mason et al. 2022), **CMU** (2,548 BVH — credit requested), **ACCAD** (304 BVH, CC BY 3.0 — requires
|
||
*"Open Motion Project by ACCAD/The Ohio State University"*). R42 can spend them by appending rows.
|
||
|
||
**Where it lives**
|
||
|
||
| file | role |
|
||
|---|---|
|
||
| `web/assets/credits.json` | **THE DATA FILE — every lane appends here.** Schema `procity-credits/1`. |
|
||
| `web/js/world/credits.js` | the panel. DOM only. No lane needs to touch it to be credited. |
|
||
| `web/js/world/hud.js` | `setCredits(credits, {osm})` — the link, bottom-left. Null-provider gated. |
|
||
| `web/index.html` | one `[Lane B R41]` dynamic-import block + one term in the `unlock` guard. |
|
||
|
||
**Reachable:** `F2`, or the HUD link at bottom-left. `Esc`, `F2`, the CLOSE button or a scrim click
|
||
closes it. Opening frees the pointer (the dig-card / sell-card / F8-note contract), which is why the
|
||
shell's `unlock` handler carries one more term — opening the credits inside a shop must not walk you
|
||
back out onto the street.
|
||
|
||
**THE ATTRIBUTION RIDES THE LINK.** On an OSM boot the link itself reads
|
||
`© OpenStreetMap contributors · credits (F2)`; on the synthetic town, where no OSM data is on screen,
|
||
claiming it would be the opposite of attribution, so it reads `credits & licences (F2)`. The shell
|
||
passes `osm: PLANSRC === 'osm'`; nothing is sniffed off the plan.
|
||
|
||
#### THE SCHEMA — `web/assets/credits.json`, `procity-credits/1` ⟨C · D · E · G: this is your bit⟩
|
||
|
||
Append an object to `entries`. **You never touch `credits.js`.** Order inside the file is preserved;
|
||
the panel only lifts `required: true` rows to the top.
|
||
|
||
```jsonc
|
||
{
|
||
"id": "one-hundred-style", // required · unique · stable kebab key
|
||
"name": "100STYLE", // required · what a player reads
|
||
"kind": "motion", // required · engine|data|motion|mesh|texture|audio|stock|tool|font
|
||
"licence": "CC BY 4.0", // required · the licence NAME, or the literal "unverified"
|
||
"attribution": "Mason et al. 2022, …", // required key · the EXACT text to show, or null if none is owed
|
||
"required": true, // required · true = a licence obligation, false = courtesy/none
|
||
"source": "…url or tool chain…", // required · where it came from
|
||
"used_for": "…what in the game…", // required · plain English, player-facing
|
||
"count": 1620, // required key · 0 if not a count
|
||
"files": ["models/clips/*.glb"], // required key · repo globs, [] if nothing ships
|
||
"evidence": "pipeline/AUDIT.md §…", // required · the in-repo file that RECORDS it, or "unverified"
|
||
"flag": "…", // OPTIONAL · an open licence question a human must answer
|
||
"lane": "E" // optional · who owns the row
|
||
}
|
||
```
|
||
|
||
**Field law, and why each one is the way it is:**
|
||
|
||
- **`licence: "unverified"` is a legal value and a guess is not.** Two rows ship unverified today
|
||
(below). A guessed licence is worse than an admitted gap: the gap gets closed, the guess gets cited.
|
||
- **`attribution` is the exact string, not a description of one.** The panel prints it verbatim inside
|
||
a box. `null` when nothing is owed — say so explicitly rather than omitting the key.
|
||
- **`required` is the licence question, not an editorial one.** `true` ⇒ the row sorts to the top and
|
||
the box is headed ATTRIBUTION REQUIRED. Mixamo and Rokoko are `false` **because their licences ask
|
||
for nothing** — they are recorded so nobody has to rediscover that. CC BY is always `true`.
|
||
- **`evidence` must name a file in this repo.** "E told me" is not evidence; `AUDIT.md §…` is.
|
||
- **`flag` is rendered in amber and counted in the footer.** Use it for a real open question (an
|
||
unrecorded source, art a human must clear). R25's lesson stands: *the print IS the mechanism* — a
|
||
flag that only exists in a lane note is a flag nobody sees.
|
||
|
||
Paste-ready when R42 spends the CC-BY banks:
|
||
|
||
```jsonc
|
||
{ "id": "accad-mocap", "name": "ACCAD Motion Capture Lab", "kind": "motion",
|
||
"licence": "CC BY 3.0",
|
||
"attribution": "Open Motion Project by ACCAD/The Ohio State University",
|
||
"required": true, "source": "https://accad.osu.edu/research/motion-lab/mocap-system-and-data",
|
||
"used_for": "…", "count": 304, "files": ["models/clips/*.glb"],
|
||
"evidence": "pipeline/AUDIT.md §R42", "lane": "E" }
|
||
```
|
||
|
||
#### What seeded it — 14 entries, read off this repo, nothing invented
|
||
|
||
| entry | licence | attribution required? |
|
||
|---|---|---|
|
||
| OpenStreetMap (23 town caches) | ODbL 1.0 | ✅ **YES** — © OpenStreetMap contributors |
|
||
| three.js r175 (vendored + 5 addons) | MIT | ✅ **YES** — © 2010–2025 three.js authors |
|
||
| Adobe Mixamo — `idle`/`walk`/`sit`/`look` | Mixamo royalty-free | no |
|
||
| Rokoko free mocap — the 4 dance clips | royalty-free incl. commercial | no |
|
||
| Citizen bodies, base roster (17+2) | **unverified** | **unknown — flagged** |
|
||
| Citizen bodies, the DJ five | on-device mesh + Mixamo auto-rig | no |
|
||
| Texture skins (103 in `assets/gen/`) | original, `flux_local` on-device | no |
|
||
| Sound and music (29 keyed, 34 files) | original, procedural synthesis | no |
|
||
| Fittings/furniture — house library (18) | **unverified** | **unknown — flagged** |
|
||
| Fittings/furniture — generated here (14) | original (CC0-equivalent) | no |
|
||
| Three props via fal-Hunyuan | original (~$0.64, the only outside spend) | no |
|
||
| GODVERSE stock art (15 atlases) | in-house only | **flagged — not cleared for release** |
|
||
| The generation stack (FLUX.2-klein-4B Apache-2.0, TRELLIS.2, RMBG-2.0) | see row | no |
|
||
| Typefaces | n/a — **no font files ship** (canvas text, house law) | no |
|
||
|
||
Sources: `web/assets/towns/SOURCES.md`, `pipeline/AUDIT.md` (R3 hero props · QUARANTINE + its blanket
|
||
caveat · R11/R12/R13 audio · R36 w1 · R38 §2/§3/§4/§6 · R39 §5/§6), `docs/LANES/LANE_E_NOTES.md`
|
||
(the by-source-zone provenance table), `web/assets/manifest.json`, the vendored three.js headers.
|
||
|
||
**→ LANE E, two findings the seeding turned up.** Both are recorded in the file as `unverified` +
|
||
`flag`, which is honest, but neither is *closed*:
|
||
|
||
1. **The base ped roster (19 GLBs) has no per-asset licence record anywhere in this repo.** They are
|
||
sha1 copies from 90sDJsim; `docs/CITY_SPEC.md` ("NPC contract") declares the zone as *"web-ok only
|
||
(Mixamo, CC0/CC-BY, CGTrader RF)"* — and **CC-BY and CGTrader RF can both carry an attribution
|
||
obligation**, so if one of the 19 is CC-BY the credit is owed *today* and unrecorded. These are the
|
||
most-seen assets in the game. AUDIT.md documents props in forensic detail and the people not at all.
|
||
2. **The 18 house-library fittings/furniture carry AUDIT.md's own blanket caveat** ("no explicit
|
||
licence/provenance record … before `publish.py` pushes to the shared CDN, record a one-line
|
||
source/licence per asset"). They have been published since R3. The caveat is 38 rounds old.
|
||
|
||
Neither is Lane B's to close, and neither blocks R41. Both now have a place to be closed *into*.
|
||
|
||
#### Cost — zero, by construction and by measurement
|
||
|
||
- **Zero draws, structurally.** Everything is DOM: no geometry, no material, no texture, no pass. The
|
||
renderer cannot see it (the R19 selector / R39 fog-map mechanism).
|
||
- **Zero boot fetches.** `credits.json` is fetched on **FIRST OPEN**, never at boot: a boot that never
|
||
opens the panel makes **0** requests for it (asserted). Opening it makes **exactly 1**.
|
||
- **Zero fetches under `?noassets=1`** — the asset law forbids it, so the panel says so honestly
|
||
rather than fetching anyway or shipping a second built-in copy of the data.
|
||
- **Classic-gated BY CONSTRUCTION, no flag test in either file.** The shell dynamic-imports
|
||
`credits.js` only when `!CLASSIC`; classic therefore fetches **neither `credits.js` nor
|
||
`credits.json`**, has **no `#pc-creditlink`**, **no overlay**, **no injected CSS**, **no `F2`
|
||
listener**, **no `PROCITY.credits`**, and an **unchanged help line** — all asserted on a live boot.
|
||
|
||
---
|
||
|
||
### §41.5-B — A's D3: the venue signage, re-measured at 81 and fixed
|
||
|
||
**The bite, re-measured on today's tree** (golden seed 20261990, gigs on, the whole shipped corpus =
|
||
synthetic + 3 osm fixtures + 23 town caches):
|
||
|
||
| | |
|
||
|---|---|
|
||
| towns | **27** |
|
||
| shops | **1,838** |
|
||
| venue lots (`pub` · `band_room` · `rsl`) | **81** — exactly **3 per town, in every town** |
|
||
| of those, falling through to op-shop signage | **81 of 81** |
|
||
| types missing from `SHOP_REGISTRY` | `band_room`, `pub`, `rsl` |
|
||
| `core/registry.js SHOP_TYPES` vs `fixture_plan.js SHOP_REGISTRY` | **12 vs 9** |
|
||
|
||
**A's R40 filing said 81; it reproduces to the digit.** The bite has drifted `:344` (A, R37) →
|
||
`:465` (A, R40) → **`:483`** on today's tree — the line moves, the defect didn't, which is why the fix
|
||
is a named resolver and not another literal at a line number.
|
||
|
||
**The fix — one resolver, explicit precedence, `buildings.js signStyleFor(type)`:**
|
||
|
||
1. `SHOP_REGISTRY[type]` — the **nine palettes Lane B has shipped since v1**, returned unchanged.
|
||
2. `SHOP_TYPES[type].sign` — **A's `core/registry.js`**, which has carried gilt (pub) / stencil
|
||
(band_room) / club (rsl) since R13, and is the source for **any type A adds next**.
|
||
3. `SHOP_REGISTRY.opshop` — a type in *neither* table. Unreachable for anything the generator emits.
|
||
|
||
**The venue treatment:** the sign frame is redrawn in the registry's `accent` and a fascia band runs
|
||
the top inside edge (a pub's painted head-board). `accent` exists **only** on registry-sourced
|
||
palettes, so this is **venue-gated by construction** — the nine cannot grow one by accident — and the
|
||
band sits at the TOP so it can never collide with the R26 crate mark at the sign's foot.
|
||
|
||
**What I deliberately did NOT do.** A's filing suggests `SHOP_REGISTRY` shrink to a fallback-only
|
||
table and all signage read A's palettes. That repaints the **whole town**: `record` would go from
|
||
muted teal `#1f2e2b` to hot-pink neon `#141428`/`#ff5470`, `stall` from dark to light `#e8e2d0`,
|
||
`pawn` from yellow-on-red to gold-on-black. That is an **art call for John**, not a bug fix, and R41 is
|
||
the round whose whole point is visible — landing it unasked inside a signage bug fix would put an
|
||
unrequested repaint into every tour frame F shoots. **Filed for the playtest session:** *unify the
|
||
nine sign palettes onto A's registry, yes or no?* One `signStyleFor` line either way.
|
||
`fixture_plan.js`'s header (the "registry.js hasn't landed yet" line A clocked at 25 rounds — it is
|
||
now 28) is rewritten to say what that table honestly is.
|
||
|
||
**Proof — a true A/B, control vs treatment, same poses, same frozen town:**
|
||
**`docs/shots/laneB_r41/venue_signage_ab.png`** is the contact sheet (crops are the *measured* diff
|
||
bounding box, not a hand-picked region); the full frames are
|
||
`{before,after}_{pub,band_room,rsl}_signage.png` + `_venue_night.png`. The control is a HEAD copy of
|
||
`web/` with `assets/`, `models/` and `vendor/` **symlinked to the live tree** (so another lane's
|
||
mid-round wave is identical on both arms), served on its own no-store port. The town is frozen on
|
||
both arms so the pair is strictly comparable.
|
||
|
||
| pair | changed pixels | where |
|
||
|---|---|---|
|
||
| pub — The Exchange Hotel | **1,088** (0.12% of frame) | **rows 336–365 only** |
|
||
| band_room — The Vinyl Shed | **9,645** (1.05%) | **rows 349–363 only** |
|
||
| rsl — Wangaratta RSL | **14,160** (1.54%) | **rows 336–365 only** |
|
||
|
||
**Every changed pixel in all three frames is inside the sign band. Nothing else in the town moves.**
|
||
|
||
**An honest note on the pub**, the venue F will photograph most: it is the *least* visible of the
|
||
three, because op-shop `#2c2820` and the registry's gilt `#3a2416` are near-neighbour dark browns and
|
||
cream `#f0e2c0` sits close to gilt `#f2d98a`. **The pub's read comes mostly from the `#c0392b` accent
|
||
frame** — which is a large part of why the venue treatment exists at all rather than a bare palette
|
||
swap. The band room (black + `#7ac74f` green) and the RSL (navy `#0f2d4a` + `#c9a227` gold) are
|
||
unmistakable at a glance.
|
||
|
||
**One measurement artefact found and fixed, worth recording:** the first A/B run showed a second diff
|
||
region at rows 670–707 on the RSL pair. It was **the v7 game bar** — `#pc-game` is built LAZILY on a
|
||
later frame, so a per-element `display:none` hide races it and one arm keeps the bar. Hiding via an
|
||
injected `!important` stylesheet at boot catches whatever is built afterwards. A hide that races the
|
||
thing it hides will quietly put a UI element into your "geometry changed" number.
|
||
|
||
---
|
||
|
||
### §41.5-C — THE BUDGET: nothing was spent (ruling 4)
|
||
|
||
**1. A/B against the HEAD control — the town FROZEN on both arms.** `roster=v1&pop=0` (no citizens) +
|
||
`weather=0&tram=0&magpie=0&washing=0`, because every one of those moves on **wall-clock** time
|
||
(R38's recorded trap) and a live town measured twice differs from itself. `gigs` stays ON — the venues
|
||
are the point. Station lists are computed from the plan on each arm and compared as JSON **before**
|
||
either sweep runs, so a drifted list fails the run instead of quietly voiding the comparison.
|
||
34 stations × 4 yaws = **136 samples per arm**:
|
||
|
||
| frozen town, 136 station×yaw samples/arm | HEAD worst | R41 worst | per-sample delta |
|
||
|---|---|---|---|
|
||
| synthetic, **NIGHT** (the budget's worst class) | 283 draws / 116,681 tris | **283 / 116,681** | **0 draws AND 0 tris on 136/136** |
|
||
| synthetic, noon | 273 / 96,396 | **273 / 96,396** | **0 and 0 on 136/136** |
|
||
| `katoomba_real`, NIGHT | 167 / 109,208 | **167 / 109,208** | **0 and 0 on 136/136** |
|
||
| `?classic=1`, NIGHT | 264 / 90,578 | **264 / 90,578** | **0 and 0 on 136/136** |
|
||
|
||
**544 of 544 samples identical in both counters, across four boot classes. 0 console errors on
|
||
either arm, every arm.**
|
||
|
||
**2. The pin method of record, on the treatment tree** — and it reproduces R40 exactly:
|
||
|
||
| synthetic, pin method (802 stations, yaw 0, laps 2) | R40 pin | R41 measured | law |
|
||
|---|---|---|---|
|
||
| default, noon (seg 2) | 282 | **282** / 100,346 tris | ≤300 ✅ |
|
||
| default, **NIGHT** (seg 5) | 291 | **291** / 120,093 tris | ≤300 ✅ — **the nine-draw margin is intact** |
|
||
| `?classic=1`, NIGHT | 269 / 90,580 | **269 / 90,580** | ≤300 ✅ **byte-exact** |
|
||
|
||
**Margin at night is still 9 draws. Lane B spent 0 of them.**
|
||
|
||
**3. The new import is free.** `buildings.js` now imports `SHOP_TYPES` from `core/registry.js` — which
|
||
`citygen/index.js → plan.js` already pulls on **every** boot including classic, so it adds **zero
|
||
fetches**: the classic fetch surface is **121 URLs on both arms, `added []`, `removed []`**.
|
||
|
||
### §41.5-D — CLASSIC PURITY, and a pre-existing non-determinism it exposed
|
||
|
||
| classic evidence | result |
|
||
|---|---|
|
||
| plan carries venue types | **0 of 493 shops** — the resolver's registry arm is *structurally* unreachable in classic |
|
||
| draws / tris, 136 station×yaw samples | **0 delta on 136/136**, both counters |
|
||
| pin, NIGHT | **269 / 90,580** — byte-exact to the R38/R40 pin |
|
||
| fetch surface | **121 URLs, added `[]`, removed `[]`** — `credits.js`/`credits.json` never requested |
|
||
| HUD DOM (`innerHTML` length : body children) | **2354:8 on both arms — IDENTICAL** |
|
||
| `#pc-creditlink` · `#pc-credits` · `#pc-credits-style` · `PROCITY.credits` · help-line text | **absent / absent / absent / absent / unchanged** |
|
||
|
||
**→ LANE F, a finding, not a defect of this round.** I also pixel-hashed five classic bookmark frames
|
||
(sha256 of the PNG) and only **2 of 5** matched — so I ran **HEAD against HEAD** as the noise floor,
|
||
and it is **the same 2 of 5**. `street_noon`, `shopfront_detail` and `crossroads_busy` are **not
|
||
byte-reproducible across boots at HEAD**, with citizens/weather/tram/washing/magpie all off and a
|
||
2.5 s settle; `night_neon` and `market_square` are rock-solid on every run. The likely mechanism is
|
||
the **sign atlas**: `buildings.js` paints `bold ${fs}px Arial` into a canvas while chunks build in a
|
||
race-dependent order, so a cell drawn before the font resolves gets different metrics. Consequence
|
||
for you: **a screenshot golden pinned on those three bookmarks cannot be a byte-hash** — it needs a
|
||
perceptual/threshold compare, or the atlas needs a font-ready await first. My treatment sits exactly
|
||
at the noise floor, which is why the draw/tri/DOM/fetch evidence above is the load-bearing part.
|
||
|
||
### §41.5-E — gates
|
||
|
||
- `node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76`
|
||
UNMOVED**. No plan golden can move: signage is a canvas texture and the credits surface is DOM —
|
||
neither writes a plan field.
|
||
- `tools/qa/r40_lane_b.py` → all assertions green (the R40 rulings still hold under R41: default
|
||
declares 300/not-diagnostic, classic's selector shows 27 options with the one named towns-fetch
|
||
exception, `?r=3` declares 420 and measures 386 ≤ 420 while still > 300).
|
||
- `tools/flags_check.py` → **GREEN, 0 fails / 0 warns.** No new `POST_V2_EXCEPTIONS` entry is needed:
|
||
that gate inspects the **classic** fetch surface, and classic requests neither credits file.
|
||
- The credits surface asserted live on four boot classes (default / classic / `?noassets=1` /
|
||
`plansrc=osm`): 14 entries render, 2 required-attribution boxes, 3 open-licence-question boxes, the
|
||
ODbL string on screen verbatim, `licence unverified` shown as such, exactly 1 `credits.json` fetch
|
||
and only on first open, **0 console errors on every arm**.
|
||
|
||
**Files:** new `web/assets/credits.json` · new `web/js/world/credits.js` · `web/js/world/hud.js` ·
|
||
`web/js/world/buildings.js` · `web/js/world/fixture_plan.js` · `web/index.html` (`[Lane B R41]`
|
||
marks) · `docs/shots/laneB_r41/` · these docs. **No git commands run** (lane treaty; Fable commits).
|
||
|
||
---
|
||
|
||
## Round 40 (§40.3 + routed §40.4) — the ?r= flag learns its own law, classic keeps its shop window, and the fog gets its signage
|
||
|
||
All numbers this section: today's tree, headless Chromium, fresh contexts, port-isolated no-store
|
||
servers, 1280×720, seed 20261990 unless a town is named. Budget cells use **the pin method of
|
||
record** (R37/R38: `budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802
|
||
stations down the main spine at yaw 0, the worst bearing).
|
||
|
||
### §40.3-1 — THE ?r=3 BREACH: re-measured 391, and the ruling is GATE, not shave
|
||
|
||
**The carried "307" was a stale bookmark, as the round suspected — the true number is 391.** The
|
||
R37-era 307/317 pin predates five rounds of default-on content; nobody had re-measured the override
|
||
since. On today's tree, same method as the budget of record:
|
||
|
||
| synthetic, pin method | noon (seg 2) | NIGHT (seg 5) | live chunks max | tris worst |
|
||
|---|---|---|---|---|
|
||
| default (`r` auto = 2) | **282** | **291** | 32 | 120,093 ✅ pin reproduced (R38: 282/292) |
|
||
| `?classic=1` | — | **269** | 32 | 90,580 ✅ **byte-exact to the R38 pin** |
|
||
| **`?r=3`** | **382** | **391** | **48** | 132,829 (tris legal) |
|
||
|
||
**Why shave was refused, with the numbers:** the breach is structural, not fat. `?r=3` carries
|
||
45–48 live chunks against the default's 31 (R+1 dispose window over a 7×7 vs 5×5 set), and the
|
||
excess is 91–100 draws. Lane B's per-chunk cost is already collapsed to ~1 draw per kind per chunk
|
||
(facade atlas, merged instancing — R2 close-out); there is no 91-draw shave available in a
|
||
diagnostic boot that is not a content cut to the DEFAULT town. Shaving a diagnostic override by
|
||
making the shipped town poorer is backwards.
|
||
|
||
**So the flag now documents its own budget law and qa asserts it (the round's option b):**
|
||
|
||
- `index.html` (`[Lane B R40]`, the RADIUS block): any `?r=` **above** the auto radius declares
|
||
`PROCITY.budget = { draws: 420, tris: 200000, radius, auto, diagnostic: true }` and prints a
|
||
`console.warn` naming the law ("DIAGNOSTIC boot … ≤420 draws … not a supported boot"). At or
|
||
below auto: `{ draws: 300, diagnostic: false }` — the street law, unchanged.
|
||
- **The ceiling is 420** = 391 measured + ~7% for crowd/streamer jitter (the R37 lesson: settle
|
||
policy and station phase move a walked worst by single digits). If the town grows past it, the
|
||
gate goes red and the ceiling is re-pinned CONSCIOUSLY — that is what a pinned law is for.
|
||
- `hud.js` warn threshold reads the boot's own law (`budget.draws`), so the readout stops shaming a
|
||
documented diagnostic at 300 and starts telling the truth at 420. Default boots render identical
|
||
DOM (same 300).
|
||
- `dbg.js` `info().budget` carries the law, so **a harness asserts against the boot's declared
|
||
ceiling, never a hardcoded 300**.
|
||
- **`tools/qa/r40_lane_b.py` (NEW file — races no lane) is the enforcing gate**, all arms
|
||
non-vacuous: default declares 300/not-diagnostic · `?r=3` declares 420/diagnostic + the warn
|
||
fired · a night stepwalk under `?r=3` measures ≤ its own ceiling (read 386 ≤ 420 at 8 m
|
||
stations) **and** > 300 (the exemption is load-bearing, not vacuous — if it ever reads ≤300 the
|
||
gate prints "retire the exemption" rather than failing) · tri law unchanged (131,893 ≤ 200k) ·
|
||
0 console errors. **→ Lane F: one line wires it into qa.sh** — and README.md's `?r=N` row still
|
||
quotes 307; please update it to the law (391 measured R40, ceiling 420, gated + warned).
|
||
|
||
### §40.3-2 — CLASSIC'S TOWN SELECTOR: the ruling was already true by construction; now it is deliberate and gated
|
||
|
||
Fable ruled classic SHOWS the selector ("information, not gameplay state"). Verified on today's
|
||
tree: `?classic=1` boots with the selector **present and visible, all 27 options** (synthetic + 3
|
||
OSM fixtures + the 23-town real/godverse pack), its towns-fetch surface is **exactly the one named
|
||
POST_V2_EXCEPTION** (`assets/towns/index.json`, Fable's R37 ruling), navigation preserves the query
|
||
string so a town picked FROM classic STAYS classic, **zero draws** (a `<select>` the renderer never
|
||
sees — draws at spawn 148 with it, and `renderer.info` cannot see DOM), zero console errors. The
|
||
ruling is now written INTO the selector block (`hud.js`, `[Lane B R40]`) and **r40_lane_b.py
|
||
asserts it** (selector present, ≥20 towns, fetch surface = the named exception only).
|
||
**→ Lane F:** `flags_check.py`'s `POST_V2_EXCEPTIONS` label still carries "debt ledger R38+: should
|
||
classic show the selector?" — that question is now RULED (R40); update the label when convenient.
|
||
|
||
### §40.3-3 — FOG SIGNAGE: the HUD and the map now say where you are, and classic never grows the DOM
|
||
|
||
Three surfaces that said nothing now consume A's layer (LANE_A_NOTES §39 contract verbatim: print
|
||
`street || label`, handle null once, never branch on town type). All zero-draw DOM/canvas:
|
||
|
||
| surface | before | after (measured strings) |
|
||
|---|---|---|
|
||
| HUD readout | no location line | **`street` row**: `Lurline Street` (katoomba_real, supplier `ways`) · `the south end of the main street` (synthetic, district supplier) |
|
||
| door tooltip | `🚪 Little Paris Cafe — click to enter` | `🚪 Little Paris Cafe · Katoomba Street — click to enter` (and `· the south end of the main street` on the synthetic) |
|
||
| fog-map caption | `… 4 shops known · 2 streets walked · press M to close` | `… 4 shops known · 2 streets walked · Lurline Street · press M to close` |
|
||
|
||
- **`discovery.js` gains `createStreetLocator(plan)`** — the probe's own spatial hash, read-only:
|
||
`edgeAt(x,z)` (kerb + 4 m pad, nearest at intersections) and `shopNear(x,z)` (nearest shopfront
|
||
≤28 m, the locality fallback where streets are nameless). Signage is deliberately NOT keyed to
|
||
the fog ledger — knowing the name of the street you are standing on is information design, not
|
||
discovery. Resolution rides the existing `frame % 6` throttles (µs per lookup, DOM writes only on
|
||
change).
|
||
- **Classic-gated by construction, the house pattern:** the shell builds the locator only
|
||
`!CLASSIC` and `hud.setAddresses` is then never called ⇒ the `#pc-street` node **does not
|
||
exist** in classic's DOM and the tooltip string is the pre-R40 byte. A/B on the same door, same
|
||
seed: classic `🚪 Anglibarn Op Shop — click to enter` vs default
|
||
`🚪 Anglibarn Op Shop · the south end of the main street — click to enter`. The map's caption
|
||
suffix lives inside the fog arm, which classic (null store) never enters. r40_lane_b.py asserts
|
||
both arms (row resolves on default, node absent on classic).
|
||
|
||
### §40.4 (routed by Fable mid-round) — THE ARCADE KIT: E's placement rule applied in B's files
|
||
|
||
E measured, B applied, exactly as specced (LANE_E_NOTES §40): **the roof spans (slab depth 2.5 =
|
||
the half-lane, opposing slabs meet flush on the centreline) and a spanned roof has no posts.**
|
||
|
||
- `chunks.js`: `ctx.arcadeBlocks` derived once per manager from **`district.kind`, never
|
||
`edge.kind`** (real towns: 1,102 arcade-KIND edges = 39 km of ordinary footpath, zero arcade
|
||
districts).
|
||
- `buildings.js`: `ARCADEKIT` flag in the explicit VERGE/YARDS idiom — **`?classic=1` forces it
|
||
off** (mandatory: the synthetic IS the classic boot), `?arcadekit=0` is the standing
|
||
falsifiability control. In-arcade lots get depth 2.5 and skip the post loop; everywhere else the
|
||
code path is byte-identical pre-R40.
|
||
- **Measured on the final tree:** post instances **−34 exactly** (E's number), lane-interior view
|
||
**draws 120 → 120 (+0)**, 0 console errors; shot pair
|
||
`docs/shots/laneB_r40/r40_arcade_lane_{before,after}.png` (before = `?arcadekit=0`) matches E's
|
||
reference pair; classic lane view unchanged by construction (flag forced off ⇒ pre-R40 branch)
|
||
and classic's pin cell re-measured **byte-exact** (269 / 90,580) after the change.
|
||
- E's kit note (the `ground-arcade-roof` ceiling skin at +1 draw × 2 chunks) is NOT taken this
|
||
round — it is a visible-cost spend and the round's items didn't ask for it; filed as the next
|
||
arcade polish.
|
||
|
||
### The budget table of record, FINAL tree (all edits in), pin method
|
||
|
||
| mode | worst draws | law | margin | worst tris |
|
||
|---|---|---|---|---|
|
||
| default noon | 282 | ≤300 | 18 | 100,538 |
|
||
| **default night** | **291** | ≤300 | **9** | 120,093 |
|
||
| `?classic=1` night | **269** | ≤300 | 31 | **90,580 (byte-exact to R38 pin)** |
|
||
| `?r=3` night (diagnostic) | **391** → gate reads 386 @ 8 m | **≤420 (its own declared law)** | 29–34 | 132,421 |
|
||
|
||
Goldens: `selfcheck.js` **157,647/157,647, fingerprint 0x5f76e76 — unmoved** (run after every edit
|
||
wave). Zero console errors on every boot exercised. Files touched: `web/index.html` (RADIUS block,
|
||
createHUD budget, address wiring, PROCITY.budget — all `[Lane B R40]`), `web/js/world/hud.js`,
|
||
`minimap.js`, `discovery.js`, `dbg.js`, `chunks.js`, `buildings.js`, NEW `tools/qa/r40_lane_b.py`,
|
||
`docs/shots/laneB_r40/`.
|
||
|
||
## Round 39 (v9 Layer 2 — **THE FOG**, item 39.2) — the map costs what it always cost: nothing
|
||
|
||
**`minimap.js` drew every lot in the plan from the first frame. It now draws what you have walked past,
|
||
and frames itself to it.** Zero draws, zero triangles, zero plan fields, one optional save block.
|
||
New file `web/js/world/discovery.js`; `minimap.js` rewritten; `save.js` gains the ledger; the shell
|
||
wires it. **`?fog=0` / `?game=0` / `?classic=1` are the pre-R39 map pixel-for-pixel** — proven below,
|
||
not asserted.
|
||
|
||
### The zero, measured rather than claimed
|
||
Two **port-isolated no-store servers** — `:8592` serving `git archive HEAD web/js web/index.html`
|
||
(assets/vendor symlinked from the live tree so the other lanes' mid-round waves are identical on both
|
||
sides), `:8591` serving the treatment — driven through the shell's own composer at 1600×1000. The A/B
|
||
walks **the same 15 stations × 4 yaws**, computed from the plan on each build and compared as JSON
|
||
before either sweep runs, so a station list that drifted would fail the run rather than quietly make
|
||
the comparison meaningless. **The citizen sim and the tram are hidden identically on both sides**
|
||
(R38's recorded trap, obeyed).
|
||
|
||
| synthetic, 60 station×yaw samples per arm | HEAD | R39 | delta |
|
||
|---|---|---|---|
|
||
| default boot, **noon** — worst draw frame | 243 | **243** | **0 on 60/60 samples** |
|
||
| gig layer off (`&gigs=0&roster=v1`), **noon** | 235 | **235** | **0 on 60/60** |
|
||
| gig layer off, **NIGHT** — the budget's worst frame class | 235 | **235** | **0 draws AND 0 triangles on 60/60** |
|
||
| triangles, every arm | — | — | **never higher on the treatment: 0 samples of 240 above zero** |
|
||
| map mode (`M` held open) | — | — | **the counter does not move at all** |
|
||
|
||
The night arm is the one the budget cares about (F's pin is a night frame) and it is **exactly
|
||
identical in both counters**. At noon the draw calls are identical too and the triangle delta is
|
||
**≤ 0 in every sample** — 13 of 60 samples where HEAD drew *more*, which is a moving town measured
|
||
twice, not a treatment that adds geometry. The fog never touches the scene graph, so the only honest
|
||
direction for a difference is the one that showed up: never positive.
|
||
|
||
**Map mode cannot cost a draw, and the reason is structural, not careful:** `index.html`'s frame loop
|
||
never calls `composer.render()` in the `map` branch (it only ticks the HUD and paints the 2D canvas),
|
||
so `renderer.info.render.calls` reads **the same number with the map open as with it closed** — 131 and
|
||
131 at the same pose in the measured run. The map is a canvas over the WebGL canvas and `hud.js:361`'s
|
||
counter never sees it.
|
||
|
||
**Two measurement bugs I made, both caught by my own numbers rather than by a gate.**
|
||
1. The harness called `lighting.togglePaused()` once per segment, so the second call **un-paused** the
|
||
clock. Fixed with the idempotent `setPaused(true)`.
|
||
2. The night arm then still read **−16 to −37 draws / +8,346 to +18,249 tris**, deterministically, on a
|
||
change that adds no geometry. It is the **gig layer**: at NIGHT `gigState` opens the doors and
|
||
`VenueQueue.spawn(scene, …)` adds 10–20 punters **straight to the scene** (not into the `citizens`
|
||
group my hide targets), and each is a placeholder primitive until the GLB fleet resolves. Fewer
|
||
draws with more triangles is exactly that swap — an async asset race between two pages, measured
|
||
twice. The night arm is therefore taken with the gig layer and the roster **off on both sides**
|
||
(`&gigs=0&roster=v1&pop=0`), which is the only night A/B in which the fog is the only variable.
|
||
*R38 recorded "hide the sim"; the queue is a second population that the same instruction misses.*
|
||
|
||
### The framing IS the payoff — the legibility numbers
|
||
Replicating `minimap.js`'s own transform (`span = extent + 40`, `sc = (720 − 48) / span`) against the
|
||
median shop lot on each town:
|
||
|
||
| | bowral | launceston | daylesford | castlemaine | katoomba | adelaide | fitzroy | synthetic |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| full-plan extent (today) | **0.83×1.11 px** | 1.07×1.42 | 1.19×1.58 | 1.38×1.84 | 1.42×1.89 | 1.74×2.32 | 2.65×3.54 | 5.22×10.24 |
|
||
| framed to the 240 m floor | **29.4×39.2** | 29.4×39.2 | 29.4×39.2 | 29.4×39.2 | 29.4×39.2 | 29.4×39.2 | 29.4×39.2 | 21.0×41.2 |
|
||
| **gain** | **35.3×** | 27.6× | 24.8× | 21.3× | 20.8× | 16.9× | 11.1× | 4.0× |
|
||
|
||
Those reproduce Fable's binding table to the digit (bowral 35×, katoomba 21×, adelaide 17×, fitzroy
|
||
11×, synthetic 4×) — the `+40` margin is part of the transform and must be carried into the framed
|
||
number or the gain reads 42× instead of 35×.
|
||
|
||
**And it is worse than "small": at the shipped scale a real town's shops do not survive rasterisation
|
||
at all.** The pixel detector counts pixels that exactly match one of the ten shop-type colours (an
|
||
exact palette, not a threshold — the vacuous-gate law's preferred instrument). On the synthetic at the
|
||
full-plan transform it reads **26,506 px**. On katoomba_real, same code, same boot, **it reads ZERO**:
|
||
a 1.42×1.89 px *rotated* rect has no fully-covered pixel, so **all 72 Katoomba shops rasterise to not
|
||
one pixel of their own colour.** Fogged and framed, the same map reads **1,902 px for the 34 shops you
|
||
walked past.** The screenshots say it faster than the numbers do: HEAD's Katoomba is 993 edges of grey
|
||
spaghetti with the shops invisible; the fogged one is a legible strip of **Katoomba Street** with
|
||
**Edwards Street** running off it, and a caption that says *34 shops known · 6 streets walked*.
|
||
|
||
Measured live, both towns, fresh boot then a 900 m walk through the real rAF loop:
|
||
|
||
| | synthetic | katoomba_real |
|
||
|---|---|---|
|
||
| fresh boot | 4/493 shops · 1/22 segs · frame **240 m** · lot **20.97×41.16 px** | 4/72 shops · 1/993 segs · frame **240 m** · lot **29.4×39.2 px** |
|
||
| after 900 m | 113/493 shops · 7 segs · frame 515 m · **9.77×19.18 px** | 34/72 shops · 18 segs · frame 965 m · **7.31×9.75 px** |
|
||
| the shipped map | — · frame 965 m · 5.22×10.24 px | — · frame **4,984 m** · **1.42×1.89 px** |
|
||
|
||
*(Fresh boot reads 4 shops rather than 0 because the `+sin` fix below landed: you now start standing in
|
||
front of four shopfronts instead of behind them. **On katoomba that fresh-boot map draws 3,478 pixels
|
||
of shop colour — against HEAD's ZERO for the entire town.** Four shops you can read beat seventy-two
|
||
you cannot see.)*
|
||
|
||
**One harness assertion of mine is wrong and F should not inherit it:** `r39_verify.py`'s fog arm
|
||
asserts *more shop-colour pixels after the walk*, which only holds at a fixed zoom — after 900 m on
|
||
katoomba the frame has grown to 965 m, so 34 shops draw 1,902 px where 4 drew 3,478. The honest
|
||
assertion is the **shop count** (4 → 34, already checked separately), or px-per-known-shop.
|
||
|
||
**The frame is the shops you have stood in front of, plus where you are standing — NOT the streets you
|
||
have walked**, and that was a real correction to my own first cut. An edge is drawn as the whole
|
||
node-to-node line, and the synthetic's main street is one ~400 m edge, so stepping onto it at spawn
|
||
expanded the frame to **340 m before the player knew a single shop**. Framing on shopfronts holds the
|
||
fresh-boot frame at the **240 m floor** (a 200 m window plus the transform's own margin), and a walked
|
||
street that runs off the edge of the frame is clipped by the canvas — which is what a map does.
|
||
|
||
### The proximity rule I settled on
|
||
**Within `FOG_RADIUS_M` = 25 m of the shopfront AND in front of it.** Not `chunks.onChunkBuilt`
|
||
(R=2 ⇒ the outermost live chunk centre is 128 m out), and not the live door meshes either: the probe
|
||
runs off a plan-derived spatial hash, so discovery means *you were there*, never *it happened to be
|
||
streamed*. At R=2 anything inside 25 m is always in a live chunk, so the two can never disagree — the
|
||
grid is chosen because it does not depend on streaming, not because it sees more.
|
||
|
||
- **Cadence:** every 6th street frame, the same throttle class as `hud.js:337`'s door raycast. At WALK
|
||
4.6 m/s / 60 fps that is a probe every **0.46 m** of travel (0.88 m at RUN) — finer than a shopfront.
|
||
- **Cost:** **1.09 µs/probe** on the synthetic, **1.37 µs** on bendigo_real (2,593 edges, 188 km of
|
||
road), 1.27 µs on fitzroy. Once every 6 frames ⇒ **0.23 µs per frame, 0.0014% of a 16.7 ms frame.**
|
||
Index build is 0.8 ms (synthetic) / 5.0 ms (bendigo), once, at boot.
|
||
- **Streets are learned too** (`e.id`, distance to the segment ≤ `width/2 + 4 m`), because "the map
|
||
shows the streets you've learned" is the round's own sentence. A lot with no shop rides its
|
||
`frontEdge`: walk the road, see the houses. A shop needs the walk-past.
|
||
- **Walking in the door discovers the shop** regardless of geometry — placed INSIDE the successful-entry
|
||
branch of `enterShop`, after the closed check and the cover charge, never on the dispatch.
|
||
|
||
**THE SIDE TEST'S FALSIFIABILITY CONTROL, WITH A RED ARM.** Walk the synthetic's two service laneways
|
||
(560 m, behind the main-street blocks): a radius-only probe reveals **93 shops**, the shipped probe
|
||
reveals **59** — **34 shopfronts refused through the back wall.** Walk the 42 m arcade and it refuses
|
||
**0 of 17**, which is correct: an arcade is a front. Sampled over every walkable square metre inside
|
||
the radius on a 2 m grid (lot interiors excluded), the side test refuses **31.6% of the disc on the
|
||
synthetic, 40.3% katoomba, 43.4% bowral, 43.5% fitzroy** — and on a shopper's walk down the strip it
|
||
refuses **0–2**, which is exactly the shape you want: free where a shopper stands, decisive behind the
|
||
buildings.
|
||
|
||
### THE SIGN WAS WRONG — the FOURTH instance of the −Z/+Z confusion *(Fable ruled: landed, `a982f3c`)*
|
||
|
||
**The lineage, because the next lane to touch a heading should find it.** Every time, the rule is the
|
||
same: **the rig/pose convention is local −Z, the visible front is local +Z**, and every time it
|
||
survived because *the thing checking it shared a convention with the thing that was wrong.*
|
||
|
||
| round | where | what it did | how it was caught |
|
||
|---|---|---|---|
|
||
| **R13** | Lane C's pose `ry` vs Lane D's rig | the band played 180° off; settled with `RY_FLIP = π` (`ROUND13_INSTRUCTIONS:26-28`) | another lane's mesh looked wrong |
|
||
| **R15** | `buildPosters` item 1, `lot + (−sin, −cos)·d/2` | the venue's own frontage poster on the **back wall** — measured **15.9 m from the street facade, 0.07 m from the back** | a screenshot of the pub |
|
||
| **R27** | real-town shopfronts | **dot = −1.000 exactly** on 35/35 redhill and 72/72 katoomba lots — every real shop had faced the paddock since v1. `CITY_SPEC:71` says the facade is −Z, `CITY_SPEC:335` says +Z; the two generators each implemented one half (`ROUND27_INSTRUCTIONS:169` already named it *"the same −Z/+Z species as R13's RY_FLIP and R15's posters"*) | standing on Musgrave Road and looking |
|
||
| **R39** | `index.html:428` patronage door points · `:500` the R32 spawn | Lane D's peds ducked into the back fence and **every non-classic boot since R32 opened behind the shop row** | the fog: my probe copied the shell's sign and read 4 shops known / 1,231 in-radius rejections |
|
||
|
||
Four instances, four rounds apart, and each one was found by a *picture* or by a consumer with a
|
||
different convention — never by a metric. That is the entry worth keeping: **a heading has no
|
||
falsifiable arm inside the file that computes it.** If you write one, check it against
|
||
`buildings.js`'s `toWorld` and take the screenshot.
|
||
|
||
### The R39 instance, measured
|
||
My first cut copied `index.html`'s own front-normal and the walk read **4 shops known and 1,231
|
||
in-radius rejections** on the synthetic — the fog hiding the town from a player standing in front of it.
|
||
The geometry says the shell is wrong:
|
||
|
||
- `buildings.js:410-416, :583-597` build the facade, the door and the `doorRect` at local **+Z** —
|
||
`lot + (sin ry, cos ry)·d/2`. `minimap.js`'s front-edge tick has always been drawn at local +Z.
|
||
`dbg.js:53` says it in words: *"Front normal = local +Z rotated by lot.ry"*, `fx = Math.sin(ry)`.
|
||
- `index.html:428` (Lane D's patronage door points, since R8) and `index.html:500` (`doorOf`, the R32
|
||
cluster-pose spawn) used **`(−sin ry, −cos ry)`** — the back of the building. **Both now `+sin/+cos`.**
|
||
- Measured against each lot's **own `frontEdge`**: the +sin point is **0.00 m** from the kerb on the
|
||
synthetic and −1.50 m on katoomba (i.e. on the kerb line, where a facade belongs); the −sin point is
|
||
**15.25 m / 12.50 m** the wrong side of it. **493 of 493 and 72 of 72 shops agree.**
|
||
|
||
**THE PATRONAGE DOOR POINT — is it on the footpath?** (in the verge band per `registry.js`'s own
|
||
`roadWidth`/`vergeBand` split, outside the carriageway, outside every lot):
|
||
|
||
| town | shipped | **fixed** | median distance from the street centreline |
|
||
|---|---|---|---|
|
||
| synthetic | **0 / 493** | **427 / 493** | 26.22 m → **9.40 m** (carriageway edge 5 m, facade 14 m) |
|
||
| katoomba_real | **0 / 72** | **67 / 72** | 19.10 m → **3.90 m** |
|
||
| fitzroy_real | **0 / 139** | **139 / 139** | 19.10 m → **3.90 m** |
|
||
| bowral_real | **0 / 30** | **28 / 30** | 21.10 m → **5.90 m** |
|
||
|
||
Not one door point in the corpus was on a footpath. Lane D's peds have been ducking into the back
|
||
fence since R8. (The residue — 31 synthetic / 5 katoomba / 2 bowral — are corner lots whose front
|
||
point lands inside a *neighbouring* lot; that class is pre-existing and **14 fewer** than before.)
|
||
|
||
**THE SPAWN — booted code on both sides**, `:8702` serving the fog commit, `:8701` the fix, nothing
|
||
monkey-patched:
|
||
|
||
| | shipped | fixed |
|
||
|---|---|---|
|
||
| synthetic | (25.5, −17.9) · 3 shops within 25 m, **0 in front**, **0 discovered** | (6.1, −19.8) · 4 within 25 m, **4 in front**, **4 discovered** |
|
||
| katoomba_real | (−219.0, −471.3) · 3 within 25 m, **0 in front**, **0 discovered** | (−200.0, −474.4) · 4 within 25 m, **4 in front**, **4 discovered** |
|
||
|
||
**And the picture, which is the whole argument** (`docs/shots/laneB_r39/sign-*`): the shipped katoomba
|
||
boot opens **standing in a paddock facing the blank back wall of a building** — which is the R31
|
||
playtest's own complaint, *"faced dirt and building-backs"*, the complaint R32 was written to fix and,
|
||
because of the sign, never did. The fixed boot opens on the footpath under the awnings with the
|
||
shopfronts and their signage running away down Katoomba Street.
|
||
|
||
**NO GOLDEN MOVES, plainly: `SPAWN` and the patronage points are RUNTIME POSE, computed in the shell
|
||
from a plan it never writes to.** Lane A's selfcheck run either side of the change:
|
||
**fingerprint `0x5f76e76` → `0x5f76e76`**, 157,647/157,647 checks. `?classic=1` is untouched by
|
||
construction — it never enters the branch and keeps the frozen (−96, 9, looking east) pose forever.
|
||
|
||
**→ LANE F, two gates photograph this pose and need re-baselining:** the **R32 cluster-pose spawn**
|
||
gate and the **R35 first-five-minutes** smoke. Both are correct to move — the pose they baselined is
|
||
the defect. The falsifiable arm to gate on instead, which needs no picture: at the spawn, **the count
|
||
of shops within 25 m that are in front of the player must be > 0** (it was 0 on 4 of 4 towns before,
|
||
4 of 4 after), and it goes red the moment the sign flips back.
|
||
|
||
### The save shape, and its bound
|
||
`known` — OPTIONAL, absent until something is learned, so a save that predates R39 stays valid forever
|
||
and a boot that discovers nothing writes the same bytes it always did (verified: a fresh export has no
|
||
`known` key, and calling `knownStore()` without writing does not create one).
|
||
|
||
```
|
||
known: { "<fogKey>": { s: [shopId…], e: [edgeId…] } } // e.g. "osm/katoomba_real", "synthetic/default@20261990"
|
||
```
|
||
|
||
**Keyed by TOWN, not by `townKey`** — I re-derived Fable's claim rather than take it: across seeds
|
||
20261990 / 7 / 424242, **all 23 caches produce byte-identical shop ids, edge ids AND lot
|
||
geometry+frontEdge**; only the synthetic moves (493 / 479 / 465 shops), so it alone keys `default@<seed>`.
|
||
A re-seed is the same Katoomba with different secrets and the map you learned survives it.
|
||
|
||
**Bounded three ways, and the bound is checked rather than trusted:**
|
||
|
||
| axis | bound | why it is enough |
|
||
|---|---|---|
|
||
| ids per town per kind | `KNOWN_IDS_CAP` 8,192 | self-bounding by the plan: **max 493 shops** (synthetic), **max 2,593 edges** (bendigo_real). 3.2× the biggest town in the corpus — no legitimate save can reach it |
|
||
| towns | `KNOWN_TOWNS_CAP` 64 | the only axis a save can grow along **without walking** (`?town=` takes any string). FIFO eviction at write time, verified: writing 74 town keys leaves 64, oldest gone |
|
||
| the ids | non-negative integers ≤ 1e6 | a plan index, not free text |
|
||
|
||
**Worst case, built and stringified rather than estimated** — every shop and every street segment of
|
||
all 24 towns learned: 1,712 shops + 31,008 edges = 32,720 ids = **137,547 bytes (134.3 KB), 2.62% of a
|
||
5 MB localStorage quota** — against 5–9 game-days of walking *per town*. A measured save after a 500 m
|
||
outing on the synthetic (68 shops, 7 segments) is **482 bytes**.
|
||
|
||
**Reject arms, all demonstrated (whole-blob, loud, stashed — the existing law):** a string id · a
|
||
negative id · a float id · a town entry that is an array · `known` itself an array · 8,193 ids · 65
|
||
towns · an empty town key. All eight rejected with their own `why`; a rejected `import()` leaves live
|
||
state untouched (counts identical before and after); a foreign town's fog imports without leaking into
|
||
this town's (this town reads 0).
|
||
|
||
### Lane A's address layer, consumed
|
||
A landed `createAddresses` mid-round (`f628bf5`) and it is wired: `getTownCache(CHAR_TOWN)` →
|
||
`createAddresses(plan, cache)` → **street names along the streets you have walked**, deduped by name
|
||
(one label per name, on the longest known edge, never upside down, capped at 40 and skipped when the
|
||
edge is shorter than its own text), plus `localityOf(shopId).street || label` for the towns where
|
||
`street` is null by construction. **The consumer never branches on town type** — it asks, and draws
|
||
whatever answers; the synthetic gets *"the south end of the main street"* from the same line that gets
|
||
*"Templeton Street"* on castlemaine. The caption counts **distinct streets** when the address layer is
|
||
present and **street segments** when it is not, because an edge is not a street. `stats().supplier` is
|
||
printed at boot: a forgotten cache degrades **visibly to district labels, never to a wrong name**.
|
||
|
||
### Classic, by construction
|
||
`createMinimap(plan, known, opts)` — **`known === null` is the pre-R39 map and `minimap.js` contains no
|
||
flag test at all.** The null store is what `?classic=1`, `?game=0` and `?fog=0` each produce in the
|
||
shell, the same shape `townCharacter(null)` gives `ground.js` (R38 §1.2). Verified as pixels, not as an
|
||
argument: the map canvas FNV hash at a fixed pose is **identical to HEAD's on all three** —
|
||
`?classic=1` `57fc623d`, `?fog=0` and `?game=0` `3eac8261` (the two hashes differ from each other
|
||
because classic also turns the gig layer and the yards off, which is the control proving the hash is
|
||
sensitive to the map's content at all).
|
||
|
||
### THE MAGPIE, SETTLED ON E'S PICTURE — the GLB is the path, the fallback is fixed anyway
|
||
*(Fable's second R39 ruling. Lane E's sheet: `docs/shots/laneE/r39_magpie_ab.png`.)*
|
||
|
||
**Wired as `furniture.js`'s use-if-ready second path, not a substitution** — `?noassets` and `?classic`
|
||
still get the procedural bird, so it had to be fixed too. `loadGLB('depot:procity_street_magpie_01.glb')`
|
||
once at module init, a synchronous ready-check in `createMagpie`, and the geometry/material swapped
|
||
into **the one InstancedMesh**. `state.asset` reports which bird is bound so a gate reads it instead of
|
||
inferring it from a fetch.
|
||
|
||
| same page, same frame, bird on vs off | draws | tris |
|
||
|---|---|---|
|
||
| GLB (default boot) | **+1** | **+894** |
|
||
| procedural (`?noassets=1`) | **+1** | **+182** |
|
||
| procedural on HEAD (before the re-placement) | +1 | +182 — **the placement fix is +0 tris, in-engine** |
|
||
|
||
**TWO THINGS I HAD TO MEASURE BEFORE WIRING IT, AND BOTH WOULD HAVE SHIPPED WRONG.**
|
||
1. **E's bird faces +Z; this file flies −Z** (`Matrix4.lookAt`'s −Z column is the travel direction, and
|
||
the primitive's nose is modelled along −Z). Sliced the GLB's 894 tris along Z, ten bins: the **+Z**
|
||
end is high and narrow (mean y **0.31**, half-width **0.009** — head and bill), the **−Z** end low
|
||
and thin (mean y **0.028** — tail). Without `rotateY(π)` the magpie swoops **tail-first**. *This is
|
||
the FIFTH −Z/+Z instance — the first one caught before it shipped, and only because the lineage
|
||
table above told me to point a measurement at it.* Verified in a picture both ways: perched with
|
||
the head leading, and a swoop frame taken **head-on**.
|
||
2. **Its origin is at the feet** (GLB law `minY = 0`) while `PERCH_Y = 4.32` is a body-centre height —
|
||
dropped 0.20 m so the feet land on the lamp arm at 4.12 (`furniture.js`: pole 4.2, arm 4.1) instead
|
||
of hovering over it. And `folded` (the 0.42× X squash that makes the primitive's flight geometry
|
||
sit) is **1 for the GLB**, which is already the perch pose.
|
||
|
||
3. **THE FETCH IS ARMED BY `createMagpie()`, NOT BY THE IMPORT — and that is the classic covenant.**
|
||
`index.html` imports this module unconditionally and gates only the *construction*, so
|
||
`furniture.js`'s module-scope load shape would have put a **brand-new GLB request on the
|
||
`?classic=1` boot** — a fetch delta on the one boot that is not allowed one. Asserted on the
|
||
**network log**, not on the code:
|
||
|
||
| boot | magpie GLB requests | `state.asset` |
|
||
|---|---|---|
|
||
| default (and `?magpie=1`) | **1** | `glb`, 894 tris |
|
||
| `?classic=1` | **0** | *(never constructed)* |
|
||
| `?magpie=0` | **0** | *(never constructed)* |
|
||
| `?noassets=1` | **0** | `primitive`, 182 tris |
|
||
|
||
**A defect my own edit introduced into ANOTHER LANE'S TOOL, caught by running it.** A module-scope
|
||
`loadGLB` fires GLTFLoader's fetch path under node, and Lane E's `pipeline/dump_bird.mjs` imports this
|
||
module unmodified — `ReferenceError: ProgressEvent is not defined`, E's A/B tool dead, my fault. The
|
||
load is now gated on `IN_BROWSER` as well as `?noassets`. E's tool then ran against my edited file and
|
||
reported **182 triangles, 216 verts** — the placement changes are exactly +0 tris, confirmed by the
|
||
lane that filed them.
|
||
|
||
**THE FALLBACK FIX, AND ITS MEASURED CEILING — which is not the answer E hoped for.** E's finding is
|
||
right: the white bars sat **3 mm above** horizontal wing planes, so from below (perch 4.32 m, EYE
|
||
1.62 m — the player is *always* below) the black wing occluded its own flash. I moved the bar under
|
||
the wing (y 0.048 → **0.042**), and did the same class of fix for the other two marks with my own
|
||
arithmetic: the **nape was buried in the head** (head-sphere surface at z = −0.10 is y **0.0728**, the
|
||
box topped out at **0.067**) so it moved back to z −0.07 where the body's back is at 0.0675, and both
|
||
nape and rump were **thickened** (0.014 → 0.030, 0.02 → 0.034) because from the flank — 80% of viewing
|
||
— a 14 mm slab on a 166 mm bird is **1.3 px at 64 px** and a slab you can see the *side* of is not.
|
||
All +0 triangles.
|
||
|
||
**Then I measured the result instead of trusting it, and it is a half-win:** counting pixels in the
|
||
same crop of the same pose, pale area (lum 110–150) went **180 → 399**, dark (<70) **3,017 → 2,755** —
|
||
but *true* white (>150) went **33 → 28**. **A downward-facing surface gets no sun**, so the white I
|
||
moved into view reads **grey**. E's GLB in the same crop: **755 px above 150.** The fallback is
|
||
measurably better and it cannot be made pied with primitives at zero cost, because the problem is
|
||
photometric as well as geometric. Worth recording as the reason the GLB is the path rather than a
|
||
preference.
|
||
|
||
**Stale comments corrected:** `magpie.js:9` and `:123` both said **154 triangles**; it is **182**
|
||
(sphere(8,6) 80 + sphere(6,5) 48 + cone(5) 10 + 3 boxes 36 + 4 planes 8), and E's dump of my own
|
||
`birdGeometry()` agrees — 216 verts, index length 546.
|
||
|
||
### → LANE E / a later round: THE ARCADE'S AWNING POSTS — filed, measured, NOT fixed
|
||
Confirmed E's number independently from the plan: the arcade lane is **5.00 m** (facades at ±2.50 m
|
||
from the centreline) and `buildings.js:643`'s post line lands at **±0.50 m — two rows 1.00 m apart,
|
||
34 posts down a 42 m lane.** `AWNING_DEPTH = 2.2` is tuned for a main street with a ~9 m footpath; in
|
||
a 2.5 m half-lane it reaches nearly to the middle, and it reads as a colonnade standing in its own
|
||
doorway (no collider, so it walks). **The fix is one clamp** — awning depth ≤ a fraction of the
|
||
front clearance, which needs a `lot.block → block.kind` lookup `buildings.js` does not do today.
|
||
**Not this round:** it changes shipped geometry on the default boot for a district whose whole
|
||
treatment (roof lights, tiled floor, reverb, E's 5.86 m `longbench`, a mouth) is a scheduled item, and
|
||
the post line should be designed with the roof rather than ahead of it.
|
||
|
||
**Lane E's transmission recall, checked rather than assumed:** the three republished GLBs staged in
|
||
`web/assets/models/` carry **no `KHR_materials_transmission`** (only `KHR_materials_specular`/`ior`,
|
||
benign) — `longbench`, `streetlight` and `magpie` all clean on disk. My lane wires none of the first
|
||
two today (`furniture.js` GLB_FILES is bench / food_cart / bin / bus_shelter), so there was nothing of
|
||
mine to re-fetch; the magpie I *am* wiring is clean.
|
||
|
||
### Held — for Fable / for John
|
||
1. **The inverted front normal at `index.html:384` and `:450`** (above). Two characters, measured to
|
||
death, not applied: it moves the default boot's opening pose mid-round.
|
||
2. **John's standing call: is the fog default-on or a mode?** Shipped **default-on** on the flagOn
|
||
precedent, with **`?fog=0` a true revert** (hash-identical to HEAD) so both answers are one
|
||
character apart and either can be felt today. Not a decision made — a decision kept available.
|
||
3. **John's standing call: how harsh?** Shipped (a) *walked within 25 m of the shopfront*. (b) *entered*
|
||
and (c) *every shop on a walked street* are both **one line at the probe** — (b) drops the radius
|
||
arm and keeps `discoverShop`, (c) unions `hasEdge(lot.frontEdge)` into the shop test. The store, the
|
||
save, the bound and the framing are identical under all three.
|
||
4. **A walked edge is drawn end to end.** Step onto the synthetic's main street and its whole ~400 m
|
||
line appears. On real towns edges are short and this is invisible; on the synthetic (22 edges) it
|
||
over-tells. Clipping to the walked span needs per-edge `t` ranges — i.e. new save state — so it is
|
||
a proposal, not a patch. It no longer affects the FRAME (which is shopfront-derived).
|
||
5. **Late game, the frame grows back toward the overview** — fully explored, fitzroy's frame *is*
|
||
today's map, which is the right map at that point. If it ever wants a zoom, the transform is one
|
||
function and the cache key already carries the box.
|
||
6. **The HUD tooltip street label** (`hud.js`, one template literal, `PROCITY.addresses.localityOf`)
|
||
is the charter's other B line for this layer. Not landed — out of 39.2's scope, and it is ready to
|
||
drop in a round that owns `hud.js`.
|
||
|
||
## Round 38 (v8 WAVE 1 — INHABITATION, all four runtime items) — **someone lives here, for +1 walked draw**
|
||
|
||
**Four items, all built procedural-first, all measured walked. The whole round costs +1 draw at the worst
|
||
walked frame and −13 draws summed over 102 of them; the only item that spends a draw at all is the
|
||
washing. Two of the four found a defect in their own first cut that every number was green through.**
|
||
|
||
### The A/B, and how it was taken
|
||
Two **port-isolated no-store servers** at 2560×1440: `:8472` serving `git archive HEAD web/js web/index.html`
|
||
(assets symlinked from the live tree, so Lane E's mid-round asset wave is identical on both sides) and
|
||
`:8471` serving the treatment. Same 102-step walk, 8 m steps down the concatenated `main` spine at
|
||
**yaw 0** (the audit's bearing, the worst one), each frame **fully streamed** (drain to `pending === 0`)
|
||
and rendered through **the shell's own composer**, not a bare `renderer.render`.
|
||
|
||
**The citizen sim and the tram are hidden for the walk, identically on both builds** — and that is not
|
||
tidiness, it is the measurement. The first run had them live and read **control 346 draws vs treatment
|
||
232 at the same step**: peds spawn on wall-clock time and each near rig is ~1 draw, so an A/B that
|
||
includes them measures the sim. R37 recorded this exact trap; R38 reproduced it inside an hour.
|
||
|
||
| walked spine, 102 steps, sim quiet | HEAD control | R38 | delta |
|
||
|---|---|---|---|
|
||
| worst-draw frame | **224** | **225** | **+1** |
|
||
| worst-tri frame | **89,502** | **93,298** | **+3,796** |
|
||
| sum of draws, all 102 frames | 20,081-era method: **16,676** | **16,663** | **−13** |
|
||
| per-frame draw delta | — | — | **min −7 · max +3** |
|
||
| per-frame tri delta | — | — | +512 … +4,468, **mean +2,119** |
|
||
| live chunk count, every step | identical | identical | **0** |
|
||
|
||
**`?yards=0` reproduces the HEAD control exactly** — worst 224 / 89,502, sum 16,676, the same three
|
||
numbers — so the flag is a true revert, not an approximation.
|
||
|
||
**The residential district**, where all 181 yards actually are (edge 16, 107 steps):
|
||
|
||
| | `?yards=0` | R38 | delta |
|
||
|---|---|---|---|
|
||
| worst draws | 138 | **139** | **+1** |
|
||
| worst tris | 19,308 | **21,128** | **+1,820** |
|
||
| sum draws / sum tris | 8,010 / 983,210 | 8,142 / 1,150,788 | +1.2 draws, +1,566 tris per frame |
|
||
|
||
**Isolated, at a fixed residential pose, by toggling each layer on one frame:**
|
||
**washing = +1 draw / +512 tris** (256 instances × 2) · **magpie = +1 draw / +182 tris** ·
|
||
**every yard box prop = +0 draws** (it is instances in a draw call that was already submitted).
|
||
|
||
### §1.2 — the per-town character vector: **+0 draws, +0 tris, and classic byte-identical BY CONSTRUCTION**
|
||
`web/js/world/character.js` — 23 town keys, each with its own entry, returning a ground palette, a sky
|
||
bias and an ambience mix. `townCharacter(null)` returns `CLASSIC_CHARACTER`: the exact v1 literals.
|
||
- **There is no `if (classic)` in the ground path.** A boot with no town key has no character, and that
|
||
is what both `?classic=1` and the synthetic default get. Proven, not argued: the default boot's ground
|
||
vertex+UV hashes are **`c4c72181 / 735250c9 / a300d701 / 12c1241b / bfa81387` on HEAD and on R38,
|
||
mesh for mesh**, with the same four maps and `color #ffffff` on all of them. `?classic=1` hashes
|
||
**`c4c72181 / c111d968 / 1a80ebcd / 12c1241b / fd0d3211`** — the R37-recorded classic set, unmoved.
|
||
- `skins.groundMat(name, tint)` keys the cache on the tint, and a **null** tint keeps the key exactly
|
||
`ground:<name>` — so classic doesn't merely look the same, it gets the same cached material object.
|
||
- **The sky never touches `setSky`.** `createLighting` takes a `sky` option feeding the base `skyName`,
|
||
because `externalSky` (lighting.js:53) is a one-way latch with no un-set path — pushing a character
|
||
sky through it would have silently killed R32's dawn dome on all 23 towns and beaten the rain dome.
|
||
- **The vacuous arm is per-town.** Keying on `state` from the towns index is the trap: `newtown_godverse`
|
||
and `redhill_godverse` carry `state: ''` (2 of 23), so a per-corpus arm drops exactly those two into a
|
||
branch nobody printed. All 23 keys have their own entry (verified against the shipped index: 0 missing);
|
||
an unmatched key returns the frozen literals **with `matched:false`**, and the shell prints which key
|
||
fell through. **21 distinct palettes across 23 towns** (the 2 repeats are the deliberate godverse↔real
|
||
sibling pairs, written as two entries so it reads as a decision rather than a fallthrough).
|
||
- **Why the two orphans were orphans, and it isn't what it looks like** (Lane E's R38 finding, confirmed):
|
||
`gravel` and `reddust` carry `use` values — `"verge"` and `"outback"` — that name **no slot** the old
|
||
`ground.js` had; it hardcoded four skin names. They were not unwired, they were *unwireable*. Fixed at
|
||
the root by making the SLOT addressable: any skin name can be road / footpath / base / plaza, and
|
||
`use` goes back to documentation (`groundMat` never read it — it builds a filename). Both are now
|
||
selected: **gravel = Castlemaine's goldfields tailings, reddust = Darwin and both Red Hills.**
|
||
- **Lane E's grounds landed mid-round and the use-if-ready upgrade was a name change in one table**,
|
||
which is what procedural-first is for: `grass-dry` (Bendigo/Adelaide/Northbridge), `grass-temperate`
|
||
(Daylesford/Katoomba/Bowral/Hobart/Launceston), `coastal-sand` (Fremantle/Newcastle). Tints re-picked
|
||
against the delivered pixels (`grass-dry` measures **#c0b7aa**, a pale tussock, not a green). `skins.js`'s
|
||
fallback table gained three entries **prepended**, because `grass-dry`/`grass-temperate` both matched the
|
||
old bare `/grass/` and would have fallen back to the one colour neither of them is.
|
||
|
||
| boot | character | ground actually bound | meshes |
|
||
|---|---|---|---|
|
||
| default / `?classic=1` | *(none — frozen v1)* | grass · djsim-road · djsim-footpath · brickpave, **no tints** | 5 |
|
||
| `darwin_real` | top-end red dust · sky `monsoon` | **reddust** · asphalt1 #c8a894 · footpath1 #e0c4a0 | 4 |
|
||
| `hobart_real` | cold-light southern · `cold-front` | **grass-temperate** #c0d4c8 · asphalt2 #9ea6ae · djsim-footpath #b4bcc2 | 4 |
|
||
| `bendigo_real` | goldfields dry · `heat-haze` | **grass-dry** #cbbf92 · asphalt1 #c8bfa8 · footpath2 #e6dcc4 | 4 |
|
||
|
||
*(4 meshes not 5 on real towns: no market district ⇒ no plaza quad. Unchanged from pre-R38.)*
|
||
|
||
### §1.4 — backyard Australia: **+0 draws for every box prop**, and a `yard` lot is finally a yard
|
||
Built **inside `buildings.js`'s existing `boxMats`** — the same per-chunk InstancedMesh that already
|
||
carries shells, parapets and verandah posts. A `world/yards.js` could not reach it and would have paid
|
||
+1 draw × up to 31 live chunks against a margin of 8.
|
||
- **181 lots on the default boot: 145 `house`, 36 `yard`** (the brief's number, confirmed at runtime).
|
||
- The dwelling is now **inset off its boundaries** — front garden, side gaps, and a back yard to put
|
||
things in. Pre-R38 the shell was the whole lot, which is why 145 houses had nowhere for a hoist.
|
||
- **A `yard` lot had fallen through to `buildShopfront()` since v1** — 36 lots Lane A explicitly labels
|
||
"yard" rendered as blank 3.2 m sheds with a decorative door and two windows. They are now fenced
|
||
blocks with a hoist, a fibro shed and often a tank. Net ~+100 tris each, because the shopfront it
|
||
replaces was ~230.
|
||
- **Collision is byte-identical**: `lotCollider(lot)` is unchanged on every lot, and the lot rect is
|
||
exactly where the fence now stands.
|
||
- Per lot: fence 5 boxes (back + 2 sides + a front picket split around a gate) · hoist 3 (mast + two
|
||
crossed arms = four arms) · aerial 3 (72% of roofs) · tank 2 (two boxes at 45° = a round tank in
|
||
silhouette for 24 tris). **12 tris a box, ~130 tris a yard.**
|
||
- **`?yards=0` reverts permanently** and reproduces HEAD exactly (see the table above); `?classic=1`
|
||
forces it off.
|
||
|
||
**THE DEFECT MY OWN NUMBERS WERE GREEN THROUGH, AGAIN.** The washing shipped at 256 instances, +1 draw,
|
||
+512 tris, cap respected — **and was invisible from every angle a player can stand at.** Two causes,
|
||
both only findable in a picture: (1) the instance origin used the unscaled `PEG_DROP` constant instead
|
||
of the per-instance `sy`, so every garment hung with its pegs *below* the line; (2) the paling fence was
|
||
**1.75 m and the hoist 1.92 m**, so a whole street of washing sat behind its own back fence. Fixed by
|
||
measuring against the object rather than the spec: fence **1.60**, hoist **2.15** (a real hoist is
|
||
adjustable to 2.4 and a real paling fence is 1.5–1.8), and the peg line is now per-instance. The hoist's
|
||
arm was also a **6.4 m machine in a 5 m yard** — the cap was on the half-span and read as the full one.
|
||
*R37's lesson cost me a second round to relearn: green metrics over a wrong world.*
|
||
|
||
### §1.4's washing — **the inverted wind weight, and why it is town-wide**
|
||
`wind.js:38-45` computes `wH = clamp(y/4.4,0,1); wH *= wH` — top-weighted, base planted. Correct for a
|
||
gum, **upside down for a sheet**, which is pegged at the top and free at the hem. On the shipped
|
||
amplitude (`updateWind`: 0.05 calm → 0.35 gusty) a 1.6 m line of washing under the canopy weight moves
|
||
**0.10 cm calm / 0.67 cm gusty at the hem, and most at the pegs.** I wrote the mode rather than dropping
|
||
the washing: `applyWind(mat, {mode:'pegged'})` weights by distance **below** the peg line — hem 1, pegs 0
|
||
— giving **4.5 cm calm / 31 cm gusty at the hem, a ~52× gain and in the right place**, plus a faster
|
||
flutter and a Z-flap because a sheet is a sail and a gum is a spring. **The `canopy` string is
|
||
byte-for-byte what it was**, so the gum-tree program compiles from the same source and the R17 covenant
|
||
argument is untouched. A third mode, `wing`, weights by |x| from the centreline for the magpie. All three
|
||
ride the one shared `uWindAmp`, so dead calm is still exactly `* 0.0`.
|
||
|
||
`web/js/world/washing.js` is **ONE town-wide InstancedMesh — 1 draw at any N**, cap **256**, spent
|
||
proximity-first and refilled off `chunks.onChunkBuilt` / `onChunkDisposed` (the seam R37 §0.4 wired for
|
||
exactly this). A per-chunk washing mesh is "+1 draw" the way any per-chunk thing is: **+1 × up to 31
|
||
live chunks = 4× the entire remaining budget.** It is **free when empty and to the vendored line**:
|
||
all 21 real towns have **0 hoists ⇒ no mesh ⇒ exactly 0**, and at night `count = 0` (the washing comes
|
||
in) — measured `washingCount: 0, magpieCount: 0` at segment NIGHT. Pegs come off `hoistAnchors(plan,
|
||
citySeed)`, exported from `buildings.js` and derived from the **same `yardLayout`** the geometry is
|
||
built from, so a line of washing cannot hang next to a hoist that moved. 181 hoists → 394 items → 256
|
||
active on the default boot. Fabric is a canvas weave: no fetch, `?noassets` is one code path.
|
||
|
||
### §1.3 — the magpie: **+1 draw while he is on screen, exactly 0 otherwise; 182 tris of 900**
|
||
`web/js/world/magpie.js`. One town-wide InstancedMesh, `count` 0 or 1. Merged primitives with baked
|
||
vertex colours — black body, **white nape, white rump, white wing bars**. `swoopPos(launch, t)` is
|
||
exported and pure (the tram's contract); the only state is *when* a launch starts, recorded as three
|
||
numbers. The ice-cream-container hat is **cut** and stays cut — no street verb exists.
|
||
|
||
**Territory scales with the road graph**, which was the binding correction and is the whole difference
|
||
between a feature and a rumour:
|
||
|
||
| town | road graph | territories | nearest to spawn |
|
||
|---|---|---|---|
|
||
| synthetic default | 22 edges, **6.78 km** | **6** | — |
|
||
| `darwin_real` | 46.85 km | **39** | — |
|
||
| `hobart_real` | 98.42 km | **82** | — |
|
||
| `bendigo_real` | **2,593 edges, 188.6 km** | **157** | **120 m** (median 1,356 m) |
|
||
|
||
One per ~1.2 km of street, and only the **nearest** is ever instantiated, re-picked on the chunk seam.
|
||
That is the *swooping* density, not the magpie density: territories are 2–4 ha so a suburb is thick with
|
||
magpies and ~90% of males never swoop, so the sparse layer IS the 10%. A capacity-4 magpie on Bendigo
|
||
would have put the nearest bird ~20 km away.
|
||
|
||
**THE SEASON MECHANISM I SHIPPED FIRST WAS VACUOUS, AND THE SWEEP CAUGHT IT.** A 364-day year with
|
||
swooping from ~1 August is real and completely unreachable (day 1 is January, a game day is 240 s). So I
|
||
seeded a per-town year offset — `rng(citySeed, 'swoopseason', 0)`. **`plan.citySeed` is the `?seed=`
|
||
value and the town key is deliberately NOT in it** (plan_osm.js:515 keeps it off the plan so the
|
||
Melbourne golden stays frozen), so all 23 towns share one seed, one offset, one answer: measured across
|
||
the corpus, **24 of 24 towns were 214 days from swooping season.** The mechanism produced a constant, and
|
||
it would have gated green because `?magpie=1` forces it. Re-keyed on **(citySeed, townKey)**:
|
||
|
||
- **`newtown_real` is in swooping season on arrival on the default seed** — a real, unforced sighting.
|
||
- `marrickville_real` 8 days out · `redhill_real` 12 · `bowral_real` 20 · `fitzroy_real` 50.
|
||
- **129 of 1,000 `?seed=` values (12.9%)** put the synthetic default in season on day 1 — against a
|
||
design ratio of 42/364 = 11.5%. The distribution is the intended one, not an accident of one seed.
|
||
|
||
**The latch, and F's gate arm, demonstrated:**
|
||
|
||
| | value |
|
||
|---|---|
|
||
| after 3 swoops at 12 m | `gotYouToday: **true**`, `swoopsToday: 3`, `launches: 3` |
|
||
| after `game.sleep()` | `gotYouToday: **false**`, `swoopsToday: **0**`, day 8→9, `dayRolls: 4` |
|
||
| **new localStorage keys** | **`[]`** |
|
||
| save file mentions magpie/swoop/washing/yard | **false** (keys: cash, collection, day, savedAt, schema, town, wants) |
|
||
|
||
The day roll also **puts him back on his pole** — and that is not decoration. The first run of this
|
||
measurement read `swoopsToday 1 → 0` but `gotYouToday` still **true**: the clear had worked and was then
|
||
re-set half a second later by the leg already in the air. A gate would have called a working clear a
|
||
failure. He now perches, and a swoop cannot be in progress across a night.
|
||
|
||
**A bird perched on thin air.** The first cut put the anchor at an arbitrary distance along the edge and
|
||
he hovered over the road. Territories now snap to `furniture.js`'s own streetlight rule (`s = 8, 26, 44…`,
|
||
alternating sides, `width/2 + 0.7`) so he sits on a pole that exists. Perched, the instance is squashed
|
||
to 0.42 in X — the wings are baked into the one shared geometry, so folding them is the only pose there
|
||
is, and a magpie sitting with its wings spread is not a magpie sitting.
|
||
|
||
### §1.1 — the audio inhabitation layer: **SIX new mixer layers, zero draws, zero tris — and it PANS**
|
||
Not a generalisation of `audio.js:300-311`: those are hardcoded bed layers, one per source class, each
|
||
with a bespoke rule. This is a generic point-source system — a table of six, each with its own mixer
|
||
layer, its own **seeded anchor set derived from the plan**, its own falloff, hours and day-of-week rule.
|
||
Nothing in it touches the scene graph; the zero is structural, not careful.
|
||
|
||
**There is panning now, and there wasn't.** `playSfx` is mono with a gain multiplier, so distance worked
|
||
and direction did not — and a dog equally loud in both ears is a dog nowhere. Rather than write that down
|
||
as a limitation, each layer gets a **`StereoPannerNode`** driven by the bearing-to-anchor dotted with the
|
||
camera's world right vector (`matrixWorld` column 0). Fail-soft to mono where `createStereoPanner` is
|
||
absent, with `state.ambient.panning:false` saying so. Measured live: pan spread **−0.71 … +0.85**.
|
||
Day-of-week is derived **locally**: `dowOf(day) = (day-1) % 7` off `PROCITY.game.day` — not read from the
|
||
gig week, not imported, not persisted.
|
||
|
||
Standing on a residential street at midday, day 5 (Friday), after a real user gesture unlocked the
|
||
context — **9 buffers loaded**, and Lane E's six sounds landed mid-round in exactly the banks the table
|
||
expects:
|
||
|
||
| source | anchors | status | measured |
|
||
|---|---|---|---|
|
||
| `dog-bark-fence` | 63 | **audible** | 15 m, gain 0.169, **1 play** |
|
||
| `magpie-carol` | 57 | **audible** | 1.1 m, gain 0.221, **pan 0.85**, 1 play |
|
||
| `radio-through-window` | 81 | **audible** | 15 m, gain 0.01 (edge of a 17 m radius) |
|
||
| `sprinkler` | 32 | idle | in range, **out of hours** (6–10) |
|
||
| `mower-distant` | 8 | idle | **weekend only** — day 5 is a Friday |
|
||
| `roller-door` | 39 | idle | 222 m, radius 42 |
|
||
|
||
**The falsifiability control, run on a third port with a patched manifest** (`sfx["dog-bark-fence"]`
|
||
renamed to `dog-bark-fence-MISSING`, everything else byte-identical): that one source reports
|
||
**`no-entry`, plays 0, and never reaches `audible`**, 8 buffers instead of 9, the other five unchanged,
|
||
**zero console errors**. A missing key is silence by house law, so `state.ambient.sources[key].status` is
|
||
the only thing that can tell a missing key from a quiet street — which is why it exists.
|
||
|
||
**Scope, stated rather than faked:** anchors come from the plan, and all 21 real towns are 100%
|
||
`use:'shop'` in one district, so on a real town **three of six report `no-anchor`** (dog, mower,
|
||
sprinkler) instead of being pretended onto shopfronts. `radio-through-window` (flats above shops,
|
||
`storeys ≥ 2`), `roller-door` and `magpie-carol` (verge trees off the road graph) work everywhere:
|
||
measured on `darwin_real` 0/0/2/0/2/379 and `bendigo_real` 0/0/2/0/7/402. **6 of 6 on the default boot,
|
||
3 of 6 on real towns, and 6 of 6 everywhere the moment A's civic fetch lands districts.**
|
||
|
||
*One honest gap: audible OUTPUT cannot be confirmed headlessly on a first pass — the autoplay policy
|
||
needs a trusted gesture, and the automation browser's synthetic click does not always count. A real
|
||
click through the browser tool did unlock it (`ready: true` before I forced anything), and everything
|
||
downstream of the unlock — manifest resolve, fetch, decode, layer gain, pan, one-shot scheduling, play
|
||
counters — is measured above. Nobody in this loop has ears.*
|
||
|
||
### Lane E's R38 findings, taken and recorded
|
||
1. **`hills_hoist` and `tv_aerial` failed decisively** (thin wire → thousands of fragments; a voxel
|
||
remesh coarse enough for 600 tris erases the pole). Already built as `boxMats` primitives; they are
|
||
now **permanently primitive**, which is also the +0-draw mechanism.
|
||
2. **`magpie` (894 tris) NOT ADOPTED this round, and the reason is the white.** E's caveat is that it
|
||
reads as a **crow** — traced to the operator's PBR bake, our decimation exonerated. Against it: my
|
||
procedural bird is **182 tris (4.9× lighter), carries the white nape/rump/wing-bars as vertex
|
||
colours, and fetches nothing**. A magpie seen for 1.2 s in peripheral vision **is** its white bar;
|
||
a uniformly dark bird is a currawong and the joke does not land. **§1.3 does not hold for the tint —
|
||
the GLB does.** The swap point is `birdGeometry()` / `birdMaterial()`, two singletons, one call site
|
||
each. Deliberately **no speculative `loadGLB`**: probing for a file nobody has shipped is the blind
|
||
probe R26 spent a round killing.
|
||
3. **`paling_fence` (1,121 tris) and `water_tank` (500 tris): no, on arithmetic.** This town wants
|
||
**905 fence runs** (5 × 181 lots) ⇒ **1.01 M triangles** against ~75 k spare, and neither can ride
|
||
`boxMats`, so both would also pay draws. 24 tris of boxes is the answer, not a stopgap.
|
||
4. **`furniture.js:30`'s "novelty_record is 26.5k tris" was stale by six epochs** — 8,000 since R5.
|
||
Comment corrected; the prop is still not in `GLB_FILES`, so the primitive draws either way, but the
|
||
reason on file was wrong. Whether to wire it is a round that measures it.
|
||
|
||
### Honest notes / handed on
|
||
1. **Zero fetch delta.** `?noassets=1` fetches **the same 23 assets, same list**, on HEAD and on R38.
|
||
All four items are procedural: they add no network at all. (The 23 skin JPEGs under `?noassets` are
|
||
pre-existing `skins.js` behaviour, not mine — the asset law as implemented gates the manifest, the
|
||
rig fleet, interior GLBs and audio, not the skin textures. Recorded, not touched.)
|
||
2. **`?classic=1` constructs none of it**: `washing` null, `magpie` null, `ambient` OFF, `yards` false,
|
||
no meshes in the scene, ground hashes unmoved.
|
||
3. **§1.4 is synthetic-only and says so.** Real towns have 0 house/yard lots, so 0 hoists, 0 washing,
|
||
0 yard props — and the washing layer's mesh is never created there, which is where its "free on real
|
||
towns" claim comes from.
|
||
4. The four `THREE.PointerLockControls` console errors are the automation browser refusing Pointer Lock,
|
||
same as R30/R37. **0** other console errors on every boot tested (default · `?classic=1` · `?yards=0` ·
|
||
`?washing=0&magpie=0&ambient=0` · `?noassets=1` · `?magpie=1` · darwin · hobart · bendigo · the
|
||
patched-manifest control).
|
||
5. Shots: `docs/shots/laneB_r38/` — the hoist with washing on it over a paling fence, a residential
|
||
street with aerials and front fences, an overhead row, the magpie perched on a streetlight and
|
||
mid-swoop, and Darwin / Hobart / Bendigo showing three different grounds and three different skies.
|
||
|
||
---
|
||
|
||
## Round 37 (v8 WAVE 0 §0.1 + §0.4) — **THE KERB IS FOOTPATH: shipped at +0 draws, and tris went DOWN**
|
||
|
||
**John's ruling is in the geometry. The road quad narrows from the whole 28 m corridor to the
|
||
carriageway and the EXISTING merged `footGeos` class extends inward to meet it — so the 9 m band
|
||
`registry.js` has called "verandah'd footpath" since R13, and that `buildings.js` has been building
|
||
an awning over for just as long, is finally paved. All 14 gig posters were standing in painted
|
||
bitumen on the boot every player gets; 0 are now. Not one poster moved — the ground did.**
|
||
Commit `47db478`, pinned at `lane-b/r37-kerb-footpath`.
|
||
|
||
### The economy: `+0 draws` is proven, and the charter's tri estimate was wrong in our favour
|
||
True A/B, fresh boots, two **port-isolated no-store servers** — `:8178` serving HEAD (`ground.js` +
|
||
`chunks.js` restored from `git show HEAD:`), `:8177` serving the treatment — at 2560×1440.
|
||
|
||
| | control (HEAD) | treatment | delta |
|
||
|---|---|---|---|
|
||
| synthetic ground | **5 draws** / 224 tris | **5 draws** / 216 tris | **0** / **−8** |
|
||
| walked spine, 102 steps @ 8 m — worst frame | **276** / 99,880 | **276** / 99,872 | **0** / −8 |
|
||
| …sum of draws over all 102 frames | **20,081** | **20,081** | **0 — identical at every step** |
|
||
| 8 bookmarks (`street_noon` … `warehouse_fringe`) | — | — | **0 draws each**, −8 tris each |
|
||
| `katoomba_real` ground | **4 draws** / 9,932 | **4 draws** / 9,644 | **0** / **−288** |
|
||
| katoomba walk, 120 steps — worst / sum draws | 100 / 7,703 | 100 / 7,703 | **0** |
|
||
|
||
**The charter budgeted ~2 k tris and the honest number is negative.** That estimate was made against
|
||
a class that already existed; extending a quad's width re-parameterises it, it does not add geometry.
|
||
The only quads that change count are the ones the `arcade` case *removes* (−8 tris per arcade edge:
|
||
no road quad, no two kerbs, one footpath quad instead of two). 36 arcade edges on katoomba ⇒ −288.
|
||
Fill-rate improves for free — 18 m of every synthetic main street stops being painted as road.
|
||
|
||
### Surfaces proven by RAYCAST, not by reading the code
|
||
Sampling every 0.25 m out from the centreline and recording the **topmost** ground hit:
|
||
|
||
| edge | band (A's `vergeBand`) | what is actually painted |
|
||
|---|---|---|
|
||
| `main`@28 (synthetic) | [5, 14] | road 0–4.75 · kerb 5 · **footpath 5.25–17.5** · grass |
|
||
| `main`@24 (real) | [5, 12] | road 0–5 · **footpath 5.25–15.25** · grass |
|
||
| `side`@12 (real — **72% of the corpus**) | [3, 6] | road 0–3 · **footpath 3.25–9.5** · grass |
|
||
| `lane`@8 (real) | **[4, 4] degenerate** | road 0–4 · footpath 4.25–9 — **pre-ruling geometry** |
|
||
| `lane`@4 (synthetic) | [2, 2] degenerate | road 0–1.75 · kerb 2 · footpath 2.25–5.25 — **unchanged** |
|
||
| `arcade`@5 / @6 | [0, w/2] | **footpath from the centreline out. No road quad, no kerbs.** |
|
||
| **residential** `side`@14 | [3, 7] | road 0–2.75 · kerb 3 · **GRASS 3.25–6.75** · footpath 7–10.5 |
|
||
|
||
**The `lane` case branches on `bandOuter <= bandInner`, never on a band literal** — Fable's R37
|
||
correction landed mid-round and it was the right call: my brief said lanes are `[2,2]`, which is
|
||
synthetic-only; **56 real lane edges are `[4,4]`**. A literal would have been the same species of bug
|
||
this whole item exists to retire. Same reason the code consumes **`vergeBand(e)[0]`, not
|
||
`roadWidth(e)/2`**: both `roadWidth` and `edge.width` are *full* widths, one of the carriageway and
|
||
one of the corridor, and that collision is why the bug survived seven epochs — reading the same call
|
||
A's corrected gate reads means the gate and the geometry cannot drift apart. Inversion (`roadWidth >
|
||
edge.width`) is 0 of 31,039 corpus edges but a `side` under 6 m would do it, so `Math.min(…, half)`
|
||
guards it and lands such an edge on the degenerate path by arithmetic rather than by special case.
|
||
|
||
### THE DEFECT I SHIPPED INTO MY OWN FIRST CUT, AND WHY THE SCREENSHOT CAUGHT IT
|
||
The first cut passed every number — +0 draws, −8 tris, posters clear, no console errors — **and
|
||
painted every intersection shut.** Four roads arriving at a pedestrian plaza; the carriageway simply
|
||
stopped at each junction. The cause is a 3.5 m assumption that was true for seven epochs: a footpath
|
||
outboard of the corridor only ever clipped the *far corner* of a crossing street, so footpath-over-road
|
||
(footpath y 0.02, road y 0.0) was invisible. At **12.5 m** the same strip crosses the whole of the
|
||
perpendicular **carriageway**. Fixed by putting the carriageway on top under the flag (`ROAD_Y`
|
||
0.0 → 0.04): roads run through junctions, footpaths stop at the kerb they meet — which is what an
|
||
intersection actually looks like. Verified on the real graph's **worst 4.3° junction** (R20's
|
||
measurement, still the worst on katoomba): clean, no z-fight, no holes; the near-parallel sliver
|
||
reads as a paved traffic island. Road-on-road overlap in the junction box is unchanged and still
|
||
invisible for R20's original reason — one skin, one y, uniform asphalt.
|
||
*The numbers were all green. Only the picture was wrong. `?verge=0` + a screenshot is why this round
|
||
did not ship a town whose streets don't connect.*
|
||
Shots: `docs/shots/laneB_r37_kerb/` — before/after at one pose (poster in the road → poster on the
|
||
footpath), before/after overhead at a degree-4 junction, street level, katoomba's 4.3° junction.
|
||
|
||
### `?verge=0` — the control, proven EXACT rather than "similar"
|
||
It reverts to pre-ruling geometry and **stays in the codebase forever** (Lane F's falsifiability
|
||
control for the poster gate; A pinned its control to *the flag*, not to a line number).
|
||
- **Ground vertex+UV FNV hashes under `?verge=0` are identical to the HEAD build**, mesh for mesh:
|
||
`c4c72181 / c111d968 / 1a80ebcd / 12c1241b / fd0d3211`. Not "same draw count" — same vertices.
|
||
- **The same detector goes RED** on the default boot (`40c54d19 / a300d701 / bfa81387`), so the
|
||
green means something. Vacuous-gate law applied to my own verification.
|
||
|
||
### CLASSIC IS FENCED, NOT AMENDED — and Fable has the call
|
||
`?classic=1` forces the ruling **off** (`!CLASSIC && params.get('verge') !== '0'` — the R31 dig-flip
|
||
pattern the other flipped flags ride), because a render-side ruling must not move a covenanted town's
|
||
output without an amendment ruling. Proven twice, and the second proof is the one that counts:
|
||
| | classic, HEAD build | classic, this build | default boot |
|
||
|---|---|---|---|
|
||
| ground geometry hashes | c4c72181 / c111d968 / 1a80ebcd / 12c1241b / fd0d3211 | **identical** | **differs** |
|
||
| **ground-only rendered pixels**, 3 poses (640×360 render target, everything but `ground` hidden) | 79df268c / 91564001 / a09d1e52 | **79df268c / 91564001 / a09d1e52** | **1f867636 / 405957ac / d51ca7c9** |
|
||
Hiding everything but the ground is what made this measurable: a full-frame pixel hash differed
|
||
between the two builds by ~30 draws and ~10 k tris **with the ground provably identical** — that was
|
||
citizens spawning on wall-clock time and a different prior chunk state, not my change. A pixel A/B
|
||
that includes a live sim is not an A/B. → **Fable: if classic should take the footpath too, that is
|
||
your amendment ruling and it is a one-token change (drop the `!classic` term).**
|
||
|
||
### §0.4 — the chunk lifecycle seam: the producer was fine, the *handle* was missing
|
||
Diagnosis, measured on HEAD: **`!!PROCITY.chunks.ctx === false`.** The producer has fired
|
||
`ctx.onChunkBuilt` since R3 and Lane D's `sim.onChunkBuilt` has existed just as long, but `ctx` is
|
||
the shell's local object and `createChunkManager` never handed it back — so a consumer holding only
|
||
the manager (which is all `PROCITY.chunks` is) had **nowhere to attach**. LANE_D_NOTES §35 documents
|
||
`chunks.ctx.onChunkBuilt = …`, against a property that did not exist. That is the whole bug.
|
||
Two ways in now, both inert until used: **`chunks.onChunkBuilt(fn)` → `unsubscribe()`** (many
|
||
consumers — the single `ctx` slot is last-writer-wins and the v8 slate has several per-chunk systems)
|
||
and **`chunks.ctx`**, so the documented single-slot form works verbatim.
|
||
|
||
| proof | result |
|
||
|---|---|
|
||
| events over a 40-step walk | **136 built · 144 disposed** |
|
||
| payload | `{cx, cz, buildings, furniture, data}`, `buildings`/`furniture` are Object3D, `data.lots` an array, and **`key === \`${cx},${cz}\`**` |
|
||
| a second consumer | fires too — **does not clobber the first** |
|
||
| the original `ctx` slot | fires, alongside the subscribers |
|
||
| `unsubscribe()` | **0** further events |
|
||
| a subscriber that THROWS | streamer survives, chunks keep building (guarded + `console.warn`) |
|
||
| **zero behaviour change when nobody subscribes** | a 102-step walk recording `[draws, tris, chunks]` **per step** hashes **`ceb1ec66` on BOTH builds** (`?verge=0` so the ground is identical too — any difference would be `chunks.js`) |
|
||
`sim.js` untouched (Lane D's file). The detail object is still allocated only inside the guard, so a
|
||
subscriber-free boot does the same two truthiness tests it has done since R3 and nothing else.
|
||
|
||
### Honest notes / handed on
|
||
1. **The market plaza gets worse before John's amendment, and that is the standing item, not new
|
||
damage.** `ground.js` paints a 46×46 m brickpave square at the origin *on top of the spine road*
|
||
(charter §Standing #1). With the footpath now 12.5 m either side, more of that square reads as
|
||
pavement, and the carriageway (`ROAD_Y` 0.04) now paints over the plaza (0.015) where they cross.
|
||
I did **not** touch the plaza: it is awaiting John's ruling and picking a layer for it is a design
|
||
decision, not a consequence of this one. Recorded so nobody reads it as a regression from §0.1.
|
||
2. Real towns have **0 residential edges** (all lots `use:'shop'`, one `mainstreet` district — the
|
||
review's ruling-2 finding, confirmed from the runtime side), so the grass-verge branch never fires
|
||
there. It fires on 6 synthetic edges. When the civic fetch lands districts, nature strips appear
|
||
on real towns with no further code.
|
||
3. The four `THREE.PointerLockControls` console errors are the automation browser refusing Pointer
|
||
Lock on a synthetic click — environment, not code, same as R30. **0** other console errors on
|
||
every boot tested (default · `?verge=0` · `?classic=1` · `katoomba_real`).
|
||
|
||
---
|
||
|
||
## 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.
|