// GODSEED — Genesis Log. Pure client-side replay: build the world at // features[0..K), then perform wish K's recorded thoughts + deltas again. // Live: /api/wishes + /api/wishes/{id} + /api/state. Dev: ?mock=1. // Deep link: book.html#w_000005 import { createPlanetApp } from "./planet/scene.js"; import { buildWishScript, ScriptRunner } from "./planet/replay.js"; import { latLonToDir, PLANET_R } from "./planet/util.js"; // Re-anchor + frame the town heart from the current world. function syncTown() { app.rig.setTownCenter(app.world.features); } const params = new URLSearchParams(location.search); const MOCK = params.has("mock"); const qs = MOCK ? "?mock=1" : ""; const els = { ledger: document.getElementById("ledger"), voiceDot: document.getElementById("voiceDot"), voiceWish: document.getElementById("voiceWish"), stream: document.getElementById("voiceStream"), epitaph: document.getElementById("epitaph"), epitaphText: document.getElementById("epitaphText"), status: document.getElementById("status"), hint: document.getElementById("replayHint"), }; const caret = document.createElement("span"); caret.className = "voice__caret"; function tickerPush(text, tone = "thought") { caret.remove(); let inner = els.stream.querySelector(".voice__innertext"); if (!inner) { inner = document.createElement("span"); inner.className = "voice__innertext"; els.stream.appendChild(inner); } const span = document.createElement("span"); span.className = `tone-${tone}`; span.textContent = text; inner.appendChild(span); inner.appendChild(caret); while (inner.children.length > 420) inner.firstChild.remove(); } let app; try { app = createPlanetApp(document.getElementById("scene"), { interactive: true }); } catch (err) { els.status.textContent = "this vessel cannot hold the world (WebGL unavailable)"; throw err; } app.start(); // left-side dark scrim so the log column reads over the bright planet rim + // gold trail (sits behind the canvas overlays, above the scene). if (!document.querySelector(".ledger-scrim")) { const scrim = document.createElement("div"); scrim.className = "ledger-scrim"; scrim.setAttribute("aria-hidden", "true"); document.body.insertBefore(scrim, document.querySelector(".vignette") || els.ledger); } // ---------------------------------------------------------------- data async function loadIndex() { if (MOCK) { const traces = await fetch("./mock/traces.json").then((r) => r.json()); const { FEED_WISH } = await import("./mock/feed.js"); const list = Object.values(traces).map((t, i) => ({ wish_id: t.wish_id, text: t.text, epitaph: t.epitaph, ts: t.submitted_at, epoch: i + 1, // grant order mirrors the API's enumerate(start=1) })); list.push({ wish_id: FEED_WISH.trace.wish_id, text: FEED_WISH.trace.text, epitaph: FEED_WISH.trace.epitaph, ts: 1718201100, epoch: list.length + 1, }); return { list, traces: { ...traces, [FEED_WISH.trace.wish_id]: FEED_WISH.trace }, feed: FEED_WISH }; } const list = await fetch("/api/wishes").then((r) => { if (!r.ok) throw new Error(`wishes ${r.status}`); return r.json(); }); return { list, traces: null, feed: null }; } async function loadState() { if (MOCK) { const state = await fetch("./mock/world.json").then((r) => r.json()); const { FEED_WISH } = await import("./mock/feed.js"); return { features: [...state.world.features, ...FEED_WISH.features], epoch: state.world.epoch + 1, }; } const state = await fetch("/api/state").then((r) => r.json()); return { features: state.world?.features ?? [], epoch: state.world?.epoch ?? 0 }; } async function loadTrace(ctx, wishId) { if (ctx.traces) return ctx.traces[wishId]; const r = await fetch(`/api/wishes/${encodeURIComponent(wishId)}`); if (!r.ok) throw new Error(`trace ${r.status}`); return r.json(); } // ---------------------------------------------------------------- replay const runner = new ScriptRunner({}); let phase = "reading"; let lastDescentDir = null; // The direction a feature landed at (descent target). Sky/weather have none. function featureDir(feature) { const a = feature?.args || {}; if (feature?.tool === "set_sky" || feature?.tool === "set_weather") return null; if (a.path && a.path.length) return latLonToDir(a.path[0][0], a.path[0][1]); if (a.lat != null && a.lon != null) return latLonToDir(a.lat, a.lon); return null; } function replayWish(trace, allFeatures) { runner.cancel(); els.stream.innerHTML = ""; els.epitaph.classList.remove("is-shown"); els.voiceWish.textContent = `“${trace.text}”`; els.voiceDot.classList.add("is-live"); phase = "reading"; lastDescentDir = null; // world at features[0..K) const mine = allFeatures.filter((f) => f.wish_id === trace.wish_id); const firstIdx = allFeatures.findIndex((f) => f.wish_id === trace.wish_id); const base = firstIdx === -1 ? allFeatures : allFeatures.slice(0, firstIdx); const features = mine.length ? mine : (trace.features ?? []); app.world.reset(base, app.simTime); app.rig.setMode("town"); // replays begin in the close town view syncTown(); runner.handlers = { started: () => {}, phase: () => {}, token: (e) => { const tone = e.text.startsWith("\n›") ? "obs" : phase === "reading" ? "reading" : "thought"; tickerPush(e.text, tone); }, call: (e) => { phase = "turn"; const a = e.call.args || {}; tickerPush(`\n⟡ ${e.call.tool}\n`, "call"); let target = null; if (a.path?.length) target = latLonToDir(a.path[0][0], a.path[0][1]).multiplyScalar(PLANET_R); else if (a.lat != null) target = latLonToDir(a.lat, a.lon).multiplyScalar(PLANET_R); app.rig.gaze(target, { hold: 4.5 }); }, delta: (e) => { const cue = app.world.applyFeature(e.feature, { animate: true, t: app.simTime }); if (e.feature.tool === "place_structure" || e.feature.tool === "build_district") { syncTown(); // the town heart follows each new building during the replay } if (cue.target) app.rig.gaze(cue.target.clone().multiplyScalar(PLANET_R), { hold: 4 }); else if (cue.wide) app.rig.gaze(null, { hold: 3.5 }); const dir = cue.target || featureDir(e.feature); if (dir) lastDescentDir = dir.clone().normalize(); }, granted: (e) => { els.epitaphText.textContent = e.epitaph; els.epitaph.classList.add("is-shown"); els.voiceDot.classList.remove("is-live"); app.world.sky.flash(0.3); // visit it: after the final delta, swoop down over the wish site, then rise if (lastDescentDir) { app.rig.setTerrain?.(app.world.terrain); app.rig.descend(lastDescentDir.clone().multiplyScalar(PLANET_R)); } else { app.rig.release(); } setTimeout(() => els.epitaph.classList.remove("is-shown"), 8500); }, }; runner.start(buildWishScript(trace, features), app.simTime); } app.onTick((t) => runner.update(t)); // ---------------------------------------------------------------- boot (async () => { try { const [ctx, state] = await Promise.all([loadIndex(), loadState()]); // newest first — a just-granted wish must appear at the TOP of the log. // epoch is the server's monotonic grant count (start=1); ts can be 0, so // epoch is the primary key, ts the tiebreak. const ord = (w, i) => (w.epoch ?? w.ts ?? i) * 1e6 + (w.ts ?? 0); const list = ctx.list .map((w, i) => ({ ...w, _ord: ord(w, i) })) .sort((a, b) => b._ord - a._ord); // world as it stands now (zero GPU, gorgeous by default). Count source is // unified with the landing page: epoch === number of granted wishes. app.world.reset(state.features, app.simTime); syncTown(); // open the log over the close town view (matches the landing page) const granted = state.epoch ?? list.length; els.status.textContent = `epoch ${granted} · ${granted} ${granted === 1 ? "wish" : "wishes"} granted`; if (!list.length) { const div = document.createElement("div"); div.className = "ledger__empty"; div.textContent = "No wishes yet. The god waits for the first voice."; els.ledger.appendChild(div); return; } const buttons = new Map(); list.forEach((w) => { const btn = document.createElement("button"); btn.className = "ledger__entry"; btn.innerHTML = `
epoch ${w.epoch ?? "—"}
`; btn.querySelector(".ledger__wish").textContent = `“${w.text}”`; btn.querySelector(".ledger__epitaph").textContent = w.epitaph ?? ""; btn.addEventListener("click", async () => { location.hash = w.wish_id; buttons.forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); els.hint.textContent = "replaying — the same words make the same world"; try { const trace = await loadTrace(ctx, w.wish_id); if (trace) replayWish(trace, state.features); } catch (err) { els.status.textContent = "that page of the log would not open"; console.error(err); } }); els.ledger.appendChild(btn); buttons.set(w.wish_id, btn); }); // deep link #w_000005 const target = location.hash.replace("#", ""); if (target && buttons.has(target)) buttons.get(target).click(); window.__godseed = { ready: true, app, tick: (s) => app.tick(s), seek: (t) => app.seek(t), pause: (v) => app.pause(v), stats: () => app.stats(), replay: (id) => buttons.get(id)?.click(), descend: (lat, lon, duration = 13) => { app.rig.setTerrain?.(app.world.terrain); app.rig.descend(latLonToDir(lat, lon).multiplyScalar(PLANET_R), { duration }); }, setCamera: (theta, phi, dist) => { Object.assign(app.rig, { theta, thetaT: theta, phi, phiT: phi, dist: dist ?? app.rig.dist, distT: dist ?? app.rig.distT, }); }, }; } catch (err) { els.status.textContent = "the log could not be opened"; console.error(err); } })();