773 lines
36 KiB
JavaScript
773 lines
36 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const KEY = "zxh_hidden_details_v1";
|
|
const now = new Date();
|
|
const hour = now.getHours();
|
|
const path = window.location.pathname;
|
|
const state = readState();
|
|
|
|
const details = [
|
|
"The footer keeps a tiny chair for anyone who scrolls this far.",
|
|
"Someone labelled the spare cables: important, mysterious, probably for a printer.",
|
|
"Memory is just a room that learned how to echo kindly.",
|
|
"If you are reading this at night, please unclench your jaw.",
|
|
"A bookmark fell asleep here and dreamed it was a door.",
|
|
"SYSTEM: kettle confidence restored.",
|
|
"Mum says the rice is never late, only thinking.",
|
|
"Dad left a note: check the plug, then check your assumptions.",
|
|
"The attic server hums like it knows an old song.",
|
|
"A small lantern named Morrow was seen under the navigation bar.",
|
|
"Home is the place where the mismatched mugs know your hands.",
|
|
"This tooltip has been waiting all afternoon to feel useful.",
|
|
"404s are just hallways with very sincere lighting.",
|
|
"The website looked away politely while you reread that sentence.",
|
|
"Winter folded the garden into a quiet letter.",
|
|
"Spring arrived wearing yesterday's coat and nobody minded.",
|
|
"Autumn keeps receipts from every golden hour.",
|
|
"Summer is a glass of water forgotten beside a book.",
|
|
"Auntie wrote: do not trust empty biscuit tins. They are emotionally complicated.",
|
|
"The comment box occasionally practices being brave.",
|
|
"There is soup in the archive, but only metaphorically. Probably.",
|
|
"Every old hard drive contains at least one photo called final_final_really.",
|
|
"The cursor paused here once. We still talk about it.",
|
|
"Friendship is two people saving the better chair without discussing it.",
|
|
"Tonight, the page is trying to be a porch light.",
|
|
"A tiny bell rings whenever someone chooses kindness and nobody notices.",
|
|
"The moon asked the stylesheet for softer margins.",
|
|
"You have visited before. The dust remembered your shoes.",
|
|
"The search box knows more than it says and less than it implies.",
|
|
"A good recipe is a family argument that became edible.",
|
|
"Grandad marked this line with a pencil and then forgot why.",
|
|
"Do not feed the calendar after midnight; it gets sentimental.",
|
|
"The page is handmade. Some fingerprints are load-bearing.",
|
|
"Some dreams are just old rooms asking to be opened gently.",
|
|
"Loneliness is not proof that you are hard to love.",
|
|
"The sidebar once tried poetry. It was mostly indentation.",
|
|
"If the site blinks, blink back. It is only being polite.",
|
|
"The archive has a cousin called Nettle who files everything under Maybe.",
|
|
"Morrow says: the lake is not empty, it is saving room.",
|
|
"A spoon knocked twice from inside the recipe page.",
|
|
"There are no achievements here. Only small doors and warm lights.",
|
|
"The browser tab is pretending to be calm.",
|
|
"This sentence was left in a coat pocket from 2009.",
|
|
"A family tradition: someone says five minutes and everyone understands twenty.",
|
|
"The oldest photo on the shelf still remembers the smell of rain.",
|
|
"Some pages do not want to be found quickly.",
|
|
"The loading screen packed snacks, just in case.",
|
|
"You are allowed to leave a page unfinished.",
|
|
"The website is not judging your tabs. It has seen worse.",
|
|
"A small blue bowl appears in three memories and no inventories.",
|
|
"The old internet was slower, which gave anticipation somewhere to sit.",
|
|
"A guestbook entry from nobody says: I was here, kindly.",
|
|
"The clock does not chase you here.",
|
|
"Nettle borrowed the semicolon and promised to bring it back.",
|
|
"Every link is a tiny act of trust.",
|
|
"The error page brought a blanket.",
|
|
"Sometimes growing older is just learning which lights to leave on.",
|
|
"The hallway smells faintly of toast and forgotten passwords.",
|
|
"A dream page exists behind a word you have not clicked enough.",
|
|
"This site remembers visits, not identities.",
|
|
"A note under the doormat reads: take your time.",
|
|
"The system log says: heart rebooted successfully.",
|
|
"Somebody dusted the pixels before you arrived.",
|
|
"Morrow has a lantern. Nettle has a clipboard. Neither has a plan.",
|
|
"The kitchen light is on because somebody might come home late.",
|
|
"If you found this by accident, that still counts as being invited.",
|
|
"The page tried to hum but settled for rendering.",
|
|
"A childhood afternoon is stored here at room temperature.",
|
|
"The comments are quiet because they are listening.",
|
|
"The sitemap has dreams of becoming a treasure map.",
|
|
"Do not worry if the day felt unfinished. Most good days do.",
|
|
"This button has been clicked before and took it personally.",
|
|
"A cupboard in the footer contains emergency biscuits and one tiny apology.",
|
|
"Nostalgia is a window with fingerprints on both sides.",
|
|
"A cousin once renamed the Wi-Fi to Pretty Please Work.",
|
|
"The site keeps a spare sunrise for difficult mornings.",
|
|
"You can rest your eyes. The paragraph will wait.",
|
|
"A missing page is still a page if it misses you back.",
|
|
"The kettle, the clock, and the router have formed a committee.",
|
|
"Sometimes the kindest archive is the one that lets things fade.",
|
|
"The moon cached this page for offline tenderness.",
|
|
"A small paper star is stuck behind the header.",
|
|
"The old forum signature reads: be right back, becoming a person.",
|
|
"SYSTEM: nostalgia level acceptable. Tea recommended.",
|
|
"A dream wrote itself in lowercase and hid under the bed.",
|
|
"There is a room in the site where every chair faces the window.",
|
|
"A soup recipe was annotated with: add patience until it smells like home.",
|
|
"The site has a memory of dial-up and uses it only for dramatic timing.",
|
|
"Kindness is often just good timing with shoes on.",
|
|
"Some nights the archive counts sheep in binary.",
|
|
"The lamp on the study page knows exactly when you are tired.",
|
|
"A little paper boat named Afterward is still crossing the margin.",
|
|
"The scroll bar has travelled far and asks for no medal.",
|
|
"A fake system warning: feelings detected in production.",
|
|
"This hidden message has no puzzle value. It is just glad you came.",
|
|
"If today was heavy, put it down beside the footer for a minute.",
|
|
"Morrow saw your cursor and pretended not to.",
|
|
"A family note says: whoever finishes the milk becomes the historian.",
|
|
"The website keeps one chair empty for the person you used to be.",
|
|
"There is a tiny weather report inside the logo: possible comfort.",
|
|
"The secret is not that the site is alive. The secret is that you are.",
|
|
"A final scrap says: come back when the page is older."
|
|
];
|
|
|
|
const poems = [
|
|
"A room remembers softly / the coat hook, the rain, the key / and you, arriving later.",
|
|
"Time put sugar in its tea / and called the afternoon / almost enough.",
|
|
"An old friend writes once / and the whole year / grows a window.",
|
|
"Dreams leave socks on the floor / and deny everything / by morning.",
|
|
"Home is not a place / so much as a light / that forgives your lateness.",
|
|
"The garden went quiet / not from sadness / but from listening to roots.",
|
|
"A childhood summer / fits in one glass marble / if you hold it up to sun.",
|
|
"The clock kept walking / so I gave it a chair / and sat beside it."
|
|
];
|
|
|
|
const greetings = [
|
|
"The page saved you a seat.",
|
|
"Welcome back to the handmade part of the internet.",
|
|
"The kettle just clicked. Good timing.",
|
|
"A small light is on in the archive.",
|
|
"Today has entered quietly.",
|
|
"You are visitor number: emotionally significant.",
|
|
"The dashboard shuffled its papers before you arrived.",
|
|
"Morrow says hello from somewhere under the floorboards."
|
|
];
|
|
|
|
const nightMessages = [
|
|
"It is late here. Drink some water before the next page.",
|
|
"Night mode inside night mode: you are allowed to be gentle with yourself.",
|
|
"The site lowered its voice because the house is sleeping.",
|
|
"A late visitor gets the soft lamp."
|
|
];
|
|
|
|
const secondLayer = {
|
|
quotes: [
|
|
"The archive does not keep score. It keeps chairs.",
|
|
"Some families pass down recipes. This one also passes down tabs left open for later.",
|
|
"A page can be a room if enough people arrive softly.",
|
|
"The oldest link on the site still expects a hand cursor.",
|
|
"If a memory changes, it may be trying to fit through a smaller door.",
|
|
"The guestbook is not empty. It is holding its breath between signatures.",
|
|
"Every backup is a small superstition against forgetting.",
|
|
"Somewhere in the CSS, a lamp is deciding whether to stay on.",
|
|
"The radio under the stairs only plays songs it has already missed.",
|
|
"A train can leave a station and still remain in the family story.",
|
|
"Kindness is the only password the house accepts without checking case.",
|
|
"The website learned your pace and stopped rushing the hallway.",
|
|
"A draft is just a letter still gathering courage.",
|
|
"The cursor is a visitor. The margin is a host.",
|
|
"If you hover long enough, even a heading may remember being handwritten.",
|
|
"The old forum banner says: welcome back, even if you are new.",
|
|
"Not all broken pages need repair. Some need a name.",
|
|
"The site keeps its mysteries low to the ground, where tired people can reach them.",
|
|
"A year is a folder that keeps renaming itself.",
|
|
"Some evenings are survived by the exact right mug."
|
|
],
|
|
poems: [
|
|
"A train in the rain / waits beside an empty platform / so memory can board.",
|
|
"Grandma's radio / tuned between two stations / found us anyway.",
|
|
"A flower pressed flat / still teaches the book / how spring once leaned.",
|
|
"The lamp stayed awake / not because it was brave / but because someone might need it.",
|
|
"Old usernames fade / but the welcome thread remains / warm at the top.",
|
|
"I found a draft / with no ending / and felt less alone.",
|
|
"The house at midnight / is mostly refrigerator hum / and unsent kindness.",
|
|
"A calendar page / falls without bitterness / into tomorrow's hands.",
|
|
"There is a road home / made entirely of small tabs / you meant to close.",
|
|
"A cassette clicks / and the room becomes younger / for one more minute."
|
|
],
|
|
conversations: [
|
|
["Nettle", "The ticket drawer is humming again.", "Morrow", "That is normal. It hums when someone is almost ready."],
|
|
["Aunt Ma", "Who put the screwdriver in the biscuit tin?", "Nadir", "A person with vision and no labelling discipline."],
|
|
["Morrow", "The lamp keeps following the visitor.", "Etta", "Good. Lamps should have hobbies."],
|
|
["Old Guest 14", "is this page still alive?", "Webmaster", "yes, but quietly, please. the baby is asleep."],
|
|
["Nettle", "Filed under: feelings disguised as infrastructure.", "Morrow", "Approved without changes."],
|
|
["Luma", "The radio said my name.", "Aunt Ma", "Then say salaam back. Manners matter."],
|
|
["Nadir", "The server made soup noises.", "Etta", "That means the deployment is nearly done."],
|
|
["Morrow", "I found a page that says do not open.", "Nettle", "That usually means open gently."]
|
|
],
|
|
journals: [
|
|
"Journal scrap, 2006: We changed the wallpaper and everyone pretended it was not emotional.",
|
|
"Journal scrap, 2011: The old router blinked like it was trying to stay in the conversation.",
|
|
"Journal scrap, 2018: I learned that homesickness can happen inside your own house.",
|
|
"Journal scrap, 2024: Somebody laughed in the kitchen and the whole week became easier.",
|
|
"Journal scrap, undated: The marigolds came back where nobody planted them.",
|
|
"Journal scrap, undated: We kept the cracked mug because it knew too much.",
|
|
"Journal scrap, winter: I saved a voicemail just to remember the background noise.",
|
|
"Journal scrap, summer: The fan turned all afternoon and made the curtains look like boats."
|
|
],
|
|
warnings: [
|
|
"COMFORT WARNING: this page may contain traces of old carpet, warm rice, and unfinished errands.",
|
|
"SOFT ERROR: the archive attempted to be impressive and became sincere instead.",
|
|
"NOTICE: do not stack heavy feelings on the top shelf.",
|
|
"LEGACY WARNING: this layout remembers 800x600 and asks you to be respectful.",
|
|
"SYSTEM MESSAGE: no achievement unlocked. You were kind anyway.",
|
|
"CACHE NOTE: the page kept a copy of the good part.",
|
|
"RECOVERY MODE: make tea, rename file, try again.",
|
|
"PERMISSION DENIED: the sadness may enter, but it must wipe its feet."
|
|
],
|
|
dreams: [
|
|
"Dream 01: a train full of empty mugs waited at a station called Later.",
|
|
"Dream 02: the search box became a letterbox and returned every question with a family recipe.",
|
|
"Dream 03: a radio under the stairs played tomorrow's weather in a grandparent's voice.",
|
|
"Dream 04: the old homepage had a porch, and every link was a moth-wing-soft knock.",
|
|
"Dream 05: someone had folded the sitemap into a paper flower and left it in a browser tab."
|
|
],
|
|
cassettes: [
|
|
"CASSETTE A / side 1: You can hear plates, rain, and someone saying, no, leave the light on.",
|
|
"CASSETTE A / side 2: Three seconds of silence, then laughter too close to the microphone.",
|
|
"CASSETTE B / side 1: Nadir explains backups to a child using rice grains and a spoon.",
|
|
"CASSETTE C / side 2: A song with no words, hummed badly, loved completely.",
|
|
"CASSETTE D / side ?: The tape label only says for when the house feels far away."
|
|
],
|
|
fakeUsers: [
|
|
"sunnydialup88: first!!!! but emotionally",
|
|
"auntie-admin: please stop uploading files named newnewnewfinal",
|
|
"railway_morrow: platform 3 smells like rain today",
|
|
"nettle_mod: moved thread from Bugs to Feelings",
|
|
"luma-lamp: does anyone else hear the margins singing",
|
|
"guest_000: I signed this once and then grew up"
|
|
],
|
|
seasonal: {
|
|
winter: "The archive put an extra blanket on the footer.",
|
|
spring: "A pressed flower appeared between two cached pages.",
|
|
summer: "The page opened a window and let the fan turn the silence.",
|
|
autumn: "The old links sound like leaves when the cursor passes."
|
|
},
|
|
homepageTakeovers: [
|
|
"For one rare minute, the homepage believes it is an old family desktop.",
|
|
"The dashboard has become a late-night noticeboard with one pin missing.",
|
|
"The homepage found its old forum skin in a drawer and is trying it on."
|
|
],
|
|
roomLinks: [
|
|
["/play/radio-room.html", "radio room"],
|
|
["/play/guestbook-1999.html", "guestbook 1999"],
|
|
["/play/do-not-open.html", "do not open"],
|
|
["/play/cassette-log.html", "cassette log"],
|
|
["/play/train-platform.html", "train platform"],
|
|
["/play/forgotten-draft.html", "forgotten draft"],
|
|
["/play/corrupted-recipe.html", "corrupted recipe"],
|
|
["/play/terminal-cupboard.html", "terminal cupboard"],
|
|
["/play/patience-game.html", "patience game"]
|
|
]
|
|
};
|
|
|
|
function readState() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(KEY) || "{}");
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function writeState(next) {
|
|
try {
|
|
localStorage.setItem(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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
el.textContent = message;
|
|
el.classList.add("is-visible");
|
|
window.clearTimeout(el._timer);
|
|
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5200);
|
|
}
|
|
|
|
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 next = {
|
|
...state,
|
|
visits,
|
|
lastPath: path,
|
|
pathCounts,
|
|
seen: Array.from(new Set([...seen, path])).slice(-80),
|
|
firstSeen: state.firstSeen || new Date().toISOString(),
|
|
lastSeen: new Date().toISOString()
|
|
};
|
|
writeState(next);
|
|
return next;
|
|
}
|
|
|
|
function addFooterNote(memory) {
|
|
const footer = document.querySelector("footer");
|
|
if (!footer) return;
|
|
const note = document.createElement("p");
|
|
note.className = "hidden-footer-note";
|
|
const base = pick(details, "footer");
|
|
note.textContent = 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) return;
|
|
const greeting = pick(greetings, `greeting-${memory.visits}`);
|
|
target.textContent = greeting;
|
|
const extra = document.createElement("p");
|
|
extra.className = "hidden-greeting";
|
|
extra.textContent = memory.visits > 1 ? "The site remembers this is not your first doorway." : "First visits count as tiny ceremonies.";
|
|
target.closest("div")?.appendChild(extra);
|
|
}
|
|
|
|
function addWhispers() {
|
|
const paragraphs = Array.from(document.querySelectorAll("main p, #content p, article p")).filter((p) => p.textContent.trim().length > 80);
|
|
paragraphs.slice(0, 4).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 = " ?";
|
|
mark.title = pick(index % 2 ? poems : details, `whisper-${index}`);
|
|
mark.addEventListener("click", () => toast(mark.title));
|
|
p.appendChild(mark);
|
|
});
|
|
}
|
|
|
|
function addCornerObject(memory) {
|
|
const object = document.createElement("button");
|
|
object.className = "hidden-corner-object";
|
|
object.type = "button";
|
|
object.title = "A very small archive object";
|
|
object.textContent = memory.visits % 2 ? "*" : "~";
|
|
document.body.appendChild(object);
|
|
|
|
let clicks = 0;
|
|
object.addEventListener("click", () => {
|
|
clicks += 1;
|
|
const messages = [
|
|
"The object is warmer than before.",
|
|
"Tap tap. Somewhere, a spoon answers.",
|
|
"Morrow whispers: that is not a button, but it appreciates the attention.",
|
|
"Nettle files a report titled Repeated Kindness.",
|
|
"A loose panel opens in the idea of the page."
|
|
];
|
|
toast(messages[Math.min(clicks - 1, messages.length - 1)]);
|
|
if (clicks === 7) {
|
|
window.location.href = "/play/left-behind.html";
|
|
}
|
|
});
|
|
}
|
|
|
|
function addPatterns(memory) {
|
|
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));
|
|
toast(taps >= 5 ? "The logo opens one eye. Try /play/dream-corridor.html" : "The logo makes a tiny ceramic sound.");
|
|
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 (value === "morrow") toast("Search result: lantern, lake, unfinished apology.");
|
|
if (value === "nettle") toast("Search result: clipboard, cupboard, suspicious semicolon.");
|
|
if (value === "home") toast("Search result: warm window, spare key, somebody humming downstairs.");
|
|
if (value === "kitchen light") window.location.href = "/play/kitchen-light.html";
|
|
});
|
|
}
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
const key = event.key.toLowerCase();
|
|
const chain = `${sessionStorage.getItem("zxh_key_chain") || ""}${key}`.slice(-12);
|
|
sessionStorage.setItem("zxh_key_chain", chain);
|
|
if (chain.endsWith("remember")) toast("The website remembers gently, which is the only useful kind.");
|
|
if (chain.endsWith("dream")) window.location.href = "/play/dream-corridor.html";
|
|
});
|
|
|
|
if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
|
|
window.setTimeout(() => {
|
|
toast("The map noticed you wandering and left the hallway lights on.");
|
|
writeState({ ...readState(), travellerNoteShown: true });
|
|
}, 1600);
|
|
}
|
|
}
|
|
|
|
function addRareEvents() {
|
|
if (hour >= 22 || hour < 5) {
|
|
document.body.classList.add("hidden-late-night");
|
|
window.setTimeout(() => toast(pick(nightMessages, "night"), 2200), 900);
|
|
}
|
|
|
|
const roll = Math.random();
|
|
if (roll < 0.004) {
|
|
window.setTimeout(() => showDriftNote("A very rare page breeze passed through.", " .--.\n ( ) <- a cloud pretending to be a thought\n `--'"), 1800);
|
|
} else if (roll < 0.012) {
|
|
window.setTimeout(() => toast("RARE SYSTEM EVENT: the archive found a biscuit in its coat pocket."), 2400);
|
|
}
|
|
}
|
|
|
|
function showDriftNote(text, art) {
|
|
const panel = document.createElement("aside");
|
|
panel.className = "hidden-drift-note";
|
|
panel.innerHTML = `<div>${escapeHtml(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"],
|
|
...secondLayer.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(pick([
|
|
"This page is missing, but you are not. Try the sitemap, or sit here a second.",
|
|
"404: the page went to check on the rice and has not returned.",
|
|
"The address is wrong, but the welcome is still valid.",
|
|
"Nothing is here except a very apologetic lamp."
|
|
], "error"));
|
|
}
|
|
}
|
|
|
|
function addContinuity(memory) {
|
|
const milestones = {
|
|
2: "The site has placed your second visit beside the spare keys.",
|
|
5: "A quiet ledger now lists you under people who came back.",
|
|
9: "Nettle has started a folder called regular visitors, handle gently.",
|
|
17: "Morrow left a platform ticket in the footer for you.",
|
|
31: "The archive trusts you enough to misplace something important nearby.",
|
|
64: "A very old counter turned over and made the sound of a cupboard closing."
|
|
};
|
|
if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) {
|
|
window.setTimeout(() => toast(milestones[memory.visits], 6500), 1200);
|
|
writeState({ ...readState(), [`milestone_${memory.visits}`]: true });
|
|
}
|
|
|
|
const count = memory.pathCounts?.[path] || 0;
|
|
if ([3, 7, 12].includes(count)) {
|
|
window.setTimeout(() => toast([
|
|
"This page recognizes the shape of your return.",
|
|
"The paragraph straightened the cushions before you arrived.",
|
|
"A repeat visit is the gentlest kind of knock."
|
|
][[3, 7, 12].indexOf(count)], 6200), 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) {
|
|
window.setTimeout(() => toast(secondLayer.seasonal[season], 5200), 2600);
|
|
}
|
|
if (month === 4 && day === 9) {
|
|
window.setTimeout(() => toast("Site birthday: the candles are imaginary, but the crumbs are real."), 900);
|
|
}
|
|
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: the site saved a paper flower from your first day here.", 7000), 1400);
|
|
}
|
|
if (now.getMinutes() === 13) {
|
|
window.setTimeout(() => toast("At minute thirteen, the radio under the stairs clears its throat."), 2100);
|
|
}
|
|
}
|
|
|
|
function addObjectConstellation(memory) {
|
|
const rail = document.createElement("div");
|
|
rail.className = "hidden-object-rail";
|
|
rail.setAttribute("aria-label", "Hidden keepsake objects");
|
|
const objects = [
|
|
["lamp", "The lamp clicks once, then pretends it did not."],
|
|
["mug", "The mug is chipped in exactly the familiar place."],
|
|
["ticket", "The ticket says RETURN, but not to where."],
|
|
["radio", "The radio hums between weather and memory."],
|
|
["flower", "The pressed flower has kept its appointment with spring."]
|
|
];
|
|
objects.forEach(([name, message]) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = `hidden-keepsake hidden-keepsake--${name}`;
|
|
button.title = name;
|
|
button.textContent = { lamp: "i", mug: "u", ticket: "=", radio: "o", flower: "+" }[name];
|
|
button.addEventListener("click", () => {
|
|
const next = readState();
|
|
next.keepsakes = Array.from(new Set([...(next.keepsakes || []), name]));
|
|
writeState(next);
|
|
toast(message);
|
|
if (next.keepsakes.length >= objects.length) {
|
|
window.setTimeout(() => toast("The five keepsakes align. Search for radio under stairs."), 1100);
|
|
}
|
|
});
|
|
rail.appendChild(button);
|
|
});
|
|
document.body.appendChild(rail);
|
|
}
|
|
|
|
function addWeatherWindow(memory) {
|
|
if (memory.visits < 3 || !("geolocation" in navigator)) return;
|
|
const button = document.createElement("button");
|
|
button.className = "hidden-weather-window";
|
|
button.type = "button";
|
|
button.title = "Ask the outside window";
|
|
button.textContent = "window";
|
|
button.addEventListener("click", () => {
|
|
toast("The window is checking whether the outside agrees with the archive.");
|
|
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,weather_code&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. Inside: the archive moved a towel closer to the door. ${temp}C.` : `Outside: ${temp}C. Inside: possible comfort, mild nostalgia.`);
|
|
})
|
|
.catch(() => toast("The weather window fogged up, so the site invented indoor weather: quiet with a chance of tea."));
|
|
}, () => toast("No outside weather today. The indoor forecast is lamp-light and manageable clouds."));
|
|
});
|
|
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";
|
|
stamp.title = pick(secondLayer.fakeUsers, "fake-user");
|
|
stamp.textContent = "best viewed with patience";
|
|
stamp.addEventListener("click", () => toast(stamp.title));
|
|
document.body.appendChild(stamp);
|
|
}
|
|
const comments = document.querySelector("#comments");
|
|
if (comments && !comments.querySelector(".hidden-guestbook-line")) {
|
|
const line = document.createElement("p");
|
|
line.className = "hidden-guestbook-line";
|
|
line.textContent = pick(secondLayer.fakeUsers, "comment-lore");
|
|
comments.appendChild(line);
|
|
}
|
|
}
|
|
|
|
function addSourceRelics() {
|
|
// RELIC: Aunt Ma says source comments are the attic of the page.
|
|
const marker = document.createComment("hidden-song: hum the lamp, hum the train, hum the long way home");
|
|
document.documentElement.appendChild(marker);
|
|
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
|
|
if (index > 2) return;
|
|
img.alt = pick([
|
|
"A quiet image that remembers being taped into a scrapbook.",
|
|
"An old visual keepsake, labelled by someone who arrived later.",
|
|
"A picture with its shoes by the door."
|
|
], `alt-${index}`);
|
|
});
|
|
}
|
|
|
|
function addSearchAndKeySecrets() {
|
|
const search = document.querySelector("#search-input");
|
|
if (search) {
|
|
search.addEventListener("input", () => {
|
|
const value = search.value.trim().toLowerCase();
|
|
const routes = {
|
|
"radio under stairs": "/play/radio-room.html",
|
|
"guestbook 1999": "/play/guestbook-1999.html",
|
|
"do not open": "/play/do-not-open.html",
|
|
"cassette": "/play/cassette-log.html",
|
|
"return ticket": "/play/train-platform.html",
|
|
"unfinished letter": "/play/forgotten-draft.html",
|
|
"burnt sugar": "/play/corrupted-recipe.html",
|
|
"cupboard": "/play/terminal-cupboard.html",
|
|
"patience": "/play/patience-game.html"
|
|
};
|
|
if (routes[value]) window.location.href = routes[value];
|
|
if (value === "marigold") toast("Search result: pressed flower, May rain, a promise nobody announced.");
|
|
if (value === "luma") toast("Search result: lamp-child, margin-song, brave small glow.");
|
|
if (value === "nadir") toast("Search result: backups, train tickets, practical tenderness.");
|
|
});
|
|
}
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
const chain = `${sessionStorage.getItem("zxh_second_chain") || ""}${event.key.toLowerCase()}`.slice(-18);
|
|
sessionStorage.setItem("zxh_second_chain", chain);
|
|
if (chain.endsWith("leave the light on")) window.location.href = "/play/kitchen-light.html";
|
|
if (chain.endsWith("oldweb")) toast("A 2004 table layout briefly considers returning, then thinks better of it.");
|
|
if (chain.endsWith("ma")) toast("Aunt ma says the screwdriver is absolutely not in the biscuit tin.");
|
|
if (chain.endsWith("luma")) playTinySong();
|
|
});
|
|
}
|
|
|
|
function addPatienceRewards(memory) {
|
|
if (memory.visits < 2) return;
|
|
let idleTimer = null;
|
|
const startIdle = () => {
|
|
window.clearTimeout(idleTimer);
|
|
idleTimer = window.setTimeout(() => {
|
|
toast(pick([
|
|
"Thank you for waiting. The page used the time to remember a window.",
|
|
"Patience noted. No badge will be shown, which is how the archive prefers it.",
|
|
"A tiny secret achievement happened quietly and did not ask to be announced."
|
|
], "idle"), 7000);
|
|
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(memory) {
|
|
const roll = Math.random();
|
|
if ((path === "/" || path === "/index.html") && roll < 0.006) {
|
|
document.body.classList.add("hidden-home-takeover");
|
|
window.setTimeout(() => showDriftNote(pick(secondLayer.homepageTakeovers, "takeover"), "WELCOME_BACK.GIF\nUNDER_CONSTRUCTION\nSIGN_GUESTBOOK"), 600);
|
|
} else if (roll < 0.002) {
|
|
window.setTimeout(() => showDriftNote("A cassette starts by itself, then politely asks permission.", "play? y/n\n[no autoplay. the site has manners.]"), 1500);
|
|
} else if (roll < 0.006) {
|
|
window.setTimeout(() => toast(pick(secondLayer.dreams, "rare-dream"), 7000), 2000);
|
|
} else if (roll < 0.014) {
|
|
window.setTimeout(() => toast(pick(secondLayer.warnings, "rare-warning"), 6200), 2300);
|
|
}
|
|
}
|
|
|
|
function addPageSpecificSecrets(memory) {
|
|
if (path.includes("/play/cassette-log")) {
|
|
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
|
|
button.addEventListener("click", () => {
|
|
toast(secondLayer.cassettes[index % secondLayer.cassettes.length], 7000);
|
|
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)) {
|
|
target.textContent = seconds === 30 ? "The lamp notices you are still here." : seconds === 90 ? "The train has not left without you." : "The room trusts your quiet.";
|
|
}
|
|
}, 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: listen, inventory, tea, open, remember, exit",
|
|
listen: "Behind the jars: a radio, rain, one sentence from 2003.",
|
|
inventory: "one cracked mug; two return tickets; marigolds; screwdriver absent, allegedly.",
|
|
tea: "The cupboard approves. Morale +1. Biscuit tin remains suspicious.",
|
|
open: "The cupboard opens into another cupboard. This is a family tradition.",
|
|
remember: "You remember a room you may never have visited.",
|
|
exit: "The cupboard lets you leave with both hands full of nothing heavy."
|
|
};
|
|
input?.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Enter") return;
|
|
const value = input.value.trim().toLowerCase();
|
|
const line = document.createElement("p");
|
|
line.textContent = `> ${value}\n${commands[value] || "The cupboard does not know that command, but respects the attempt."}`;
|
|
log.appendChild(line);
|
|
input.value = "";
|
|
});
|
|
}
|
|
}
|
|
|
|
function addInvisibleHoverSecrets() {
|
|
document.querySelectorAll("h1, h2").forEach((heading, index) => {
|
|
if (index > 5) return;
|
|
heading.classList.add("hidden-hover-memory");
|
|
heading.dataset.hiddenMemory = pick([...secondLayer.quotes, ...secondLayer.poems], `heading-${index}`);
|
|
});
|
|
}
|
|
|
|
function playTinySong() {
|
|
try {
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
if (!AudioContext) return;
|
|
const ctx = new AudioContext();
|
|
const notes = [392, 440, 330, 294, 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]));
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const memory = rememberVisit();
|
|
addFooterNote(memory);
|
|
addHomepageGreeting(memory);
|
|
addWhispers();
|
|
addCornerObject(memory);
|
|
addPatterns(memory);
|
|
addRareEvents();
|
|
addSecretLinks();
|
|
enhanceErrors();
|
|
addContinuity(memory);
|
|
addSeasonAndDates(memory);
|
|
addObjectConstellation(memory);
|
|
addWeatherWindow(memory);
|
|
addOldWebLayer(memory);
|
|
addSourceRelics();
|
|
addSearchAndKeySecrets();
|
|
addPatienceRewards(memory);
|
|
addRareSecondLayer(memory);
|
|
addPageSpecificSecrets(memory);
|
|
addInvisibleHoverSecrets();
|
|
});
|
|
}());
|