// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 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); } }); }