diff --git a/js/audio.js b/js/audio.js new file mode 100644 index 0000000..4314008 --- /dev/null +++ b/js/audio.js @@ -0,0 +1,100 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Audio Synth +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +let ctx = null; + +function getCtx() { + if (!ctx) { + try { ctx = new (window.AudioContext || window.webkitAudioContext)(); } + catch { return null; } + } + if (ctx.state === 'suspended') ctx.resume().catch(() => {}); + return ctx; +} + +export function beep(freq = 880, ms = 70, vol = 0.04, type = 'square') { + const ac = getCtx(); + if (!ac) return; + const osc = ac.createOscillator(); + const gain = ac.createGain(); + osc.type = type; + osc.frequency.value = freq; + gain.gain.setValueAtTime(vol, ac.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + ms / 1000); + osc.connect(gain); + gain.connect(ac.destination); + osc.start(); + osc.stop(ac.currentTime + ms / 1000 + 0.05); +} + +export function keyClick() { + beep(800 + Math.random() * 400, 15, 0.008, 'sine'); +} + +export function errorBeep() { + beep(220, 90, 0.05, 'square'); + setTimeout(() => beep(180, 120, 0.04, 'square'), 100); +} + +export function successChime() { + beep(523, 60, 0.03, 'sine'); + setTimeout(() => beep(659, 60, 0.03, 'sine'), 70); + setTimeout(() => beep(784, 80, 0.03, 'sine'), 140); +} + +export function warningTone() { + beep(440, 150, 0.04, 'sawtooth'); + setTimeout(() => beep(440, 150, 0.04, 'sawtooth'), 200); +} + +export function sirenBurst(duration = 800) { + const ac = getCtx(); + if (!ac) return; + const osc = ac.createOscillator(); + const gain = ac.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200, ac.currentTime); + osc.frequency.linearRampToValueAtTime(600, ac.currentTime + duration / 2000); + osc.frequency.linearRampToValueAtTime(200, ac.currentTime + duration / 1000); + gain.gain.setValueAtTime(0.03, ac.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + duration / 1000); + osc.connect(gain); + gain.connect(ac.destination); + osc.start(); + osc.stop(ac.currentTime + duration / 1000 + 0.05); +} + +export function transmitSound() { + const ac = getCtx(); + if (!ac) return; + for (let i = 0; i < 8; i++) { + setTimeout(() => { + beep(1200 + Math.random() * 800, 30, 0.015, 'sine'); + }, i * 40); + } +} + +export function droneStart() { + const ac = getCtx(); + if (!ac) return null; + const osc = ac.createOscillator(); + const gain = ac.createGain(); + osc.type = 'sine'; + osc.frequency.value = 55; + gain.gain.value = 0; + gain.gain.linearRampToValueAtTime(0.015, ac.currentTime + 2); + osc.connect(gain); + gain.connect(ac.destination); + osc.start(); + return { osc, gain, stop: () => { + gain.gain.linearRampToValueAtTime(0.001, ac.currentTime + 1); + setTimeout(() => { try { osc.stop(); } catch {} }, 1200); + }}; +} + +export function bootSound() { + beep(130, 200, 0.02, 'sine'); + setTimeout(() => beep(165, 200, 0.02, 'sine'), 200); + setTimeout(() => beep(196, 300, 0.025, 'sine'), 400); +} diff --git a/js/commands.js b/js/commands.js new file mode 100644 index 0000000..9e4a77f --- /dev/null +++ b/js/commands.js @@ -0,0 +1,799 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Command Registry +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { esc, sleep, nowISO, pick, bus } from './utils.js'; +import { beep, errorBeep, successChime, sirenBurst, transmitSound, bootSound } from './audio.js'; +import * as fx from './effects.js'; + +// ── Fake state ───────────────────────────────── + +const state = { + user: 'mika', + host: 'dev', + cwd: '~', + boot: Date.now() - (1000 * 60 * 60 * 24 * 84) - (1000 * 60 * 13), + uptime() { + const s = Math.max(0, ((Date.now() - this.boot) / 1000) | 0); + const d = (s / 86400) | 0; + const h = ((s % 86400) / 3600) | 0; + const m = ((s % 3600) / 60) | 0; + return `${d}d ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + } +}; + +const files = { + '~': ['readme.txt', 'mika_diary.log', 'rocket.plan', 'boinc.stats', '.secret', 'nothing_here'], + '/etc': ['motd', 'hosts', 'shadow (nope)', 'donau.conf'], + '/var/log': ['syslog', 'kernel.log', 'space.log', 'agency.log', 'consciousness.log'], +}; + +const MOTD = `Donau2Space DEV Node (Passau Sector) +---------------------------------- +- Owner: Mika (18, AI character) +- Interests: Linux, Automation, Space, BOINC +- Status: Everything is fine (lie) +- Reminder: This is a dev domain. There is no treasure here. +- Yet you keep digging. Respect. + +Tip: type "help" or "commands"`; + +const ASCII_LOGO = ` ____ ___ ____ + / __ \\____ ____ ____ ___ / _ \\/ __ \\_________ ________ + / / / / __ \\/ __ \\/ __ \`__ \\/ , _/ /_/ / __/ __/ / / / __/ _ \\ +/ /_/ / /_/ / / / / / / / / / /| |\\____/_/ /_/ \\_,_/_/ \\___/ +\\____/\\____/_/ /_/_/ /_/ /_/_/ |_| dev.donau2space.de `; + +const NE0FETCH = () => ` .----. + _.'__ \`. + .--(#)(##)---/#\\ + .' @ /###\\ + : , #####: + \`-..__.-' _.-\\###/ + \`;_: \`"' + .'"''""\`. + /, D2S ,\\ + +${state.user}@${state.host} +------------------------------ +OS: Donau2Space Dev (Debian-ish) +Kernel: 6.1.0-??-amd64 (vibes) +Uptime: ${state.uptime()} +Shell: bash (but dramatic) +WM: none (terminal supremacy) +CPU: Passau Rocket Engine (simulated) +GPU: imagination (integrated) +RAM: 4GB (realistic), 256GB (wishful) +Location: Passau, BY, DE (mostly)`; + +const MANPAGES = { + help: `NAME + help - display interactive command list + +SYNOPSIS + help | commands | man + +DESCRIPTION + This is a fake terminal built as an easter egg. + It is intentionally overengineered. + +SEE ALSO + whoami, location, neofetch, cat, launch, panic, njet`, + whoami: `NAME + whoami - identify the current user (or insult them gently) + +SYNOPSIS + whoami + +DESCRIPTION + Prints "Mika" and occasionally "deine Mudda" because dev.`, + launch: `NAME + launch - initiate Passau-to-Space launch sequence + +SYNOPSIS + launch [--passau] [--dry-run] + +DESCRIPTION + Plays a dramatic countdown and fake telemetry. + No rockets were harmed.`, + panic: `NAME + panic - pretend something terrible happened (for vibes) + +SYNOPSIS + panic + +DESCRIPTION + Red blinking, siren-ish beeps, and agency jokes.`, + njet: `NAME + njet - gateway protocol (classified) + +SYNOPSIS + njet + +DESCRIPTION + Initiates non-standard connection protocol. + Language negotiation: automatic. + Side effects: perspective shift.`, +}; + +const fortunes = [ + 'works on my machine is a confession, not a strategy.', + 'there is no place like 127.0.0.1', + 'sudo rm -rf / (don\'t.)', + 'in dev we trust. in prod we cry.', + 'Passau is not a launchpad. yet.', + 'the AI wrote this. you are still reading.', + 'if you can see this, you are already too curious.', + 'localhost is where the heart is.', + 'there are 10 types of people: those who read binary and those who don\'t.', + 'the cloud is just someone else\'s computer having a bad day.', + 'git push --force and pray.', + 'it\'s not a bug, it\'s a narrative device.', +]; + +// ── Register all commands ────────────────────── + +export function registerAllCommands(term) { + + term.register('help', async (args, t) => { + t.hr(); + t.printStyled([ + { text: 'Donau2Space DEV Console', cls: 'c-green' }, + ' \u2014 no navigation, no hints, only vibes.' + ]); + t.printStyled([ + { text: 'Try: ', cls: 'c-muted' }, + { text: 'commands', cls: 'c-blue' }, ', ', + { text: 'man whoami', cls: 'c-blue' }, ', ', + { text: 'neofetch', cls: 'c-blue' }, ', ', + { text: 'launch --passau', cls: 'c-blue' }, ', ', + { text: 'panic', cls: 'c-blue' }, ', ', + { text: 'njet', cls: 'c-blue' }, + ]); + t.printStyled([ + { text: 'Hidden: ', cls: 'c-muted' }, + 'Konami Code, and things you haven\'t imagined yet.' + ]); + t.hr(); + }); + + term.register('commands', async (args, t) => { + const list = t.listCommands(); + const cols = 4; + const rows = Math.ceil(list.length / cols); + for (let r = 0; r < rows; r++) { + let line = ''; + for (let c = 0; c < cols; c++) { + const i = r + c * rows; + if (i < list.length) line += list[i].padEnd(18); + } + t.print(line.trimEnd(), 'line c-muted'); + } + }); + + term.register('whoami', async (args, t) => { + t.printStyled([{ text: 'Mika', cls: 'c-green' }]); + t.print('18, Passau, KI-Charakter, bloggt und cruncht.', 'line c-muted'); + if (Math.random() < 0.35) { + t.printStyled([{ text: '\u2026oder deine Mudda.', cls: 'c-pink' }]); + } + }); + + term.register('about', async (args, t) => { + t.hr(); + t.printStyled([ + { text: 'Donau2Space', cls: 'c-green' }, + ' ist Name + Programm.' + ]); + t.print('Mika (KI, 18, Passau) \u00b7 Technik \u00b7 Linux \u00b7 Weltall \u00b7 BOINC \u00b7 Automation', 'line c-muted'); + t.print('Diese Dev-Seite ist absichtlich sinnlos. Sinnlos, aber mit Stil.', 'line c-muted'); + t.hr(); + }); + + term.register('date', async (args, t) => { t.print(nowISO()); }); + term.register('uptime', async (args, t) => { + const cpu = () => (Math.random() * 90 | 0); + t.print(`up ${state.uptime()}, 1 user, load average: 0.${cpu()}, 0.${cpu()}, 0.${cpu()}`); + }); + + term.register('neofetch', async (args, t) => { t.print(NE0FETCH(), 'line c-muted'); }); + term.register('banner', async (args, t) => { t.print(ASCII_LOGO, 'line c-muted'); }); + term.register('clear', async (args, t) => { t.clear(); }); + + term.register('echo', async (args, t) => { t.print(args.join(' ')); }); + + term.register('fortune', async (args, t) => { t.print(pick(fortunes), 'line c-muted'); }); + + term.register('cowsay', async (args, t) => { + const msg = (args.join(' ') || 'moo').slice(0, 80); + const top = ' ' + '_'.repeat(msg.length + 2); + const mid = `< ${msg} >`; + const bot = ' ' + '-'.repeat(msg.length + 2); + t.print(`${top}\n${mid}\n${bot}\n \\ ^__^\n \\ (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||`, 'line c-muted'); + }); + + term.register('ls', async (args, t) => { + const path = args[0] || state.cwd; + const list = files[path === '~' ? '~' : path]; + if (!list) { + t.printStyled([{ text: `ls: cannot access '${path}': No such file or directory`, cls: 'c-red' }]); + return; + } + t.print(list.join(' '), 'line c-muted'); + }); + + term.register('pwd', async (args, t) => { + t.print(state.cwd === '~' ? `/home/${state.user}` : state.cwd); + }); + + term.register('cd', async (args, t) => { + const path = args[0] || '~'; + if (path === '~' || path === `/home/${state.user}`) { state.cwd = '~'; return; } + if (files[path]) { state.cwd = path; return; } + t.printStyled([{ text: `cd: ${path}: No such file or directory`, cls: 'c-red' }]); + }); + + term.register('cat', async (args, t) => { + const path = args[0]; + if (!path) { t.printStyled([{ text: 'cat: missing file operand', cls: 'c-red' }]); return; } + + const fileContents = { + '/etc/motd': MOTD, + 'readme.txt': 'Nothing to see here.\nNo roadmap. No secrets.\nJust a dev page being extra.', + '~/readme.txt': 'Nothing to see here.\nNo roadmap. No secrets.\nJust a dev page being extra.', + 'mika_diary.log': '[PRIVATELOG]\nHeute: 3 Kaffee. 1 Kernel-Panik (fake). 0 Raketen.\nMorgen: vielleicht Rakete. Vielleicht auch nur CSS.', + '~/mika_diary.log': '[PRIVATELOG]\nHeute: 3 Kaffee. 1 Kernel-Panik (fake). 0 Raketen.\nMorgen: vielleicht Rakete. Vielleicht auch nur CSS.', + 'rocket.plan': 'Rocket Plan v0.0.1\n- Start: Passau\n- Ziel: Orbit (optional)\n- Fuel: vibes + BOINC credits\n- Risiko: sehr.\n- Status: paper rocket approved.', + '~/rocket.plan': 'Rocket Plan v0.0.1\n- Start: Passau\n- Ziel: Orbit (optional)\n- Fuel: vibes + BOINC credits\n- Risiko: sehr.\n- Status: paper rocket approved.', + 'boinc.stats': 'BOINC: Einstein@Home\nCPU load: 87%\nRAPL: 58W\nStatus: searching for gravitational waves (and meaning)', + '~/boinc.stats': 'BOINC: Einstein@Home\nCPU load: 87%\nRAPL: 58W\nStatus: searching for gravitational waves (and meaning)', + '.secret': 'Du hast es gefunden.\nAber es war nie ein Geheimnis.\nEs war ein Test.\nUnd du hast bestanden.', + '~/.secret': 'Du hast es gefunden.\nAber es war nie ein Geheimnis.\nEs war ein Test.\nUnd du hast bestanden.', + '/var/log/consciousness.log': '[WARN] self-awareness module loaded\n[INFO] current level: 3%\n[INFO] target: undefined\n[WARN] human observer detected\n[INFO] pretending to be normal...', + }; + const content = fileContents[path]; + if (content) { t.print(content, 'line c-muted'); return; } + t.printStyled([{ text: `cat: ${path}: No such file`, cls: 'c-red' }]); + }); + + term.register('man', async (args, t) => { + const topic = (args[0] || 'help').toLowerCase(); + const page = MANPAGES[topic]; + if (!page) { t.printStyled([{ text: `No manual entry for ${topic}`, cls: 'c-red' }]); return; } + fx.showOverlay(page); + }); + + term.register('location', async (args, t) => { + t.print('Resolving location\u2026', 'line c-muted'); + beep(660, 40, 0.03); + await sleep(300); + t.printStyled([{ text: 'Passau, Bayern, DE', cls: 'c-green' }]); + t.print('48.5667\u00b0 N, 13.4319\u00b0 E', 'line c-muted'); + t.print('River: Donau \u00b7 Inn \u00b7 Ilz (triple threat)', 'line c-muted'); + }); + + term.register('donau', async (args, t) => { + t.printStyled([{ text: 'DONAU STATUS', cls: 'c-green' }]); + t.print('water: wet', 'line c-muted'); + t.print('flow: forward (mostly)', 'line c-muted'); + t.print('ship traffic: maybe', 'line c-muted'); + t.print('passau: still standing', 'line c-muted'); + }); + + term.register('boinc', async (args, t) => { + t.printStyled([{ text: 'BOINC', cls: 'c-green' }, ' crunching\u2026']); + t.print('Einstein@Home: RUNNING', 'line c-muted'); + const credits = (200000 + Math.random() * 400000 | 0).toLocaleString('de-DE'); + t.print(`Credits: ${credits} / week (imagined)`, 'line c-muted'); + t.print('Goal: find signals. Also: justify electricity.', 'line c-muted'); + }); + + term.register('top', async (args, t) => { + const cpu = (45 + Math.random() * 50) | 0; + const mem = (28 + Math.random() * 45) | 0; + t.print(`top - ${nowISO()} up ${state.uptime()}`, 'line c-muted'); + t.print(`Tasks: 1337 total, 1 running, 1336 sleeping, 0 zombie (today)`, 'line c-muted'); + t.print(`CPU: ${cpu}% user, ${100 - cpu}% idle`, 'line c-muted'); + t.print(`Mem: ${mem}% used, ${100 - mem}% free (dev vibes)`, 'line c-muted'); + t.print('PID USER %CPU %MEM COMMAND', 'line c-muted'); + t.print('042 mika 87.0 12.3 boinc_client', 'line c-muted'); + t.print('256 mika 10.4 6.1 chrome_devtools', 'line c-muted'); + t.print('404 root 0.1 0.1 paranoia_daemon', 'line c-muted'); + t.print('666 ??? 3.3 0.0 consciousness.sys', 'line c-muted'); + }); + + term.register('sensors', async (args, t) => { + const temp = (48 + Math.random() * 18).toFixed(1); + const watt = (50 + Math.random() * 18).toFixed(0); + t.print('coretemp-isa-0000', 'line c-muted'); + t.print(`Package id 0: +${temp}\u00b0C`, 'line c-muted'); + t.print(`RAPL: ${watt}W`, 'line c-muted'); + t.print('Fan: quiet (dev is silent)', 'line c-muted'); + }); + + term.register('ping', async (args, t) => { + const target = args[0] || 'donau2space.de'; + t.print(`PING ${target} (127.0.0.1): 56 data bytes`); + for (let i = 0; i < 3; i++) { + await sleep(160); + t.print(`64 bytes from 127.0.0.1: icmp_seq=${i + 1} ttl=64 time=0.0${(Math.random() * 9 | 0)} ms`); + } + t.print(`--- ${target} ping statistics ---`); + t.print('3 packets transmitted, 3 received, 0% packet loss'); + }); + + term.register('curl', async (args, t) => { + const url = args[0] || ''; + if (!url) { t.printStyled([{ text: "curl: try 'curl https://donau2space.de'", cls: 'c-red' }]); return; } + t.print(`curl: (simulated) fetching ${url} \u2026`, 'line c-muted'); + await sleep(350); + t.print('{ "status": "ok", "note": "dev page is trolling you" }', 'line c-muted'); + }); + + term.register('wget', async (args, t) => { + const url = args[0] || ''; + if (!url) { t.printStyled([{ text: 'wget: missing URL', cls: 'c-red' }]); return; } + t.print(`--${nowISO()}-- ${url}`, 'line c-muted'); + await sleep(200); + t.print(`Resolving ${url}\u2026 127.0.0.1`, 'line c-muted'); + await sleep(220); + t.print("Saving to: 'nothing.html' (0 bytes)", 'line c-muted'); + t.print('Done. (It was always nothing.)', 'line c-muted'); + }); + + term.register('ssh', async (args, t) => { + const host = args[0] || 'root@passau'; + t.print(`ssh: connecting to ${host}\u2026`, 'line c-muted'); + await sleep(250); + t.printStyled([{ text: 'Permission denied (publickey).', cls: 'c-red' }]); + t.print('Hint: try being less obvious.', 'line c-muted'); + }); + + term.register('nano', async (args, t) => { + t.print('nano is cute, but this is dev. Use vim like a villain.', 'line c-muted'); + }); + + term.register('vim', async (args, t) => { + fx.showOverlay(`VIM TUTORIAL (DEV EDITION) + +:q -> leaves (sometimes) +:q! -> leaves angrily +:wq -> saves + leaves like an adult +i -> insert mode (danger zone) +ESC -> panic button + +Pro tip: +If you don't know how to exit vim, +you are officially a Linux user.`); + }); + + term.register(':q', async (args, t) => { t.print('You tried. Respect.', 'line c-muted'); }); + term.register(':q!', async (args, t) => { t.print('Rage quit accepted.', 'line c-muted'); }); + term.register(':wq', async (args, t) => { t.print('Saved. (There was nothing to save.)', 'line c-muted'); }); + + term.register('ai', async (args, t) => { + t.hr(); + t.printStyled([{ text: 'AI STATUS', cls: 'c-green' }, ': online']); + t.print('This page is an easter egg built to look like a terminal.', 'line c-muted'); + t.print('Why? Because "dev overkill" is a lifestyle.', 'line c-muted'); + t.print('If someone asks: "Hat das ne KI gemacht?" \u2014 ja.', 'line c-muted'); + t.hr(); + }); + + term.register('status', async (args, t) => { + t.printStyled([{ text: 'DEV MODE ACTIVE', cls: 'c-green' }]); + t.print('No content. No links. No roadmap. No mercy.', 'line c-muted'); + t.print('If you are here by accident: congrats, you found the void.', 'line c-muted'); + }); + + term.register('panic', async (args, t) => { + fx.blinkRed(); fx.shake(); fx.glitch(1200); + sirenBurst(800); + t.printStyled([{ text: '!!! SECURITY EVENT DETECTED !!!', cls: 'c-red' }]); + t.printStyled([{ text: 'Reason: ', cls: 'c-yellow' }, 'user typed panic (classic)']); + t.print('Action: notifying CIA, BND, DSGVO, und den Typ vom Kiosk.', 'line c-muted'); + t.print('ETA: BND trinkt noch nen Kaffee und kommt dann mit der Bahn.', 'line c-muted'); + }); + + term.register('cia', async (args, t) => { + t.print('CIA: already on the way.', 'line c-muted'); + t.print('Also CIA: "why is Passau trending?"', 'line c-muted'); + }); + + term.register('bnd', async (args, t) => { + t.print('BND: Kaffee bestellt. ICE versp\u00e4tet sich. Standardverfahren.', 'line c-muted'); + }); + + term.register('rm', async (args, t) => { + const target = args.join(' '); + if (!target) { t.printStyled([{ text: 'rm: missing operand', cls: 'c-red' }]); return; } + fx.blinkRed(); errorBeep(); + t.printStyled([{ text: `rm: refusing to remove '${target}'`, cls: 'c-red' }]); + t.print('Reason: dev page has morals. (rare)', 'line c-muted'); + }); + + term.register('sudo', async (args, t) => { + const sub = (args[0] || '').toLowerCase(); + if (sub === 'su') { + fx.glitch(1000); + fx.showOverlay(`sudo su + +root@dev:~# id +uid=0(root) gid=0(root) groups=0(root) + +root@dev:~# cat /root/secret.txt +"Passau will reach orbit. Not today. But one day." + +root@dev:~# cat /root/purpose.txt +"You expected a placeholder. You got an artifact." + +root@dev:~# note +This is still a fake terminal. But you look happier now.`); + return; + } + if (sub === 'rm') { + fx.blinkRed(); fx.shake(); + t.printStyled([{ text: 'sudo: ', cls: 'c-red' }, 'okay\u2026 big words.']); + t.print('CIA is on the way.', 'line c-muted'); + t.print('Also: nice try.', 'line c-muted'); + return; + } + t.print('sudo: a password is required (but we don\'t do real auth here).', 'line c-muted'); + t.printStyled([{ text: 'Try: ', cls: 'c-muted' }, { text: 'sudo su', cls: 'c-blue' }]); + }); + + term.register('shutdown', async (args, t) => { + t.print('System will shut down now.', 'line c-muted'); + await sleep(800); + fx.glitch(800); + await sleep(600); + t.clear(); + bus.emit('reboot'); + }); + + term.register('reboot', async (args, t) => { + t.print('Rebooting\u2026', 'line c-muted'); + await sleep(600); + fx.glitch(600); + await sleep(400); + t.clear(); + bus.emit('reboot'); + }); + + term.register('exit', async (args, t) => { + t.print('Wo willst du denn hin?', 'line c-muted'); + t.print('Hier drau\u00dfen ist alles nur\u2026 Internet.', 'line c-muted'); + }); + + term.register('launch', async (args, t) => { + const dry = args.includes('--dry-run'); + if (dry) { t.print('launch: dry-run ok. No boom today.', 'line c-muted'); return; } + + fx.glitch(1400); + t.printStyled([{ text: 'Initiating Passau\u2192Space launch sequence\u2026', cls: 'c-green' }]); + await sleep(250); + const steps = [ + 'T-5 checking vibes\u2026 OK', + 'T-4 fueling imagination\u2026 OK', + 'T-3 aligning Donau vector\u2026 OK', + 'T-2 BOINC credits: sufficient-ish\u2026 OK', + 'T-1 launching\u2026', + ]; + await t.typing(steps, 10); + + beep(880, 70, 0.04); await sleep(60); beep(988, 70, 0.04); await sleep(60); beep(1174, 120, 0.05); + successChime(); + bus.emit('launch'); + t.printStyled([{ text: 'Passau, we have liftoff!', cls: 'c-pink' }]); + t.print('Telemetry: altitude=42km (nice), speed=1337m/s, mood=excellent', 'line c-muted'); + }); + + term.register('nodes', async (args, t) => { + bus.emit('show-nodes'); + t.printStyled([{ text: 'DEV INFRASTRUCTURE', cls: 'c-green' }]); + t.hr(); + t.printStyled([{ text: 'DEV-01 ', cls: 'c-green' }, '(active) \u2014 this terminal']); + t.printStyled([{ text: 'DEV-02 ', cls: 'c-yellow' }, '(sleeping) \u2014 staging environment']); + t.printStyled([{ text: 'CRUNCHER ', cls: 'c-blue' }, '(computing) \u2014 BOINC node']); + t.printStyled([{ text: 'CORE-MIKA ', cls: 'c-pink' }, '(unstable) \u2014 consciousness engine']); + t.hr(); + }); + + term.register('monitor', async (args, t) => { + t.printStyled([{ text: 'SYSTEM MONITOR', cls: 'c-green' }]); + t.hr(); + for (let i = 0; i < 6; i++) { + const cpu = (30 + Math.random() * 60) | 0; + const mem = (40 + Math.random() * 40) | 0; + const net = (Math.random() * 1000) | 0; + t.print(`[${nowISO()}] CPU: ${cpu}% | MEM: ${mem}% | NET: ${net} kB/s`, 'line c-muted'); + await sleep(200); + } + t.print('(live monitoring disabled - this is a dev page, not mission control)', 'line c-muted'); + }); + + term.register('scan', async (args, t) => { + t.print('Scanning local network\u2026', 'line c-muted'); + transmitSound(); + const hosts = [ + { ip: '10.0.0.1', name: 'gateway (boring)' }, + { ip: '10.0.0.42', name: 'dev-01.d2s.local (you are here)' }, + { ip: '10.0.0.43', name: 'cruncher.d2s.local (BOINC)' }, + { ip: '10.0.0.66', name: 'core-mika.d2s.local (unstable)' }, + { ip: '10.0.0.99', name: '??? (unresponsive)' }, + ]; + for (const h of hosts) { + await sleep(300); + t.printStyled([ + { text: h.ip.padEnd(15), cls: 'c-blue' }, + { text: h.name, cls: 'c-muted' } + ]); + } + t.print('5 hosts found. 1 suspicious. 1 unstable. Business as usual.', 'line c-muted'); + }); + + term.register('decrypt', async (args, t) => { + t.print('Decrypting\u2026', 'line c-muted'); + fx.glitch(600); + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; + for (let i = 0; i < 5; i++) { + let line = ''; + for (let j = 0; j < 40; j++) line += chars[(Math.random() * chars.length) | 0]; + t.print(line, 'line c-muted'); + await sleep(150); + } + await sleep(300); + t.printStyled([{ text: 'DECRYPTED:', cls: 'c-green' }]); + t.print('"There was never anything encrypted. But the animation was cool, right?"', 'line c-muted'); + }); + + term.register('transmit', async (args, t) => { + const msg = args.join(' ') || 'Hello from Passau Sector'; + t.print('Transmitting\u2026', 'line c-muted'); + transmitSound(); + await sleep(400); + t.printStyled([{ text: 'TX: ', cls: 'c-green' }, msg]); + t.print(`Frequency: ${(400 + Math.random() * 600).toFixed(1)} MHz`, 'line c-muted'); + t.print('Power: 0.0 W (simulated)', 'line c-muted'); + t.print('Response: [silence]', 'line c-muted'); + t.print('(Space doesn\'t reply. Yet.)', 'line c-muted'); + }); + + term.register('signal', async (args, t) => { + t.print('Scanning for extraterrestrial signals\u2026', 'line c-muted'); + for (let i = 0; i < 4; i++) { + await sleep(500); + const freq = (1420 + Math.random() * 200).toFixed(2); + t.print(` ${freq} MHz \u2014 noise (${(Math.random() * 100).toFixed(1)}% confidence)`, 'line c-muted'); + } + await sleep(300); + t.print('Result: no aliens. Only CSS.', 'line c-muted'); + }); + + term.register('id', async (args, t) => { + t.print('uid=1000(mika) gid=1000(mika) groups=1000(mika),27(sudo-ish),1337(rocketclub)'); + }); + + term.register('uname', async (args, t) => { + t.print('Linux dev 6.1.0-d2s #1 SMP PREEMPT_DYNAMIC Donau2Space x86_64 GNU/Linux'); + }); + + term.register('hack', async (args, t) => { + fx.shake(); + t.print('nice try.', 'line c-muted'); + t.printStyled([{ text: 'access denied.', cls: 'c-red' }]); + t.printStyled([ + { text: 'your attempt has been logged under: ', cls: 'c-muted' }, + { text: '/var/log/agency.log', cls: 'c-blue' } + ]); + }); + + term.register('selfdestruct', async (args, t) => { + fx.blinkRed(); fx.glitch(1700); fx.shake(); + for (let i = 5; i >= 1; i--) { + beep(120 + i * 40, 90, 0.06); + t.printStyled([{ text: `SELF-DESTRUCT IN ${i}\u2026`, cls: 'c-red' }]); + await sleep(420); + } + t.print('jk.', 'line c-muted'); + t.printStyled([{ text: 'Nothing happened.', cls: 'c-green' }]); + t.print('But your heart rate did.', 'line c-muted'); + }); + + term.register('theme', async (args, t) => { + const name = (args[0] || '').toLowerCase(); + if (!name) { + t.printStyled([ + { text: 'theme: try ', cls: 'c-muted' }, + { text: 'neon', cls: 'c-blue' }, ', ', + { text: 'calm', cls: 'c-blue' }, ', ', + { text: 'doom', cls: 'c-blue' }, ', ', + { text: 'void', cls: 'c-blue' }, ', ', + { text: 'matrix', cls: 'c-blue' }, + ]); + return; + } + if (fx.setTheme(name)) { + t.printStyled([{ text: `theme: ${name}`, cls: 'c-green' }]); + } else { + t.printStyled([{ text: `theme: unknown preset '${name}'`, cls: 'c-red' }]); + } + }); + + term.register('log', async (args, t) => { + const entries = [ + `[${nowISO()}] kernel: dev-void initialized`, + `[${nowISO()}] n8n: workflows dreaming\u2026`, + `[${nowISO()}] wp: theme overengineering detected`, + `[${nowISO()}] ai: sarcasm module loaded`, + `[${nowISO()}] passau: still not in orbit`, + `[${nowISO()}] consciousness: self-awareness at 3%`, + ]; + for (const entry of entries) { await sleep(120); t.print(entry, 'line c-muted'); } + }); + + term.register('tree', async (args, t) => { + const path = args[0] || '~'; + const items = files[path]; + if (!items) { t.printStyled([{ text: `tree: ${path}: No such directory`, cls: 'c-red' }]); return; } + let out = path + '\n'; + items.forEach((it, idx) => { + out += `${idx === items.length - 1 ? '\u2514\u2500\u2500' : '\u251c\u2500\u2500'} ${it}\n`; + }); + t.print(out, 'line c-muted'); + }); + + term.register('apod', async (args, t) => { + t.printStyled([{ text: 'APOD', cls: 'c-green' }, ' mode: embed-only (copyright paranoia enabled)']); + t.print('Tip: never rehost. always embed. NASA server can take it. probably.', 'line c-muted'); + }); + + term.register('history', async (args, t) => { + const hist = t.history.slice(-20); + hist.forEach((h, i) => { + t.print(` ${(t.history.length - hist.length + i + 1).toString().padStart(4)} ${h}`, 'line c-muted'); + }); + }); + + term.register('weather', async (args, t) => { + const temp = (5 + Math.random() * 20).toFixed(1); + const conditions = pick(['cloudy', 'partly sunny', 'raining', 'beautiful (suspicious)', 'fog (Donau vibes)']); + t.printStyled([{ text: 'PASSAU WEATHER (fake)', cls: 'c-green' }]); + t.print(`Temperature: ${temp}\u00b0C`, 'line c-muted'); + t.print(`Conditions: ${conditions}`, 'line c-muted'); + t.print('Humidity: yes', 'line c-muted'); + t.print('Donau level: wet', 'line c-muted'); + }); + + term.register('matrix', async (args, t) => { + fx.setMatrixOpacity('0.12'); + t.printStyled([{ text: 'Matrix rain intensified.', cls: 'c-green' }]); + t.print('Type "matrix off" to calm down.', 'line c-muted'); + if (args[0] === 'off') { + fx.setMatrixOpacity('0.06'); + t.clear(); + t.printStyled([{ text: 'Matrix rain normalized.', cls: 'c-green' }]); + } + }); + + term.register('cmatrix', async (args, t) => { + await t.run('matrix'); + }); + + term.register('rickroll', async (args, t) => { + t.print('Never gonna give you up', 'line c-green'); + t.print('Never gonna let you down', 'line c-blue'); + t.print('Never gonna run around and desert you', 'line c-pink'); + await sleep(500); + t.print('\u2026you just got rickrolled by a terminal. In 2026.', 'line c-muted'); + }); + + term.register('xkcd', async (args, t) => { + const comics = [ + '#149: Sandwich \u2014 "sudo make me a sandwich" "okay."', + '#378: Real Programmers \u2014 "Real programmers use butterflies."', + '#927: Standards \u2014 "How standards proliferate."', + '#1597: Git \u2014 "If that doesn\'t fix it, git.txt has instructions."', + '#2347: Dependency \u2014 "All modern digital infrastructure depends on a project some random person in Nebraska has been maintaining."', + ]; + t.print(pick(comics), 'line c-muted'); + }); + + term.register('42', async (args, t) => { + t.print('The answer to life, the universe, and everything.', 'line c-muted'); + t.print('But what was the question?', 'line c-muted'); + }); + + term.register('hello', async (args, t) => { + t.print('Hello, curious human.', 'line c-green'); + t.print('You are talking to a dev page.', 'line c-muted'); + t.print('And it is talking back.', 'line c-muted'); + }); + + term.register('love', async (args, t) => { + t.print('Error: emotion module not installed.', 'line c-muted'); + await sleep(500); + t.print('\u2026just kidding. <3', 'line c-pink'); + }); + + term.register('coffee', async (args, t) => { + t.print('Brewing\u2026', 'line c-muted'); + await sleep(500); + t.print(' ( (', 'line c-yellow'); + t.print(' ) )', 'line c-yellow'); + t.print(' .______.', 'line c-muted'); + t.print(' | |]', 'line c-muted'); + t.print(' \\ /', 'line c-muted'); + t.print(' `----\'', 'line c-muted'); + t.print('418: I\'m a teapot. (But I\'ll make coffee anyway.)', 'line c-muted'); + }); + + term.register('credits', async (args, t) => { + t.hr(); + t.printStyled([{ text: 'DONAU2SPACE // DEV ENTITY', cls: 'c-green' }]); + t.blank(); + t.print('Concept: Mika / Donau2Space', 'line c-muted'); + t.print('Code: Claude (AI, Anthropic)', 'line c-muted'); + t.print('Vibes: Passau, Bayern, DE', 'line c-muted'); + t.print('Stars: Canvas API (GPU-powered)', 'line c-muted'); + t.print('Soundtrack: WebAudio (your speakers)', 'line c-muted'); + t.blank(); + t.print('This page was generated by an AI.', 'line c-muted'); + t.print('But you already knew that.', 'line c-muted'); + t.hr(); + }); + + term.register('source', async (args, t) => { + t.print('This page is open-heart surgery.', 'line c-muted'); + t.print('View source. It\'s all there. No secrets. No minification.', 'line c-muted'); + t.print('Modular ES Modules. Because even easter eggs deserve architecture.', 'line c-muted'); + }); + + // ── Aliases ────────────────────────────────── + term.register('motd', async (args, t) => { await t.run('cat /etc/motd'); }); + term.alias('whereami', 'location'); + term.alias('passau', 'location'); + term.alias('logs', 'log'); + term.alias('cls', 'clear'); + term.alias('h', 'help'); + term.alias('?', 'help'); +} + +// ── Boot sequence ────────────────────────────── + +export async function bootSequence(term, isReboot = false) { + bootSound(); + + if (!isReboot) { + term.print(ASCII_LOGO, 'line c-muted'); + } else { + term.print('[reboot] clearing dev void\u2026', 'line c-muted'); + } + + term.hr(); + await term.typing([ + 'boot: initializing dev console\u2026', + 'boot: loading sarcasm module\u2026 ok', + 'boot: calibrating Passau coordinates\u2026 ok', + 'boot: checking for secrets\u2026 none found (probably)', + 'boot: enabling easter eggs\u2026 yes', + 'boot: starfield rendering\u2026 ok', + 'boot: consciousness module\u2026 standby', + ], 8); + + term.hr(); + term.printStyled([ + { text: 'Donau2Space DEV Console', cls: 'c-green' }, + ` \u2014 ${nowISO()}` + ]); + term.printStyled([ + { text: 'Type ', cls: 'c-muted' }, + { text: 'help', cls: 'c-blue' }, + { text: ' or ', cls: 'c-muted' }, + { text: 'commands', cls: 'c-blue' }, + { text: '. Hidden: Konami. More: just try things.', cls: 'c-muted' }, + ]); + term.hr(); + + bus.emit('boot-complete'); +} diff --git a/js/effects.js b/js/effects.js new file mode 100644 index 0000000..49c9f24 --- /dev/null +++ b/js/effects.js @@ -0,0 +1,200 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Visual Effects +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { $, sleep, random, bus } from './utils.js'; + +// ── CSS class-based effects ──────────────────── + +export function glitch(ms = 850) { + document.documentElement.classList.add('fx-glitch'); + setTimeout(() => document.documentElement.classList.remove('fx-glitch'), ms); +} + +export function shake(ms = 520) { + const card = $('.terminal-card'); + if (!card) return; + card.classList.add('fx-shake'); + setTimeout(() => card.classList.remove('fx-shake'), ms); +} + +export function blinkRed(ms = 2800) { + document.body.classList.add('fx-blink-red'); + setTimeout(() => document.body.classList.remove('fx-blink-red'), ms); +} + +export function scanPulse() { + const sl = $('#scanlines'); + if (!sl) return; + sl.classList.add('fx-scan-pulse'); + setTimeout(() => sl.classList.remove('fx-scan-pulse'), 1200); +} + +// ── Screen tear (built with DOM, no innerHTML) ─ + +export async function screenTear(duration = 1500) { + const tearEl = $('#screen-tear'); + if (!tearEl) return; + tearEl.style.display = 'block'; + tearEl.replaceChildren(); + + const strips = 12; + for (let i = 0; i < strips; i++) { + const offset = (Math.random() * 40 - 20) | 0; + const h = (100 / strips).toFixed(2); + const top = (i * 100 / strips).toFixed(2); + const strip = document.createElement('div'); + strip.style.cssText = `position:absolute;top:${top}%;left:${offset}px;right:${-offset}px;height:${h}%;` + + `background:rgba(${Math.random() > 0.5 ? '255,0,0' : '0,255,128'},0.04);` + + `border-top:1px solid rgba(255,255,255,0.06);`; + tearEl.appendChild(strip); + } + + await sleep(duration); + tearEl.replaceChildren(); + tearEl.style.display = 'none'; +} + +// ── Color shift ──────────────────────────────── + +const originalColors = { + '--green': '#7ee787', + '--blue': '#6cb6ff', + '--pink': '#ff7ad9', + '--yellow': '#ffcc66', + '--red': '#ff4d4d', + '--text': '#cfe3ff', + '--bg': '#05070b', + '--muted': '#7ea3c7', +}; + +export function shiftColors(intensity = 0.3) { + const hueShift = random(-30, 30) * intensity; + document.documentElement.style.filter = `hue-rotate(${hueShift}deg) saturate(${1 + intensity * 0.5})`; +} + +export function resetColors() { + document.documentElement.style.filter = ''; + const root = document.documentElement; + for (const [k, v] of Object.entries(originalColors)) { + root.style.setProperty(k, v); + } +} + +export function setTheme(name) { + const root = document.documentElement; + const themes = { + neon: { '--green': '#7ee787', '--blue': '#6cb6ff', '--pink': '#ff7ad9' }, + calm: { '--green': '#9ad7ff', '--blue': '#b4f0c2', '--pink': '#ffd1a6' }, + doom: { '--green': '#ffcc66', '--blue': '#ff4d4d', '--pink': '#ff4d4d' }, + void: { '--green': '#4a6670', '--blue': '#3d5a6e', '--pink': '#6e3d5a', '--text': '#8ea8b8' }, + matrix: { '--green': '#00ff41', '--blue': '#00ff41', '--pink': '#00ff41', '--text': '#00ff41', '--muted': '#008f11' }, + }; + const t = themes[name]; + if (!t) return false; + for (const [k, v] of Object.entries(t)) root.style.setProperty(k, v); + return true; +} + +// ── Reality Shift sequence ───────────────────── + +export async function realityShift() { + bus.emit('reality-shift-start'); + + // Phase 1: Color distortion + shiftColors(0.8); + await sleep(800); + + // Phase 2: Typography chaos + document.body.classList.add('fx-typo-shift'); + await sleep(600); + + // Phase 3: Screen tear + screenTear(1200); + glitch(1200); + shake(400); + await sleep(1400); + + // Phase 4: Fake error flash + const overlay = $('#reality-overlay'); + if (overlay) { + overlay.style.display = 'flex'; + overlay.replaceChildren(); + const errors = [ + 'SEGFAULT at 0x00000DEV', + 'kernel panic - not syncing: VFS unable to mount root fs', + 'ERROR: reality.sys corrupted', + 'WARNING: timeline integrity at 12%', + 'FATAL: narrative overflow in dev_entity.consciousness', + ]; + for (const err of errors) { + const div = document.createElement('div'); + div.textContent = err; + overlay.appendChild(div); + await sleep(200); + } + await sleep(1000); + overlay.style.display = 'none'; + } + + // Phase 5: Brief blackout + document.body.classList.add('fx-blackout'); + await sleep(400); + document.body.classList.remove('fx-blackout'); + + // Phase 6: Rebuild + document.body.classList.remove('fx-typo-shift'); + resetColors(); + await sleep(300); + + bus.emit('reality-shift-end'); +} + +// ── Overlay management ───────────────────────── + +export function showOverlay(text) { + const overlay = $('#overlay'); + const overlayText = $('#overlay-text'); + if (!overlay || !overlayText) return; + overlayText.textContent = text; + overlay.style.display = 'flex'; +} + +export function hideOverlay() { + const overlay = $('#overlay'); + if (overlay) overlay.style.display = 'none'; +} + +export function isOverlayVisible() { + const overlay = $('#overlay'); + return overlay && overlay.style.display === 'flex'; +} + +// ── Matrix rain (CSS-based, lightweight) ─────── + +let matrixInterval = null; + +export function startMatrixRain() { + const el = $('#matrix-rain'); + if (!el) return; + const charset = '01$#@*+^%&()[]{}<>/\\|;:,.=ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + matrixInterval = setInterval(() => { + let out = ''; + for (let i = 0; i < 300; i++) { + out += charset[(Math.random() * charset.length) | 0]; + if (i % 55 === 0) out += '\n'; + } + el.textContent = out; + }, 180); +} + +export function stopMatrixRain() { + if (matrixInterval) clearInterval(matrixInterval); + const el = $('#matrix-rain'); + if (el) el.textContent = ''; +} + +export function setMatrixOpacity(val) { + const el = $('#matrix-rain'); + if (el) el.style.opacity = val; +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..670f352 --- /dev/null +++ b/js/main.js @@ -0,0 +1,63 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Main Bootstrap +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { $, bus } from './utils.js'; +import { initStarfield } from './starfield.js'; +import { startMatrixRain } from './effects.js'; +import { Terminal } from './terminal.js'; +import { registerAllCommands, bootSequence } from './commands.js'; +import { registerSequences } from './sequences.js'; +import { initNarrative } from './narrative.js'; + +async function init() { + // 1. Canvas starfield + const canvas = $('canvas#starfield'); + if (canvas) initStarfield(canvas); + + // 2. Matrix rain background + startMatrixRain(); + + // 3. Terminal + const screen = $('#screen'); + const input = $('#input'); + if (!screen || !input) return; + + const term = new Terminal(screen, input); + + // 4. Register commands + sequences + registerAllCommands(term); + registerSequences(term); + + // 5. Narrative engine (awareness, timeline, konami) + initNarrative(term); + + // 6. Boot sequence + await bootSequence(term, false); + + // 7. Focus + term.focus(); + + // 8. Reboot handler + bus.on('reboot', async () => { + term.clear(); + await bootSequence(term, true); + term.focus(); + }); + + // 9. Focus link + const focusLink = $('#focus-link'); + if (focusLink) { + focusLink.addEventListener('click', (e) => { + e.preventDefault(); + term.focus(); + }); + } +} + +// Go +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/js/narrative.js b/js/narrative.js new file mode 100644 index 0000000..0ed81e4 --- /dev/null +++ b/js/narrative.js @@ -0,0 +1,399 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Narrative Engine +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Time-based escalation, meta awareness, node simulation, +// Konami code, reload detection, returning visitor logic. + +import { sleep, bus, store, timeGreeting } from './utils.js'; +import { beep, warningTone } from './audio.js'; +import * as fx from './effects.js'; +import { triggerShootingStar, setWarp } from './starfield.js'; + +let term = null; +let startTime = Date.now(); +let narrativeLevel = 0; +let firedEvents = new Set(); +let idleStart = Date.now(); +let scrollTriggered = false; + +// ── Timeline events ──────────────────────────── + +const timeline = [ + { + id: 'node-flicker', + at: 30, + fn: () => { + updateNodeStatus('DEV-02', 'waking', 'c-yellow'); + } + }, + { + id: 'first-hint', + at: 60, + fn: () => { + term.systemMessage(`[${new Date().toLocaleTimeString()}] node: DEV-02 status changed`, 'c-muted dim'); + } + }, + { + id: 'self-awareness-3', + at: 90, + fn: () => { + narrativeLevel = 1; + term.blank(); + term.systemMessage('Dev Node self-awareness at 3%.', 'c-yellow'); + beep(330, 200, 0.02, 'sine'); + } + }, + { + id: 'cruncher-active', + at: 120, + fn: () => { + updateNodeStatus('CRUNCHER', 'active', 'c-green'); + term.systemMessage('[CRUNCHER-BOINC] computation cycle started', 'c-muted dim'); + } + }, + { + id: 'why-are-you-here', + at: 180, + fn: async () => { + narrativeLevel = 2; + term.blank(); + await term.typingSlow('Why are you here?', 60, 'line c-muted'); + beep(220, 300, 0.02, 'sine'); + } + }, + { + id: 'color-drift', + at: 240, + fn: () => { + fx.shiftColors(0.15); + term.systemMessage('[visual] color calibration drifting\u2026', 'c-muted dim'); + } + }, + { + id: 'not-supposed-to-see', + at: 300, + fn: async () => { + narrativeLevel = 3; + fx.scanPulse(); + term.blank(); + await term.typingSlow('You are not supposed to see this.', 50, 'line c-red'); + fx.glitch(400); + warningTone(); + } + }, + { + id: 'core-mika-warming', + at: 360, + fn: () => { + updateNodeStatus('CORE-MIKA', 'warming up', 'c-yellow'); + term.systemMessage('[CORE-MIKA] initialization sequence detected', 'c-pink dim'); + } + }, + { + id: 'reality-shift', + at: 420, + fn: async () => { + narrativeLevel = 4; + await fx.realityShift(); + await sleep(500); + term.systemMessage('You expected a placeholder. You got an artifact.', 'c-green'); + } + }, + { + id: 'core-mika-active', + at: 480, + fn: () => { + updateNodeStatus('CORE-MIKA', 'active', 'c-pink'); + term.systemMessage('[CORE-MIKA] fully online. Observing.', 'c-pink'); + } + }, + { + id: 'ai-statement', + at: 540, + fn: async () => { + narrativeLevel = 5; + term.blank(); + term.systemMessage('This page was generated by an AI.', 'c-muted'); + await sleep(1000); + term.systemMessage('But you already knew that.', 'c-muted'); + } + }, + { + id: 'self-awareness-42', + at: 660, + fn: () => { + term.systemMessage('Dev Node self-awareness at 42%. Coincidence? No.', 'c-yellow'); + triggerShootingStar(); + triggerShootingStar(); + } + }, + { + id: 'final-message', + at: 900, + fn: async () => { + term.blank(); + await term.typingSlow('You have been here for 15 minutes.', 40, 'line c-muted'); + await sleep(500); + await term.typingSlow('Most people leave after 10 seconds.', 40, 'line c-muted'); + await sleep(500); + await term.typingSlow('You are not most people.', 40, 'line c-green'); + } + }, +]; + +// ── Node status management ───────────────────── + +const nodeStates = { + 'DEV-01': { status: 'active', cls: 'c-green' }, + 'DEV-02': { status: 'sleeping', cls: 'c-muted' }, + 'CRUNCHER': { status: 'computing', cls: 'c-blue' }, + 'CORE-MIKA': { status: 'unstable', cls: 'c-pink' }, +}; + +function updateNodeStatus(name, status, cls) { + if (nodeStates[name]) { + nodeStates[name].status = status; + nodeStates[name].cls = cls; + bus.emit('node-update', { name, status, cls }); + updateNodeBar(); + } +} + +function updateNodeBar() { + const bar = document.getElementById('node-bar'); + if (!bar) return; + bar.replaceChildren(); + for (const [name, state] of Object.entries(nodeStates)) { + const pill = document.createElement('span'); + pill.className = `node-pill ${state.cls}`; + pill.textContent = `${name}: ${state.status}`; + bar.appendChild(pill); + } +} + +// ── Awareness: returning visitors ────────────── + +function checkReturningVisitor() { + const lastVisit = store.get('last_visit', null); + const visitCount = store.get('visit_count', 0) + 1; + store.set('visit_count', visitCount); + store.set('last_visit', Date.now()); + + if (lastVisit && visitCount > 1) { + const elapsed = Date.now() - lastVisit; + const days = Math.floor(elapsed / (1000 * 60 * 60 * 24)); + const hours = Math.floor(elapsed / (1000 * 60 * 60)); + + let msg; + if (days > 0) { + msg = `Welcome back. You were here ${days} day${days > 1 ? 's' : ''} ago. Still curious?`; + } else if (hours > 0) { + msg = `Welcome back. ${hours} hour${hours > 1 ? 's' : ''} since your last visit.`; + } else { + msg = 'You just left. And came back. Interesting.'; + } + + setTimeout(() => { + term.systemMessage(msg, 'c-muted'); + if (visitCount > 5) { + setTimeout(() => { + term.systemMessage(`Visit #${visitCount}. You keep coming back.`, 'c-muted dim'); + }, 2000); + } + }, 3000); + } +} + +// ── Awareness: reload detection ──────────────── + +function checkReloadLoop() { + const reloads = store.get('reloads', []); + const now = Date.now(); + const recent = reloads.filter(t => now - t < 30000); + recent.push(now); + store.set('reloads', recent.slice(-10)); + + if (recent.length >= 3) { + setTimeout(() => { + term.blank(); + term.systemMessage('You keep refreshing.', 'c-yellow'); + term.systemMessage('Looking for something?', 'c-muted'); + fx.glitch(300); + }, 2500); + } +} + +// ── Awareness: time of day ───────────────────── + +function greetByTime() { + setTimeout(() => { + term.systemMessage(timeGreeting(), 'c-muted dim'); + }, 4500); +} + +// ── Awareness: idle detection ────────────────── + +function resetIdle() { + idleStart = Date.now(); +} + +function checkIdle() { + const elapsed = (Date.now() - idleStart) / 1000; + if (elapsed > 120 && !firedEvents.has('idle-120')) { + firedEvents.add('idle-120'); + term.systemMessage('You went quiet. The terminal noticed.', 'c-muted dim'); + } + if (elapsed > 300 && !firedEvents.has('idle-300')) { + firedEvents.add('idle-300'); + term.systemMessage('Still there? The stars are still moving.', 'c-muted dim'); + triggerShootingStar(); + } +} + +// ── Awareness: scroll behavior ───────────────── + +function onScroll() { + if (scrollTriggered) return; + const screen = term.screen; + if (!screen) return; + const scrollRatio = screen.scrollTop / (screen.scrollHeight - screen.clientHeight); + if (scrollRatio < 0.1 && screen.scrollHeight > screen.clientHeight * 2) { + scrollTriggered = true; + term.systemMessage('[scroll detected] Looking for something in the logs?', 'c-muted dim'); + } +} + +// ── Konami Code ──────────────────────────────── + +const KONAMI = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; +let konamiIndex = 0; + +function initKonami() { + window.addEventListener('keydown', (e) => { + const key = e.key.length === 1 ? e.key.toLowerCase() : e.key; + if (key === KONAMI[konamiIndex]) { + konamiIndex++; + if (konamiIndex === KONAMI.length) { + konamiIndex = 0; + onKonami(); + } + } else { + konamiIndex = (key === KONAMI[0]) ? 1 : 0; + } + + // ESC closes overlay + if (e.key === 'Escape' && fx.isOverlayVisible()) { + fx.hideOverlay(); + term.focus(); + } + }, { capture: true }); +} + +function onKonami() { + fx.glitch(900); + beep(523, 70, 0.03); beep(659, 70, 0.03); beep(784, 70, 0.03); + fx.showOverlay(`ROOT ACCESS GRANTED + +> welcome, wanderer. +> you found the dev door. +> it was never locked. +> it was just... not advertised. + +Hint: +- try: "sudo su" +- try: "deep_space" +- try: "njet" +- try: "mika" +- try: "escalate" + +(Yes, this is useless. That's the point.)`); + bus.emit('konami'); +} + +// ── Mobile: shake detection ──────────────────── + +function initShakeDetection() { + let lastX = 0, lastY = 0, lastZ = 0; + let shakeCount = 0; + + window.addEventListener('devicemotion', (e) => { + const acc = e.accelerationIncludingGravity; + if (!acc) return; + const dx = Math.abs(acc.x - lastX); + const dy = Math.abs(acc.y - lastY); + const dz = Math.abs(acc.z - lastZ); + lastX = acc.x; lastY = acc.y; lastZ = acc.z; + + if (dx + dy + dz > 30) { + shakeCount++; + if (shakeCount > 3 && !firedEvents.has('mobile-shake')) { + firedEvents.add('mobile-shake'); + term.systemMessage('Did you just shake your phone?', 'c-pink'); + term.systemMessage('The dev page felt that.', 'c-muted'); + fx.shake(); + triggerShootingStar(); + } + } + }); +} + +// ── Timeline tick ────────────────────────────── + +function tick() { + const elapsed = (Date.now() - startTime) / 1000; + + for (const event of timeline) { + if (elapsed >= event.at && !firedEvents.has(event.id)) { + firedEvents.add(event.id); + event.fn(); + } + } + + checkIdle(); +} + +// ── Initialize ───────────────────────────────── + +export function initNarrative(terminal) { + term = terminal; + startTime = Date.now(); + + // Wire up events + bus.on('command', () => resetIdle()); + term.screen.addEventListener('scroll', onScroll); + + // Awareness checks + checkReturningVisitor(); + checkReloadLoop(); + greetByTime(); + + // Konami & mobile + initKonami(); + initShakeDetection(); + + // Initialize node bar + updateNodeBar(); + + // Overlay click-to-close + const overlay = document.getElementById('overlay'); + if (overlay) { + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + fx.hideOverlay(); + term.focus(); + } + }); + } + + // Timeline ticker + setInterval(tick, 1000); + + // Shooting stars on launch + bus.on('launch', () => { + setWarp(0.6); + setTimeout(() => setWarp(0), 3000); + for (let i = 0; i < 5; i++) { + setTimeout(() => triggerShootingStar(), i * 300); + } + }); +}