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