donau2space-dev/js/narrative.js

399 lines
11 KiB
JavaScript

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