This commit is contained in:
@@ -9615,6 +9615,165 @@
|
||||
"thematicLinks": [],
|
||||
"symbolicLinks": [],
|
||||
"triggerLinks": []
|
||||
},
|
||||
{
|
||||
"id": "search-route-gentle-constellarium",
|
||||
"type": "search route",
|
||||
"title": "Route: gentle constellarium",
|
||||
"content": "/play/gentle-constellarium.html",
|
||||
"characters": [
|
||||
"lima",
|
||||
"aphy",
|
||||
"sensei chi"
|
||||
],
|
||||
"emotionalTone": "soft",
|
||||
"rarity": "rare",
|
||||
"triggerConditions": "gentle constellarium",
|
||||
"tags": [
|
||||
"observatory",
|
||||
"thread",
|
||||
"constellation"
|
||||
],
|
||||
"category": "search",
|
||||
"pageLocation": "/play/gentle-constellarium.html",
|
||||
"familyLayer": "2",
|
||||
"enabled": true,
|
||||
"createdDate": "2026-05-10",
|
||||
"modifiedDate": "2026-05-10",
|
||||
"notes": "Hidden entry into the constellation restoration ritual.",
|
||||
"audioSettings": "",
|
||||
"animationTrigger": "observatory-sky-soften",
|
||||
"cssClassHooks": "observatory-ritual gentle-constellarium",
|
||||
"chainReferences": [],
|
||||
"continuationLinks": [],
|
||||
"emotionalRole": "restoration",
|
||||
"discoveryDifficulty": "gentle",
|
||||
"mysteryLevel": "quiet",
|
||||
"resonanceScore": 7,
|
||||
"symbols": [
|
||||
"thread",
|
||||
"star",
|
||||
"ring"
|
||||
],
|
||||
"narrativeArcs": [
|
||||
"observatory rituals",
|
||||
"emotional constellations"
|
||||
],
|
||||
"parentLinks": [],
|
||||
"childLinks": [],
|
||||
"echoes": [],
|
||||
"mirroredEntries": [],
|
||||
"thematicLinks": [],
|
||||
"symbolicLinks": [],
|
||||
"triggerLinks": []
|
||||
},
|
||||
{
|
||||
"id": "search-route-future-z-return-log",
|
||||
"type": "search route",
|
||||
"title": "Route: future z return log",
|
||||
"content": "/play/future-z-return-log.html",
|
||||
"characters": [
|
||||
"future z",
|
||||
"sensei chi",
|
||||
"lima"
|
||||
],
|
||||
"emotionalTone": "nostalgic",
|
||||
"rarity": "timed",
|
||||
"triggerConditions": "future z return log, return log, revisit",
|
||||
"tags": [
|
||||
"observatory",
|
||||
"time",
|
||||
"return"
|
||||
],
|
||||
"category": "search",
|
||||
"pageLocation": "/play/future-z-return-log.html",
|
||||
"familyLayer": "4",
|
||||
"enabled": true,
|
||||
"createdDate": "2026-05-10",
|
||||
"modifiedDate": "2026-05-10",
|
||||
"notes": "Hidden route for revisit-aware future z annotations.",
|
||||
"audioSettings": "",
|
||||
"animationTrigger": "temporal-echo",
|
||||
"cssClassHooks": "observatory-ritual future-return-log",
|
||||
"chainReferences": [],
|
||||
"continuationLinks": [],
|
||||
"emotionalRole": "revisit",
|
||||
"discoveryDifficulty": "patient",
|
||||
"mysteryLevel": "quietly alive",
|
||||
"resonanceScore": 8,
|
||||
"symbols": [
|
||||
"clock",
|
||||
"door",
|
||||
"return"
|
||||
],
|
||||
"narrativeArcs": [
|
||||
"time echoes",
|
||||
"observatory rituals"
|
||||
],
|
||||
"parentLinks": [],
|
||||
"childLinks": [],
|
||||
"echoes": [],
|
||||
"mirroredEntries": [],
|
||||
"thematicLinks": [],
|
||||
"symbolicLinks": [],
|
||||
"triggerLinks": []
|
||||
},
|
||||
{
|
||||
"id": "search-route-subconscious-index",
|
||||
"type": "search route",
|
||||
"title": "Route: subconscious index",
|
||||
"content": "/play/subconscious-index.html",
|
||||
"characters": [
|
||||
"young z",
|
||||
"aphy",
|
||||
"sensei chi",
|
||||
"lima",
|
||||
"future z"
|
||||
],
|
||||
"emotionalTone": "strange",
|
||||
"rarity": "very rare",
|
||||
"triggerConditions": "subconscious index, dream index, recurring symbols",
|
||||
"tags": [
|
||||
"observatory",
|
||||
"symbols",
|
||||
"dream"
|
||||
],
|
||||
"category": "search",
|
||||
"pageLocation": "/play/subconscious-index.html",
|
||||
"familyLayer": "5",
|
||||
"enabled": true,
|
||||
"createdDate": "2026-05-10",
|
||||
"modifiedDate": "2026-05-10",
|
||||
"notes": "Hidden symbolic dream-navigation layer for recurring observatory objects.",
|
||||
"audioSettings": "",
|
||||
"animationTrigger": "symbolic-drift",
|
||||
"cssClassHooks": "observatory-ritual subconscious-index",
|
||||
"chainReferences": [],
|
||||
"continuationLinks": [],
|
||||
"emotionalRole": "symbolic synthesis",
|
||||
"discoveryDifficulty": "hidden",
|
||||
"mysteryLevel": "subconscious",
|
||||
"resonanceScore": 9,
|
||||
"symbols": [
|
||||
"ring",
|
||||
"tea",
|
||||
"clock",
|
||||
"door",
|
||||
"mirror",
|
||||
"crayon sun",
|
||||
"console"
|
||||
],
|
||||
"narrativeArcs": [
|
||||
"symbolic subconscious",
|
||||
"observatory rituals"
|
||||
],
|
||||
"parentLinks": [],
|
||||
"childLinks": [],
|
||||
"echoes": [],
|
||||
"mirroredEntries": [],
|
||||
"thematicLinks": [],
|
||||
"symbolicLinks": [],
|
||||
"triggerLinks": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -358,6 +358,9 @@
|
||||
"sensei chi": "sensei chi: the search is also searching you.",
|
||||
"young z": "Search result: age 7, blanket cape, crayon sun.",
|
||||
"future z": "future z: I cannot spoil the ending. I can say keep going.",
|
||||
"gentle constellarium": "The observatory sky loosens. A thread is waiting to be steadied.",
|
||||
"return log": "future z has delayed the message just enough for it to mean something.",
|
||||
"subconscious index": "The website answers in objects when ordinary search gets too direct.",
|
||||
"2022": "lima layer: beginning stored as warmth, not trivia.",
|
||||
"oct 2025": "Engagement memory: the calendar learned to glow.",
|
||||
"age 7": "young z layer: crayons, cartoons, and a cape made from a blanket.",
|
||||
@@ -372,6 +375,11 @@
|
||||
"sensei garden": "/play/sensei-garden.html",
|
||||
"young z desk": "/play/young-z-desk.html",
|
||||
"future log": "/play/future-log.html",
|
||||
"gentle constellarium": "/play/gentle-constellarium.html",
|
||||
"return log": "/play/future-z-return-log.html",
|
||||
"future z return log": "/play/future-z-return-log.html",
|
||||
"subconscious index": "/play/subconscious-index.html",
|
||||
"dream index": "/play/subconscious-index.html",
|
||||
"family layer index": "/play/family-layer-index.html",
|
||||
"do not open": "/play/do-not-open.html",
|
||||
"voice note": "/play/cassette-log.html",
|
||||
@@ -392,6 +400,10 @@
|
||||
"phrase": "dream",
|
||||
"route": "/play/dream-corridor.html"
|
||||
},
|
||||
{
|
||||
"phrase": "thread",
|
||||
"route": "/play/gentle-constellarium.html"
|
||||
},
|
||||
{
|
||||
"phrase": "oldweb",
|
||||
"message": "aphy briefly considers a table layout, then apologizes to CSS."
|
||||
@@ -416,6 +428,18 @@
|
||||
"phrase": "leave the light on",
|
||||
"route": "/play/kitchen-light.html"
|
||||
},
|
||||
{
|
||||
"phrase": "restore the thread",
|
||||
"route": "/play/gentle-constellarium.html"
|
||||
},
|
||||
{
|
||||
"phrase": "future z return log",
|
||||
"route": "/play/future-z-return-log.html"
|
||||
},
|
||||
{
|
||||
"phrase": "subconscious index",
|
||||
"route": "/play/subconscious-index.html"
|
||||
},
|
||||
{
|
||||
"phrase": "sensei chi",
|
||||
"message": "sensei chi: a hidden word is still a word."
|
||||
@@ -890,6 +914,32 @@
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function addObservatoryPlayDoorways() {
|
||||
let playState = {};
|
||||
try {
|
||||
playState = JSON.parse(localStorage.getItem("zxh_observatory_play_v1") || "{}");
|
||||
} catch (_) {}
|
||||
const restored = (playState.restoredThreads || []).length;
|
||||
const symbols = Object.keys(playState.discoveredSymbols || {}).length;
|
||||
const patient = Number(playState.patientMoments || 0);
|
||||
const options = [];
|
||||
if (restored > 0) options.push(["thread", "/play/future-z-return-log.html", "future z remembers the thread you restored."]);
|
||||
if (symbols > 0) options.push(["symbol", "/play/subconscious-index.html", "a symbol you found is still casting a shadow."]);
|
||||
if (patient > 0) options.push(["return", "/play/gentle-constellarium.html", "sensei chi notices the slower way you move through the site."]);
|
||||
if (!options.length && memory.visits >= 5 && (playState.crossGameFlags || {})["future-z:observed"]) {
|
||||
options.push(["echo", "/play/subconscious-index.html", "aphy found an echo between the return log and the dream index."]);
|
||||
}
|
||||
if (!options.length) return;
|
||||
const [label, href, message] = pick(options, "observatory-doorway");
|
||||
const link = document.createElement("a");
|
||||
link.className = "hidden-oldweb-stamp";
|
||||
link.href = href;
|
||||
link.textContent = label;
|
||||
link.title = message;
|
||||
link.addEventListener("click", () => toast(message));
|
||||
document.body.appendChild(link);
|
||||
}
|
||||
|
||||
function addPageSpecificSecrets() {
|
||||
// Events only used by specific `/play/...` pages.
|
||||
if (path.includes("/play/cassette-log")) {
|
||||
@@ -1001,6 +1051,7 @@
|
||||
addPatienceRewards,
|
||||
addRareSecondLayer,
|
||||
addFamilyLayerIndex,
|
||||
addObservatoryPlayDoorways,
|
||||
addPageSpecificSecrets,
|
||||
addInvisibleHoverSecrets
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
const root = $(".play-root[data-play-page]");
|
||||
if (!root) return;
|
||||
const page = root.dataset.playPage;
|
||||
const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg };
|
||||
const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg, gentleConstellarium, futureZReturnLog, subconsciousIndex };
|
||||
if (inits[page]) inits[page](root);
|
||||
});
|
||||
|
||||
@@ -839,6 +839,314 @@
|
||||
load();
|
||||
}
|
||||
|
||||
const observatoryKey = "zxh_observatory_play_v1";
|
||||
|
||||
function readObservatoryState() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(observatoryKey) || "{}");
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeObservatoryState(next) {
|
||||
localStorage.setItem(observatoryKey, JSON.stringify(next));
|
||||
}
|
||||
|
||||
async function loadObservatory() {
|
||||
try {
|
||||
const response = await fetch("/assets/content/hidden-details.json", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("hidden details unavailable");
|
||||
const data = await response.json();
|
||||
return (data.entries || []).filter((entry) => entry.enabled !== false);
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function entryDepth(entry) {
|
||||
const layer = Number(entry.familyLayer);
|
||||
if (Number.isFinite(layer)) return Math.max(0, Math.min(5, layer));
|
||||
if ((entry.characters || []).includes("future z")) return 4;
|
||||
if ((entry.characters || []).includes("sensei chi")) return 3;
|
||||
if ((entry.characters || []).includes("young z") || (entry.characters || []).includes("lima")) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function characterLine(entry, fallback) {
|
||||
const chars = entry.characters || [];
|
||||
if (chars.includes("lima")) return "lima steadies this without making it smaller.";
|
||||
if (chars.includes("aphy")) return "aphy logs the relationship, then softens the label.";
|
||||
if (chars.includes("sensei chi")) return "sensei chi waits until the pattern stops trying to impress you.";
|
||||
if (chars.includes("young z")) return "young z thinks this memory needs a door and a sun.";
|
||||
if (chars.includes("future z")) return "future z has seen this thread from farther down the path.";
|
||||
return fallback || "the observatory accepts the shape for now.";
|
||||
}
|
||||
|
||||
function relationScore(a, b) {
|
||||
const shared = (left, right) => left.filter((item) => right.includes(item)).length;
|
||||
let score = 0;
|
||||
score += shared(a.characters || [], b.characters || []) * 3;
|
||||
score += shared(a.symbols || [], b.symbols || []) * 4;
|
||||
score += shared(a.narrativeArcs || [], b.narrativeArcs || []) * 3;
|
||||
score += a.emotionalTone && a.emotionalTone === b.emotionalTone ? 2 : 0;
|
||||
score += Math.max(0, 3 - Math.abs(entryDepth(a) - entryDepth(b)));
|
||||
["continuationLinks", "chainReferences", "echoes", "thematicLinks", "symbolicLinks", "triggerLinks", "parentLinks", "childLinks", "mirroredEntries"].forEach((key) => {
|
||||
if ((a[key] || []).includes(b.id) || (b[key] || []).includes(a.id)) score += 5;
|
||||
});
|
||||
return score;
|
||||
}
|
||||
|
||||
function importantEntries(entries, wantedCharacters) {
|
||||
return entries
|
||||
.filter((entry) => entry.title && entry.content)
|
||||
.filter((entry) => !wantedCharacters || (entry.characters || []).some((name) => wantedCharacters.includes(name)))
|
||||
.sort((a, b) => Number(b.resonanceScore || 0) - Number(a.resonanceScore || 0) || entryDepth(b) - entryDepth(a))
|
||||
.slice(0, 18);
|
||||
}
|
||||
|
||||
async function gentleConstellarium(root) {
|
||||
const entries = importantEntries(await loadObservatory(), ["lima", "aphy", "sensei chi", "young z", "future z", "z"]);
|
||||
const sky = $("[data-constellarium-sky]", root);
|
||||
const status = $("[data-constellarium-status]", root);
|
||||
const title = $("[data-constellarium-title]", root);
|
||||
const reading = $("[data-constellarium-reading]", root);
|
||||
const log = $("[data-constellarium-log]", root);
|
||||
let state = readObservatoryState();
|
||||
let selected = [];
|
||||
let relation = "continuationLinks";
|
||||
|
||||
function saveThread(a, b) {
|
||||
const score = relationScore(a, b);
|
||||
const thread = {
|
||||
id: `${a.id}:${b.id}:${relation}`,
|
||||
source: a.id,
|
||||
target: b.id,
|
||||
relation,
|
||||
score,
|
||||
title: `${a.title} / ${b.title}`,
|
||||
restoredAt: new Date().toISOString()
|
||||
};
|
||||
const restoredThreads = [...(state.restoredThreads || []).filter((item) => item.id !== thread.id), thread].slice(-24);
|
||||
state = { ...state, restoredThreads, lastRestoredThread: thread, characterTrust: bumpTrust(state.characterTrust, [...(a.characters || []), ...(b.characters || [])]) };
|
||||
if (score >= 8) state.crossGameFlags = { ...(state.crossGameFlags || {}), "thread:strong-restored": true };
|
||||
writeObservatoryState(state);
|
||||
status.textContent = score >= 8 ? "The thread holds. The sky remembers that shape." : "The thread is gentle but uncertain. Lima keeps it from falling.";
|
||||
reading.textContent = score >= 8 ? characterLine(b, "two memories agree without needing to be identical.") : "aphy marks this as possible. sensei chi says possible is a real room.";
|
||||
renderLog();
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!entries.length) {
|
||||
sky.innerHTML = "<p class=\"play-live-note\">The observatory data is resting. Try again after the hidden details are available.</p>";
|
||||
return;
|
||||
}
|
||||
sky.innerHTML = entries.map((entry, index) => {
|
||||
const angle = (index / entries.length) * Math.PI * 2 - Math.PI / 2;
|
||||
const radius = 34 + entryDepth(entry) * 8;
|
||||
const active = selected.some((item) => item.id === entry.id);
|
||||
return `<button type="button" class="observatory-star${active ? " is-active" : ""}" data-id="${escapeAttr(entry.id)}" style="--x:${50 + Math.cos(angle) * radius}%;--y:${50 + Math.sin(angle) * radius * 0.74}%;--size:${18 + Number(entry.resonanceScore || 3) * 2}px">
|
||||
<span>${escapeHtml((entry.characters || ["z"])[0].slice(0, 1))}</span>
|
||||
</button>`;
|
||||
}).join("");
|
||||
$$("[data-id]", sky).forEach((button) => {
|
||||
button.addEventListener("click", () => choose(entries.find((entry) => entry.id === button.dataset.id)));
|
||||
});
|
||||
renderLog();
|
||||
}
|
||||
|
||||
function choose(entry) {
|
||||
if (!entry) return;
|
||||
if (selected.length === 2) selected = [];
|
||||
selected.push(entry);
|
||||
title.textContent = entry.title;
|
||||
reading.textContent = characterLine(entry);
|
||||
if (selected.length === 2) saveThread(selected[0], selected[1]);
|
||||
render();
|
||||
}
|
||||
|
||||
function renderLog() {
|
||||
const threads = state.restoredThreads || [];
|
||||
log.innerHTML = threads.length
|
||||
? threads.slice(-6).reverse().map((thread) => `<p><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</p>`).join("")
|
||||
: "<p>No thread has been restored here yet.</p>";
|
||||
}
|
||||
|
||||
$$("[data-constellarium-relation]", root).forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
relation = button.dataset.constellariumRelation;
|
||||
status.textContent = `The next thread will be a ${relation.replace("Links", "").replace("Entries", "")}.`;
|
||||
});
|
||||
});
|
||||
$("[data-constellarium-reset]", root).addEventListener("click", () => {
|
||||
selected = [];
|
||||
status.textContent = "The sky rests without forgetting.";
|
||||
title.textContent = "Waiting for a first light";
|
||||
reading.textContent = "lima keeps a hand near the thread, not pulling, just steadying.";
|
||||
render();
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
async function futureZReturnLog(root) {
|
||||
const entries = importantEntries(await loadObservatory(), ["future z", "young z", "lima", "sensei chi"]).slice(0, 9);
|
||||
const now = Date.now();
|
||||
let state = readObservatoryState();
|
||||
const visits = { ...(state.returnVisits || {}) };
|
||||
const page = visits["future-z-return-log"] || { count: 0, firstSeenAt: now, lastSeenAt: 0 };
|
||||
const gap = page.lastSeenAt ? now - page.lastSeenAt : 0;
|
||||
visits["future-z-return-log"] = { ...page, count: page.count + 1, lastSeenAt: now };
|
||||
state = { ...state, returnVisits: visits };
|
||||
writeObservatoryState(state);
|
||||
|
||||
const stage = $("[data-return-stage]", root);
|
||||
const heading = $("[data-return-heading]", root);
|
||||
const message = $("[data-return-message]", root);
|
||||
const context = $("[data-return-context]", root);
|
||||
const status = $("[data-return-status]", root);
|
||||
const list = $("[data-return-list]", root);
|
||||
const restored = (state.restoredThreads || []).length;
|
||||
const symbols = Object.keys(state.discoveredSymbols || {}).length;
|
||||
const patient = Number(state.patientMoments || 0);
|
||||
|
||||
function render() {
|
||||
const count = visits["future-z-return-log"].count;
|
||||
const minutes = Math.floor(gap / 60000);
|
||||
stage.textContent = count <= 1 ? "First opening" : count < 4 ? `${count} returns` : "familiar return";
|
||||
heading.textContent = restored ? "future z found one of your restored threads." : count > 1 ? "The page recognizes your shape in time." : "The log is still warming up.";
|
||||
message.textContent = restored
|
||||
? "future z: I remember the thread you helped hold together. Some repairs become routes."
|
||||
: count > 2
|
||||
? "future z: returning is not the same as being stuck. Sometimes it is how care checks the lock."
|
||||
: "future z has left the first note face down, so the ink does not smudge.";
|
||||
context.textContent = minutes
|
||||
? `Last gap: about ${minutes} minutes. ${symbols ? "One discovered symbol is travelling with you." : "No symbol has followed you here yet."}`
|
||||
: patient ? "sensei chi notices the quiet you brought from another room." : "aphy has no elapsed-time anomaly to report yet.";
|
||||
list.innerHTML = entries.map((entry, index) => {
|
||||
const seen = visits[entry.id] || {};
|
||||
return `<button type="button" class="return-memory" data-id="${escapeAttr(entry.id)}">
|
||||
<strong>${escapeHtml(entry.title)}</strong>
|
||||
<span>${seen.count ? `observed ${seen.count} time${seen.count === 1 ? "" : "s"}` : "not observed here yet"}</span>
|
||||
</button>`;
|
||||
}).join("");
|
||||
$$("[data-id]", list).forEach((button) => button.addEventListener("click", () => observe(button.dataset.id)));
|
||||
}
|
||||
|
||||
function observe(id) {
|
||||
const entry = entries.find((item) => item.id === id);
|
||||
if (!entry) return;
|
||||
const seen = visits[id] || { count: 0, firstSeenAt: now };
|
||||
visits[id] = { ...seen, count: seen.count + 1, lastSeenAt: Date.now() };
|
||||
state = { ...readObservatoryState(), returnVisits: visits };
|
||||
if ((entry.characters || []).includes("future z")) state.crossGameFlags = { ...(state.crossGameFlags || {}), "future-z:observed": true };
|
||||
writeObservatoryState(state);
|
||||
status.textContent = characterLine(entry, "future z folds the observation into the log.");
|
||||
render();
|
||||
}
|
||||
|
||||
$("[data-return-observe]", root).addEventListener("click", () => entries[0] && observe(entries[0].id));
|
||||
$("[data-return-wait]", root).addEventListener("click", () => {
|
||||
state = readObservatoryState();
|
||||
state.patientMoments = Number(state.patientMoments || 0) + 1;
|
||||
state.crossGameFlags = { ...(state.crossGameFlags || {}), "patience:return-log": true };
|
||||
writeObservatoryState(state);
|
||||
status.textContent = "sensei chi notices that you did not rush the page.";
|
||||
render();
|
||||
});
|
||||
$("[data-return-reset]", root).addEventListener("click", () => {
|
||||
state = readObservatoryState();
|
||||
delete state.returnVisits;
|
||||
writeObservatoryState(state);
|
||||
status.textContent = "The local drift was cleared. Future z leaves the kindness, not the counter.";
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
async function subconsciousIndex(root) {
|
||||
const entries = await loadObservatory();
|
||||
let state = readObservatoryState();
|
||||
let selected = [];
|
||||
const cloud = $("[data-symbol-cloud]", root);
|
||||
const status = $("[data-subconscious-status]", root);
|
||||
const sentence = $("[data-dream-sentence]", root);
|
||||
const reading = $("[data-dream-reading]", root);
|
||||
const routes = $("[data-subconscious-routes]", root);
|
||||
const counts = new Map();
|
||||
entries.forEach((entry) => (entry.symbols || []).forEach((symbol) => counts.set(symbol, (counts.get(symbol) || 0) + 1)));
|
||||
[["ring", 1], ["tea", 1], ["crayon sun", 1], ["clock", 1], ["door", 1], ["mirror", 1], ["console", 1], ["kitchen light", 1]].forEach(([symbol, count]) => {
|
||||
if (!counts.has(symbol)) counts.set(symbol, count);
|
||||
});
|
||||
const symbols = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 16);
|
||||
|
||||
function render() {
|
||||
cloud.innerHTML = symbols.map(([symbol, count]) => `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
|
||||
<strong>${escapeHtml(symbol)}</strong><span>${count}</span>
|
||||
</button>`).join("");
|
||||
$$("[data-symbol]", cloud).forEach((button) => button.addEventListener("click", () => toggleSymbol(button.dataset.symbol)));
|
||||
const known = state.discoveredSymbols || {};
|
||||
routes.innerHTML = Object.keys(known).length
|
||||
? Object.entries(known).slice(-8).map(([symbol, info]) => `<p><strong>${escapeHtml(symbol)}</strong>: ${escapeHtml(info.meaning)}</p>`).join("")
|
||||
: "<p>No object has admitted its meaning yet.</p>";
|
||||
}
|
||||
|
||||
function toggleSymbol(symbol) {
|
||||
selected = selected.includes(symbol) ? selected.filter((item) => item !== symbol) : [...selected, symbol].slice(-3);
|
||||
const phrase = dreamPhrase(selected);
|
||||
sentence.textContent = phrase.title;
|
||||
reading.textContent = phrase.reading;
|
||||
if (selected.length >= 2) {
|
||||
const discoveredSymbols = { ...(state.discoveredSymbols || {}) };
|
||||
selected.forEach((item) => {
|
||||
discoveredSymbols[item] = { meaning: phrase.meaning, discoveredAt: new Date().toISOString(), path: selected };
|
||||
});
|
||||
state = { ...state, discoveredSymbols, lastDreamPath: selected, crossGameFlags: { ...(state.crossGameFlags || {}), "symbol:dream-sentence": true } };
|
||||
writeObservatoryState(state);
|
||||
status.textContent = selected.length === 3 ? "The website answers in objects, then pretends it was only navigation." : "A symbolic route has begun to breathe.";
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
$("[data-subconscious-clear]", root).addEventListener("click", () => {
|
||||
selected = [];
|
||||
sentence.textContent = "Nothing has combined yet.";
|
||||
reading.textContent = "young z has drawn a door in the margin and refuses to explain why that helps.";
|
||||
status.textContent = "The current dream closed without being lost.";
|
||||
render();
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function dreamPhrase(symbols) {
|
||||
const key = symbols.slice().sort().join("|");
|
||||
const table = {
|
||||
"clock|ring": ["A future kept warm", "future z recognizes the ring as a promise that learned to wait.", "promise and return"],
|
||||
"crayon sun|door": ["Safety drawn before it was understood", "young z makes a door because a page can become a room.", "play and shelter"],
|
||||
"console|tea": ["Logic learned to bow", "aphy accepts the cup from sensei chi and stops explaining for one line.", "systems softened by patience"],
|
||||
"kitchen light|ring": ["Home on purpose", "lima turns the symbol back toward the ordinary room where it belongs.", "warmth and vow"],
|
||||
"mirror|ring": ["The promise has a reflection", "the observatory shows the same memory from the side that was quiet.", "echo and vow"]
|
||||
};
|
||||
const picked = table[key] || (symbols.length
|
||||
? [`${symbols.join(" / ")}`, "sensei chi says the meaning is not late. It is arriving by a slower path.", "private weather"]
|
||||
: ["Nothing has combined yet.", "young z has drawn a door in the margin and refuses to explain why that helps.", "waiting"]);
|
||||
return { title: picked[0], reading: picked[1], meaning: picked[2] };
|
||||
}
|
||||
|
||||
function bumpTrust(current, characters) {
|
||||
const next = { ...(current || {}) };
|
||||
characters.forEach((name) => {
|
||||
if (["lima", "aphy", "sensei chi", "young z", "future z"].includes(name)) next[name] = Number(next[name] || 0) + 1;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" })[char]);
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/`/g, "`");
|
||||
}
|
||||
|
||||
function setupSortable(container, _render, after) {
|
||||
let dragged = null;
|
||||
container.addEventListener("dragstart", (event) => {
|
||||
|
||||
@@ -495,6 +495,132 @@
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.observatory-ritual {
|
||||
--ritual-sky: #17130f;
|
||||
--ritual-gold: #d6b36a;
|
||||
--ritual-blue: #75a9bd;
|
||||
--ritual-red: #d06b78;
|
||||
--ritual-green: #7fb089;
|
||||
--ritual-violet: #a58ac9;
|
||||
}
|
||||
|
||||
.observatory-panel {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.observatory-sky {
|
||||
position: relative;
|
||||
min-height: clamp(340px, 58vw, 620px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(214, 179, 106, 0.16), transparent 12%),
|
||||
radial-gradient(circle at 28% 34%, rgba(117, 169, 189, 0.14), transparent 18%),
|
||||
radial-gradient(circle at 72% 70%, rgba(208, 107, 120, 0.12), transparent 16%),
|
||||
repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07) 0 1px, transparent 1px 58px),
|
||||
var(--ritual-sky);
|
||||
}
|
||||
|
||||
.observatory-sky::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 12%;
|
||||
border: 1px dashed rgba(246, 216, 137, 0.16);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.observatory-star {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
top: var(--y);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border-color: rgba(255, 248, 232, 0.58);
|
||||
background: radial-gradient(circle, #fff8e8 0 20%, var(--ritual-gold) 21% 52%, rgba(214, 179, 106, 0.18) 53% 100%);
|
||||
color: #17130f;
|
||||
font-weight: 900;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 18px rgba(214, 179, 106, 0.28);
|
||||
}
|
||||
|
||||
.observatory-star:hover,
|
||||
.observatory-star.is-active {
|
||||
background: radial-gradient(circle, #fff8e8 0 18%, var(--ritual-red) 19% 56%, rgba(208, 107, 120, 0.22) 57% 100%);
|
||||
color: #fff8e8;
|
||||
box-shadow: 0 0 28px rgba(208, 107, 120, 0.46);
|
||||
}
|
||||
|
||||
.observatory-star span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.observatory-drawer,
|
||||
.dream-sentence {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, var(--surface) 86%, var(--play-paper) 14%);
|
||||
}
|
||||
|
||||
.observatory-thread-actions,
|
||||
.symbol-cloud,
|
||||
.return-memory-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.temporal-console {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(190px, auto);
|
||||
}
|
||||
|
||||
.return-memory {
|
||||
display: grid;
|
||||
flex: 1 1 250px;
|
||||
gap: 0.35rem;
|
||||
min-height: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.return-memory span,
|
||||
.symbol-chip span {
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.symbol-cloud {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.symbol-chip {
|
||||
flex: 1 1 150px;
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
text-align: left;
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in oklab, var(--surface) 88%, var(--ritual-blue) 12%), var(--surface));
|
||||
}
|
||||
|
||||
.symbol-chip.is-active {
|
||||
background: var(--play-ink);
|
||||
color: #fff8e8;
|
||||
}
|
||||
|
||||
.symbol-chip.is-active span {
|
||||
color: rgba(255, 248, 232, 0.72);
|
||||
}
|
||||
|
||||
.rpg-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 960px) minmax(280px, 1fr);
|
||||
@@ -804,6 +930,7 @@
|
||||
.play-console,
|
||||
.recipe-tool,
|
||||
.sigil-tool,
|
||||
.temporal-console,
|
||||
.rpg-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
37
play/future-z-return-log.org
Normal file
37
play/future-z-return-log.org
Normal file
@@ -0,0 +1,37 @@
|
||||
#+TITLE: Future Z's Return Log
|
||||
#+OPTIONS: num:nil title:nil toc:nil
|
||||
#+NO_SIDENOTES: t
|
||||
#+HIDDEN_PAGE: t
|
||||
#+DATE: <2026-05-10 Sun>
|
||||
|
||||
#+BEGIN_EXPORT html
|
||||
<main class="play-root play-page observatory-ritual" data-play-page="futureZReturnLog">
|
||||
<a class="play-back" href="/play.html">Back to Play</a>
|
||||
<header class="play-page-head">
|
||||
<p class="play-kicker">Layer 4 / return weather</p>
|
||||
<h1>Future Z's Return Log</h1>
|
||||
<p>This room changes by being visited. Future z does not hurry the message; the page keeps count quietly.</p>
|
||||
</header>
|
||||
|
||||
<section class="play-console temporal-console">
|
||||
<div class="play-console__screen">
|
||||
<p class="play-console__eyebrow" data-return-stage>First opening</p>
|
||||
<h2 data-return-heading>The log is still warming up.</h2>
|
||||
<p data-return-message>future z has left the first note face down, so the ink does not smudge.</p>
|
||||
<p class="play-live-note" data-return-context></p>
|
||||
</div>
|
||||
<div class="play-console__controls">
|
||||
<button type="button" data-return-observe>Observe a memory</button>
|
||||
<button type="button" data-return-wait>Stay a moment</button>
|
||||
<button type="button" data-return-reset>Clear local drift</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="play-tool observatory-panel">
|
||||
<div class="play-scorebar">
|
||||
<span data-return-status>Temporal echoes are settling.</span>
|
||||
</div>
|
||||
<div class="return-memory-list" data-return-list></div>
|
||||
</section>
|
||||
</main>
|
||||
#+END_EXPORT
|
||||
42
play/gentle-constellarium.org
Normal file
42
play/gentle-constellarium.org
Normal file
@@ -0,0 +1,42 @@
|
||||
#+TITLE: Gentle Constellarium
|
||||
#+OPTIONS: num:nil title:nil toc:nil
|
||||
#+NO_SIDENOTES: t
|
||||
#+HIDDEN_PAGE: t
|
||||
#+DATE: <2026-05-10 Sun>
|
||||
|
||||
#+BEGIN_EXPORT html
|
||||
<main class="play-root play-page observatory-ritual" data-play-page="gentleConstellarium">
|
||||
<a class="play-back" href="/play.html">Back to Play</a>
|
||||
<header class="play-page-head">
|
||||
<p class="play-kicker">Layer 2 / restored thread</p>
|
||||
<h1>Gentle Constellarium</h1>
|
||||
<p>The observatory has loosened into a sky. Choose a dim memory, then a second light that feels close enough to hold it.</p>
|
||||
</header>
|
||||
|
||||
<section class="play-tool observatory-panel">
|
||||
<div class="play-scorebar">
|
||||
<span data-constellarium-status>The sky is gathering memory nodes.</span>
|
||||
<button type="button" data-constellarium-reset>Let the sky rest</button>
|
||||
</div>
|
||||
<div class="observatory-sky" data-constellarium-sky aria-label="Restorable emotional constellation"></div>
|
||||
<div class="observatory-drawer">
|
||||
<p class="play-console__eyebrow">Selected thread</p>
|
||||
<h2 data-constellarium-title>Waiting for a first light</h2>
|
||||
<p data-constellarium-reading>lima keeps a hand near the thread, not pulling, just steadying.</p>
|
||||
<div class="observatory-thread-actions">
|
||||
<button type="button" data-constellarium-relation="continuationLinks">Continue</button>
|
||||
<button type="button" data-constellarium-relation="echoes">Echo</button>
|
||||
<button type="button" data-constellarium-relation="symbolicLinks">Symbol</button>
|
||||
<button type="button" data-constellarium-relation="mirroredEntries">Mirror</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="play-console">
|
||||
<div class="play-console__screen">
|
||||
<p class="play-console__eyebrow">Recovered constellations</p>
|
||||
<div data-constellarium-log></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
#+END_EXPORT
|
||||
36
play/subconscious-index.org
Normal file
36
play/subconscious-index.org
Normal file
@@ -0,0 +1,36 @@
|
||||
#+TITLE: The Subconscious Index
|
||||
#+OPTIONS: num:nil title:nil toc:nil
|
||||
#+NO_SIDENOTES: t
|
||||
#+HIDDEN_PAGE: t
|
||||
#+DATE: <2026-05-10 Sun>
|
||||
|
||||
#+BEGIN_EXPORT html
|
||||
<main class="play-root play-page observatory-ritual" data-play-page="subconsciousIndex">
|
||||
<a class="play-back" href="/play.html">Back to Play</a>
|
||||
<header class="play-page-head">
|
||||
<p class="play-kicker">Layer 5 / symbolic drift</p>
|
||||
<h1>The Subconscious Index</h1>
|
||||
<p>The website keeps speaking through objects. Choose recurring symbols until they begin arranging themselves into a sentence.</p>
|
||||
</header>
|
||||
|
||||
<section class="play-tool observatory-panel">
|
||||
<div class="play-scorebar">
|
||||
<span data-subconscious-status>The index is reading the recurring objects.</span>
|
||||
<button type="button" data-subconscious-clear>Close the current dream</button>
|
||||
</div>
|
||||
<div class="symbol-cloud" data-symbol-cloud aria-label="Recurring observatory symbols"></div>
|
||||
<div class="dream-sentence">
|
||||
<p class="play-console__eyebrow">Dream sentence</p>
|
||||
<h2 data-dream-sentence>Nothing has combined yet.</h2>
|
||||
<p data-dream-reading>young z has drawn a door in the margin and refuses to explain why that helps.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="play-console">
|
||||
<div class="play-console__screen">
|
||||
<p class="play-console__eyebrow">Subconscious routes</p>
|
||||
<div data-subconscious-routes></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
#+END_EXPORT
|
||||
Reference in New Issue
Block a user