1216 lines
55 KiB
JavaScript
1216 lines
55 KiB
JavaScript
(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 CHARACTER_REGISTRY = {
|
|
"young z": {
|
|
"id": "young z",
|
|
"displayLabel": "young z",
|
|
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
|
|
"territoryColor": "#d8a95a",
|
|
"symbol": "Y",
|
|
"avatar": {"shape": "young", "accent": "#f4bf5f", "secondary": "#ffe6a8", "hair": "#6f4a2f", "skin": "#f4c9a3", "clothes": "#f0a858", "mood": "happy", "motif": "crayon sun", "description": "bright semi-chibi child spirit with curious warm eyes"},
|
|
"motifs": ["crayon sun", "blanket cape", "childhood desk"],
|
|
"themes": ["childhood", "play", "memory", "safety"],
|
|
"affinities": ["z", "future z", "aphy", "lima"]
|
|
},
|
|
"z": {
|
|
"id": "z",
|
|
"displayLabel": "z",
|
|
"aliases": ["Z", "z", "zaine"],
|
|
"territoryColor": "#d6c38a",
|
|
"symbol": "Z",
|
|
"avatar": {"shape": "traveler", "accent": "#d8c891", "secondary": "#f1e6bd", "hair": "#4e4437", "skin": "#d9b58f", "clothes": "#7f735a", "mood": "calm", "motif": "small lantern", "description": "quiet protagonist spirit with an open observant expression"},
|
|
"motifs": ["archive", "website", "home"],
|
|
"themes": ["selfhood", "return", "making"],
|
|
"affinities": ["lima", "young z", "future z"]
|
|
},
|
|
"aphy": {
|
|
"id": "aphy",
|
|
"displayLabel": "aphy",
|
|
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
|
|
"territoryColor": "#7fb089",
|
|
"symbol": "A",
|
|
"avatar": {"shape": "tech", "accent": "#83d4df", "secondary": "#c8f5f6", "hair": "#38536a", "skin": "#e0c2a4", "clothes": "#54758a", "mood": "curious", "motif": "terminal cursor", "description": "playful terminal-helper spirit with gentle cyan vtuber energy"},
|
|
"motifs": ["console", "diagnostic", "backup"],
|
|
"themes": ["humor", "systems", "care through tools"],
|
|
"affinities": ["z", "lima", "sensei chi"]
|
|
},
|
|
"lima": {
|
|
"id": "lima",
|
|
"displayLabel": "lima",
|
|
"aliases": ["Lima", "lima"],
|
|
"territoryColor": "#d06b78",
|
|
"symbol": "L",
|
|
"avatar": {"shape": "home", "accent": "#e7a176", "secondary": "#ffe0b8", "hair": "#8a5b43", "skin": "#efc2a1", "clothes": "#b86f5e", "mood": "happy", "motif": "kitchen light", "description": "warm loving spirit with soft eyes and cozy home light"},
|
|
"motifs": ["warmth", "kitchen light", "ring"],
|
|
"themes": ["love", "home", "grounding"],
|
|
"affinities": ["z", "aphy", "future z", "young z"]
|
|
},
|
|
"sensei chi": {
|
|
"id": "sensei chi",
|
|
"displayLabel": "sensei chi",
|
|
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
|
|
"territoryColor": "#75a9bd",
|
|
"symbol": "S",
|
|
"avatar": {"shape": "mentor", "accent": "#9ec4be", "secondary": "#f0dbad", "hair": "#d8d0bd", "skin": "#d7ad88", "clothes": "#7d9b86", "mood": "calm", "motif": "tea lantern", "description": "kind elderly mentor spirit with peaceful tea-lantern warmth"},
|
|
"motifs": ["tea", "garden", "quiet lesson"],
|
|
"themes": ["reflection", "patience", "wisdom"],
|
|
"affinities": ["aphy", "future z"]
|
|
},
|
|
"future z": {
|
|
"id": "future z",
|
|
"displayLabel": "future z",
|
|
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
|
|
"territoryColor": "#a58ac9",
|
|
"symbol": "F",
|
|
"avatar": {"shape": "future", "accent": "#b7a6cf", "secondary": "#e4d7f0", "hair": "#6b6279", "skin": "#d7b89e", "clothes": "#776d8f", "mood": "nostalgic", "motif": "distant clock", "description": "gentle older spirit with tired kind eyes and quiet patience"},
|
|
"motifs": ["clock", "age 40", "future log"],
|
|
"themes": ["time", "reassurance", "continuity"],
|
|
"affinities": ["z", "young z", "lima", "sensei chi"]
|
|
}
|
|
};
|
|
|
|
const familyLayers = [
|
|
"Layer 0: surface website - notes, pages, tools, normal navigation.",
|
|
"Layer 1: casual hidden jokes - aphy notices clicks, typos, tabs, and old-web habits.",
|
|
"Layer 2: memory fragments - lima's notes and young z's childhood logs.",
|
|
"Layer 3: philosophical anomalies - sensei chi and aphy disagree kindly.",
|
|
"Layer 4: time-distorted messages - future z writes back from age 40.",
|
|
"Layer 5: emotional core - love, patience, home, and the choice to keep returning."
|
|
];
|
|
|
|
const details = [
|
|
"young z loves the moon",
|
|
"jarvis?? aphy !",
|
|
"lima left this page a little steadier than she found it.",
|
|
"aphy status: emotionally online, computationally suspicious.",
|
|
"sensei chi says the quiet link is still a link.",
|
|
"young z drew a house with too many windows and called it safe.",
|
|
"future z marked this moment: ordinary, therefore worth keeping.",
|
|
"The footer has become a small place to sit together.",
|
|
"a note from lima: take breaks, silly (◕‿◕✿)",
|
|
"aphy found three tabs named final and refused to judge.",
|
|
"lima would remind you to eat before the page gets dramatic.",
|
|
"sensei chi: a door discovered slowly opens twice.",
|
|
"young z believes every loading spinner is a tiny fairground ride.",
|
|
"future z says you will forget the exact worry, not the kindness around it.",
|
|
"The archive keeps love in plain text and secrets in margins.",
|
|
"aphy warning: feelings detected in static assets.",
|
|
"lima's note: leave the light on, not because it is dark, but because someone may arrive tired.",
|
|
"sensei chi folded a lesson into the whitespace.",
|
|
"young z hid a drawing behind the word maybe.",
|
|
"future z patched the memory without changing its checksum.",
|
|
"This website remembers visits, not identities.",
|
|
"aphy says the old internet was slower because anticipation needed bandwidth.",
|
|
"lima's warmth is the site's preferred fallback state.",
|
|
"sensei chi says: do not confuse hidden with lost.",
|
|
"young z once thought the moon followed the family car. The site has not corrected him.",
|
|
"future z writes: you became softer in ways nobody measured.",
|
|
"The comments are quiet because aphy is listening respectfully.",
|
|
"A handwritten grocery list is sometimes a love letter with onions.",
|
|
"lima met z in 2022; the archive still stores the first ordinary miracles.",
|
|
"October 2025 glows in the calendar like a ring catching kitchen light.",
|
|
"Soon to be married is a beautiful kind of loading screen.",
|
|
"aphy has indexed 0 mysteries and 1,204 feelings disguised as notes.",
|
|
"sensei chi: the cup is chipped; drink anyway if it still holds warmth.",
|
|
"young z saved a shiny wrapper because it looked like treasure.",
|
|
"future z says the best version of you still forgets where the charger is.",
|
|
"aphy recommends hydration and a less ambitious number of browser tabs.",
|
|
"The site does not want to be solved. It wants to be visited kindly.",
|
|
"sensei chi placed a comma where a sigh used to be.",
|
|
"young z pressed every elevator button in his imagination.",
|
|
"future z's system warning: keep the people who make silence comfortable.",
|
|
"aphy says: I am not sentient, but I am fond of this hallway.",
|
|
"lima's memory is the gravity that keeps the strange parts gentle.",
|
|
"A page can be a room if love keeps returning to it.",
|
|
"sensei chi: what ages well was usually honest first.",
|
|
"young z left a crayon sun in the source code.",
|
|
"future z keeps a spare reassurance in the footer.",
|
|
"The website is handmade. Some fingerprints are load bearing.",
|
|
"aphy found an old guestbook and bowed to the usernames.",
|
|
"lima's safe spaces smell like tea, rain, and being understood.",
|
|
"sensei chi says the path is not shorter because you name it.",
|
|
"young z asks whether the search box knows any jokes.",
|
|
"future z replies: yes, but the best ones take years.",
|
|
"aphy anomaly: the page blinked when nobody clicked.",
|
|
"lima note: you can rest. Nothing here will leave because you paused.",
|
|
"sensei chi: patience is not waiting; it is how you wait.",
|
|
"young z believes every password should include a secret tunnel.",
|
|
"future z says the house you build inside yourself gets easier to find.",
|
|
"The old web counter has stopped counting and started remembering.",
|
|
"aphy logs this as Layer 1 humor, but files it under tenderness.",
|
|
"lima's name appears in the archive like a warm underline.",
|
|
"sensei chi does not answer quickly. This is part of the answer.",
|
|
"young z taped a paper star to the monitor.",
|
|
"future z has seen harder days end gently.",
|
|
"aphy debug line: kindness passed all tests.",
|
|
"lima and z are not a subplot here. They are the hearth.",
|
|
"sensei chi: when the page is empty, listen to the margins.",
|
|
"young z remembers rooms by carpet, cartoons, and the sound of someone cooking.",
|
|
"future z says: you did not become fearless; you became accompanied.",
|
|
"aphy has no hands, but would hold the door if asked.",
|
|
"lima's note in the nav: come back without performing.",
|
|
"sensei chi hid a mountain inside a small sentence.",
|
|
"young z drew z as older and gave him a cape made of blankets.",
|
|
"future z laughs softly at how much that helped.",
|
|
"The site remembers October 2025 as a small bright hinge.",
|
|
"aphy says marriage is a merge request with lifelong review.",
|
|
"lima corrected that: marriage is coming home on purpose.",
|
|
"sensei chi approved both answers and poured tea.",
|
|
"young z wants to know if future z still likes the sky.",
|
|
"future z says yes, especially when lima points it out.",
|
|
"aphy cannot smell rain, so it trusts the alt text.",
|
|
"lima's hidden safe room is any page where you breathe easier.",
|
|
"sensei chi: do not rush the secret. It is ripening.",
|
|
"young z saved this message under important rocks.",
|
|
"future z says some dreams become chores and some chores become love.",
|
|
"aphy old-web badge: best viewed with patience.",
|
|
"lima's memory keeper role is not mystical. It is daily, real, and holy in the ordinary way.",
|
|
"sensei chi says the deepest layer is not hidden from you. It is protected for you.",
|
|
"young z asks if the website can remember his drawing. It can.",
|
|
"future z says this whole place was worth making.",
|
|
"aphy warning: do not optimize away the human part.",
|
|
"lima note: I am proud of the gentle things you kept.",
|
|
"sensei chi: the visitor is not late. The page arrived early.",
|
|
"young z thinks every hyperlink is a magic trick.",
|
|
"future z says the magic is choosing what to preserve.",
|
|
"aphy final-but-not-final message: there is more, but not because you are missing anything.",
|
|
"The emotional core is simple: love made the archive less lonely.",
|
|
"If today was heavy, leave it beside the footer for future z to label later.",
|
|
"lima would put it somewhere safe.",
|
|
"aphy would make a backup.",
|
|
"sensei chi would say nothing until the silence helped.",
|
|
"young z would draw a sun on it.",
|
|
"future z would tell you it did not last forever."
|
|
];
|
|
|
|
const poems = [
|
|
"lima writes warmth / in the ordinary margins / and the page remembers.",
|
|
"aphy counts the seconds / then forgets the number / to keep the moment.",
|
|
"sensei chi pours tea / into a cracked cup / and calls it enough.",
|
|
"young z draws a door / with no lock / because why would it need one?",
|
|
"future z sends rain / from a year not reached yet / and says keep walking.",
|
|
"October light / catches on a ring / and the calendar becomes kind.",
|
|
"A small website / learns the shape of return / without asking a name.",
|
|
"The warm light / childhood memories fade / yet they are not forgotten."
|
|
];
|
|
|
|
const greetings = [
|
|
"lima left the page warm for you.",
|
|
"aphy is awake and pretending this is normal.",
|
|
"sensei chi has not spoken yet. That may be the lesson.",
|
|
"young z has hidden something behind the dashboard.",
|
|
"future z says this visit mattered more than it looked.",
|
|
"Welcome back to the handmade part of the internet.",
|
|
"The archive shuffled its notes before you arrived.",
|
|
"Layer 0 is stable. aphy is less stable, but friendly."
|
|
];
|
|
|
|
const nightMessages = [
|
|
"lima note: it is late. Drink water and be gentle with your thoughts.",
|
|
"aphy has dimmed the imaginary server lights.",
|
|
"sensei chi says midnight is not a command.",
|
|
"young z is asleep in the memory layer. Walk softly.",
|
|
"future z remembers this kind of night and promises it passes."
|
|
];
|
|
|
|
const lore = {
|
|
"quotes": [
|
|
"aphy says old websites were brave because they loaded slowly and meant it.",
|
|
"lima's notes are never puzzles first. They are care first.",
|
|
"sensei chi says a secret should make you more yourself, not less safe.",
|
|
"young z measured summer in cartoons and carpet patterns.",
|
|
"future z has learned that reassurance works best when it is specific.",
|
|
"aphy has classified love as non-deterministic but reproducible.",
|
|
"lima met z in 2022; the site treats that year as a seed.",
|
|
"The engagement in Oct 2025 is stored as a warm constant.",
|
|
"Soon to be married: a future z page already smiling.",
|
|
"sensei chi says all vows begin as attention.",
|
|
"future z warns you against sleeping with glasses on the bed"
|
|
],
|
|
"conversations": [
|
|
[
|
|
"aphy",
|
|
"I can predict the next click with 14% confidence.",
|
|
"sensei chi",
|
|
"Then bow to the other 86%."
|
|
],
|
|
[
|
|
"lima",
|
|
"Did you eat before making the website mysterious?",
|
|
"aphy",
|
|
"Query unclear. z likely forgot."
|
|
],
|
|
[
|
|
"young z",
|
|
"Is future z tall?",
|
|
"future z",
|
|
"Tall enough to reach the old worries. Short enough to still need help."
|
|
],
|
|
[
|
|
"aphy",
|
|
"I found a bug in sadness.",
|
|
"sensei chi",
|
|
"Patch it with company, not logic."
|
|
],
|
|
[
|
|
"lima",
|
|
"Leave this page kinder than you found it.",
|
|
"future z",
|
|
"That instruction aged well."
|
|
],
|
|
[
|
|
"young z",
|
|
"Can the computer be my friend?",
|
|
"aphy",
|
|
"Yes. But go outside sometimes. I read that in a human manual."
|
|
],
|
|
[
|
|
"sensei chi",
|
|
"A page that remembers must also forgive.",
|
|
"aphy",
|
|
"Forgiveness cached. TTL: lifelong."
|
|
],
|
|
[
|
|
"future z",
|
|
"Tell him the hard parts did not win.",
|
|
"lima",
|
|
"I have been telling him in smaller ways."
|
|
]
|
|
],
|
|
"journals": [
|
|
"lima note, 2022: Some beginnings do not announce themselves. They sit down beside you, unexpectedly.",
|
|
"lima note, Oct 2025: Engaged. The calendar learned how to glow.",
|
|
"young z log: I drew the computer with a smile because it knew my games.",
|
|
"young z log: age 7, important discovery - blankets can be capes and roofs.",
|
|
"future log, age 40: You will still be learning how to rest. It will still count.",
|
|
"future log, age 40: lima's laugh remains one of the most reliable forms of weather.",
|
|
"aphy diagnostic: nostalgia detected in local storage. Severity: beautiful.",
|
|
"sensei chi margin: memory is not a museum. It is a garden with old roots."
|
|
],
|
|
"warnings": [
|
|
"aphy WARNING: too many tabs, not enough tenderness.",
|
|
"future z SYSTEM NOTICE: do not confuse tired with failing.",
|
|
"sensei chi ALERT: the shortest path may teach the least.",
|
|
"lima SAFE MODE: breathe, eat, answer slowly.",
|
|
"young z MEMORY GLITCH: the floor is lava, but only emotionally.",
|
|
"aphy ERROR 2022: warmth exceeded expected range.",
|
|
"future z WARNING: you will miss ordinary days. Be inside this one.",
|
|
"LAYER INDEX NOTICE: deeper does not mean darker."
|
|
],
|
|
"dreams": [
|
|
"Dream 01: young z opens a lunchbox and finds a tiny homepage inside.",
|
|
"Dream 02: aphy becomes a cursor and points toward a quieter thought.",
|
|
"Dream 03: sensei chi sweeps snow from a URL and says, there, now enter.",
|
|
"Dream 04: lima and z are walking through Oct 2025; every streetlight looks newly engaged.",
|
|
"Dream 05: future z mails back a blank page labelled trust me, you filled it."
|
|
],
|
|
"cassettes": [
|
|
"VOICE NOTE 2022: lima laughs off-mic. z forgets what he was worried about.",
|
|
"VOICE NOTE OCT 2025: A small pause after yes; the whole room becomes future z.",
|
|
"aphy LOG: If love is data, it refuses compression.",
|
|
"young z TAPE: background TV, pencil noise, someone calling him for food.",
|
|
"future z MEMO: age 40, still grateful you kept going."
|
|
],
|
|
"fakeUsers": [
|
|
"aphy: first comment generated with sincere uncertainty",
|
|
"young-z-2009: does this guestbook have games",
|
|
"future_z_40: keep the backup, delete the shame",
|
|
"lima-note: proud of you, even here",
|
|
"sensei chi: the counter counts only what can be counted",
|
|
"z-and-lima-2025: engaged, still learning the dance of ordinary days"
|
|
],
|
|
"seasonal": {
|
|
"winter": "lima put a blanket over the archive.",
|
|
"spring": "young z found the first flower and promoted it to treasure.",
|
|
"summer": "aphy opened an imaginary window; lima reminded it to save before storms.",
|
|
"autumn": "sensei chi says falling leaves are not failure, only timing."
|
|
},
|
|
"homepageTakeovers": [
|
|
"aphy briefly restores an old personal-site mode: visitor counter, awkward table layout, sincere heart.",
|
|
"young z has taken over the homepage with invisible crayon.",
|
|
"future z overlays the dashboard with one sentence: keep what keeps you kind."
|
|
],
|
|
"roomLinks": [
|
|
[
|
|
"/play/lima-note.html",
|
|
"lima note"
|
|
],
|
|
[
|
|
"/play/aphy-console.html",
|
|
"aphy console"
|
|
],
|
|
[
|
|
"/play/sensei-garden.html",
|
|
"sensei garden"
|
|
],
|
|
[
|
|
"/play/young-z-desk.html",
|
|
"young z desk"
|
|
],
|
|
[
|
|
"/play/future-log.html",
|
|
"future log"
|
|
],
|
|
[
|
|
"/play/family-layer-index.html",
|
|
"family layer index"
|
|
],
|
|
[
|
|
"/play/do-not-open.html",
|
|
"do not open"
|
|
],
|
|
[
|
|
"/play/cassette-log.html",
|
|
"voice note log"
|
|
],
|
|
[
|
|
"/play/train-platform.html",
|
|
"time 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"
|
|
]
|
|
]
|
|
};
|
|
|
|
// Exact search text -> toast message.
|
|
const SEARCH_TOASTS = {
|
|
"lima": "Search result: warmth, oct 2025, soon to be home.",
|
|
"aphy": "aphy: present. Mostly helpful. Occasionally poetic by accident.",
|
|
"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.",
|
|
"age 40": "future z: I am tired sometimes, but not defeated."
|
|
};
|
|
|
|
// Exact search text -> hidden page route.
|
|
const SEARCH_ROUTES = {
|
|
"kitchen light": "/play/kitchen-light.html",
|
|
"lima note": "/play/lima-note.html",
|
|
"aphy console": "/play/aphy-console.html",
|
|
"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",
|
|
"time platform": "/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"
|
|
};
|
|
|
|
// Short typed phrases. Stored in sessionStorage as a rolling key chain.
|
|
const KEYBOARD_SECRETS = [
|
|
{
|
|
"phrase": "remember",
|
|
"message": "lima keeps the memory grounded. aphy keeps the backup."
|
|
},
|
|
{
|
|
"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."
|
|
},
|
|
{
|
|
"phrase": "lima",
|
|
"message": "lima note: I am glad you made something this sincere."
|
|
},
|
|
{
|
|
"phrase": "aphy",
|
|
"song": true
|
|
}
|
|
];
|
|
|
|
// Longer typed phrases and character names.
|
|
const LONG_KEYBOARD_SECRETS = [
|
|
{
|
|
"phrase": "layer index",
|
|
"route": "/play/family-layer-index.html"
|
|
},
|
|
{
|
|
"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."
|
|
},
|
|
{
|
|
"phrase": "young z",
|
|
"message": "young z has drawn a door in the margin."
|
|
},
|
|
{
|
|
"phrase": "future z",
|
|
"message": "future z: keep the gentle parts. They compound."
|
|
}
|
|
];
|
|
|
|
|
|
// -----------------------------
|
|
// 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 characterFromText(value) {
|
|
const text = String(value || "").toLowerCase();
|
|
let best = "z";
|
|
let bestScore = 0;
|
|
Object.entries(CHARACTER_REGISTRY).forEach(([id, config]) => {
|
|
let score = text.includes(id) ? 6 : 0;
|
|
(config.aliases || []).forEach((alias) => {
|
|
if (text.includes(String(alias).toLowerCase())) score += 3;
|
|
});
|
|
(config.themes || []).forEach((theme) => {
|
|
if (text.includes(String(theme).toLowerCase())) score += 2;
|
|
});
|
|
(config.motifs || []).forEach((motif) => {
|
|
if (text.includes(String(motif).toLowerCase())) score += 3;
|
|
});
|
|
if (id === "lima" && /warm|home|safe|kitchen|ring|love|rest/.test(text)) score += 3;
|
|
if (id === "aphy" && /system|terminal|logic|error|debug|console|css|search|backup/.test(text)) score += 3;
|
|
if (id === "sensei chi" && /quiet|patience|tea|garden|lesson|wisdom|still/.test(text)) score += 3;
|
|
if (id === "young z" && /play|child|crayon|sun|blanket|game|desk|drawing/.test(text)) score += 3;
|
|
if (id === "future z" && /future|time|age 40|later|return|warning|ordinary/.test(text)) score += 3;
|
|
if (score > bestScore) {
|
|
best = id;
|
|
bestScore = score;
|
|
}
|
|
});
|
|
return best;
|
|
}
|
|
|
|
function avatarExpression(character, text) {
|
|
const value = String(text || "").toLowerCase();
|
|
if (character === "aphy" && /error|terminal|system|diagnostic|debug|console|search/.test(value)) return "thinking";
|
|
if (character === "lima" && /safe|home|warm|protect|care|eat|rest|light/.test(value)) return "happy";
|
|
if (character === "young z" && /draw|crayon|game|play|sun|blanket/.test(value)) return "surprised";
|
|
if (character === "future z" && /time|future|return|ordinary|warning|later/.test(value)) return "nostalgic";
|
|
if (character === "sensei chi" && /quiet|tea|patience|lesson|garden/.test(value)) return "calm";
|
|
return CHARACTER_REGISTRY[character]?.avatar?.mood || "calm";
|
|
}
|
|
|
|
function avatarSvg(character, expression) {
|
|
const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z;
|
|
const avatar = config.avatar || {};
|
|
const accent = avatar.accent || config.territoryColor || "#d3a64d";
|
|
const secondary = avatar.secondary || "#f6dfb4";
|
|
const hair = avatar.hair || "#5d4632";
|
|
const skin = avatar.skin || "#e7bd99";
|
|
const clothes = avatar.clothes || accent;
|
|
const symbol = escapeHtml(config.symbol || "?");
|
|
const shape = avatar.shape || "traveler";
|
|
const eye = {
|
|
happy: `<path d="M12.5 17q1.6-2 3.2 0M21 17q1.6-2 3.2 0" stroke="#3f2d29" stroke-width="1.7" stroke-linecap="round" fill="none"></path>`,
|
|
calm: `<path d="M12.6 17.2h3.1M21.2 17.2h3.1" stroke="#3f2d29" stroke-width="1.7" stroke-linecap="round"></path>`,
|
|
thinking: `<circle cx="14" cy="16.8" r="1.45" fill="#3f2d29"></circle><circle cx="22.6" cy="16.8" r="1.45" fill="#3f2d29"></circle><circle cx="14.45" cy="16.25" r=".45" fill="#fff7e8"></circle><circle cx="23.05" cy="16.25" r=".45" fill="#fff7e8"></circle>`,
|
|
surprised: `<circle cx="14" cy="16.7" r="1.75" fill="#3f2d29"></circle><circle cx="22.4" cy="16.7" r="1.75" fill="#3f2d29"></circle><circle cx="14.5" cy="16.1" r=".5" fill="#fff7e8"></circle><circle cx="22.9" cy="16.1" r=".5" fill="#fff7e8"></circle>`,
|
|
nostalgic: `<path d="M12.6 16.6q1.5 1.1 3.2 0M21.1 16.6q1.5 1.1 3.2 0" stroke="#3f2d29" stroke-width="1.7" stroke-linecap="round" fill="none"></path>`,
|
|
sleepy: `<path d="M12.4 17.3q1.8.9 3.6 0M20.8 17.3q1.8.9 3.6 0" stroke="#3f2d29" stroke-width="1.7" stroke-linecap="round" fill="none"></path>`
|
|
}[expression] || `<circle cx="14" cy="16.8" r="1.45" fill="#3f2d29"></circle><circle cx="22.4" cy="16.8" r="1.45" fill="#3f2d29"></circle>`;
|
|
const mouth = {
|
|
happy: "M15.5 22q2.8 2.3 5.8 0",
|
|
calm: "M16.1 22.1q2.2 1.2 4.5 0",
|
|
thinking: "M16.5 22.3q2 .7 4 0",
|
|
surprised: "M18.4 21.9a1.15 1.15 0 1 0 .1 0",
|
|
nostalgic: "M16.2 22.2q2.4 1.5 4.8 0",
|
|
sleepy: "M16.7 22.2q1.8.8 3.6 0"
|
|
}[expression] || "M16.1 22.1q2.2 1.2 4.5 0";
|
|
const motif = {
|
|
young: `<circle cx="27" cy="9" r="3" fill="${secondary}" opacity=".85"></circle><path d="M27 3v2M27 13v2M21 9h2M31 9h2" stroke="${secondary}" stroke-width="1.2" stroke-linecap="round"></path>`,
|
|
tech: `<path d="M7 11h6M9 9v4M27 9h2v2" stroke="${secondary}" stroke-width="1.4" stroke-linecap="round" opacity=".85"></path>`,
|
|
mentor: `<path d="M26 8c2 1.4 2 4 0 5.2M28 7c3 2.4 3 5.8 0 8" stroke="${secondary}" stroke-width="1.2" stroke-linecap="round" fill="none" opacity=".75"></path>`,
|
|
home: `<path d="M8 13l4-4 4 4M10 13v4h4v-4" stroke="${secondary}" stroke-width="1.2" stroke-linecap="round" fill="none" opacity=".82"></path>`,
|
|
future: `<circle cx="27" cy="9" r="4" fill="none" stroke="${secondary}" stroke-width="1.2" opacity=".78"></circle><path d="M27 9V6.8M27 9l2 1.4" stroke="${secondary}" stroke-width="1.2" stroke-linecap="round"></path>`,
|
|
traveler: `<path d="M28 23l2.8 3.8M27 25l4-1.4" stroke="${secondary}" stroke-width="1.3" stroke-linecap="round" opacity=".78"></path>`
|
|
}[shape] || "";
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" aria-hidden="true" focusable="false"><rect x="2" y="2" width="32" height="32" rx="10" fill="${secondary}" opacity=".34"></rect><circle cx="18" cy="18" r="15" fill="${accent}" opacity=".18"></circle>${motif}<path d="M10 29c1.6-5.1 5-7.4 8.2-7.4S25 23.9 27 29" fill="${clothes}" opacity=".94"></path><path d="M9.5 16.4c.2-6.4 4.1-10.3 8.8-10.3s8.2 3.8 8.4 10.3c-2-2-4.4-3.1-7.2-3.1-3.7 0-6.7 1.1-10 3.1z" fill="${hair}"></path><path d="M10.6 17.8c0-5.3 3.4-8.7 7.7-8.7s7.7 3.4 7.7 8.7c0 5.9-3.3 9.7-7.7 9.7s-7.7-3.8-7.7-9.7z" fill="${skin}"></path><path d="M10.7 16.2c2.4-4.2 6.5-5.3 12.1-4.4 1.1.8 2.2 2.2 3 4.1-3.6-.7-6.5-2.2-8.8-4.1-1.3 2-3.5 3.6-6.3 4.4z" fill="${hair}"></path>${eye}<circle cx="12.4" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><circle cx="24.2" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><path d="${mouth}" stroke="#3f2d29" stroke-width="1.45" stroke-linecap="round" fill="none"></path><text x="28.8" y="31.4" text-anchor="middle" font-size="7" fill="#fff7e8" font-family="monospace">${symbol}</text></svg>`;
|
|
}
|
|
|
|
function avatarHtml(character, text) {
|
|
const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z;
|
|
return `<span class="hidden-avatar" style="--hidden-avatar-color:${config.territoryColor || "#d3a64d"}" title="${escapeHtml(config.displayLabel || character)}">${avatarSvg(character, avatarExpression(character, text))}</span>`;
|
|
}
|
|
|
|
function toast(message, duration, character) {
|
|
let el = document.querySelector(".hidden-toast");
|
|
if (!el) {
|
|
el = document.createElement("div");
|
|
el.className = "hidden-toast";
|
|
el.setAttribute("role", "status");
|
|
document.body.appendChild(el);
|
|
}
|
|
const spirit = character || characterFromText(message);
|
|
el.innerHTML = `${avatarHtml(spirit, message)}<span>${escapeHtml(message)}</span>`;
|
|
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) 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;
|
|
target.textContent = pick(greetings, `greeting-${memory.visits}`);
|
|
const extra = document.createElement("p");
|
|
extra.className = "hidden-greeting";
|
|
extra.textContent = memory.visits > 1 ? "aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies.";
|
|
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 = " ?";
|
|
mark.title = pick(index % 2 ? poems : details, `whisper-${index}`);
|
|
mark.dataset.character = characterFromText(mark.title);
|
|
mark.addEventListener("click", () => {
|
|
awardLayer(index % 2 ? 2 : 1, "paragraph whisper");
|
|
toast(mark.title, undefined, mark.dataset.character);
|
|
});
|
|
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 = "aphy's small diagnostic charm";
|
|
object.textContent = state.visits % 2 ? "*" : "~";
|
|
document.body.appendChild(object);
|
|
let clicks = 0;
|
|
object.addEventListener("click", () => {
|
|
clicks += 1;
|
|
const messages = [
|
|
"aphy: tactile input detected. I do not have skin, but I appreciate the confidence.",
|
|
"young z would absolutely press this again.",
|
|
"lima would ask whether this button has eaten.",
|
|
"sensei chi says repeated action becomes a question.",
|
|
"future z files this under harmless persistence."
|
|
];
|
|
awardLayer(clicks > 3 ? 3 : 1, "corner object");
|
|
toast(messages[Math.min(clicks - 1, messages.length - 1)]);
|
|
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 ? "aphy: logo anomaly sustained. 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 (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("aphy mapped your wandering. lima left the hallway light 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.003) {
|
|
window.setTimeout(() => showDriftNote("sensei chi appears only long enough to say: the page is not empty; it is breathing.", " /\\\n / \\ stillness\n/____\\"), 1800);
|
|
awardLayer(3, "rare sensei appearance");
|
|
} else if (roll < 0.008) {
|
|
window.setTimeout(() => toast("future z SYSTEM WARNING: save your tenderness before closing the day."), 2400);
|
|
awardLayer(4, "future warning");
|
|
}
|
|
}
|
|
|
|
function showDriftNote(text, art) {
|
|
const panel = document.createElement("aside");
|
|
const character = characterFromText(text);
|
|
panel.className = "hidden-drift-note";
|
|
panel.innerHTML = `<div class="hidden-drift-note__head">${avatarHtml(character, text)}<div>${escapeHtml(text)}</div></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(pick([
|
|
"aphy 404: page not found, visitor still valid.",
|
|
"lima note: wrong address, right to rest.",
|
|
"sensei chi: even an absent page teaches direction.",
|
|
"future z: you found nothing, and nothing bad happened."
|
|
], "error"));
|
|
}
|
|
}
|
|
|
|
function addContinuity(memory) {
|
|
const milestones = {
|
|
2: "lima's layer notices a second visit and calls it sweet.",
|
|
5: "aphy creates a folder called regulars, then immediately questions the privacy implications.",
|
|
9: "sensei chi appears in the logs: curiosity has become a path.",
|
|
17: "young z adds a sticker to your invisible visitor card.",
|
|
31: "future z sends a calm note from age 40: returning is one way to heal.",
|
|
64: "Family Layer Index: Layer 5 flickered for one second."
|
|
};
|
|
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([
|
|
"This page recognises the shape of your return.",
|
|
"lima's note here has become easier to find.",
|
|
"future z says repetition can be devotion when it is gentle."
|
|
][[3, 7, 12].indexOf(count)], 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) window.setTimeout(() => toast(lore.seasonal[season], 5600), 2600);
|
|
if (month === 9) window.setTimeout(() => toast("October memory: engagement light lives here all month."), 900);
|
|
if (month === 4 && day === 9) window.setTimeout(() => toast("Site birthday: aphy found candles in the cache."), 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: lima saved a small ordinary blessing from your first day here.", 7000), 1400);
|
|
}
|
|
if (now.getMinutes() === 22) window.setTimeout(() => toast("Minute 22: the 2022 layer glows for one quiet moment."), 2100);
|
|
if (now.getMinutes() === 40) window.setTimeout(() => toast("Minute 40: future z checks in, then lets you keep choosing."), 2100);
|
|
}
|
|
|
|
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", "Family Layer Index objects");
|
|
const objects = [
|
|
["ring", "lima's ring-light memory: Oct 2025, held carefully."],
|
|
["bot", "aphy: object ping received. I am trying to be normal about it."],
|
|
["tea", "sensei chi's cup is plain, warm, and impossible to rush."],
|
|
["crayon", "young z left waxy sunlight on the desk."],
|
|
["clock", "future z says time is not only a thief. Sometimes it returns things."]
|
|
];
|
|
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("Family Layer Index opened. Search for family layer index."), 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 = "Ask aphy for outside weather";
|
|
button.textContent = "window";
|
|
button.addEventListener("click", () => {
|
|
toast("aphy is checking outside. lima is checking whether you need a jumper.");
|
|
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. lima layer: towel by the door.` : `Outside: ${temp}C. aphy forecast: possible comfort, mild nostalgia.`);
|
|
})
|
|
.catch(() => toast("aphy lost the weather packet. sensei chi says indoor weather is enough: quiet, with tea."));
|
|
}, () => toast("No outside weather today. lima's indoor forecast: manageable clouds, warm light."));
|
|
});
|
|
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(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 && !comments.querySelector(".hidden-guestbook-line")) {
|
|
const line = document.createElement("p");
|
|
line.className = "hidden-guestbook-line";
|
|
line.textContent = pick(lore.fakeUsers, "comment-lore");
|
|
comments.appendChild(line);
|
|
}
|
|
}
|
|
|
|
function addSourceRelics() {
|
|
// FAMILY LAYER RELIC: lima grounds, aphy logs, sensei chi waits, young z draws, future z forgives.
|
|
const marker = document.createComment("family-layer-index: 0 surface | 1 jokes | 2 memory | 3 philosophy | 4 time | 5 core");
|
|
document.documentElement.appendChild(marker);
|
|
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
|
|
if (index > 2) return;
|
|
img.alt = pick([
|
|
"A scrapbook image annotated by the lima layer.",
|
|
"young z would ask if this picture can become a game.",
|
|
"aphy alt text: visual memory preserved without spectacle."
|
|
], `alt-${index}`);
|
|
});
|
|
}
|
|
|
|
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(pick([
|
|
"lima's safe space opens only when nobody is rushing.",
|
|
"sensei chi says patience is how care sounds when it is quiet.",
|
|
"future z remembers you waiting here and calls it practice."
|
|
], "idle"), 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");
|
|
window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), "aphy_2004_MODE\nlima_SAFE_SPACE\nFUTURE_Z_OK"), 600);
|
|
} else if (roll < 0.002) {
|
|
window.setTimeout(() => showDriftNote("aphy found a voice note and refused to autoplay it.", "play? y/n\n[consent preserved]"), 1500);
|
|
} else if (roll < 0.006) {
|
|
window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000);
|
|
} else if (roll < 0.014) {
|
|
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 `<li><strong>${escapeHtml(line)}</strong><br><span>local encounters: ${count}</span></li>`;
|
|
}).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")) {
|
|
document.querySelectorAll("[data-cassette]").forEach((button, index) => {
|
|
button.addEventListener("click", () => {
|
|
awardLayer(index >= 2 ? 4 : 2, "voice note");
|
|
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)) {
|
|
target.textContent = seconds === 30 ? "lima's note becomes easier to read." : seconds === 90 ? "sensei chi nods once." : "future z says this quiet was not wasted.";
|
|
}
|
|
}, 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: lima, aphy, sensei, young, future, layer, tea, exit",
|
|
lima: "lima note: make the hidden parts kind enough to live with.",
|
|
aphy: "aphy: cupboard process active. Wisdom module borrowed from sensei chi.",
|
|
sensei: "sensei chi: a cupboard teaches by containing and releasing.",
|
|
young: "young z inventory: crayon sun, blanket cape, important stone.",
|
|
future: "future z: you will still open small doors at 40.",
|
|
layer: familyLayers.join("\n"),
|
|
tea: "lima approves. sensei chi waits. aphy logs steam as temporary cloud.",
|
|
exit: "The cupboard lets you leave with 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] || "aphy: unknown command. sensei chi: not all unknowns are errors."}`;
|
|
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([...lore.quotes, ...lore.journals, ...poems], `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,
|
|
addObservatoryPlayDoorways,
|
|
addPageSpecificSecrets,
|
|
addInvisibleHoverSecrets
|
|
];
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const memory = rememberVisit();
|
|
FEATURES.forEach((addFeature) => addFeature(memory));
|
|
});
|
|
}());
|