(function () { "use strict"; /* * Hidden site details * ------------------- * This file adds small hidden interactions across the site: footer notes, * click targets, search/keyboard secrets, rare messages, and page-specific * behavior for the hidden /play pages. * * How to add your own things: * - Add new text to EDITABLE CONTENT arrays such as `details`, `poems`, * `greetings`, or `lore`. * - Add search-triggered toast messages to `SEARCH_TOASTS`. * - Add search-triggered page redirects to `SEARCH_ROUTES`. * - Add typed keyboard phrases to `KEYBOARD_SECRETS` or `LONG_KEYBOARD_SECRETS`. * - Add a new feature by writing an `addYourFeature(memory)` function and * adding it to the `FEATURES` list at the bottom of the file. */ // Storage keys. v1 is read once so old visitors keep their hidden progress. const STORAGE_KEY = "zxh_hidden_details_v2"; const OLD_STORAGE_KEY = "zxh_hidden_details_v1"; // Shared page context. These are captured once so daily random picks stay stable. const now = new Date(); const hour = now.getHours(); const path = window.location.pathname; const state = readState(); // ----------------------------- // EDITABLE CONTENT // ----------------------------- // Generated by the hidden narrative authoring page. // Friendly source of truth: assets/content/hidden-details.json const familyLayers = []; const details = [ "Testing" ]; const poems = []; const greetings = []; const nightMessages = []; const lore = { "quotes": [ "Temp 2 2" ], "conversations": [], "journals": [], "warnings": [], "dreams": [], "cassettes": [], "fakeUsers": [], "seasonal": {}, "homepageTakeovers": [], "roomLinks": [] }; // Exact search text -> toast message. const SEARCH_TOASTS = {}; // Exact search text -> hidden page route. const SEARCH_ROUTES = {}; // Short typed phrases. Stored in sessionStorage as a rolling key chain. const KEYBOARD_SECRETS = []; // Longer typed phrases and character names. const LONG_KEYBOARD_SECRETS = []; // ----------------------------- // STATE HELPERS // ----------------------------- function readState() { let current = {}; try { current = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"); } catch (_) {} if (!current.visits) { try { const oldState = JSON.parse(localStorage.getItem(OLD_STORAGE_KEY) || "{}"); current = { ...oldState, migratedFromV1: true }; localStorage.setItem(STORAGE_KEY, JSON.stringify(current)); } catch (_) {} } return current; } function writeState(next) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch (_) {} } function pick(list, salt) { const seed = hash(`${path}|${now.toDateString()}|${salt || ""}`); return list[seed % list.length]; } function hash(value) { let h = 2166136261; for (let i = 0; i < value.length; i += 1) { h ^= value.charCodeAt(i); h = Math.imul(h, 16777619); } return Math.abs(h >>> 0); } // ----------------------------- // UI HELPERS // ----------------------------- function hiddenMessageMarkup(message) { return `${escapeHtml(message)}`; } function setHiddenMessage(el, message) { delete el.dataset.hiddenCharacter; el.innerHTML = hiddenMessageMarkup(message); } function toast(message, duration) { let el = document.querySelector(".hidden-toast"); if (!el) { el = document.createElement("div"); el.className = "hidden-toast"; el.setAttribute("role", "status"); document.body.appendChild(el); } setHiddenMessage(el, message); el.classList.add("is-visible"); window.clearTimeout(el._timer); el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600); } function redirectTo(route) { window.location.href = route; } function runSecretAction(secret) { if (secret.route) redirectTo(secret.route); if (secret.message) toast(secret.message); if (secret.song) playTinySong(); } function layerForSearch(value) { if (value.includes("future")) return 4; if (value.includes("sensei") || value.includes("aphy")) return 3; return 2; } function rememberVisit() { const visits = (state.visits || 0) + 1; const seen = Array.isArray(state.seen) ? state.seen : []; const pathCounts = { ...(state.pathCounts || {}) }; pathCounts[path] = (pathCounts[path] || 0) + 1; const layerVisits = { ...(state.layerVisits || {}) }; const next = { ...state, visits, lastPath: path, pathCounts, layerVisits, seen: Array.from(new Set([...seen, path])).slice(-100), firstSeen: state.firstSeen || new Date().toISOString(), lastSeen: new Date().toISOString() }; writeState(next); return next; } function awardLayer(layer, reason) { const next = readState(); next.layerVisits = { ...(next.layerVisits || {}) }; next.layerVisits[layer] = (next.layerVisits[layer] || 0) + 1; next.lastLayerReason = reason; writeState(next); } // ----------------------------- // GLOBAL FEATURES // ----------------------------- function addFooterNote(memory) { const footer = document.querySelector("footer"); if (!footer || !details.length) return; const note = document.createElement("p"); note.className = "hidden-footer-note"; const base = pick(details, "footer"); setHiddenMessage(note, memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base); footer.appendChild(note); } function addHomepageGreeting(memory) { const target = document.querySelector("#db-greeting"); if (!target || !greetings.length) return; setHiddenMessage(target, pick(greetings, `greeting-${memory.visits}`)); const extra = document.createElement("p"); extra.className = "hidden-greeting"; setHiddenMessage(extra, ""); target.closest("div")?.appendChild(extra); } function addWhispers() { // Event: click one of the tiny "?" marks appended to long paragraphs. const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80); paragraphs.slice(0, 5).forEach((p, index) => { if ((hash(path + index) + index) % 3 !== 0) return; const mark = document.createElement("span"); mark.className = "hidden-whisper"; mark.tabIndex = 0; mark.textContent = " ?"; const source = index % 2 ? poems : details; if (!source.length) return; mark.title = pick(source, `whisper-${index}`); mark.addEventListener("click", () => { awardLayer(index % 2 ? 2 : 1, "paragraph whisper"); toast(mark.title); }); p.appendChild(mark); }); } function addCornerObject() { // Event: click the small fixed button in the bottom-left corner. const object = document.createElement("button"); object.className = "hidden-corner-object"; object.type = "button"; object.title = "hidden interaction"; object.textContent = state.visits % 2 ? "*" : "~"; document.body.appendChild(object); let clicks = 0; object.addEventListener("click", () => { clicks += 1; awardLayer(clicks > 3 ? 3 : 1, "corner object"); toast("Hidden interaction recorded."); if (clicks === 7) window.location.href = "/play/left-behind.html"; }); } function addLogoSearchAndKeyboardSecrets(memory) { // Events: // - Click `.site-brand` repeatedly on the homepage. // - Type exact words into `#search-input`. // - Type phrases anywhere on the page. const nav = document.querySelector(".site-brand"); if (nav) { let taps = Number(sessionStorage.getItem("zxh_logo_taps") || 0); nav.addEventListener("click", (event) => { if (path === "/" || path === "/index.html") { event.preventDefault(); taps += 1; sessionStorage.setItem("zxh_logo_taps", String(taps)); awardLayer(taps >= 5 ? 3 : 1, "logo taps"); toast(taps >= 5 ? "Hidden route available: /play/dream-corridor.html" : "Hidden interaction recorded."); if (taps >= 8) window.location.href = "/play/dream-corridor.html"; } }); } const search = document.querySelector("#search-input"); if (search) { search.addEventListener("input", () => { const value = search.value.trim().toLowerCase(); if (SEARCH_TOASTS[value]) toast(SEARCH_TOASTS[value]); if (SEARCH_ROUTES[value]) { awardLayer(layerForSearch(value), `search ${value}`); redirectTo(SEARCH_ROUTES[value]); } }); } document.addEventListener("keydown", (event) => { const key = event.key.toLowerCase(); const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-18); sessionStorage.setItem("zxh_key_chain", chain); KEYBOARD_SECRETS .filter((secret) => chain.endsWith(secret.phrase)) .forEach(runSecretAction); }); if (memory.seen?.length >= 6 && !state.travellerNoteShown) { window.setTimeout(() => { toast("Hidden path recorded."); writeState({ ...readState(), travellerNoteShown: true }); }, 1600); } } function addRareEvents() { if (hour >= 22 || hour < 5) { document.body.classList.add("hidden-late-night"); if (nightMessages.length) window.setTimeout(() => toast(pick(nightMessages, "night"), 2200), 900); } const roll = Math.random(); if (roll < 0.003) { window.setTimeout(() => showDriftNote("", ""), 1800); awardLayer(3, "rare sensei appearance"); } else if (roll < 0.008) { window.setTimeout(() => toast("Hidden warning."), 2400); awardLayer(4, "future warning"); } } function showDriftNote(text, art) { const panel = document.createElement("aside"); panel.className = "hidden-drift-note"; panel.innerHTML = `
${hiddenMessageMarkup(text)}
${escapeHtml(art)}
`; panel.querySelector("button").addEventListener("click", () => panel.remove()); document.body.appendChild(panel); } function addSecretLinks() { [ ["/play/left-behind.html", "left behind"], ["/play/dream-corridor.html", "dream corridor"], ["/play/kitchen-light.html", "kitchen light"], ...lore.roomLinks ].forEach(([href, label], index) => { const link = document.createElement("a"); link.className = "hidden-secret-link"; link.href = href; link.textContent = label; link.style.left = `${index + 1}px`; document.body.appendChild(link); }); } function enhanceErrors() { if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) { toast("Page not found."); } } function addContinuity(memory) { const milestones = { 64: "Hidden layer unlocked." }; if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) { const layer = memory.visits >= 31 ? 4 : memory.visits >= 9 ? 3 : 2; awardLayer(layer, `visit milestone ${memory.visits}`); window.setTimeout(() => toast(milestones[memory.visits], 7000), 1200); writeState({ ...readState(), [`milestone_${memory.visits}`]: true }); } const count = memory.pathCounts?.[path] || 0; if ([3, 7, 12].includes(count)) { window.setTimeout(() => toast("Page revisit recorded.", 6400), 1700); } } function addSeasonAndDates(memory) { const month = now.getMonth(); const day = now.getDate(); const season = month < 2 || month === 11 ? "winter" : month < 5 ? "spring" : month < 8 ? "summer" : "autumn"; if (Math.random() < 0.18 && lore.seasonal[season]) window.setTimeout(() => toast(lore.seasonal[season], 5600), 2600); const first = memory.firstSeen ? new Date(memory.firstSeen) : null; if (first && memory.visits > 1 && first.getMonth() === month && first.getDate() === day) { window.setTimeout(() => toast("Visitor anniversary recorded.", 7000), 1400); } } function addObjectConstellation() { // Event: click the small keepsake buttons along the bottom rail. const rail = document.createElement("div"); rail.className = "hidden-object-rail"; rail.setAttribute("aria-label", "Hidden objects"); const objects = [ ["ring", "Hidden object recorded."], ["bot", "Hidden object recorded."], ["tea", "Hidden object recorded."], ["crayon", "Hidden object recorded."], ["clock", "Hidden object recorded."] ]; objects.forEach(([name, message]) => { const button = document.createElement("button"); button.type = "button"; button.className = `hidden-keepsake hidden-keepsake--${name}`; button.title = name; button.textContent = { ring: "o", bot: "#", tea: "u", crayon: "/", clock: ":" }[name]; button.addEventListener("click", () => { const next = readState(); next.keepsakes = Array.from(new Set([...(next.keepsakes || []), name])); writeState(next); awardLayer(next.keepsakes.length >= 3 ? 3 : 2, `keepsake ${name}`); toast(message); if (next.keepsakes.length >= objects.length) { window.setTimeout(() => toast("Hidden object set completed."), 1100); } }); rail.appendChild(button); }); document.body.appendChild(rail); } function addWeatherWindow(memory) { // Event: click the small "window" button. This asks for browser geolocation. if (memory.visits < 3 || !("geolocation" in navigator)) return; const button = document.createElement("button"); button.className = "hidden-weather-window"; button.type = "button"; button.title = "Check outside weather"; button.textContent = "window"; button.addEventListener("click", () => { toast("Checking outside weather."); navigator.geolocation.getCurrentPosition((pos) => { const { latitude, longitude } = pos.coords; fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude.toFixed(3)}&longitude=${longitude.toFixed(3)}¤t=temperature_2m,precipitation&timezone=auto`) .then((res) => res.json()) .then((data) => { const current = data.current || {}; const rain = Number(current.precipitation || 0) > 0; const temp = Math.round(Number(current.temperature_2m)); toast(rain ? `Outside: rain, ${temp}C.` : `Outside: ${temp}C.`); }) .catch(() => toast("Weather check failed.")); }, () => toast("No outside weather today.")); }); document.body.appendChild(button); } function addOldWebLayer(memory) { if (Math.random() < 0.08 || memory.visits > 10) { const stamp = document.createElement("div"); stamp.className = "hidden-oldweb-stamp"; if (!lore.fakeUsers.length) return; stamp.title = pick(lore.fakeUsers, "fake-user"); stamp.textContent = "best viewed with patience"; stamp.addEventListener("click", () => { awardLayer(1, "old web stamp"); toast(stamp.title); }); document.body.appendChild(stamp); } const comments = document.querySelector("#comments"); if (comments && lore.fakeUsers.length && !comments.querySelector(".hidden-guestbook-line")) { const line = document.createElement("p"); line.className = "hidden-guestbook-line"; setHiddenMessage(line, pick(lore.fakeUsers, "comment-lore")); comments.appendChild(line); } } function addSourceRelics() { const marker = document.createComment("hidden-details"); document.documentElement.appendChild(marker); document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => { if (index > 2) return; img.alt = "Decorative image."; }); } function addLongKeyboardSecrets() { // Event: type longer hidden phrases anywhere on the page. document.addEventListener("keydown", (event) => { const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-24); sessionStorage.setItem("zxh_second_chain", chain); LONG_KEYBOARD_SECRETS .filter((secret) => chain.endsWith(secret.phrase)) .forEach(runSecretAction); }); } function addPatienceRewards(memory) { // Event: no movement, key presses, scrolling, or clicking for 90 seconds. if (memory.visits < 2) return; let idleTimer = null; const startIdle = () => { window.clearTimeout(idleTimer); idleTimer = window.setTimeout(() => { awardLayer(5, "patience"); toast("Idle moment recorded.", 7600); const next = readState(); next.patientMoments = (next.patientMoments || 0) + 1; writeState(next); }, 90000); }; ["mousemove", "keydown", "scroll", "click"].forEach((name) => document.addEventListener(name, startIdle, { passive: true })); startIdle(); } function addRareSecondLayer() { const roll = Math.random(); if ((path === "/" || path === "/index.html") && roll < 0.006) { document.body.classList.add("hidden-home-takeover"); if (lore.homepageTakeovers.length) window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), ""), 600); } else if (roll < 0.002) { window.setTimeout(() => showDriftNote("", ""), 1500); } else if (roll < 0.006) { if (lore.dreams.length) window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000); } else if (roll < 0.014) { if (lore.warnings.length) window.setTimeout(() => toast(pick(lore.warnings, "rare-warning"), 6800), 2300); } } function addFamilyLayerIndex(memory) { if (!path.includes("/play/family-layer-index")) return; const target = document.querySelector("[data-family-layer-index]"); if (!target) return; const visits = readState().layerVisits || {}; target.innerHTML = familyLayers.map((line, layer) => { const count = visits[layer] || 0; return `
  • ${hiddenMessageMarkup(line)}
    local encounters: ${count}
  • `; }).join(""); } function addPageSpecificSecrets() { // Events only used by specific `/play/...` pages. if (path.includes("/play/cassette-log")) { document.querySelectorAll("[data-cassette]").forEach((button, index) => { button.addEventListener("click", () => { awardLayer(index >= 2 ? 4 : 2, "voice note"); if (lore.cassettes.length) toast(lore.cassettes[index % lore.cassettes.length], 7600); playTinySong(); }); }); } if (path.includes("/play/patience-game")) { const target = document.querySelector("[data-patience-target]"); const count = document.querySelector("[data-patience-count]"); if (target && count) { let seconds = 0; setInterval(() => { seconds += 1; count.textContent = String(seconds); if ([30, 90, 180].includes(seconds)) { setHiddenMessage(target, "Patience milestone recorded."); } }, 1000); } } if (path.includes("/play/terminal-cupboard")) { const input = document.querySelector("[data-cupboard-input]"); const log = document.querySelector("[data-cupboard-log]"); const commands = { help: "commands: layer, exit", layer: familyLayers.join("\n"), exit: "Closed." }; input?.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; const value = input.value.trim().toLowerCase(); const line = document.createElement("p"); setHiddenMessage(line, `> ${value}\n${commands[value] || "Unknown command."}`); log.appendChild(line); input.value = ""; }); } } function addInvisibleHoverSecrets() { document.querySelectorAll("h1, h2").forEach((heading, index) => { if (index > 5) return; heading.classList.add("hidden-hover-memory"); const source = [...lore.quotes, ...lore.journals, ...poems]; if (!source.length) return; heading.dataset.hiddenMemory = pick(source, `heading-${index}`); }); } function playTinySong() { try { const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) return; const ctx = new AudioContext(); const notes = [392, 494, 440, 330, 392]; notes.forEach((freq, index) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.frequency.value = freq; osc.type = "sine"; gain.gain.setValueAtTime(0.0001, ctx.currentTime + index * 0.18); gain.gain.exponentialRampToValueAtTime(0.05, ctx.currentTime + index * 0.18 + 0.03); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + index * 0.18 + 0.16); osc.connect(gain); gain.connect(ctx.destination); osc.start(ctx.currentTime + index * 0.18); osc.stop(ctx.currentTime + index * 0.18 + 0.18); }); } catch (_) {} } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } // Features run in this order on every page after the DOM is ready. // Each function receives the current `memory` object. const FEATURES = [ addFooterNote, addHomepageGreeting, addWhispers, addCornerObject, addLogoSearchAndKeyboardSecrets, addRareEvents, addSecretLinks, enhanceErrors, addContinuity, addSeasonAndDates, addObjectConstellation, addWeatherWindow, addOldWebLayer, addSourceRelics, addLongKeyboardSecrets, addPatienceRewards, addRareSecondLayer, addFamilyLayerIndex, addPageSpecificSecrets, addInvisibleHoverSecrets ]; document.addEventListener("DOMContentLoaded", () => { const memory = rememberVisit(); FEATURES.forEach((addFeature) => addFeature(memory)); }); }());