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 };