removing author and cleaning js / css
This commit is contained in:
@@ -1,691 +1,7 @@
|
||||
(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();
|
||||
const CHARACTER_AVATARS = {
|
||||
"lima": {
|
||||
avatar: "/assets/avatars/lima.jpg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["lima"],
|
||||
glow: "#f2c58b"
|
||||
},
|
||||
"aphy": {
|
||||
avatar: "/assets/avatars/aphy.jpeg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["aphy"],
|
||||
glow: "#9dd3a6"
|
||||
},
|
||||
"sensei chi": {
|
||||
avatar: "/assets/avatars/sensei-chi.jpeg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["sensei chi", "sensei"],
|
||||
glow: "#9ccddd"
|
||||
},
|
||||
"young z": {
|
||||
avatar: "/assets/avatars/young-z.jpeg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["young z", "young"],
|
||||
glow: "#f0b85c"
|
||||
},
|
||||
"future z": {
|
||||
avatar: "/assets/avatars/future-z.jpeg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["future z", "future"],
|
||||
glow: "#c2a4ee"
|
||||
},
|
||||
"z": {
|
||||
avatar: "/assets/avatars/z.jpeg",
|
||||
fallback: "/assets/avatars/z.jpeg",
|
||||
aliases: ["z", "zaine"],
|
||||
glow: "#ead68e"
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// EDITABLE CONTENT
|
||||
// -----------------------------
|
||||
// Generated by the hidden narrative authoring page.
|
||||
// Friendly source of truth: assets/content/hidden-details.json
|
||||
|
||||
const familyLayers = [];
|
||||
|
||||
const details = [];
|
||||
|
||||
const poems = [];
|
||||
|
||||
const greetings = [];
|
||||
|
||||
const nightMessages = [];
|
||||
|
||||
const lore = {
|
||||
"quotes": [],
|
||||
"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 characterForMessage(message) {
|
||||
const text = String(message || "").toLowerCase();
|
||||
return Object.entries(CHARACTER_AVATARS).find(([, config]) => {
|
||||
return (config.aliases || []).some((alias) => {
|
||||
const escaped = alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`(^|[^a-z0-9-])${escaped}([^a-z0-9-]|$)`).test(text);
|
||||
});
|
||||
})?.[0] || "";
|
||||
}
|
||||
|
||||
function avatarMarkup(character) {
|
||||
const config = CHARACTER_AVATARS[character];
|
||||
if (!config) return "";
|
||||
return `<img class="hidden-message-avatar" src="${escapeHtml(config.avatar)}" alt="" loading="lazy" style="--hidden-avatar-glow:${escapeHtml(config.glow)}" onerror="this.onerror=null;this.src='${escapeHtml(config.fallback)}'">`;
|
||||
}
|
||||
|
||||
function hiddenMessageMarkup(message) {
|
||||
const character = characterForMessage(message);
|
||||
return character
|
||||
? `${avatarMarkup(character)}<span class="hidden-message-text">${escapeHtml(message)}</span>`
|
||||
: `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
|
||||
}
|
||||
|
||||
function setHiddenMessage(el, message) {
|
||||
const character = characterForMessage(message);
|
||||
el.classList.toggle("has-hidden-avatar", Boolean(character));
|
||||
if (character) {
|
||||
el.dataset.hiddenCharacter = character;
|
||||
el.innerHTML = hiddenMessageMarkup(message);
|
||||
} else {
|
||||
delete el.dataset.hiddenCharacter;
|
||||
el.textContent = 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";
|
||||
const character = characterForMessage(text);
|
||||
panel.innerHTML = `<div class="hidden-drift-message${character ? " has-hidden-avatar" : ""}">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
|
||||
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;
|
||||
const character = characterForMessage(line);
|
||||
return `<li class="${character ? "hidden-layer-message" : ""}"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
|
||||
}).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));
|
||||
});
|
||||
var script = document.createElement("script");
|
||||
script.src = "/assets/scripts/features/hidden-details.js";
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user