From 2f4ab5912fc1be5f992077aa2dc7c624f864df86 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 09:49:54 +1000 Subject: [PATCH] Lane B R26 (v5.0-beta): the stocked shopfront reads from the footpath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger #5 — B's polish pick. A stocked godverse shop now tells you it's diggable before you reach the door: a crate of seeded spines under the shop name, and a warm-lamp-over-crates room behind the glass. Cost: +0 draws / +0 tris. The mark is painted into the chunk's EXISTING sign-atlas cell; the window cue rides the EXISTING per-shop aTint attribute. Nothing new is allocated or submitted. The manifest is the authority, deliberately NOT the godverseShopId key: keyed means "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 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. (Second job for ledger #6's manifest, free.) Measured — true A/B at one pose, manifest 200 vs 404: spine-marked sign cells 0 → 3 (18,768 exact-palette px) stocked window tint verts 0 → 36 street_noon 65 draws / 115,502 tris → UNCHANGED Stocked shopfront (Goulds Books, footpath): 85 draws / 144,730 tris — inside street law (<=300 / <=200k). Cues agree with ground truth: 3 stocked shops live → 3 marked cells, 9 stocked window quads. Covenants re-proved (the prefetch is new code on the boot path), tested with the manifest present on disk so a stray fetch would 200 and be unmissable: ?classic=1 0 keyed, 0 stock fetches, 0 errors (zero-fetch covenant) default synthetic 0 keyed, 0 stock fetches, 0 errors manifest 404 18 keyed, fail-soft, 0 page errors Three traps, self-caught: (1) a module-load fetch would have put a fetch delta on ?classic=1 — restructured to a lazy reader + plan-driven prefetch, keyed plans only; (2) the lazy trigger alone raced the first chunk build, so the first stocked shop you approach built unmarked — prefetch moved to createChunkManager, before any chunk builds; (3) the first "zero cost" was VACUOUS (manifest never fetched, so nothing was applied) and the first pixel detector reported 60 marked cells with no treatment at all — replaced with an exact-palette signature that reads 0 in control. Vacuous-gate law applies to a lane's own verification too. Corollary: force-cache serves a deleted file (transferSize 0, status 200 while disk 404s) — poison the cache or you are testing your own memory. B consumes stock_godverse/index.json read-only and emits nothing into it; the temp manifest used to prove this round was deleted — the directory is G's. Shape-tolerant + fail-soft, so G's real emission needs no coordination with B. Co-Authored-By: Claude Opus 4.8 --- docs/LANES/LANE_B_NOTES.md | 82 +++++++++++++++++++++++++++++++++++ web/js/world/buildings.js | 87 +++++++++++++++++++++++++++++++++++++- web/js/world/chunks.js | 3 +- 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/docs/LANES/LANE_B_NOTES.md b/docs/LANES/LANE_B_NOTES.md index bdb23c6..fd25725 100644 --- a/docs/LANES/LANE_B_NOTES.md +++ b/docs/LANES/LANE_B_NOTES.md @@ -1,5 +1,87 @@ # LANE B — NOTES (measured) +## 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 diff --git a/web/js/world/buildings.js b/web/js/world/buildings.js index 6713954..f5becf5 100644 --- a/web/js/world/buildings.js +++ b/web/js/world/buildings.js @@ -24,6 +24,62 @@ const WALL_TINTS = ['#c9b8a0', '#b0b8ab', '#c4b0b0', '#a8b4c0', '#c4bc9c', '#cab const ROOF_TINT = '#5b5148'; const AWNING_SKINS = ['red', 'green', 'blue']; +// ── R26 #5 — the stocked shopfront (B's polish pick) ────────────────────────────────────────────── +// A crate-diggers' shop should read as one FROM THE FOOTPATH. The beta makes every keyed crate +// distinct *inside*; this closes the loop outside, so the player can spot a shop worth digging while +// walking past instead of door-roulette. +// +// THE MANIFEST IS THE AUTHORITY (ledger #6 — the same file F's wire consults). Deliberately NOT the +// `godverseShopId` key: keyed only means "has a stock identity", while the manifest means "a crate is +// actually here". Advertising a crate the shop hasn't got would make the street lie — the one thing +// this epoch keeps refusing to do. Fail-soft: no manifest (or ?noassets, or a shape we don't +// recognise) ⇒ no treatment ⇒ the street simply makes no promise. Never an error. +// +// The mark says "you can dig here", which is equally true of `real` and `mint` crates, so it does NOT +// leak tier — provenance belongs on C's price card and in the gates, not shouted from the footpath. +// +// Cost: ZERO extra draws / tris. The mark is drawn into the chunk's EXISTING sign-atlas cell and the +// window cue rides the EXISTING per-shop aTint attribute. Nothing new is allocated or submitted. +// The manifest is fetched LAZILY — only once a plan actually carries a keyed shop. A module-load fetch +// would have hit every boot: a pointless request (and, before G emits the file, a console 404) on the +// synthetic default, and a fetch delta on ?classic=1, whose covenant is zero-fetch. Godverse towns pay +// for it; nobody else knows it exists. +const STOCK_INDEX = 'assets/stock_godverse/index.json'; +let STOCKED = null; // Set | null = "unknown" (⇒ no treatment) +let stockReq = null; // in-flight/settled marker — the fetch happens at most once per session +function ensureStockIndex() { + if (stockReq) return; + stockReq = (async () => { + try { + const p = new URLSearchParams(location.search); + if (p.get('noassets') != null && p.get('noassets') !== '0') return; // asset law: zero fetch + const r = await fetch(STOCK_INDEX, { cache: 'force-cache' }); + if (!r.ok) return; // 404 before G emits it — fine + const j = await r.json(); + // Tolerant of the exact shape (C pins §7 this round, G emits next wave): accept an array at + // .shops/.entries/.atlases or the root, of ids or of objects carrying godverseShopId|id. + const list = Array.isArray(j) ? j : (Array.isArray(j?.shops) ? j.shops + : Array.isArray(j?.entries) ? j.entries : Array.isArray(j?.atlases) ? j.atlases : null); + if (!list) return; + const set = new Set(); + for (const e of list) { + const id = typeof e === 'number' ? e : (e && (e.godverseShopId ?? e.id)); + if (Number.isSafeInteger(id) && id > 0) set.add(id); + } + if (set.size) STOCKED = set; + } catch { /* fail-soft by design */ } + })(); +} +const isStocked = (shop) => !!(STOCKED && shop && Number.isSafeInteger(shop.godverseShopId) && STOCKED.has(shop.godverseShopId)); + +// Called once by the chunk manager at construction: start the manifest fetch as early as possible, but +// still ONLY for a plan that has keyed shops. Without this the fetch would start on the first keyed +// CHUNK — i.e. the first stocked shop you walk up to would be built before the manifest landed and +// would miss its mark. Synthetic / real / classic plans have no keyed shops ⇒ no request, ever. +export function prefetchStockIndex(plan) { + if ((plan?.shops || []).some((s) => Number.isSafeInteger(s && s.godverseShopId))) ensureStockIndex(); +} + // ── Shop-hours facade state (§3.5): closed shops show dark windows + a CLOSED plate ───────────── // ONE shared window material + ONE shared CLOSED-plate material across every chunk, driven by two // shared uniforms that update ONCE on a day-segment change (via the 'procity:segment' event @@ -65,7 +121,14 @@ const ROOM_TINTS = [ // muted 90s interior wall colours (RGB 0..1), seeded per s [0.56, 0.44, 0.33], [0.42, 0.47, 0.52], [0.52, 0.47, 0.37], [0.47, 0.42, 0.46], [0.54, 0.50, 0.41], [0.40, 0.45, 0.40], [0.58, 0.49, 0.40], [0.45, 0.48, 0.55], ]; -function roomTint(shop) { return ROOM_TINTS[((shop ? shop.seed : 12345) >>> 0) % ROOM_TINTS.length]; } +// R26 #5: a stocked shop's window room reads warm-lamp-over-crates rather than a generic 90s wall — +// the second half of the footpath tell, and free (it rides the existing per-shop aTint attribute, so +// the winmap glass is still ONE shared material / one draw per chunk). +const STOCKED_ROOM_TINT = [0.50, 0.33, 0.22]; +function roomTint(shop) { + if (isStocked(shop)) return STOCKED_ROOM_TINT; + return ROOM_TINTS[((shop ? shop.seed : 12345) >>> 0) % ROOM_TINTS.length]; +} let _winmapMat = null; function interiorWindowMaterial() { @@ -241,6 +304,9 @@ export function buildChunkBuildings(chunkData, ctx) { const { skins } = ctx; const group = new THREE.Group(); group.name = 'chunk-buildings'; + // R26 #5: ask for the stock manifest only if this town has keyed shops at all (once per session). + // Synthetic / real / classic boots never carry a godverseShopId, so they never make the request. + if ((chunkData.shops || []).some((s) => Number.isSafeInteger(s && s.godverseShopId))) ensureStockIndex(); const boxMats = []; // {matrix, color} — shells + parapets + posts, ONE instanced draw/chunk const awnings = []; // {matrix, color} — all awning/canopy skins, ONE instanced draw/chunk @@ -286,12 +352,31 @@ export function buildChunkBuildings(chunkData, ctx) { do { actx.font = `bold ${fs}px Arial, sans-serif`; fs -= 4; } while (actx.measureText(label).width > CELL_W - 40 && fs > 22); actx.fillText(label, px + CELL_W / 2, py + CELL_H / 2); + if (isStocked(shop)) drawCrateMark(px, py, shop); // R26 #5 — same cell, no extra draw const uv = [px / ATLAS_W, 1 - (py + CELL_H) / ATLAS_H, (px + CELL_W) / ATLAS_W, 1 - py / ATLAS_H]; signUV.set(shop.id, uv); signCell++; return uv; } + // A crate of records seen end-on, along the sign's foot: the shop is worth digging. Seeded off + // shop.seed so two stocked shops don't wear the same spines (the beta's whole theme, on the + // street). Pure canvas into the cell already being drawn ⇒ zero draws, zero tris. + const SPINES = ['#2f2a26', '#7a2f28', '#2c4553', '#6b5a2e', '#3f2f45', '#8a6a3a', '#26463a']; + function drawCrateMark(px, py, shop) { + const y = py + CELL_H - 40, h = 24; + const x0 = px + 30, x1 = px + CELL_W - 30; + actx.fillStyle = 'rgba(0,0,0,0.30)'; // the crate itself, in shadow + actx.fillRect(x0 - 7, y - 5, (x1 - x0) + 14, h + 10); + let r = ((shop.seed >>> 0) || 1) ^ 0x5bf03635; + const rnd = () => ((r = (r * 1664525 + 1013904223) >>> 0) / 4294967296); + for (let x = x0; x < x1 - 3; x += 6) { // the spines: leaning, uneven, dug-through + const hh = h - ((rnd() * 7) | 0); + actx.fillStyle = SPINES[(rnd() * SPINES.length) | 0]; + actx.fillRect(x, y + (h - hh), 4, hh); + } + } + // ── per-lot build ── for (const lot of chunkData.lots) { const shop = shopByLot.get(lot.id); diff --git a/web/js/world/chunks.js b/web/js/world/chunks.js index 94fbf1f..a490d08 100644 --- a/web/js/world/chunks.js +++ b/web/js/world/chunks.js @@ -5,7 +5,7 @@ // buildings (buildings.js) + furniture (furniture.js); ground/sky/lighting are global. import { chunkIndex, chunkCoord, chunkKey, parseKey } from './planutil.js'; -import { buildChunkBuildings } from './buildings.js'; +import { buildChunkBuildings, prefetchStockIndex } from './buildings.js'; import { buildChunkFurniture } from './furniture.js'; const BUILD_BUDGET_MS = 4; @@ -14,6 +14,7 @@ export function createChunkManager(plan, scene, ctx) { const R = ctx.radius ?? 3; const DISPOSE_R = R + 1; const index = chunkIndex(plan); + prefetchStockIndex(plan); // R26 #5: keyed plans only — warm the stock manifest before any chunk builds const live = new Map(); // key → { buildings, furniture, colliders, cx, cz } const doorMeshes = []; // live door meshes (for HUD raycast)