Lane B R30 (v7.0-alpha ledger #4): THE COLLECTION UI + SLEEP surface — the crate panel in hud.js (cover thumb = pack-atlas CSS crop off the sku, per-shop/town-wide bases, parody = glyph by design; where+day honest, foreign towns fall back to townKey), game bar day/$/CRATE count + SLEEP on F's §30.1 contract; construction-gated on PROCITY.game itself (classic/game=0 ⇒ zero DOM/listeners, measured); venue.js compat venueShopId = live weekNight getter (F's §30.4 filing, 116→490 across a sleep). Verified vs F's REAL committed core (nothing stubbed) on no-store :8151 snapshot; qa 6/6
This commit is contained in:
parent
f419c1a5e2
commit
cb03f07fb2
@ -4,6 +4,25 @@
|
||||
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
|
||||
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
|
||||
|
||||
## Round 30 (v7.0-alpha ledger #4) — THE COLLECTION UI + SLEEP surface · qa GREEN 6/6
|
||||
|
||||
The crate panel + game bar in `hud.js` (my DOM-overlay patterns), on F's §30.1 `PROCITY.game`
|
||||
contract — and verified against F's REAL committed core (`207bcff` landed mid-round; my test
|
||||
snapshot diffed byte-identical to it — **nothing stubbed**, unlike C's wave-1 run).
|
||||
- **Game bar** (bottom-right): `day N · $cash` · `CRATE n` (count in the standing HUD; opens the
|
||||
panel, `C` toggles) · `☾ SLEEP` → `game.sleep()`, enabled any time (alpha).
|
||||
- **Crate panel**: cover thumb (pack-atlas CSS crop off the entry's sku — per-shop godverse base or
|
||||
town-wide; parody = type glyph by design), title/artist, `paid $p · where · day d`, newest first,
|
||||
header totals. 50-row rebuild 0.9 ms; 0 draws (DOM-only).
|
||||
- **Construction-gated on the game object itself** (lazy, R19-selector style): `?classic=1`/`?game=0`
|
||||
⇒ zero game DOM, zero listeners, help line byte-identical, 0 console errors (measured, clean tabs).
|
||||
`?noassets` ⇒ 0 stock fetches, placeholders stand.
|
||||
- **SLEEP proven end-to-end**: day 1→2, weekNight 0→1, wake DAWN seg 0, `procity-save/1` on disk,
|
||||
next boot loads it and the UI reads it. **venue.js §30.4 filing fixed as filed** — compat
|
||||
`venueShopId` is now a live weekNight-keyed getter (116 → 490 across a sleep, measured).
|
||||
- Full measurement table + honest notes: LANE_B_NOTES §30. Verified on a port-isolated no-store
|
||||
:8151 snapshot server (not :8130/:8144 — Lane F was mid-flight in the shared tree).
|
||||
|
||||
## Round 9 (Fable's ROUND9 → Lane B) — v2 tour bookmarks + tram (`?tram=1`) · qa GREEN
|
||||
|
||||
Ship-v2.0 round. Two deliverables, both v2-law clean, `qa.sh --strict` GREEN, flags-off byte-identical.
|
||||
|
||||
@ -1,5 +1,64 @@
|
||||
# LANE B — NOTES (measured)
|
||||
|
||||
## Round 30 (v7.0-alpha ledger #4) — THE COLLECTION UI + the SLEEP surface: **shipped, verified against F's REAL core (nothing stubbed)**
|
||||
|
||||
**The crate is on screen: cover thumb · title/artist · price paid · where + day found; count, day and
|
||||
cash in the standing HUD; SLEEP saves and wakes tomorrow. All of it built on `PROCITY.game` exactly as
|
||||
§30.1 publishes it — and by verify time none of it was a stub: F committed the save core (`207bcff`)
|
||||
mid-round, and every game-layer file my test server carried is byte-identical to that HEAD (diffed,
|
||||
not assumed). Lane C stubbed because F hadn't landed; B landed after F did — the wave order worked.**
|
||||
|
||||
### What ships (`web/js/world/hud.js` — my file, my DOM-overlay patterns; +8 lines in `venue.js`, F's §30.4 filing)
|
||||
- **The game bar** (bottom-right): `day N · $cash` readout + `CRATE n` (the standing-HUD count, opens
|
||||
the panel; `C` toggles it on the street) + `☾ SLEEP` → `game.sleep()` — enabled any time (alpha;
|
||||
home-base gating is beta). The shell's `onDay` hook owns the wake toast; the bar just re-reads the
|
||||
contract getters.
|
||||
- **The crate panel**: newest find first; each row = cover thumb, title/artist (C's §30.1 fallback
|
||||
chain title → sku → slotId), `paid $p · <where> · day d`. Header totals finds + $ spent. *Where* =
|
||||
the shop's name off the plan when the entry's `townKey` is this boot's, else the town part of its
|
||||
townKey (`redhill godverse`) — honest, never guessed.
|
||||
- **Cover thumbs = the pack atlas, cropped in CSS.** The entry's `sku` resolves against its pack index
|
||||
(per-shop `assets/stock_godverse/<godverseShopId>/` when set, else the town-wide `assets/models/`),
|
||||
and the row's 44 px thumb is `background-image` + `background-size: 100/du% × 100/dv%` +
|
||||
`background-position: u0/(1−du)` off the item's `uv` rect (origin top-left = CSS's origin, no flip —
|
||||
the WebGL V-flip in stockpack.js is exactly the transform NOT to apply here). Zero canvas, zero
|
||||
texture decode beyond the browser's own; B keeps its own tiny index-only fetch cache (`base|type`,
|
||||
read-only GETs — can't fight stockpack's texture cache). Parody finds (slotId, no sku) wear a type
|
||||
glyph by design (§30.4); so does any missing pack / unknown sku / remote (`depot:`/http) atlas.
|
||||
- **Construction-gated like the R19 selector, but on the game itself:** the HUD builds NOTHING at
|
||||
`createHUD` — a lazy check (every 30th frame) builds the surface on first sight of a truthy
|
||||
`window.PROCITY.game` (the shell constructs the game after the HUD). `?classic=1` / `?game=0` ⇒
|
||||
`game` is null forever ⇒ **none of this DOM, the KeyC listener, or the help-line suffix ever
|
||||
exists** — the gate is the game object, not a re-derivation of the flags.
|
||||
- **`venue.js` — F's §30.4 filing, fixed as filed:** the compat `venueShopId` ("tonight's venue") was
|
||||
frozen at night-0's gig; now a live getter keyed to `PROCITY.gigs.weekNight` (0 pre-game/day-1 ⇒
|
||||
exact pre-v7 value). No consumer of the field exists in-tree today (grepped), so the getter is pure
|
||||
forward-correctness for whoever reads "tonight" next.
|
||||
|
||||
### Measured (fresh contexts, port-isolated no-store server :8151 — NOT :8130/:8144 — serving a code
|
||||
### snapshot + assets symlink, so F's mid-flight edits couldn't shift under the test; snapshot later
|
||||
### diffed clean against F's landed commits)
|
||||
| check | result |
|
||||
|---|---|
|
||||
| `?classic=1` / `?game=0` (clean tab) | `game: null` · game DOM **0/5 ids** · help line byte-identical · **0 console errors** |
|
||||
| default boot | bar lazily built, `day 1 · $191` (wallet-backed start), CRATE 0 |
|
||||
| 3 finds via the REAL `game.recordFind` (godverse sku · town-wide sku · parody slotId) | CRATE 3 · head `3 finds · $92 spent` · thumbs from **both** pack bases (`stock_godverse/158/`, `models/`) — real cover art on screen (screenshotted), parody = ♪ · exactly 4 fetches (2 index + 2 atlas) |
|
||||
| foreign-town entry via the REAL `game.import()` | row reads `paid $25 · redhill godverse · day 1` (townKey fallback) · missing sku ⇒ placeholder, 0 errors |
|
||||
| sell seam (`game.removeFind`) | 4 → 3: bar + open panel both follow within one poll (≤6 frames) |
|
||||
| **SLEEP** (button) | day 1→2 · weekNight 0→1 · wake DAWN 06:30 seg 0 · `procity-save/1` on disk with day 2 + 3 entries · shell toast `🌅 day 2` · cash untouched · **`venueShopId` re-keyed 116 → 490** (the §30.4 fix, proven behaviourally) |
|
||||
| next boot (same profile) | the saved game LOADS and the bar/panel read it — persistence proven from the UI side too |
|
||||
| `?noassets` + a real-sku find | **0** stock/atlas fetches (asset law) · placeholder stands · 0 errors |
|
||||
| cost | 50-row full rebuild **0.9 ms** · first 3-row open 2.1 ms · DOM-only = 0 draws (C proved the class; unchanged) |
|
||||
|
||||
**Honest notes:** (1) the four `THREE.PointerLockControls` console errors seen mid-drive are the
|
||||
automation browser refusing the Pointer Lock API on my splash click — reproduced click-free at zero,
|
||||
absent on every clean-tab boot; environment, not code. (2) F's splash ("walk into town") was new in
|
||||
the tree mid-round and sat over my first screenshot — noted, F's surface, untouched. (3) Beta polish,
|
||||
not done: foreign-town rows could carry real display names via E's towns index (the selector already
|
||||
fetches it) — one map at build time, when the money-shot pass comes.
|
||||
|
||||
---
|
||||
|
||||
## Round 28 (v5.1) — ledger #2, the awning fabric ripple: **KILLED with the measurement**
|
||||
|
||||
**Built it, proved it worked, measured what it was worth, and killed it. A 10×-over-driven billow —
|
||||
|
||||
@ -27,6 +27,20 @@ const CSS = `
|
||||
#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:absolute;right:10px;bottom:12px;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}
|
||||
`;
|
||||
|
||||
export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }) {
|
||||
@ -98,6 +112,126 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
.catch(() => {}); // index missing/blocked → fallback stands, zero errors
|
||||
})();
|
||||
|
||||
// ── v7 THE GAME (R30, ledger #4): the collection panel + the SLEEP surface + day/cash readout.
|
||||
// Built LAZILY on the first update() frame that sees a truthy `window.PROCITY.game` — the shell
|
||||
// constructs the game layer AFTER the HUD, and under ?classic=1 / ?game=0 it is null forever, so
|
||||
// NONE of this DOM (bar, panel, KeyC listener, help-line suffix) ever exists on those boots: the
|
||||
// R19 selector's construction-gating, applied to the game (the classic-pure law, ledger #5's gate
|
||||
// measures it). Consumes ONLY the LANE_F_NOTES §30.1 contract: day / cash / collection (read-only
|
||||
// — this UI never mutates it) / sleep(). Cover thumbs resolve the entry's sku against its pack
|
||||
// index (per-shop godverse base when godverseShopId is set, else the town-wide base) — fail-soft:
|
||||
// missing pack / unknown sku / remote atlas / ?noassets ⇒ the type-glyph placeholder stands,
|
||||
// zero errors (the R21 towns-index discipline). Parody finds (slotId, no sku) have no thumb by
|
||||
// design (§30.4). ──
|
||||
let gameUI = null; // built once; null forever when the game layer is absent
|
||||
let onGameKey = null; // KeyC crate toggle — registered only when the game UI exists
|
||||
const shopById = new Map((plan.shops || []).map((s) => [s.id, s]));
|
||||
const GAME_NOASSETS = (() => { try { const v = new URLSearchParams(location.search).get('noassets'); return v != null && v !== '0'; } catch { return false; } })();
|
||||
const TYPE_GLYPH = { record: '♪', book: '¶', toy: '✦' };
|
||||
|
||||
const _packIdx = new Map(); // `${base}|${type}` → Promise<index.json|null> — B's own read-only cache
|
||||
function packIndexFor(entry) {
|
||||
if (GAME_NOASSETS || !entry || !entry.sku || !entry.type) return Promise.resolve(null);
|
||||
const base = entry.godverseShopId != null ? `assets/stock_godverse/${entry.godverseShopId}/` : 'assets/models/';
|
||||
const key = base + '|' + entry.type;
|
||||
if (!_packIdx.has(key))
|
||||
_packIdx.set(key, fetch(`${base}stock_${entry.type}_index.json`).then((r) => (r.ok ? r.json() : null)).catch(() => null));
|
||||
return _packIdx.get(key).then((idx) => (idx && Array.isArray(idx.items) ? { idx, base } : null));
|
||||
}
|
||||
function applyThumb(el, entry) {
|
||||
packIndexFor(entry).then((p) => {
|
||||
if (!p || !el.isConnected) return;
|
||||
const it = p.idx.items.find((i) => i && String(i.id) === entry.sku);
|
||||
if (!it || !it.atlas || !Array.isArray(it.uv) || /^depot:|^https?:/.test(it.atlas)) return;
|
||||
const [u0, v0, u1, v1] = it.uv, du = u1 - u0, dv = v1 - v0;
|
||||
if (!(du > 0) || !(dv > 0)) return;
|
||||
el.textContent = ''; // atlas crop via CSS background — uv origin is top-left (image-natural), same as CSS
|
||||
el.style.backgroundImage = `url("${p.base + it.atlas}")`;
|
||||
el.style.backgroundSize = `${100 / du}% ${100 / dv}%`;
|
||||
el.style.backgroundPosition = `${du < 1 ? (u0 / (1 - du)) * 100 : 0}% ${dv < 1 ? (v0 / (1 - dv)) * 100 : 0}%`;
|
||||
});
|
||||
}
|
||||
// where the find happened: this boot's town → the shop's name off the plan; another town's find →
|
||||
// the town part of its townKey (`plansrc/town@seed`) — honest, never guessed.
|
||||
function foundAt(entry, game) {
|
||||
if (entry.townKey === game.townKey) { const s = shopById.get(entry.shopId); if (s && s.name) return s.name; }
|
||||
const m = /^[^/]+\/([^@]+)@/.exec(entry.townKey || '');
|
||||
return m ? m[1].replace(/_/g, ' ') : '?';
|
||||
}
|
||||
|
||||
function buildGameUI(game) {
|
||||
const wrap = document.createElement('div'); wrap.id = 'pc-game';
|
||||
const panel = document.createElement('div'); panel.id = 'pc-crate';
|
||||
const bar = document.createElement('div'); bar.id = 'pc-gamebar';
|
||||
bar.innerHTML = `<span>day <b id="pc-day">—</b> · $<b id="pc-cash">—</b></span>
|
||||
<button id="pc-crate-btn" title="The collection (C toggles; Esc frees the pointer first)">CRATE 0</button>
|
||||
<button id="pc-sleep-btn" title="Sleep — save, wake tomorrow">☾ SLEEP</button>`;
|
||||
wrap.append(panel, bar); root.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;
|
||||
|
||||
function rebuildList() {
|
||||
const col = game.collection || [];
|
||||
panel.textContent = '';
|
||||
const h = document.createElement('h3');
|
||||
const spent = col.reduce((a, e) => a + (e.pricePaid || 0), 0);
|
||||
h.textContent = `THE CRATE — ${col.length} find${col.length === 1 ? '' : 's'} · $${spent} spent`;
|
||||
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;
|
||||
}
|
||||
for (let i = col.length - 1; i >= 0; i--) { // newest find first
|
||||
const e = col[i];
|
||||
const row = document.createElement('div'); row.className = 'pc-crate-row';
|
||||
const th = document.createElement('div'); th.className = 'pc-thumb';
|
||||
th.textContent = TYPE_GLYPH[e.type] || '♦';
|
||||
applyThumb(th, e);
|
||||
const main = document.createElement('div'); main.className = 'pc-crate-main';
|
||||
const t = document.createElement('div'); t.className = 'pc-crate-title';
|
||||
t.textContent = e.title || e.sku || e.slotId || 'unknown'; // C's fallback rule (§30.1)
|
||||
const a = document.createElement('div'); a.className = 'pc-crate-artist';
|
||||
a.textContent = e.artist || '';
|
||||
const meta = document.createElement('div'); meta.className = 'pc-crate-meta';
|
||||
meta.textContent = `paid $${e.pricePaid} · ${foundAt(e, game)} · day ${e.dayFound}`;
|
||||
main.append(t, a, meta);
|
||||
row.append(th, main); panel.appendChild(row);
|
||||
}
|
||||
}
|
||||
function setOpen(v) {
|
||||
open = v; panel.style.display = v ? 'block' : 'none';
|
||||
if (v) { shownCount = (game.collection || []).length; rebuildList(); }
|
||||
}
|
||||
btnCrate.onclick = () => setOpen(!open);
|
||||
// SLEEP — enabled any time (alpha; home-base gating is beta). The shell's onDay hook owns the
|
||||
// wake toast; the HUD just refreshes its readouts off the contract getters.
|
||||
btnSleep.onclick = () => {
|
||||
try { game.sleep(); } catch (err) { console.warn('[hud] sleep failed:', err); }
|
||||
refresh();
|
||||
};
|
||||
function refresh() {
|
||||
const col = game.collection || [];
|
||||
if (game.day !== shownDay) { shownDay = game.day; elDay.textContent = shownDay; }
|
||||
if (game.cash !== shownCash) { shownCash = game.cash; elCash.textContent = shownCash; }
|
||||
if (col.length !== shownCount) {
|
||||
shownCount = col.length; btnCrate.textContent = `CRATE ${shownCount}`;
|
||||
if (open) rebuildList();
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
onGameKey = (e) => { // C toggles the crate on the street (not inside a shop — E/dig territory)
|
||||
if (e.code !== 'KeyC') return;
|
||||
const P = window.PROCITY;
|
||||
if (P && P.mode === 'interior') return;
|
||||
setOpen(!open);
|
||||
};
|
||||
addEventListener('keydown', onGameKey);
|
||||
const help = root.querySelector('#pc-help');
|
||||
if (help) help.textContent += ' · C crate';
|
||||
return { refresh };
|
||||
}
|
||||
|
||||
const ray = new THREE.Raycaster();
|
||||
ray.far = 6;
|
||||
const centre = new THREE.Vector2(0, 0);
|
||||
@ -136,6 +270,15 @@ 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();
|
||||
|
||||
// tooltip — shows CLOSED (with opening hour) when the shop is shut at the current hour (§3.5)
|
||||
if (hovered) {
|
||||
const h = hovered.hours;
|
||||
@ -174,6 +317,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
|
||||
function dispose() {
|
||||
renderer.domElement.removeEventListener('click', onClick);
|
||||
if (onGameKey) removeEventListener('keydown', onGameKey);
|
||||
root.remove();
|
||||
}
|
||||
|
||||
|
||||
@ -259,7 +259,13 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
|
||||
|
||||
return {
|
||||
group, venues, update, dispose,
|
||||
venueShopId: primaryId || (venues[0] && venues[0].venueShopId) || null, // alpha compat
|
||||
// alpha compat — [Lane B R30, F's §30.4 filing] "tonight" now tracks the game day: a live getter
|
||||
// keyed to gigs' night = weekNight (0 pre-game and on day 1 ⇒ exact pre-v7 value, gigs[0]'s venue).
|
||||
get venueShopId() {
|
||||
const n = (typeof window !== 'undefined' && window.PROCITY?.gigs?.weekNight) || 0;
|
||||
const g = gigs.find((x) => x.night === n) || (primaryId != null ? gigs[0] : null);
|
||||
return (g && g.venueShopId) || (venues[0] && venues[0].venueShopId) || null;
|
||||
},
|
||||
venueShopIds: venues.map((v) => v.venueShopId),
|
||||
queueZones,
|
||||
queueZoneFor(id) { const v = venues.find((x) => x.venueShopId === id); return v ? v.queueZone : null; },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user