// NEMOCITY — page shell. Boot the city, wire the HUD placards, then either: // live : GET /api/state -> EventSource /api/stream (SSE protocol) // mock : ?mock=1 -> web/mock/world.json (or inline genesis) + scripted feed // The deterministic engine owns all facts; this file only performs them. // The ONLY wall-clock reads on the sim path are the clock anchors below // (server_now in live mode, one Date.now in mock boot) — all motion, mock // scheduling, and construction stamping run on simTime. import * as THREE from "three"; const params = new URLSearchParams(location.search); const MOCK = params.has("mock"); const STATIC = params.has("static"); // ---------------------------------------------------------------- HUD els const $ = (id) => document.getElementById(id); const els = { pop: $("hud-pop"), buildings: $("hud-buildings"), traffic: $("hud-traffic"), trafficChip: $("chip-traffic"), clock: $("hud-clock"), visitors: $("hud-visitors"), queueChip: $("queue-chip"), lifecycle: $("lifecycle"), form: $("petition-form"), input: $("petition-input"), send: $("petition-send"), toast: $("toast"), tickerInner: $("ticker-inner"), alert: $("traffic-alert"), alertText: $("traffic-alert-text"), btnFix: $("btn-fix"), btnWalk: $("btn-walk"), btnPhoto: $("btn-photo"), btnSound: $("btn-sound"), btnHeatmap: $("btn-heatmap"), paperwork: $("paperwork"), paperworkToggle: $("paperwork-toggle"), paperworkBody: $("paperwork-body"), fpvUi: $("fpv-ui"), status: $("status"), }; function setStatus(text) { els.status.textContent = text; } setStatus("waking the city…"); // ---------------------------------------------------------------- constants // hand-seeded ./city/constants.js may land after this file in the parallel // build; the contract values double as fallbacks. const C = { CITY_EPOCH_S: 1781222400, DAY_S: 240, CELL: 4 }; let BUILDINGS = null; try { const m = await import("./city/constants.js"); for (const k of Object.keys(C)) if (m[k] != null) C[k] = m[k]; BUILDINGS = m.BUILDINGS ?? null; } catch { /* contract fallbacks above */ } // residents/duration mirror of the contract table — HUD fallbacks for when // world.stats()/constants.js aren't wired yet. const RES = { house: 4, townhouse: 6, apartments: 16 }; const DUR = { house: 20, townhouse: 20, apartments: 45, cafe: 20, shop: 20, market: 30, bank: 30, office: 30, tower: 45, school: 30, hospital: 45, fire_station: 30, warehouse: 30, factory: 45, park: 20, plaza: 20, stadium: 45, church: 30, town_hall: 45, }; function kindMeta(kind) { const row = BUILDINGS?.[kind]; return { residents: row?.residents ?? RES[kind] ?? 0, duration: row?.duration_s ?? row?.duration ?? DUR[kind] ?? 30, }; } // ---------------------------------------------------------------- boot scene let createCityApp = null; try { const sceneMod = await import("./city/scene.js"); createCityApp = sceneMod.createCityApp ?? sceneMod.createPlanetApp; // tolerate the unadapted copy } catch (err) { setStatus("the city renderer failed to load"); console.error(err); throw err; } let app; try { app = createCityApp($("scene"), { interactive: true }); } catch (err) { setStatus("WebGL unavailable — the city cannot render here"); console.error(err); throw err; } // ------------------------------------------------------- rig + fpv + post wiring const { CameraRig } = await import("./city/camera.js"); const rig = new CameraRig(app.camera, app.renderer.domElement); app.setRig(rig); rig.landing(); // T0-T10 choreography; any pointer/wheel input cancels it app.world.citizens?.setCamera?.(app.camera); let alertLive = null; // {name, until} — live traffic telemetry (traffic.onAlert) if (app.world.traffic) { app.world.traffic.onAlert = ({ streetName }) => { alertLive = { name: streetName || "Old Bridge", until: app.simTime + 40 }; }; } app.onTick(() => { app.post.setDaylight(app.world.daylightValue ?? 1); if (!app.fpv) { // FPV holds tilt-shift at 0 (hard requirement) app.post.setFocusY(rig.focusScreenY()); app.post.setTiltShift(rig.tiltShiftAmount()); } }); try { const { FPV, isTouchDevice } = await import("./city/fpv.js"); if (isTouchDevice()) { els.btnWalk.hidden = true; // v0: no mobile FPV } else { const fpv = new FPV(app.camera, app.renderer.domElement, app.world.ground, (pos) => rig.resume(pos)); fpv.bindKeys(app, rig); // owns the F-key toggle fpv.setRoadLookup(app.world.roadCells); // body.fpv + #fpv-ui follow the nemocity:fpv event; wrap enter/exit to emit it const baseEnter = fpv.enter.bind(fpv); const baseExit = fpv.exit.bind(fpv); fpv.enter = (p) => { baseEnter(p ?? rig.lookPoint()); if (fpv.active) window.dispatchEvent(new CustomEvent("nemocity:fpv", { detail: { active: true } })); }; fpv.exit = () => { const was = fpv.active; baseExit(); if (was) window.dispatchEvent(new CustomEvent("nemocity:fpv", { detail: { active: false } })); }; window.__fpv = fpv; } } catch (err) { console.warn("fpv unavailable:", err); } // ---------------------------------------------------------------- shared clock let anchorServer = null; let anchorPerf = null; function anchorClock(serverNow) { anchorServer = serverNow; anchorPerf = performance.now() / 1000; } function serverNowEst() { return anchorServer + performance.now() / 1000 - anchorPerf; } let warnedNoSetSim = false; function setSim(t, { quiet = false } = {}) { if (typeof app.setSimTime === "function") { app.setSimTime(t); return true; } if (t >= app.simTime && t - app.simTime < 900) { app.seek(t); app.pause(false); return true; } if (!warnedNoSetSim && !quiet) { warnedNoSetSim = true; console.warn("scene.js exposes no setSimTime(t) — shared clock left unsynced (integration hook)"); } return false; } // Keep the sim clock locked to the server clock. serverNowEst() is wall-clock // based (accurate even when rAF is throttled); app.simTime is advanced by the // render loop and FALLS BEHIND when the tab is backgrounded or the frame rate // dips (the dt cap). If it lags, a freshly-built building reads as "not started" // and sits at 0% dirt until the clock catches up (June 15 bug). So: gentle slew // for tiny drift (cars/day-night don't jump), but SNAP on large drift (after a // throttle gap) and whenever the tab regains focus. function syncClock() { if (!LIVE || anchorPerf == null) return; const target = serverNowEst() - C.CITY_EPOCH_S; const drift = target - app.simTime; if (Math.abs(drift) <= 1.0) return; if (Math.abs(drift) > 3) setSim(target, { quiet: true }); // snap (post-throttle) else setSim(app.simTime + Math.sign(drift) * 0.25, { quiet: true }); // gentle catch-up } setInterval(syncClock, 1000); document.addEventListener("visibilitychange", () => { if (!document.hidden) syncClock(); }); // ---------------------------------------------------------------- helpers const truncate = (s, n) => (s && s.length > n ? `${s.slice(0, n - 1)}…` : s ?? ""); function ordinal(n) { const v = n % 100; if (v >= 11 && v <= 13) return `${n}th`; return `${n}${["th", "st", "nd", "rd"][n % 10] ?? "th"}`; } function countWishes(features) { const s = new Set(); for (const f of features) if (f.wish_id !== "genesis") s.add(f.wish_id); return s.size; } const cellCenter = (cx, cz) => new THREE.Vector3(cx * C.CELL + C.CELL / 2, 0, cz * C.CELL + C.CELL / 2); // where a feature lands, from its args alone (the contract's grid formulas) function featureTarget(f) { const a = f?.args ?? {}; if (f?.tool === "place_building" && a.cx != null) { return new THREE.Vector3((a.cx + (a.w ?? 1) / 2) * C.CELL, 0, (a.cz + (a.d ?? 1) / 2) * C.CELL); } if (a.cells?.length) { const [cx, cz] = a.cells[a.cells.length >> 1]; return cellCenter(cx, cz); } return null; } function gaze(target, hold = 5) { if (!target) return; try { app.rig?.gaze?.(target.clone(), { hold }); } catch { /* rig not wired yet */ } } function fmtCall(tool, a) { if (tool === "place_building") { return `place_building · ${a.kind}${a.name ? ` "${a.name}"` : ""} @ (${a.cx},${a.cz}) ` + `${a.w ?? 1}x${a.d ?? 1}${a.floors ? ` · ${a.floors} fl` : ""}`; } if (tool === "lay_road") return `lay_road · ${a.klass}${a.name ? ` "${a.name}"` : ""} · ${a.cells?.length ?? 0} cells`; if (tool === "apply_fix") return `apply_fix · ${a.action}${a.name ? ` "${a.name}"` : ""} · ${a.cells?.length ?? 0} cells`; if (tool === "note") return `note · ${a.text ?? ""}`; return tool; } // ---------------------------------------------------------------- ticker const headlines = ["NEMOCITY IS LISTENING — ASK FOR A BUILDING"]; function renderTicker() { const joined = headlines.join(" +++ ") + " +++ "; els.tickerInner.textContent = ""; for (let i = 0; i < 2; i++) { const span = document.createElement("span"); span.className = "ticker__copy"; span.textContent = joined; els.tickerInner.appendChild(span); } els.tickerInner.style.animationDuration = `${Math.max(18, Math.min(90, joined.length * 0.22))}s`; } function headline(text) { if (!text) return; headlines.unshift(String(text).toUpperCase()); if (headlines.length > 6) headlines.pop(); renderTicker(); } renderTicker(); // ---------------------------------------------------------------- toast + queue let toastTimer = null; function toast(text, { warn = false, ms = 6000 } = {}) { els.toast.textContent = text; els.toast.classList.toggle("is-warn", warn); els.toast.hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { els.toast.hidden = true; }, ms); } let queueLen = 0; function setQueue(length, minePos = null) { if (minePos != null) { els.queueChip.hidden = false; els.queueChip.textContent = `your petition is ${ordinal(minePos)} in line`; } else if (length > 0) { els.queueChip.hidden = false; els.queueChip.textContent = `${length} petition${length === 1 ? "" : "s"} in line`; } else { els.queueChip.hidden = true; } } // ---------------------------------------------------------------- odometer function setOdometer(el, value) { const str = String(Math.max(0, Math.floor(value))); if (el.dataset.v === str) return; el.dataset.v = str; while (el.children.length > str.length) el.firstChild.remove(); while (el.children.length < str.length) { const col = document.createElement("span"); col.className = "odo-digit"; const roll = document.createElement("span"); roll.className = "odo-roll"; for (let i = 0; i < 10; i++) { const d = document.createElement("span"); d.textContent = String(i); roll.appendChild(d); } col.appendChild(roll); el.insertBefore(col, el.firstChild); } [...el.children].forEach((col, i) => { col.firstChild.style.transform = `translateY(${-(+str[i]) * 10}%)`; }); } // ---------------------------------------------------------------- lifecycle chips const myWishes = new Set(); const chips = new Map(); // wish_id -> {el, label, state, timer} const lastTargets = new Map(); // wish_id -> Vector3 (jump-camera link) const lastNames = new Map(); // wish_id -> building name const wishTexts = new Map(); function chipRemove(id) { const c = chips.get(id); if (!c) return; clearTimeout(c.timer); c.el.remove(); chips.delete(id); } function chipFor(wishId) { let c = chips.get(wishId); if (c) return c; if (chips.size >= 3) { const order = [...chips.keys()]; const victim = order.find((id) => ["built", "rejected"].includes(chips.get(id).state)) ?? order[0]; chipRemove(victim); } const el = document.createElement("span"); el.className = "lc-chip"; const label = document.createElement("span"); label.className = "lc-chip__label"; el.appendChild(label); els.lifecycle.appendChild(el); c = { el, label, state: null, timer: null }; chips.set(wishId, c); return c; } function chipSet(wishId, state, extra = {}) { if (!wishId) return; const c = chipFor(wishId); c.state = state; c.el.className = `lc-chip is-${state}${myWishes.has(wishId) ? " is-mine" : ""}`; const texts = { queued: "queued", reading: "city hall is reading", approved: "permit approved", building: extra.name ? `under construction — ${extra.name}` : "under construction", built: extra.name ? `built — ${extra.name}` : "built", rejected: "declined", }; c.label.textContent = texts[state] ?? state; c.el.querySelector(".lc-chip__view")?.remove(); const target = lastTargets.get(wishId); if (target && (state === "building" || state === "built")) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "lc-chip__view"; btn.textContent = "view"; btn.addEventListener("click", () => gaze(target, 8)); c.el.appendChild(btn); } clearTimeout(c.timer); c.timer = setTimeout(() => chipRemove(wishId), state === "built" || state === "rejected" ? 14000 : 150000); } // ---------------------------------------------------------------- paperwork const permits = new Map(); // wish_id ->
function paperworkAdd(wishId, plan, text) { const art = document.createElement("article"); art.className = "permit"; const head = document.createElement("header"); head.textContent = `${wishId}${text ? ` · "${truncate(text, 48)}"` : ""}`; const pre = document.createElement("pre"); pre.textContent = JSON.stringify(plan, null, 1); art.append(head, pre); els.paperworkBody.prepend(art); permits.set(wishId, art); while (els.paperworkBody.children.length > 8) els.paperworkBody.lastChild.remove(); if (!els.paperwork.classList.contains("is-open")) els.paperworkToggle.classList.add("has-new"); } function paperworkLine(wishId, line) { const art = permits.get(wishId); if (!art) return; const div = document.createElement("div"); div.className = "permit__line"; div.textContent = line; art.appendChild(div); } els.paperworkToggle.addEventListener("click", () => { const open = els.paperwork.classList.toggle("is-open"); els.paperworkToggle.setAttribute("aria-expanded", String(open)); els.paperworkToggle.classList.remove("has-new"); }); // ---------------------------------------------------------------- traffic alert let alertMock = null; // {name, until} — scripted alerts (mock feed) let alertFix = null; // {text, until} — "78 -> 41" banner after apply_fix function showAlert(text, { fix = false } = {}) { els.alert.hidden = false; els.alertText.textContent = text; els.alert.classList.toggle("is-fixed", fix); els.btnFix.hidden = fix; } function hideAlert() { els.alert.hidden = true; } function renderAlert(t) { if (alertFix) { if (t < alertFix.until) return showAlert(alertFix.text, { fix: true }); alertFix = null; } let name = null; try { const tr = app.world?.traffic; const al = typeof tr?.alert === "function" ? tr.alert() : null; if (al) name = al.name ?? String(al); } catch { /* traffic not wired yet */ } if (!name && alertMock && t < alertMock.until) name = alertMock.name; if (name) showAlert(`Traffic alert: ${name}`); else hideAlert(); } // ---------------------------------------------------------------- sound (synth, OFF by default) let audio = null; let soundOn = false; function makeAudio() { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const master = ctx.createGain(); master.gain.value = 0; master.connect(ctx.destination); // traffic hum: looped seeded-noise through a lowpass (no Math.random) const len = ctx.sampleRate * 2; const buf = ctx.createBuffer(1, len, ctx.sampleRate); const data = buf.getChannelData(0); let s = 1234567; for (let i = 0; i < len; i++) { s = (s * 1664525 + 1013904223) >>> 0; data[i] = (s / 2 ** 32) * 2 - 1; } const src = ctx.createBufferSource(); src.buffer = buf; src.loop = true; const lp = ctx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 220; lp.Q.value = 0.4; const humGain = ctx.createGain(); humGain.gain.value = 0.05; src.connect(lp).connect(humGain).connect(master); src.start(); return { ctx, master, chime() { // marimba-ish triad on a grant const t0 = ctx.currentTime; for (const [f, dt, g] of [[523.25, 0, 0.16], [784, 0.09, 0.11], [1046.5, 0.18, 0.07]]) { const o = ctx.createOscillator(); o.type = "sine"; o.frequency.value = f; const og = ctx.createGain(); og.gain.setValueAtTime(0, t0 + dt); og.gain.linearRampToValueAtTime(g, t0 + dt + 0.012); og.gain.exponentialRampToValueAtTime(0.0001, t0 + dt + 0.7); o.connect(og).connect(master); o.start(t0 + dt); o.stop(t0 + dt + 0.8); } }, }; } els.btnSound.addEventListener("click", () => { soundOn = !soundOn; els.btnSound.setAttribute("aria-pressed", String(soundOn)); audio ??= makeAudio(); audio.ctx.resume(); audio.master.gain.linearRampToValueAtTime(soundOn ? 1 : 0, audio.ctx.currentTime + 0.4); }); // ---------------------------------------------------------------- modes function setPhoto(v) { const on = v ?? !document.body.classList.contains("photo"); document.body.classList.toggle("photo", on); } els.btnPhoto.addEventListener("click", () => setPhoto()); function enterWalk() { // integration wires fpv.js to this event (and may also expose window.__fpv) window.dispatchEvent(new CustomEvent("nemocity:walk")); window.__fpv?.enter?.(); } els.btnWalk.addEventListener("click", enterWalk); window.addEventListener("nemocity:fpv", (e) => { const active = !!e.detail?.active; document.body.classList.toggle("fpv", active); els.fpvUi.hidden = !active; }); const heat = { desired: false, applied: false }; els.btnHeatmap.addEventListener("click", () => { heat.desired = !heat.desired; els.btnHeatmap.setAttribute("aria-pressed", String(heat.desired)); if (heat.desired && !app.world?.traffic) { toast("traffic telemetry is still warming up — the overlay appears once cars commute"); } }); window.addEventListener("keydown", (e) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === "f" || e.key === "F") enterWalk(); else if (e.key === "p" || e.key === "P") setPhoto(); else if (e.key === "Escape" && document.body.classList.contains("photo")) setPhoto(false); }); // ---------------------------------------------------------------- placeholders const PLACEHOLDERS = [ "a ramen shop near the park…", "a fire station downtown…", "a glass tower by the river…", "homes for forty new families…", "a stadium on the east bank…", "a little chapel on Elm St…", "a school for the new quarter…", ]; let phIdx = 0; setInterval(() => { if (document.activeElement === els.input || els.input.value) return; phIdx = (phIdx + 1) % PLACEHOLDERS.length; els.input.placeholder = PLACEHOLDERS[phIdx]; }, 4200); setTimeout(() => { els.input.classList.add("pulse"); setTimeout(() => els.input.classList.remove("pulse"), 2400); }, 10000); // ---------------------------------------------------------------- event applier let LIVE = false; let stateVersion = null; let epochCount = 0; let sawDelta = false; function setEpoch(n) { epochCount = n ?? epochCount; } function applyDelta(feature) { if (!feature) return; sawDelta = true; if (LIVE && stateVersion != null) stateVersion += 1; try { app.world.applyFeature(feature, { animate: true, t: app.simTime }); } catch (err) { console.warn("applyFeature failed:", feature.tool, err); } const a = feature.args ?? {}; const target = featureTarget(feature); if (target) { lastTargets.set(feature.wish_id, target); gaze(target, 5.5); } if (feature.tool === "place_building") { if (a.name) lastNames.set(feature.wish_id, a.name); chipSet(feature.wish_id, "building", { name: a.name }); } else if (feature.tool === "apply_fix") { headline(a.diagnosis || "the City Engineer applied a fix"); const b = a.metrics_before?.traffic_index; const p = a.metrics_predicted?.traffic_index; alertFix = { text: b != null && p != null ? `City Engineer: fix applied — index ${b} -> ${p}` : "City Engineer: fix applied", until: app.simTime + 9, }; alertMock = null; } else if (feature.tool === "note" && a.text) { headline(a.text); } } function handleEvent(m) { switch (m.type) { case "your_turn": // ZeroGPU dance: only the petitioner's browser may begin the grant if (!LIVE) break; if (myWishes.has(m.wish_id)) invokeGrant(m.wish_id); else pendingTurns.add(m.wish_id); break; case "hello": if (m.server_now != null) anchorClock(m.server_now); if (LIVE && stateVersion != null && m.world_version !== stateVersion) loadState().catch(() => {}); if (m.epoch != null) setEpoch(m.epoch); break; case "queue": queueLen = m.length ?? 0; setQueue(queueLen); break; case "wish_started": wishTexts.set(m.wish_id, m.text ?? ""); chipSet(m.wish_id, "reading"); setStatus(`city hall is reading — "${truncate(m.text, 56)}"`); break; case "plan": // City Hall paperwork: the raw validated Nemotron JSON paperworkAdd(m.wish_id, m.plan, wishTexts.get(m.wish_id)); chipSet(m.wish_id, "approved"); break; case "tool_call": { paperworkLine(m.wish_id, fmtCall(m.tool, m.args ?? {})); const tgt = featureTarget({ tool: m.tool, args: m.args }); if (tgt) gaze(tgt, 5); break; } case "world_delta": applyDelta(m.feature); break; case "wish_granted": setEpoch(m.epoch != null ? m.epoch : epochCount + 1); headline(m.epitaph); chipSet(m.wish_id, "built", { name: lastNames.get(m.wish_id) }); if (myWishes.has(m.wish_id)) toast("granted — watch the crane"); if (soundOn) audio?.chime(); setStatus(LIVE ? "live · city hall is listening" : "demo city · scripted petitions"); break; case "wish_rejected": chipSet(m.wish_id, "rejected"); if (!m.wish_id || myWishes.has(m.wish_id)) { toast(m.reason || "city hall declines this petition.", { warn: true }); } break; case "heartbeat": if (m.server_now != null) anchorClock(m.server_now); break; case "traffic_alert": // mock feed only — live alerts come from traffic telemetry alertMock = { name: m.name || "Old Bridge", until: app.simTime + 40 }; break; } } // ---------------------------------------------------------------- script runner // Generic performer for feed.js scripts — used by ?mock=1 AND by the live // "welcome build" (a crane is ALWAYS rising when a judge arrives). let feedMod = null; const played = { active: null, appliedIds: new Set() }; const mockState = { enabled: false, nextAt: Infinity, idx: 0, pendingText: null, fixDone: false }; function playScript(script) { played.active = { script, start: app.simTime, i: 0 }; } function emitScript(ev, script) { if (ev.type === "world_delta") { const f = ev.feature; if (!f || played.appliedIds.has(f.id)) return; // replays skip placement — cells already taken played.appliedIds.add(f.id); // stamp t now so construction progress starts at this shared moment handleEvent({ ...ev, feature: { ...f, t: C.CITY_EPOCH_S + app.simTime } }); return; } if (ev.type === "wish_started" && script.textOverride) { handleEvent({ ...ev, text: script.textOverride }); return; } handleEvent(ev); } app.onTick((t) => { const a = played.active; if (a) { const evs = a.script.events; while (a.i < evs.length && t - a.start >= evs[a.i].at) { emitScript(evs[a.i].ev, a.script); a.i += 1; } if (a.i >= evs.length) played.active = null; } else if (mockState.enabled && t >= mockState.nextAt) { startNextScript(); } }); function pickScript(text) { const all = [...feedMod.SCRIPTS, feedMod.FIX_SCRIPT]; return all.find((s) => s.match && new RegExp(s.match, "i").test(text)) ?? feedMod.SCRIPTS[0]; } function startNextScript() { if (!feedMod) return; let sc = null; if (mockState.pendingText) { sc = { ...pickScript(mockState.pendingText), textOverride: mockState.pendingText }; myWishes.add(sc.wish_id); mockState.pendingText = null; setQueue(0); } else { const rotation = [...feedMod.SCRIPTS, feedMod.FIX_SCRIPT]; sc = rotation[mockState.idx % rotation.length]; mockState.idx += 1; if (sc === feedMod.FIX_SCRIPT && mockState.fixDone) { sc = rotation[mockState.idx % rotation.length]; mockState.idx += 1; } } if (sc === feedMod.FIX_SCRIPT || sc.wish_id === feedMod.FIX_SCRIPT.wish_id) mockState.fixDone = true; playScript(sc); mockState.nextAt = app.simTime + 45; // a scripted petition every ~45 s keeps the demo city performing } // relocate the welcome cafe to a cell the live city hasn't claimed function patchWelcome(welcome) { const occ = new Set(); for (const f of app.world?.features ?? []) { const a = f.args ?? {}; if (f.tool === "place_building" && a.cx != null) { for (let x = 0; x < (a.w ?? 1); x++) { for (let z = 0; z < (a.d ?? 1); z++) occ.add(`${a.cx + x},${a.cz + z}`); } } for (const [cx, cz] of a.cells ?? []) occ.add(`${cx},${cz}`); } const water = (cx, cz) => (cz <= -5 ? [7, 8] : cz <= 3 ? [8, 9] : [9, 10]).includes(cx); const spot = [[2, 1], [3, 1], [4, 1], [1, -4], [-3, -1], [2, -7]] .find(([cx, cz]) => !occ.has(`${cx},${cz}`) && !water(cx, cz)); if (!spot) return null; const sc = structuredClone(welcome); sc.wish_id = `${welcome.wish_id}_live`; for (const e of sc.events) { if (e.ev.wish_id) e.ev.wish_id = sc.wish_id; const f = e.ev.feature; if (f) { f.id = `${f.id}_live`; f.wish_id = sc.wish_id; } const a = e.ev.args ?? f?.args; if (a && a.cx != null) { a.cx = spot[0]; a.cz = spot[1]; } } return sc; } // ---------------------------------------------------------------- live mode const pendingTurns = new Set(); let gradioClient = null; // Browser-invoked granting (ZeroGPU): the GPU call must run inside OUR gradio // request so the HF auth headers carry the visitor's quota. When the SSE says // it's our petition's turn, call the named endpoint with the one-time token. async function invokeGrant(wishId) { setStatus("city hall turns to your petition…"); try { // the token route is cookie-bound — never broadcast over SSE const tr = await fetch(`/api/turn?wish_id=${encodeURIComponent(wishId)}`, { credentials: "same-origin", }); if (!tr.ok) { if (tr.status === 403) return; // not our petition (a stranger's turn) throw new Error(`turn ${tr.status}`); } const { token } = await tr.json(); if (!gradioClient) { const { Client } = await import("https://cdn.jsdelivr.net/npm/@gradio/client/+esm"); gradioClient = await Client.connect(window.location.origin); } const r = await gradioClient.predict("/grant", { wish_id: wishId, token }); const out = Array.isArray(r?.data) ? r.data[0] : r?.data; if (out && out.ok === false && out.reason) toast(out.reason, { warn: true }); } catch (err) { console.error("grant invocation failed", err); toast("the permit office stalled — sign in to Hugging Face, then petition again", { warn: true }); } } async function loadState() { const res = await fetch("/api/state"); if (!res.ok) throw new Error(`state ${res.status}`); const state = await res.json(); stateVersion = state.world?.version ?? null; if (state.server_now != null) { anchorClock(state.server_now); setSim(serverNowEst() - C.CITY_EPOCH_S); } const features = state.world?.features ?? []; app.world.reset(features, app.simTime); setEpoch(state.world?.epoch ?? countWishes(features)); queueLen = state.queue?.length ?? 0; setQueue(queueLen); return state; } async function bootLive() { try { await loadState(); } catch (err) { console.warn("no live backend:", err); setStatus("demo city · no backend"); return bootMock(); } LIVE = true; feedMod = await import("./mock/feed.js").catch(() => null); const es = new EventSource("/api/stream"); es.onmessage = (e) => { let m; try { m = JSON.parse(e.data); } catch { return; } handleEvent(m); }; es.onerror = () => setStatus("the line to city hall flickers · reconnecting"); es.addEventListener("open", () => setStatus("live · city hall is listening")); setStatus("live · city hall is listening"); // welcome build: if the queue is idle, a scripted crane rises client-side // (visual only — never written to the server) within the first 10 s setTimeout(() => { if (sawDelta || queueLen > 0 || played.active || !feedMod?.WELCOME) return; const sc = patchWelcome(feedMod.WELCOME); if (sc) playScript(sc); }, 5500); } // ---------------------------------------------------------------- mock mode async function bootMock() { LIVE = false; feedMod = await import("./mock/feed.js"); let features = null; try { const r = await fetch("./mock/world.json"); if (r.ok) { const j = await r.json(); features = j.world?.features ?? j.features ?? null; } } catch { /* world.json is generated later by tools/gen_web.py */ } features ??= feedMod.GENESIS_FEATURES; const t0 = params.has("t") ? Math.max(0, parseFloat(params.get("t")) || 0) : Math.max(0, Date.now() / 1000 - C.CITY_EPOCH_S); // the mock clock anchor (sole wall read) setSim(t0); app.world.reset(features, app.simTime); setEpoch(countWishes(features)); setStatus(MOCK ? "demo city · ?mock=1" : "demo city · scripted petitions"); if (!STATIC) { mockState.enabled = true; mockState.nextAt = app.simTime + 4; // welcome build inside the first 10 s } } function mockPetition(text) { els.input.value = ""; mockState.pendingText = text.slice(0, 160); mockState.nextAt = Math.min(mockState.nextAt, app.simTime + 3); setQueue(0, 1); toast("demo city — city hall will read your words"); } // ---------------------------------------------------------------- petition form els.form.addEventListener("submit", async (e) => { e.preventDefault(); const text = els.input.value.trim(); if (!text) return; if (!LIVE) return mockPetition(text); els.send.disabled = true; try { const res = await fetch("/api/wish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }), }); const body = await res.json().catch(() => ({})); if (res.ok) { els.input.value = ""; if (body.wish_id) { myWishes.add(body.wish_id); chipSet(body.wish_id, "queued"); if (pendingTurns.has(body.wish_id)) { pendingTurns.delete(body.wish_id); invokeGrant(body.wish_id); } } setQueue(queueLen, body.position ?? 1); toast("city hall has your petition — stay on the page; your visit powers the build"); } else if (res.status === 429) { toast(body.detail || body.reason || "the queue is full right now — try again in a little while", { warn: true }); } else { toast(body.detail || body.reason || "city hall declines this petition.", { warn: true }); } } catch { toast("the petition was lost in the mail — try again", { warn: true }); } finally { els.send.disabled = false; } }); // ---------------------------------------------------------------- fix button els.btnFix.addEventListener("click", async () => { if (!LIVE) { if (mockState.fixDone) return toast("the Engineer already widened the crossing — traffic is settling"); if (played.active) return toast("the Engineer is mid-shift — one moment"); mockState.fixDone = true; playScript(feedMod.FIX_SCRIPT); return; } els.btnFix.disabled = true; try { const res = await fetch("/api/fix", { method: "POST" }); const body = await res.json().catch(() => ({})); if (res.ok) { if (body.wish_id) { myWishes.add(body.wish_id); if (pendingTurns.has(body.wish_id)) { pendingTurns.delete(body.wish_id); invokeGrant(body.wish_id); } } toast("the City Engineer is reading the telemetry…"); } else if (res.status === 409) { toast(body.detail || "Traffic is flowing smoothly — no fix needed."); } else { toast(body.detail || "the Engineer is busy — try again soon", { warn: true }); } } catch { toast("could not reach the City Engineer — try again", { warn: true }); } finally { els.btnFix.disabled = false; } }); // ---------------------------------------------------------------- HUD loop function hudTick() { const t = app.simTime; const dayH = ((((t % C.DAY_S) + C.DAY_S) % C.DAY_S) / C.DAY_S) * 24; els.clock.textContent = `${String(Math.floor(dayH)).padStart(2, "0")}:${String(Math.floor((dayH % 1) * 60)).padStart(2, "0")}`; const w = app.world; let s = null; try { s = w?.stats?.(); } catch { /* not wired yet */ } const feats = w?.features ?? []; const blds = feats.filter((f) => f.tool === "place_building"); let pop = s?.population; if (pop == null) { pop = 0; for (const f of blds) { const meta = kindMeta(f.args?.kind); if (!meta.residents) continue; const p = f.t ? Math.min(1, Math.max(0, (C.CITY_EPOCH_S + t - f.t) / meta.duration)) : 1; pop += meta.residents * p; } } setOdometer(els.pop, pop); els.buildings.textContent = String(s?.buildings ?? blds.length); els.visitors.textContent = `built by ${epochCount} visitor${epochCount === 1 ? "" : "s"}`; const tr = w?.traffic; // may be absent until integration — every read is guarded let idx = null; try { if (typeof tr?.index === "function") idx = tr.index(); else if (typeof tr?.index === "number") idx = tr.index; else if (typeof tr?.trafficIndex === "number") idx = tr.trafficIndex; } catch { /* not wired yet */ } els.traffic.textContent = idx == null ? "–" : String(Math.round(idx)); els.trafficChip.classList.toggle("is-jam", idx != null && idx >= 70); els.trafficChip.classList.toggle("is-warn", idx != null && idx >= 40 && idx < 70); if (tr?.setHeatmap && heat.applied !== heat.desired) { try { tr.setHeatmap(heat.desired); heat.applied = heat.desired; } catch { /* retry next loop */ } } renderAlert(t); } setInterval(hudTick, 200); // ---------------------------------------------------------------- harness API window.__app = { ready: false, app, get world() { return app.world; }, tick: (s) => app.tick(s), seek: (t) => app.seek(t), pause: (v = true) => app.pause(v), setSim, stats: () => { let s = {}; try { s = app.stats?.() ?? {}; } catch { /* scene stats not wired */ } return { ...s, simTime: +app.simTime.toFixed(2), epoch: epochCount, headlines: headlines.length }; }, setPhoto, setHeatmap: (v) => { heat.desired = !!v; els.btnHeatmap.setAttribute("aria-pressed", String(!!v)); }, gazeCell: (cx, cz, hold = 30) => gaze(cellCenter(cx, cz), hold), runScript: (i = 0) => { if (feedMod) playScript(feedMod.SCRIPTS[i] ?? feedMod.SCRIPTS[0]); }, runFix: () => { if (feedMod) { mockState.fixDone = true; playScript(feedMod.FIX_SCRIPT); } }, enterWalk, }; // ---------------------------------------------------------------- start (MOCK ? bootMock() : bootLive()) .then(() => { window.__app.ready = true; }) .catch((err) => { setStatus("the city failed to load"); console.error(err); }); app.start();