// PROCITY Lane B — hud.js // The street HUD: crosshair, shop-name tooltip (throttled centre-screen raycast, far = 6m), // time-of-day + seed + live perf readout (renderer.info: draw calls & triangles), and the // door-click → `procity:enterShop` hand-off. Creates its own DOM overlay so index.html stays thin. import * as THREE from 'three'; import { osmTownKeys } from '../citygen/index.js'; // R19 town selector: enumerate OSM fixtures // 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} #pc-cross{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px 0 0 -3px;border-radius:50%;background:#fff;opacity:.7;box-shadow:0 0 3px #000} #pc-tip{position:absolute;left:50%;top:calc(50% + 22px);transform:translateX(-50%);background:rgba(20,16,10,.82);padding:6px 12px;border-radius:6px;white-space:nowrap;opacity:0;transition:opacity .12s;text-shadow:0 1px 2px #000} #pc-dbg{position:absolute;right:10px;top:10px;text-align:right;background:rgba(20,16,10,.55);padding:8px 11px;border-radius:8px;min-width:150px} #pc-dbg b{color:#ffd75e;font-weight:600} #pc-dbg .warn{color:#ff9a6a} #pc-clock{font-size:15px;margin-bottom:3px} #pc-toast{position:fixed;left:50%;bottom:64px;transform:translateX(-50%);background:rgba(20,16,10,.9);padding:10px 18px;border-radius:10px;font-size:15px;opacity:0;transition:opacity .25s;text-shadow:0 1px 2px #000;z-index:31;max-width:80vw;font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6;pointer-events:none} #pc-help{position:absolute;left:12px;bottom:12px;background:rgba(20,16,10,.5);padding:7px 11px;border-radius:8px;opacity:.85} #pc-townpick{position:absolute;left:10px;top:10px;pointer-events:auto;opacity:.8;transition:opacity .12s} #pc-townpick:hover{opacity:1} #pc-townpick select{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);border-radius:8px;padding:5px 9px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;max-width:230px;cursor:pointer} #pc-townpick select:focus{outline:1px solid rgba(255,215,94,.6)} #pc-game{position:fixed;right:10px;bottom:12px;z-index:30;display:flex;flex-direction:column;align-items:flex-end;gap:8px} #pc-gamebar{background:rgba(20,16,10,.72);padding:6px 10px;border-radius:10px;display:flex;gap:10px;align-items:center;pointer-events:auto;text-shadow:0 1px 2px #000} #pc-gamebar b{color:#ffd75e;font-weight:600} #pc-gamebar button{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);border-radius:8px;padding:4px 10px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;cursor:pointer} #pc-gamebar button:hover{border-color:rgba(255,215,94,.7);color:#ffd75e} #pc-crate{width:344px;max-height:min(58vh,470px);overflow-y:auto;background:rgba(20,16,10,.93);border:1px solid rgba(255,215,94,.35);border-radius:12px;padding:10px 12px;pointer-events:auto;display:none} #pc-crate h3{font-size:13px;color:#ffd75e;margin:0 0 6px;font-weight:600;letter-spacing:.04em} .pc-crate-row{display:flex;gap:9px;align-items:center;padding:5px 0;border-top:1px solid rgba(255,244,230,.08)} .pc-thumb{width:44px;height:44px;flex:none;border-radius:4px;background-color:#3a3128;background-repeat:no-repeat;display:flex;align-items:center;justify-content:center;color:rgba(255,244,230,.55);font-size:17px;box-shadow:0 1px 3px rgba(0,0,0,.5)} .pc-crate-main{min-width:0} .pc-crate-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .pc-crate-artist{opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .pc-crate-meta{font-size:11px;opacity:.62} .pc-crate-empty{opacity:.7;padding:4px 0 2px} `; // [Lane B R40 §40.3] `budget` — the draw/tri law THIS boot answers to ({ draws, tris }). The default // is the street law; the shell passes its diagnostic ceiling under a ?r= override so the readout's // red warn tells the truth about the boot being run rather than shaming a documented diagnostic. export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop, budget = { draws: 300, tris: 200_000 } }) { if (!document.getElementById('pc-hud-style')) { const st = document.createElement('style'); st.id = 'pc-hud-style'; st.textContent = CSS; document.head.appendChild(st); } const root = document.createElement('div'); root.id = 'pc-hud'; root.innerHTML = `
town
seed
fps
draws · tris
chunks
WASD move · shift run · mouse look · click a door · [ ] time · P shot · Esc release
`; document.body.appendChild(root); const $ = (id) => root.querySelector('#' + id); const elTip = $('pc-tip'), elClock = $('pc-clock'), elFps = $('pc-fps'), elDraws = $('pc-draws'), elTris = $('pc-tris'), elChunks = $('pc-chunks'), elToast = $('pc-toast'); // [R35] toasts surface in EVERY mode (beats fire inside shops; a SLEEP taken at a counter must // still show its morning paper) — same body-mount rule as #pc-game, same dispose duty. document.body.appendChild(elToast); $('pc-town').textContent = plan.name || '—'; $('pc-seed').textContent = plan.citySeed; // ── town selector (R19; R21 reads E's towns index) — synthetic + OSM fixtures + real-road towns. // [Lane B R40 §40.3 — RULED, no longer a debt-ledger question] `?classic=1` SHOWS this selector: // "classic is the shop-window mode; hiding the 23 towns from it serves nobody — it's information, // not gameplay state" (ROUND40 rulings). It was already true by construction (this block has no // flag test) and is now DELIBERATE: the selector is zero-draw DOM chrome (a