godseed / web /main.js
AndresCarreon's picture
Town Mode: close town view by default, build_district + place_road, bank/market/house, grow-one-town steering, behold-the-world reveal, tamed needle
b0d758d verified
Raw
History Blame Contribute Delete
21.4 kB
// GODSEED — main page. Boot the planet, wire the HUD, then either:
// live : GET /api/state → EventSource /api/stream (SSE protocol)
// mock : ?mock=1 → web/mock/world.json + scripted looping wish feed
// The deterministic engine owns the facts; this file only performs them.
import { createPlanetApp } from "./planet/scene.js";
import { buildWishScript, ScriptRunner } from "./planet/replay.js";
import { latLonToDir, PLANET_R, ordinal, clamp, townCenter } from "./planet/util.js";
const params = new URLSearchParams(location.search);
const MOCK = params.has("mock");
const STATIC = params.has("static");
const UPTO = params.has("upto") ? Math.max(0, parseInt(params.get("upto"), 10) || 0) : null;
// ---------------------------------------------------------------- HUD
const els = {
epoch: document.getElementById("epoch"),
voiceDot: document.getElementById("voiceDot"),
voiceWish: document.getElementById("voiceWish"),
stream: document.getElementById("voiceStream"),
epitaph: document.getElementById("epitaph"),
epitaphText: document.getElementById("epitaphText"),
epitaphLabel: document.getElementById("epitaphLabel"),
epitaphDescend: document.getElementById("epitaphDescend"),
queueChip: document.getElementById("queueChip"),
form: document.getElementById("wishForm"),
input: document.getElementById("wishInput"),
send: document.getElementById("wishSend"),
hint: document.getElementById("altarHint"),
status: document.getElementById("status"),
behold: document.getElementById("beholdToggle"),
};
const HINT_DEFAULT = "the god grants one wish at a time";
let hintTimer = null;
function hint(text, { warn = false, revert = 6000 } = {}) {
els.hint.textContent = text;
els.hint.classList.toggle("is-warn", warn);
clearTimeout(hintTimer);
if (revert) {
hintTimer = setTimeout(() => {
els.hint.textContent = HINT_DEFAULT;
els.hint.classList.remove("is-warn");
}, revert);
}
}
const caret = document.createElement("span");
caret.className = "voice__caret";
function tickerClear() {
els.stream.innerHTML = "";
}
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();
}
function tickerIdle() {
caret.remove();
els.voiceDot.classList.remove("is-live");
}
function tickerLive(wishText) {
els.voiceDot.classList.add("is-live");
els.voiceWish.textContent = wishText ? `“${wishText}”` : "";
}
let epitaphTimer = null;
function showEpitaph(text, label = "the wish is granted") {
els.epitaphText.textContent = text;
els.epitaphLabel.textContent = label;
els.epitaph.classList.add("is-shown");
clearTimeout(epitaphTimer);
// hold long enough for the eye to find the "descend ↓" affordance
epitaphTimer = setTimeout(() => {
els.epitaph.classList.remove("is-shown");
if (els.epitaphDescend) els.epitaphDescend.hidden = true;
}, 9000);
}
function setEpoch(epoch, granted = epoch) {
els.epoch.textContent = `Epoch ${epoch} · ${granted} ${granted === 1 ? "wish" : "wishes"} granted`;
}
function setQueue(length, mine = null) {
if (mine != null) {
els.queueChip.hidden = false;
els.queueChip.textContent = `your wish is ${ordinal(mine)} in line`;
} else if (length > 0) {
els.queueChip.hidden = false;
els.queueChip.textContent = `${length} ${length === 1 ? "wish waits" : "wishes wait"}`;
} else {
els.queueChip.hidden = true;
}
}
function fmtCall(call) {
const a = call.args || {};
const bits = [];
if (a.kind) bits.push(a.kind);
if (a.palette) bits.push(a.palette);
if (a.lat != null) bits.push(`${(+a.lat).toFixed(0)}°, ${(+a.lon).toFixed(0)}°`);
if (a.path) bits.push(`${a.path.length} waypoints`);
if (a.radius_deg != null) bits.push(`r ${(+a.radius_deg).toFixed(0)}°`);
if (a.height != null) bits.push(`h ${a.height}`);
if (a.depth != null) bits.push(`d ${a.depth}`);
if (a.intensity != null) bits.push(`i ${a.intensity}`);
if (a.moons != null) bits.push(`${a.moons} moon${a.moons === 1 ? "" : "s"}`);
if (a.style) bits.push(a.style);
return `\n⟡ ${call.tool} · ${bits.join(" · ")}\n`;
}
function callTarget(call) {
const a = call.args || {};
if (call.tool === "set_sky" || call.tool === "set_weather") return null;
if (call.tool === "inscribe_wish") return null; // delta will refine (stone seats by seed)
if (a.path && a.path.length) return latLonToDir(a.path[0][0], a.path[0][1]).multiplyScalar(PLANET_R);
if (a.lat != null && a.lon != null) return latLonToDir(a.lat, a.lon).multiplyScalar(PLANET_R);
return null;
}
// The direction on the surface a feature landed at (for the descent target).
// Sky/weather have no place, so they don't count toward "where to visit".
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;
}
// "descend ↓" affordance on the epitaph card → swoop down over the wish site.
function armDescent(dir) {
if (!els.epitaphDescend) return;
if (dir) {
lastDescentDir = dir.clone();
els.epitaphDescend.hidden = false;
} else if (!lastDescentDir) {
els.epitaphDescend.hidden = true;
}
}
// ---------------------------------------------------------------- boot
let app;
try {
app = createPlanetApp(document.getElementById("scene"), { interactive: true });
} catch (err) {
els.status.textContent = "this vessel cannot hold the world (WebGL unavailable)";
console.error(err);
throw err;
}
// last surface direction a wish landed at — the descent target.
let lastDescentDir = null;
if (els.epitaphDescend) {
els.epitaphDescend.addEventListener("click", () => {
if (!lastDescentDir) return;
app.rig.setTerrain?.(app.world.terrain); // clear live terrain analytically
app.rig.descend(lastDescentDir.clone().multiplyScalar(PLANET_R));
});
}
// Re-anchor the town heart from the current world (camera follows it as it
// densifies). Called on every state load + world_delta.
function syncTown() {
app.rig.setTownCenter(app.world.features);
}
// "behold the world ↕" — pull back to the full planet; "↻ return to the town" —
// drop back into the close town view. Reflects the rig's current mode.
function updateBeholdLabel() {
if (!els.behold) return;
const world = app.rig.mode === "world";
els.behold.classList.toggle("is-world", world);
els.behold.setAttribute("aria-pressed", world ? "true" : "false");
els.behold.querySelector(".behold__label").textContent =
world ? "return to the town" : "behold the world";
}
if (els.behold) {
els.behold.addEventListener("click", () => {
app.rig.beholdWorld();
updateBeholdLabel();
});
updateBeholdLabel();
}
const featuresByWish = (features, upto) => {
if (upto == null) return features;
const order = [];
const seen = new Set();
for (const f of features) {
if (!seen.has(f.wish_id)) { seen.add(f.wish_id); order.push(f.wish_id); }
}
const keep = new Set(order.slice(0, upto + 1)); // genesis is index 0
return features.filter((f) => keep.has(f.wish_id));
};
function applyDevOverrides() {
if (params.has("sky")) {
app.world.sky.set(params.get("sky"), parseFloat(params.get("stars") ?? "0.8"),
parseInt(params.get("moons") ?? "1", 10), { animate: false });
}
if (params.has("weather")) {
app.world.weather.set(params.get("weather"), parseFloat(params.get("wi") ?? "0.6"), { animate: false });
}
}
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;
}
// ---------------------------------------------------------------- live mode
async function bootLive() {
let stateVersion = null;
let readingPhase = 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;
app.world.reset(state.world?.features ?? [], app.simTime);
syncTown(); // anchor the town heart, then frame it (the default close view)
const epoch = state.world?.epoch ?? countWishes(state.world?.features ?? []);
setEpoch(epoch, epoch);
setQueue(state.queue?.length ?? 0);
return state;
}
try {
await loadState();
els.status.textContent = "live · the god is listening";
} catch (err) {
console.warn("no live backend:", err);
els.status.textContent = "the god is dreaming · demo vision";
return bootMock(); // graceful fall-back to the mock world
}
// Browser-invoked granting (ZeroGPU): the GPU rite must run inside OUR
// gradio request so the HF auth headers carry the quota. When the SSE says
// it's our wish's turn, call the named gradio endpoint with the one-time token.
const myWishes = new Set();
// your_turn can arrive BEFORE the POST response registers our wish_id (an
// empty queue turns to a wish instantly) — buffer unmatched turns and replay
// them the moment the wish registers.
const pendingTurns = new Set();
let gradioClient = null;
async function invokeGrant(wishId) {
els.status.textContent = "the god turns to your wish…";
try {
// Fetch our secret token from the cookie-authenticated route — it is
// never broadcast over SSE, so only the wish's owner can begin the rite.
const tr = await fetch(`/api/turn?wish_id=${encodeURIComponent(wishId)}`, {
credentials: "same-origin",
});
if (!tr.ok) {
if (tr.status === 403) return; // not our turn (a stranger's wish)
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) hint(out.reason, { warn: true });
} catch (err) {
console.error("grant invocation failed", err);
hint("the rite could not begin — sign in to hugging face, then wish again", { warn: true });
}
}
const es = new EventSource("/api/stream");
es.onmessage = (e) => {
let m;
try { m = JSON.parse(e.data); } catch { return; }
switch (m.type) {
case "your_turn":
if (myWishes.has(m.wish_id)) invokeGrant(m.wish_id);
else pendingTurns.add(m.wish_id);
break;
case "hello":
if (stateVersion != null && m.world_version !== stateVersion) loadState().catch(() => {});
if (m.epoch != null) setEpoch(m.epoch, m.epoch);
break;
case "queue":
setQueue(m.length ?? 0);
if (m.current?.text_preview) tickerLive(m.current.text_preview);
break;
case "wish_started":
tickerClear();
readingPhase = true;
tickerLive(m.text);
lastDescentDir = null;
if (els.epitaphDescend) els.epitaphDescend.hidden = true;
break;
case "thought_token":
tickerPush(m.text, readingPhase ? "reading" : "thought");
break;
case "tool_call": {
readingPhase = false;
tickerPush(fmtCall(m), "call");
app.rig.gaze(callTarget(m), { hold: 5 });
break;
}
case "world_delta": {
if (m.feature) {
stateVersion = stateVersion == null ? null : stateVersion + 1;
const cue = app.world.applyFeature(m.feature, { animate: true, t: app.simTime });
// a new building shifts the town's heart — re-anchor before framing so
// the camera follows the town as it densifies (no-op for sky/weather).
if (m.feature.tool === "place_structure" || m.feature.tool === "build_district") {
syncTown();
}
// gaze toward where the world is actually changing — the cue target is
// the resolved surface point (refines inscriptions/structures by seed).
// In TOWN mode gaze() keeps this WITHIN the town zoom (never the far limb).
if (cue.target) app.rig.gaze(cue.target.clone().multiplyScalar(PLANET_R), { hold: 5.5 });
else if (cue.wide) app.rig.gaze(null, { hold: 4 });
const dir = cue.target || featureDir(m.feature);
if (dir) lastDescentDir = dir.clone().normalize();
}
break;
}
case "wish_granted":
showEpitaph(m.epitaph || "it is made");
if (m.epoch != null) setEpoch(m.epoch, m.epoch);
app.world.sky.flash(0.35);
armDescent(lastDescentDir);
// hold a beat on the wish site, THEN release to the wide shot
if (lastDescentDir && !app.rig.descending) {
app.rig.gaze(lastDescentDir.clone().multiplyScalar(PLANET_R), { hold: 3.6 });
} else {
app.rig.release();
}
tickerIdle();
els.status.textContent = "live · the god is listening";
break;
case "wish_rejected":
hint(m.reason || "the god declines this wish.", { warn: true });
tickerIdle();
break;
case "heartbeat":
break;
}
};
es.onerror = () => {
els.status.textContent = "the link to the god flickers · reconnecting";
};
es.addEventListener("open", () => {
els.status.textContent = "live · the god is listening";
});
// wish submission
els.form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = els.input.value.trim();
if (!text) return;
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);
if (pendingTurns.has(body.wish_id)) {
pendingTurns.delete(body.wish_id);
invokeGrant(body.wish_id);
}
}
setQueue(body.position ?? 1, body.position ?? 1);
hint("the god has heard you — stay; your presence completes the rite", { revert: 6000 });
} else {
hint(body.reason || body.detail || "the god declines this wish.", { warn: true });
}
} catch {
hint("the offering was lost — try again", { warn: true });
} finally {
els.send.disabled = false;
}
});
}
// ---------------------------------------------------------------- mock mode
let mockApi = {};
async function bootMock() {
const [state, { FEED_WISH }] = await Promise.all([
fetch("./mock/world.json").then((r) => r.json()),
import("./mock/feed.js"),
]);
const baseFeatures = featuresByWish(state.world.features, UPTO);
const baseEpoch = countWishes(baseFeatures);
app.world.reset(baseFeatures, app.simTime);
syncTown(); // anchor + frame the town heart (the default close view)
applyDevOverrides();
setEpoch(baseEpoch, baseEpoch);
els.status.textContent = MOCK ? "demo vision · ?mock=1" : "the god is dreaming · demo vision";
const looping = !STATIC && UPTO == null;
// -- looping scripted feed: one full wish, replayed forever ------------
const runner = new ScriptRunner({
started: (e) => { tickerClear(); tickerLive(e.text); },
phase: () => {},
token: (e) => {
const tone = e.text.startsWith("\n›") ? "obs" : phase === "reading" ? "reading" : "thought";
tickerPush(e.text, tone);
},
call: (e) => {
phase = "turn";
tickerPush(fmtCall(e.call), "call");
app.rig.gaze(callTarget(e.call), { 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
}
if (cue.target) app.rig.gaze(cue.target.clone().multiplyScalar(PLANET_R), { hold: 4.5 });
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) => {
showEpitaph(e.epitaph);
setEpoch(baseEpoch + 1, baseEpoch + 1);
app.world.sky.flash(0.35);
armDescent(lastDescentDir);
if (lastDescentDir && !app.rig.descending) {
app.rig.gaze(lastDescentDir.clone().multiplyScalar(PLANET_R), { hold: 3.6 });
} else {
app.rig.release();
}
tickerIdle();
cooldown = 12; // dwell on the granted world (descend affordance) before reset
},
});
let phase = "reading";
let cooldown = 7; // contemplative pause before the first / between loops
let pendingWishText = null;
function startFeed(customText) {
phase = "reading";
lastDescentDir = null;
if (els.epitaphDescend) els.epitaphDescend.hidden = true;
const trace = customText ? { ...FEED_WISH.trace, text: customText } : FEED_WISH.trace;
runner.start(buildWishScript(trace, FEED_WISH.features), app.simTime);
}
app.onTick((t, dt) => {
runner.update(t);
if (!looping || runner.active) return;
cooldown -= dt;
if (cooldown <= 0) {
// reset to the base world, then perform the wish again — determinism on display
if (app.world.features.length > baseFeatures.length) {
app.world.reset(baseFeatures, t);
syncTown(); // back to the base town heart before the wish runs again
setEpoch(baseEpoch, baseEpoch);
tickerClear();
tickerIdle();
els.voiceWish.textContent = "";
}
startFeed(pendingWishText);
if (pendingWishText) { setQueue(0); pendingWishText = null; }
cooldown = 10;
}
});
if (looping) {
els.form.addEventListener("submit", (e) => {
e.preventDefault();
const text = els.input.value.trim();
if (!text) return;
pendingWishText = text.slice(0, 140);
els.input.value = "";
setQueue(0, 1);
hint("demo vision — the god will dream your words", { revert: 7000 });
});
}
mockApi.runFeedOnce = startFeed;
mockApi.resetTo = (upto) => {
runner.cancel();
const feats = featuresByWish(state.world.features, upto ?? null);
app.world.reset(feats, app.simTime);
setEpoch(countWishes(feats), countWishes(feats));
};
}
// ---------------------------------------------------------------- start
const ready = (MOCK ? bootMock() : bootLive())
.then(() => { window.__godseed.ready = true; })
.catch((err) => {
els.status.textContent = "the vision failed to load";
console.error(err);
});
app.start();
// status heartbeat (epoch + sky, quiet diagnostics)
setInterval(() => {
const s = app.stats();
// base = everything that isn't a previous "<palette> sky" suffix (the status
// text varies between 1 and 2 segments; a fixed slice duplicated the suffix)
const base = els.status.textContent
.split(" · ")
.filter((p) => !p.trim().endsWith(" sky"))
.join(" · ");
els.status.textContent = `${base} · ${s.sky} sky`;
}, 4000);
// ---------------------------------------------------------------- harness API
window.__godseed = {
ready: false,
app,
get world() { return app.world; },
get rig() { return app.rig; },
tick: (seconds) => app.tick(seconds),
seek: (t) => app.seek(t),
pause: (v) => app.pause(v),
stats: () => app.stats(),
gazeLatLon: (lat, lon, hold = 30) => {
app.rig.userHold = 0;
app.rig.gaze(latLonToDir(lat, lon).multiplyScalar(PLANET_R), { hold });
},
gazeWide: () => { app.rig.setMode("world"); updateBeholdLabel(); },
setMode: (mode) => { app.rig.setMode(mode); updateBeholdLabel(); },
beholdWorld: () => { const m = app.rig.beholdWorld(); updateBeholdLabel(); return m; },
townCenter: () => townCenter(app.world.features),
descend: (lat, lon, duration = 13) => {
app.rig.setTerrain?.(app.world.terrain);
app.rig.descend(latLonToDir(lat, lon).multiplyScalar(PLANET_R), { duration });
},
descendLast: (duration = 13) => {
if (!lastDescentDir) return false;
app.rig.setTerrain?.(app.world.terrain);
app.rig.descend(lastDescentDir.clone().multiplyScalar(PLANET_R), { duration });
return true;
},
setSky: (p, stars = 0.8, moons = 1) => app.world.sky.set(p, stars, moons, { animate: false }),
setWeather: (k, i = 0.6) => app.world.weather.set(k, i, { animate: false }),
setCamera: (theta, phi, dist) => {
Object.assign(app.rig, {
theta, thetaT: theta, phi, phiT: phi,
dist: dist ?? app.rig.dist, distT: dist ?? app.rig.distT,
});
},
runFeedOnce: (text) => mockApi.runFeedOnce?.(text),
resetTo: (n) => mockApi.resetTo?.(n),
};