From 4b0144989bced48875d14170b53f453b6f53af9d Mon Sep 17 00:00:00 2001 From: m3ultra Date: Thu, 16 Jul 2026 20:35:47 +1000 Subject: [PATCH] Lane B round 21 (v4.0-beta): town selector reads E's towns index (ledger #2, the missed R20 #5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the 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 -- tram/ground work is done and verified. Fail-soft by construction: createHUD is synchronous, so the picker is built immediately from the fallback literal and UPGRADED IN PLACE when the index resolves -- if the index never arrives, a working list is already on screen. - index present -> real names/states/shop counts: "Katoomba, NSW · 80 shops", "Fremantle, WA · 80 shops", "Newtown, NSW · 72 shops", "Bendigo, VIC · 36 shops", "Castlemaine, VIC · 24 shops" (roads:false -> "· marched"). - ?noassets -> the fetch is SKIPPED entirely (asset law; the shell doesn't load town caches there either). Measured: 0 index fetches, fallback list, 0 errors. - anything wrong with the index -> the fallback simply stands. Exercised the exact chain: 404, network error, malformed JSON, towns:[], towns-not-an-array, junk entries (no key) -> none throws, none rebuilds from junk; only a good index rebuilds. REAL_TOWNS_FALLBACK is retained ONLY as that fallback (comment says so), no longer the source of truth. Verified live: booting katoomba_real shows "Katoomba, NSW · 80 shops" pre-selected; fits (193px of 230px max); 0 page errors; navigation unchanged (onchange is set once on the `, 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 diff --git a/web/js/world/hud.js b/web/js/world/hud.js index fc93cb5..f9a0371 100644 --- a/web/js/world/hud.js +++ b/web/js/world/hud.js @@ -6,9 +6,12 @@ import * as THREE from 'three'; import { osmTownKeys } from '../citygen/index.js'; // R19 town selector: enumerate OSM fixtures -// R19 (v4.0-alpha, beta pull-forward): the real-roads town caches under web/assets/towns/*.json. Hardcoded -// for the alpha — there is no towns manifest yet; beta derives this from an index file. New towns: add here. -const REAL_TOWNS = ['katoomba_real', 'newtown_real', 'fremantle_real', 'bendigo_real', 'castlemaine_real']; +// R21 (v4.0-beta, ledger #2): the real-roads towns come from E's index (web/assets/towns/index.json), so the +// list grows with the town pack instead of being edited here. This literal is now only the FAIL-SOFT fallback +// — used when the index is missing/unparseable, or under ?noassets (where the shell doesn't fetch town caches +// either, so reading the index would be both pointless and against the asset law). Zero errors either way. +const REAL_TOWNS_FALLBACK = ['katoomba_real', 'newtown_real', 'fremantle_real', 'bendigo_real', 'castlemaine_real']; +const TOWNS_INDEX = 'assets/towns/index.json'; const CSS = ` #pc-hud{position:fixed;inset:0;pointer-events:none;font:13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6;z-index:10} @@ -53,24 +56,46 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } $('pc-town').textContent = plan.name || '—'; $('pc-seed').textContent = plan.citySeed; - // ── town selector (R19, beta pull-forward) — synthetic + OSM fixtures + real-road caches. Navigates - // to the matching URL params, preserving everything else (seed, flags). Lives in the HUD overlay, so it - // hides with the HUD for money shots; usable when the pointer is free (Esc). No index.html seam. ── + // ── town selector (R19; R21 reads E's towns index) — synthetic + OSM fixtures + real-road towns. + // Navigates to the matching URL params, preserving everything else (seed, flags). Lives in the HUD + // overlay, so it hides with the HUD for money shots; usable when the pointer is free (Esc). No + // index.html seam. The real-town list is built SYNCHRONOUSLY from the fallback (so the picker always + // works, including under ?noassets and if the index 404s), then upgraded in place once E's index.json + // resolves — which also gives real names, states and shop counts. ── (() => { const sel = $('pc-town-select'); if (!sel) return; const params = new URLSearchParams(location.search); const cur = params.get('plansrc') === 'osm' ? (params.get('town') || '') : ''; + const NOASSETS = params.get('noassets') != null && params.get('noassets') !== '0'; const cap = (k) => k.replace(/_real$/, '').replace(/(^|[\s_])\w/g, (c) => c.toUpperCase()).replace(/_/g, ' '); - const add = (key, label) => { const o = document.createElement('option'); o.value = key; o.textContent = label; if (key === cur) o.selected = true; sel.appendChild(o); }; - add('', '▸ Synthetic — procedural'); - for (const k of osmTownKeys().filter((k) => !k.endsWith('_real'))) add(k, `${cap(k)} · OSM fixture`); - for (const k of REAL_TOWNS) add(k, `${cap(k)} · real roads`); - if (cur && !Array.from(sel.options).some((o) => o.value === cur)) add(cur, `${cap(cur)} · (current)`); // unknown current town → still shown - sel.onchange = () => { + const opt = (key, label) => { const o = document.createElement('option'); o.value = key; o.textContent = label; if (key === cur) o.selected = true; return o; }; + const build = (realTowns) => { + sel.textContent = ''; + sel.appendChild(opt('', '▸ Synthetic — procedural')); + for (const k of osmTownKeys().filter((k) => !k.endsWith('_real'))) sel.appendChild(opt(k, `${cap(k)} · OSM fixture`)); + for (const t of realTowns) sel.appendChild(opt(t.key, t.label)); + if (cur && !Array.from(sel.options).some((o) => o.value === cur)) sel.appendChild(opt(cur, `${cap(cur)} · (current)`)); // unknown current town → still shown + }; + build(REAL_TOWNS_FALLBACK.map((k) => ({ key: k, label: `${cap(k)} · real roads` }))); + sel.onchange = () => { // set once; rebuilding the options below never clears it const v = sel.value, p = new URLSearchParams(location.search); if (!v) { p.delete('plansrc'); p.delete('town'); } else { p.set('plansrc', 'osm'); p.set('town', v); } location.search = p.toString(); // reload into the chosen town }; + if (NOASSETS) return; // asset law: no fetch — the fallback list stands + fetch(TOWNS_INDEX, { cache: 'force-cache' }) + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { + const towns = j && Array.isArray(j.towns) ? j.towns.filter((t) => t && t.key) : null; + if (!towns || !towns.length) return; // keep the fallback, silently + build(towns.map((t) => ({ + key: t.key, + label: `${t.town || cap(t.key)}${t.state ? ', ' + t.state : ''}` + + (t.shops != null ? ` · ${t.shops} shops` : '') + + (t.roads === false ? ' · marched' : ''), + }))); + }) + .catch(() => {}); // index missing/blocked → fallback stands, zero errors })(); const ray = new THREE.Raycaster();