Lane B R26 (v5.0-beta): the stocked shopfront reads from the footpath
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 <noreply@anthropic.com>
This commit is contained in:
parent
4f0adf12d8
commit
2f4ab5912f
@ -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
|
||||
|
||||
@ -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<godverseShopId> | 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);
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user