godseed / web /book.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
10.3 kB
// 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 = `
<div class="ledger__epoch">epoch ${w.epoch ?? "—"}</div>
<div class="ledger__wish"></div>
<div class="ledger__epitaph"></div>`;
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);
}
})();