From 7b26e45fa07346cd5d531ae69d0e3c0c8cebd31c Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 20 Jul 2026 19:01:40 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20B=20R33:=20THE=20WANTLIST=20in=20the=20c?= =?UTF-8?q?rate=20panel=20=C2=B7=20the=20arrival=20toast=20=C2=B7=20the=20?= =?UTF-8?q?build-race=20fix=20the=20shot=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hud.js: THE WANTLIST section in the crate (non-empty only — no header over nothing): hunted rows, ✕ stops hunting, refresh watches wants length so an open panel follows adds/clears. THE FIX: R32's game surface BUILT only in street update() frames — a door entered fast after boot had no crate/bar inside at all, and the interior tickGame() refreshed a UI that never existed. Every gate idles on the street long enough to build, so green gates missed it; the R33 money-shot script entered faster and exposed it. tickGame() now builds-or-refreshes in ANY mode (shots are a detector, not decoration — filed in LANE_F_NOTES §33). index.html: game.traveled at boot → '🚌 — the ride cost a day. Day N' toast (save.js already charged the fare and re-keyed the week before the shell read game.day). --- web/index.html | 3 +++ web/js/world/hud.js | 56 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/web/index.html b/web/index.html index 82504fe..7e2a4b3 100644 --- a/web/index.html +++ b/web/index.html @@ -281,6 +281,9 @@ const gigState = (GIGS_ON && plan.gigs && plan.gigs.length) ? createGigState({ p // day-1 stock streams (§30.3). Skipped entirely when either layer is off. const weekNightOf = (day) => (Math.max(1, day) - 1) % 7; if (game && gigState) gigState.setWeekNight(weekNightOf(game.day)); +// [R33 — TRAVEL COSTS A DAY] this boot adopted a save from another town: the ride ate a day (save.js +// adopt() did the increment + pull-pruning; weekNight above already read the incremented day). Tell John. +if (game && game.traveled) hud.showToast(`🚌 ${plan.name || 'a new town'} — the ride cost a day. Day ${game.day}`, 4); // [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter() // time (door click), long after citizens is initialized — so the forward reference is safe. const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, diff --git a/web/js/world/hud.js b/web/js/world/hud.js index 155c458..00de507 100644 --- a/web/js/world/hud.js +++ b/web/js/world/hud.js @@ -173,7 +173,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } wrap.append(panel, bar); document.body.appendChild(wrap); const elDay = bar.querySelector('#pc-day'), elCash = bar.querySelector('#pc-cash'), btnCrate = bar.querySelector('#pc-crate-btn'), btnSleep = bar.querySelector('#pc-sleep-btn'); - let shownDay = null, shownCash = null, shownCount = null, open = false; + let shownDay = null, shownCash = null, shownCount = null, shownWants = null, open = false; function rebuildList() { const col = game.collection || []; @@ -184,7 +184,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } panel.appendChild(h); if (!col.length) { const d = document.createElement('div'); d.className = 'pc-crate-empty'; - d.textContent = 'Empty — go dig.'; panel.appendChild(d); return; + d.textContent = 'Empty — go dig.'; panel.appendChild(d); } for (let i = col.length - 1; i >= 0; i--) { // newest find first const e = col[i]; @@ -202,6 +202,29 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } main.append(t, a, meta); row.append(th, main); panel.appendChild(row); } + // [R33] THE WANTLIST — what you're hunting (☆ on a pull card adds; ✕ here removes; buying one + // clears it automatically). Section exists only when non-empty — no stray header over nothing. + const wants = game.wants || []; + if (wants.length) { + const wh = document.createElement('h3'); + wh.style.marginTop = '10px'; + wh.textContent = `THE WANTLIST — ${wants.length} hunted`; + panel.appendChild(wh); + for (const w of wants) { + const row = document.createElement('div'); row.className = 'pc-crate-row'; + const main = document.createElement('div'); main.className = 'pc-crate-main'; + const t = document.createElement('div'); t.className = 'pc-crate-title'; + t.textContent = w.artist || w.title || w.sku; + const a = document.createElement('div'); a.className = 'pc-crate-artist'; + a.textContent = w.artist && w.title ? `“${w.title}”` : ''; + main.append(t, a); + const x = document.createElement('div'); + x.textContent = '✕'; x.title = 'stop hunting this'; + x.style.cssText = 'cursor:pointer;opacity:.6;padding:0 4px;flex:none'; + x.onclick = () => { game.removeWant(w); rebuildList(); }; + row.append(main, x); panel.appendChild(row); + } + } } function setOpen(v) { open = v; panel.style.display = v ? 'block' : 'none'; @@ -222,6 +245,8 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } shownCount = col.length; btnCrate.textContent = `CRATE ${shownCount}`; if (open) rebuildList(); } + const nw = (game.wants || []).length; // [R33] want add/clear refreshes an open panel too + if (nw !== shownWants) { shownWants = nw; if (open) rebuildList(); } // [R32] the surface yields to the map and to the dig/sell cards (they own the screen), and shows // everywhere else — street AND interior. Cheap: a style write only when the answer changes. const P = window.PROCITY, im = P && P.interiorMode; @@ -281,14 +306,11 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } frame++; if (frame % 6 === 0) raycastDoor(); - // v7 game surface — lazy construction (the shell builds the game after the HUD; null under - // ?classic=1 / ?game=0 ⇒ this never fires), then cheap getter polls offset from the raycast frames. - if (!gameUI) { - if (frame % 30 === 0) { - const g = window.PROCITY && window.PROCITY.game; - if (g) gameUI = buildGameUI(g); - } - } else if (frame % 6 === 3) gameUI.refresh(); + // v7 game surface — lazy construction + refresh both live in tickGame() (R33 fix: the build used + // to happen ONLY here in street frames, so a door entered fast after boot had no crate/bar inside + // at all — the interior branch could refresh a surface that never got built). Throttled here; the + // interior/map branches call tickGame() per frame (cheap: change-guarded property reads). + if (frame % 6 === 3) tickGame(); // tooltip — shows CLOSED (with opening hour) when the shop is shut at the current hour (§3.5) if (hovered) { @@ -333,9 +355,17 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } root.remove(); } - // [R32] the game surface ticks in EVERY mode (the street path reaches it via update(); interior/map - // call this directly) — day/cash/crate stay live while you shop, and the card-yield check runs there. - function tickGame() { if (gameUI) gameUI.refresh(); } + // [R32/R33] the game surface ticks in EVERY mode (the street path reaches it via update(); interior/ + // map call this directly) — and BUILDS here too, so the surface exists no matter which mode first + // sees a truthy game. day/cash/crate stay live while you shop; the card-yield check runs here. + function tickGame() { + if (!gameUI) { + const g = window.PROCITY && window.PROCITY.game; + if (g) gameUI = buildGameUI(g); + return; + } + gameUI.refresh(); + } return { update, tickToast, showToast, setVisible, tickGame, dispose, getHovered: () => hovered, getFps: () => fpsShown };