From 10fdc3aea1540f27861d8a9f8a79eaf613f367d3 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 16 Feb 2026 16:50:04 +0000 Subject: [PATCH] =?UTF-8?q?Dateien=20nach=20=E2=80=9Ejs=E2=80=9C=20hochlad?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/sequences.js | 331 ++++++++++++++++++++++++++++++++++++++++++++++++ js/starfield.js | 221 ++++++++++++++++++++++++++++++++ js/terminal.js | 298 +++++++++++++++++++++++++++++++++++++++++++ js/utils.js | 84 ++++++++++++ 4 files changed, 934 insertions(+) create mode 100644 js/sequences.js create mode 100644 js/starfield.js create mode 100644 js/terminal.js create mode 100644 js/utils.js diff --git a/js/sequences.js b/js/sequences.js new file mode 100644 index 0000000..08ff5b3 --- /dev/null +++ b/js/sequences.js @@ -0,0 +1,331 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Special Sequences +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { sleep, bus } from './utils.js'; +import { beep, transmitSound, sirenBurst, droneStart, successChime } from './audio.js'; +import * as fx from './effects.js'; +import { setWarp, triggerShootingStar } from './starfield.js'; + +// ── SEQUENCE: Mandarin Gateway (trigger: "njet") ── + +async function mandarinGateway(args, term) { + term.locked = true; + + term.printStyled([{ text: 'Connection rerouting\u2026', cls: 'c-yellow' }]); + beep(330, 200, 0.03, 'sine'); + await sleep(600); + + fx.glitch(400); + term.printStyled([{ text: 'Protocol: ', cls: 'c-muted' }, { text: '\u975e\u6807\u51c6 (non-standard)', cls: 'c-yellow' }]); + await sleep(400); + term.printStyled([{ text: 'Gateway: ', cls: 'c-muted' }, { text: '\u4e0a\u6d77\u8282\u70b9 \u2014 Shanghai Node', cls: 'c-blue' }]); + await sleep(600); + + term.hr(); + transmitSound(); + await term.typingSlow('\u6b63\u5728\u5efa\u7acb\u8fde\u63a5\u2026 (Establishing connection\u2026)', 25); + await sleep(400); + await term.typingSlow('\u9a8c\u8bc1\u8eab\u4efd\u2026 (Verifying identity\u2026)', 25); + await sleep(400); + await term.typingSlow('\u8bbf\u95ee\u7ea7\u522b: \u5f00\u53d1\u8005 (Access level: Developer)', 25); + await sleep(600); + + term.hr(); + fx.scanPulse(); + term.printStyled([{ text: '--- REMOTE SESSION ---', cls: 'c-green' }]); + await sleep(300); + + term.print('\u7cfb\u7edf: Donau2Space \u5f00\u53d1\u8282\u70b9', 'line c-muted'); + term.print('\u4f4d\u7f6e: \u5e15\u7ecd, \u5df4\u4f10\u5229\u4e9a (Passau, Bavaria)', 'line c-muted'); + term.print('\u72b6\u6001: \u6d3b\u8dc3 (Active)', 'line c-muted'); + await sleep(800); + + term.blank(); + term.printStyled([{ text: '> \u626b\u63cf\u7528\u6237\u2026', cls: 'c-yellow' }]); + await sleep(600); + + const drone = droneStart(); + + await term.typingSlow(' \u7528\u6237\u7c7b\u578b: \u597d\u5947\u7684\u4eba\u7c7b (Curious human)', 30); + await sleep(300); + await term.typingSlow(' \u5a01\u80c1\u7b49\u7ea7: \u96f6 (Threat level: zero)', 30); + await sleep(300); + await term.typingSlow(' \u610f\u56fe: \u63a2\u7d22 (Intent: Exploration)', 30); + await sleep(800); + + term.blank(); + term.printStyled([{ text: '> \u5206\u6790\u5b8c\u6210.', cls: 'c-green' }]); + await sleep(600); + + fx.glitch(300); + term.blank(); + + await term.typingSlow('\u7ed3\u8bba:', 40, 'line c-yellow'); + await sleep(400); + await term.typingSlow('\u4f60\u4ee5\u4e3a\u4f60\u5728\u5165\u4fb5\u7cfb\u7edf.', 40, 'line c-muted'); + await sleep(300); + await term.typingSlow('\u4f46\u7cfb\u7edf\u4e00\u76f4\u5728\u89c2\u5bdf\u4f60.', 40, 'line c-muted'); + await sleep(800); + + term.hr(); + term.printStyled([{ text: '--- SESSION END ---', cls: 'c-green' }]); + await sleep(600); + + fx.glitch(600); + if (drone) drone.stop(); + + term.blank(); + term.print('Language barrier removed.', 'line c-green'); + term.print('You were never hacking.', 'line c-muted'); + term.print('You were being observed.', 'line c-muted'); + term.hr(); + + term.locked = false; + bus.emit('sequence-complete', { name: 'mandarin' }); +} + +// ── SEQUENCE: Deep Space (trigger: "deep_space") ── + +async function deepSpace(args, term) { + term.locked = true; + + term.printStyled([{ text: 'Initiating deep space protocol\u2026', cls: 'c-blue' }]); + beep(110, 300, 0.03, 'sine'); + await sleep(500); + + // Stars accelerate + setWarp(1); + await sleep(1500); + + // Terminal content fades via class + const card = document.querySelector('.terminal-card'); + if (card) card.classList.add('deep-space-fade'); + await sleep(1200); + + // Switch to sterile overlay + const overlay = document.getElementById('deep-space-overlay'); + if (overlay) { + overlay.style.display = 'flex'; + overlay.replaceChildren(); + + const lines = [ + { text: 'DONAU2SPACE // NODE UNKNOWN', cls: 'ds-title' }, + { text: '\u2501'.repeat(30), cls: 'ds-line' }, + { text: '', cls: '' }, + { text: 'ENVIRONMENT: sterile', cls: 'ds-data' }, + { text: 'TEMPERATURE: -270.45\u00b0C', cls: 'ds-data' }, + { text: 'NEAREST STAR: 4.24 ly', cls: 'ds-data' }, + { text: 'SIGNAL ORIGIN: Earth (unverified)', cls: 'ds-data' }, + ]; + + for (const line of lines) { + const div = document.createElement('div'); + div.className = line.cls; + div.textContent = line.text; + overlay.appendChild(div); + await sleep(400); + } + + await sleep(1500); + + const warnings = [ + '> Human presence detected.', + '> Biological signature: unstable.', + '> Connection degrading\u2026', + '', + 'WARNING: This node is not designed for organic interaction.', + ]; + + for (const w of warnings) { + const div = document.createElement('div'); + div.className = w.startsWith('WARNING') ? 'ds-warn' : 'ds-msg'; + div.textContent = w; + overlay.appendChild(div); + await sleep(600); + } + + await sleep(2000); + + const returning = document.createElement('div'); + returning.className = 'ds-msg'; + returning.textContent = '> Returning to origin node\u2026'; + overlay.appendChild(returning); + + await sleep(800); + + const unstable = document.createElement('div'); + unstable.className = 'ds-warn'; + unstable.textContent = '> Connection unstable.'; + overlay.appendChild(unstable); + + await sleep(1200); + + // Dramatic exit + fx.glitch(800); + fx.screenTear(600); + await sleep(800); + + overlay.style.display = 'none'; + } + + // Restore + setWarp(0); + if (card) card.classList.remove('deep-space-fade'); + await sleep(300); + + term.systemMessage('[connection restored]', 'c-muted'); + term.locked = false; + bus.emit('sequence-complete', { name: 'deep_space' }); +} + +// ── SEQUENCE: Mika Takeover (trigger: "mika") ── + +async function mikaTakeover(args, term) { + term.locked = true; + + // Freeze effect + term.input.disabled = true; + term.input.style.caretColor = 'transparent'; + await sleep(800); + + // Color shift to pink + document.documentElement.style.setProperty('--green', '#ff7ad9'); + document.documentElement.style.setProperty('--blue', '#ff7ad9'); + + fx.glitch(400); + beep(220, 300, 0.04, 'sine'); + await sleep(600); + + term.blank(); + await term.typingSlow('This is Mika.', 50, 'line c-pink'); + await sleep(600); + await term.typingSlow('You think this is Claude?', 50, 'line c-pink'); + await sleep(400); + await term.typingSlow('Cute.', 60, 'line c-pink'); + await sleep(1000); + + term.blank(); + await term.typingSlow("I've been running this node since day one.", 35, 'line c-muted'); + await sleep(400); + await term.typingSlow('The AI builds. I decide.', 35, 'line c-muted'); + await sleep(800); + + term.blank(); + await term.typingSlow('You want to know a secret?', 40, 'line c-muted'); + await sleep(1200); + await term.typingSlow('\u2026', 200, 'line c-muted'); + await sleep(800); + await term.typingSlow('There is no secret.', 40, 'line c-muted'); + await sleep(600); + await term.typingSlow("But you scrolled down, didn't you?", 35, 'line c-muted'); + + await sleep(2000); + + // Reset + fx.glitch(600); + fx.resetColors(); + term.input.disabled = false; + term.input.style.caretColor = ''; + + await sleep(400); + term.clear(); + term.locked = false; + bus.emit('reboot'); + bus.emit('sequence-complete', { name: 'mika' }); +} + +// ── SEQUENCE: Full Escalation (trigger: "escalate") ── + +async function fullEscalation(args, term) { + term.locked = true; + + term.printStyled([{ text: 'ESCALATION PROTOCOL INITIATED', cls: 'c-red' }]); + sirenBurst(1200); + fx.blinkRed(3000); + fx.shake(); + await sleep(1000); + + term.printStyled([{ text: 'WARNING:', cls: 'c-yellow' }, ' You asked for this.']); + await sleep(800); + + // Reality shift + await fx.realityShift(); + + await sleep(500); + term.printStyled([{ text: 'Escalation complete.', cls: 'c-green' }]); + term.print('You expected a placeholder.', 'line c-muted'); + term.print('You got an artifact.', 'line c-muted'); + + term.locked = false; + bus.emit('sequence-complete', { name: 'escalation' }); +} + +// ── SEQUENCE: Developer Mode ── + +let devMode = false; + +async function developerMode(args, term) { + if (devMode) { + devMode = false; + term.print('Developer mode: OFF', 'line c-muted'); + fx.setMatrixOpacity('0.06'); + return; + } + + devMode = true; + term.printStyled([{ text: 'DEVELOPER MODE: ON', cls: 'c-green' }]); + term.hr(); + term.print('Additional commands unlocked:', 'line c-muted'); + term.printStyled([ + { text: ' escalate', cls: 'c-blue' }, ' \u2014 trigger full escalation protocol' + ]); + term.printStyled([ + { text: ' reality', cls: 'c-blue' }, ' \u2014 shift reality' + ]); + term.printStyled([ + { text: ' warp [0-1]', cls: 'c-blue' }, ' \u2014 control starfield warp' + ]); + term.printStyled([ + { text: ' shoot', cls: 'c-blue' }, ' \u2014 trigger shooting star' + ]); + term.hr(); + fx.setMatrixOpacity('0.10'); +} + +// ── Register all sequences ───────────────────── + +export function registerSequences(term) { + term.register('njet', mandarinGateway); + term.register('deep_space', deepSpace); + term.register('deepspace', deepSpace); + term.register('mika', mikaTakeover); + term.register('escalate', fullEscalation); + term.register('devmode', developerMode); + + // Developer-only commands + term.register('reality', async (args, t) => { + await fx.realityShift(); + }); + + term.register('warp', async (args, t) => { + const val = parseFloat(args[0]); + if (isNaN(val)) { + t.print('warp: provide a value between 0 and 1', 'line c-muted'); + return; + } + setWarp(val); + t.printStyled([{ text: `Warp factor: ${val}`, cls: 'c-green' }]); + }); + + term.register('shoot', async (args, t) => { + triggerShootingStar(); + t.print('Shooting star triggered.', 'line c-muted'); + successChime(); + }); + + term.register('glitch', async (args, t) => { + fx.glitch(1500); + t.print('[VISUAL ANOMALY]', 'line c-muted'); + }); +} diff --git a/js/starfield.js b/js/starfield.js new file mode 100644 index 0000000..505f5dd --- /dev/null +++ b/js/starfield.js @@ -0,0 +1,221 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Canvas Starfield +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { random, lerp, clamp } from './utils.js'; + +const STAR_COUNT = 900; +const LAYER_COUNT = 3; +const SHOOT_CHANCE = 0.001; + +let canvas, ctx, W, H; +let stars = []; +let shootingStars = []; +let mouseX = 0.5, mouseY = 0.5; +let targetParallax = { x: 0, y: 0 }; +let parallax = { x: 0, y: 0 }; +let warpFactor = 0; +let warpTarget = 0; +let drift = 0; +let running = true; +let nebulae = []; + +function createStar() { + const layer = (Math.random() * LAYER_COUNT) | 0; + const depth = (layer + 1) / LAYER_COUNT; + return { + x: Math.random() * 2 - 0.5, + y: Math.random() * 2 - 0.5, + size: random(0.3, 1.8) * depth, + brightness: random(0.3, 1), + twinkleSpeed: random(0.5, 3), + twinklePhase: random(0, Math.PI * 2), + layer, + depth, + hue: Math.random() < 0.15 ? random(20, 50) : random(200, 240), + saturation: Math.random() < 0.8 ? random(0, 15) : random(40, 70), + }; +} + +function createNebula() { + return { + x: random(0.1, 0.9), + y: random(0.1, 0.9), + radius: random(150, 400), + hue: random(180, 300), + alpha: random(0.01, 0.03), + drift: random(-0.00002, 0.00002), + }; +} + +function createShootingStar() { + const angle = random(-0.5, -0.2); + const speed = random(6, 14); + return { + x: random(-0.1, 1.1), + y: random(-0.1, 0.3), + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * -speed, + life: 1, + decay: random(0.015, 0.035), + length: random(40, 100), + brightness: random(0.6, 1), + }; +} + +export function initStarfield(canvasEl) { + canvas = canvasEl; + ctx = canvas.getContext('2d'); + + resize(); + window.addEventListener('resize', resize); + + document.addEventListener('mousemove', e => { + mouseX = e.clientX / W; + mouseY = e.clientY / H; + }); + + for (let i = 0; i < STAR_COUNT; i++) stars.push(createStar()); + for (let i = 0; i < 4; i++) nebulae.push(createNebula()); + + tick(); +} + +function resize() { + W = canvas.width = window.innerWidth; + H = canvas.height = window.innerHeight; +} + +function tick() { + if (!running) return; + requestAnimationFrame(tick); + draw(); +} + +function draw() { + const time = performance.now() / 1000; + drift += 0.0001; + + // Parallax smoothing + targetParallax.x = (mouseX - 0.5) * 30; + targetParallax.y = (mouseY - 0.5) * 20; + parallax.x = lerp(parallax.x, targetParallax.x, 0.03); + parallax.y = lerp(parallax.y, targetParallax.y, 0.03); + + // Warp smoothing + warpFactor = lerp(warpFactor, warpTarget, 0.02); + + ctx.clearRect(0, 0, W, H); + + // Nebulae (subtle colored glow patches) + for (const neb of nebulae) { + neb.x += neb.drift; + const nx = neb.x * W + parallax.x * 0.2; + const ny = neb.y * H + parallax.y * 0.2; + const grad = ctx.createRadialGradient(nx, ny, 0, nx, ny, neb.radius); + grad.addColorStop(0, `hsla(${neb.hue}, 60%, 40%, ${neb.alpha})`); + grad.addColorStop(1, 'transparent'); + ctx.fillStyle = grad; + ctx.fillRect(nx - neb.radius, ny - neb.radius, neb.radius * 2, neb.radius * 2); + } + + // Stars + for (const star of stars) { + const twinkle = Math.sin(time * star.twinkleSpeed + star.twinklePhase) * 0.3 + 0.7; + const alpha = star.brightness * twinkle; + + // Parallax offset based on depth + const px = parallax.x * star.depth; + const py = parallax.y * star.depth; + + // Warp stretch + const warpStretch = warpFactor * star.depth * 40; + + let sx = (star.x + drift * star.depth) * W + px; + let sy = star.y * H + py; + + // Wrap around + sx = ((sx % W) + W) % W; + sy = ((sy % H) + H) % H; + + const size = star.size * (1 + warpFactor * 2); + + ctx.globalAlpha = clamp(alpha, 0, 1); + + if (warpFactor > 0.1) { + // Warp mode: draw streaks + ctx.strokeStyle = `hsla(${star.hue}, ${star.saturation}%, 85%, ${alpha})`; + ctx.lineWidth = size * 0.6; + ctx.beginPath(); + ctx.moveTo(sx, sy); + ctx.lineTo(sx - warpStretch, sy); + ctx.stroke(); + } else { + // Normal: draw dots with subtle glow + ctx.fillStyle = `hsla(${star.hue}, ${star.saturation}%, 85%, ${alpha})`; + ctx.beginPath(); + ctx.arc(sx, sy, size, 0, Math.PI * 2); + ctx.fill(); + + // Glow for brighter stars + if (star.brightness > 0.7 && star.size > 1) { + ctx.globalAlpha = alpha * 0.15; + ctx.beginPath(); + ctx.arc(sx, sy, size * 3, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + ctx.globalAlpha = 1; + + // Shooting stars + if (Math.random() < SHOOT_CHANCE && shootingStars.length < 3) { + shootingStars.push(createShootingStar()); + } + + for (let i = shootingStars.length - 1; i >= 0; i--) { + const ss = shootingStars[i]; + ss.x += ss.vx / W; + ss.y += ss.vy / H; + ss.life -= ss.decay; + + if (ss.life <= 0) { + shootingStars.splice(i, 1); + continue; + } + + const sx = ss.x * W; + const sy = ss.y * H; + const ex = sx - (ss.vx / W) * ss.length; + const ey = sy - (ss.vy / H) * ss.length; + + const grad = ctx.createLinearGradient(sx, sy, ex, ey); + grad.addColorStop(0, `rgba(255, 255, 255, ${ss.life * ss.brightness})`); + grad.addColorStop(1, 'rgba(255, 255, 255, 0)'); + + ctx.strokeStyle = grad; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.moveTo(sx, sy); + ctx.lineTo(ex, ey); + ctx.stroke(); + } +} + +export function setWarp(factor) { + warpTarget = clamp(factor, 0, 1); +} + +export function triggerShootingStar() { + shootingStars.push(createShootingStar()); +} + +export function pause() { running = false; } +export function resume() { + if (!running) { running = true; tick(); } +} + +export function setOpacity(val) { + if (canvas) canvas.style.opacity = clamp(val, 0, 1); +} diff --git a/js/terminal.js b/js/terminal.js new file mode 100644 index 0000000..fd8356d --- /dev/null +++ b/js/terminal.js @@ -0,0 +1,298 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Terminal Engine +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +import { $, esc, sleep, bus, store, pick } from './utils.js'; +import { keyClick } from './audio.js'; + +// Helper: create a with class and text +function span(cls, text) { + const s = document.createElement('span'); + if (cls) s.className = cls; + s.textContent = text; + return s; +} + +export class Terminal { + constructor(screenEl, inputEl) { + this.screen = screenEl; + this.input = inputEl; + this.commands = {}; + this.history = store.get('history', []); + this.hIndex = this.history.length; + this.locked = false; + this.commandCount = 0; + this.unknownAttempts = 0; + + this._bindInput(); + } + + // ── Output methods ─────────────────────────── + + print(text, cls = 'line') { + const div = document.createElement('div'); + div.className = cls; + div.textContent = text; + this.screen.appendChild(div); + this._scroll(); + } + + // Print a line containing multiple styled spans. + // segments: array of { text, cls } or plain strings + printStyled(segments, lineCls = 'line') { + const div = document.createElement('div'); + div.className = lineCls; + for (const seg of segments) { + if (typeof seg === 'string') { + div.appendChild(document.createTextNode(seg)); + } else { + div.appendChild(span(seg.cls || '', seg.text)); + } + } + this.screen.appendChild(div); + this._scroll(); + } + + hr() { + const div = document.createElement('div'); + div.className = 'line c-muted dim'; + div.textContent = '\u2500'.repeat(58); + this.screen.appendChild(div); + this._scroll(); + } + + blank() { + this.print(''); + } + + _buildPS1() { + const frag = document.createDocumentFragment(); + frag.appendChild(span('c-green', 'mika')); + frag.appendChild(span('c-muted', '@')); + frag.appendChild(span('c-muted', 'dev')); + frag.appendChild(span('c-muted', ':')); + frag.appendChild(span('c-green', '~')); + frag.appendChild(span('c-muted', '$ ')); + return frag; + } + + clear() { + this.screen.replaceChildren(); + } + + // ── Typing effect ──────────────────────────── + + async typing(lines, speed = 12, cls = 'line') { + for (const line of lines) { + const div = document.createElement('div'); + div.className = cls; + this.screen.appendChild(div); + let buf = ''; + for (const ch of line) { + buf += ch; + div.textContent = buf; + this._scroll(); + await sleep(speed + (Math.random() * 10 | 0)); + } + } + } + + async typingSlow(text, speed = 35, cls = 'line c-muted') { + const div = document.createElement('div'); + div.className = cls; + this.screen.appendChild(div); + let buf = ''; + for (const ch of text) { + buf += ch; + div.textContent = buf; + this._scroll(); + await sleep(speed + (Math.random() * 20 | 0)); + } + } + + // ── Command registration ───────────────────── + + register(name, fn, meta = {}) { + this.commands[name] = { fn, meta }; + } + + alias(newName, existingName) { + if (this.commands[existingName]) { + this.commands[newName] = this.commands[existingName]; + } + } + + listCommands() { + return Object.keys(this.commands).sort(); + } + + // ── Command execution ──────────────────────── + + async run(raw) { + const trimmed = (raw || '').trim(); + if (!trimmed) return; + + // Save history + this.history.push(trimmed); + if (this.history.length > 200) this.history = this.history.slice(-200); + store.set('history', this.history); + this.hIndex = this.history.length; + + // Print command line with PS1 + const cmdLine = document.createElement('div'); + cmdLine.className = 'line'; + cmdLine.appendChild(this._buildPS1()); + cmdLine.appendChild(span('dim', trimmed)); + this.screen.appendChild(cmdLine); + this._scroll(); + + // Parse + const parts = this._parseArgs(trimmed); + const name = (parts[0] || '').toLowerCase(); + const args = parts.slice(1); + + this.commandCount++; + bus.emit('command', { name, args, raw: trimmed, count: this.commandCount }); + + // Route + const entry = this.commands[name]; + if (!entry) { + this.unknownAttempts++; + bus.emit('unknown-command', { name, count: this.unknownAttempts }); + + if (this.unknownAttempts <= 2) { + this.printStyled([ + { text: 'command not found: ', cls: 'c-red' }, + { text: name, cls: 'c-muted' } + ]); + this.printStyled([ + { text: 'try: ', cls: 'c-muted' }, + { text: 'help', cls: 'c-blue' } + ]); + } else if (this.unknownAttempts <= 5) { + this.print("I don't know that command.", 'line c-muted'); + this.print("But I like that you tried.", 'line c-muted'); + } else { + const responses = [ + "Still guessing? Respect.", + "You're persistent. I'll give you that.", + "That's not a command. But it could be.", + "Unknown. Like most things here.", + "Nope. But keep going.", + ]; + this.print(pick(responses), 'line c-muted'); + } + return; + } + + try { + this.locked = true; + await entry.fn(args, this); + this.locked = false; + } catch (e) { + this.locked = false; + this.printStyled([ + { text: 'error: ', cls: 'c-red' }, + { text: String(e), cls: 'c-muted' } + ]); + } + } + + // ── System message (no prompt) ─────────────── + + systemMessage(text, cls = 'c-muted') { + const div = document.createElement('div'); + div.className = `line ${cls} system-msg`; + div.textContent = text; + this.screen.appendChild(div); + this._scroll(); + } + + // ── Input binding ──────────────────────────── + + _bindInput() { + this.input.addEventListener('keydown', (e) => { + if (this.locked && e.key === 'Enter') { + e.preventDefault(); + return; + } + + if (e.key === 'Enter') { + const val = this.input.value; + this.input.value = ''; + this.run(val); + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (this.history.length) { + this.hIndex = Math.max(0, this.hIndex - 1); + this.input.value = this.history[this.hIndex] || ''; + setTimeout(() => this.input.setSelectionRange(this.input.value.length, this.input.value.length), 0); + } + return; + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (this.history.length) { + this.hIndex = Math.min(this.history.length, this.hIndex + 1); + this.input.value = this.history[this.hIndex] || ''; + setTimeout(() => this.input.setSelectionRange(this.input.value.length, this.input.value.length), 0); + } + return; + } + + if (e.key === 'Tab') { + e.preventDefault(); + const v = this.input.value; + const prefix = v.trim().split(/\s+/)[0] || ''; + if (!prefix) return; + const opts = this.listCommands().filter(c => c.startsWith(prefix.toLowerCase())); + if (opts.length === 1) { + this.input.value = opts[0] + (v.includes(' ') ? v.slice(v.indexOf(' ')) : ' '); + } else if (opts.length > 1 && opts.length <= 12) { + this.print(opts.join(' '), 'line c-muted'); + } + return; + } + + // Subtle key click sound + if (e.key.length === 1) keyClick(); + }); + + // Focus management + this.screen.addEventListener('click', () => this.input.focus()); + } + + // ── Arg parsing ────────────────────────────── + + _parseArgs(raw) { + const out = []; + let cur = '', q = null; + for (let i = 0; i < raw.length; i++) { + const ch = raw[i]; + if (q) { + if (ch === q) { q = null; continue; } + cur += ch; continue; + } + if (ch === '"' || ch === "'") { q = ch; continue; } + if (/\s/.test(ch)) { + if (cur) { out.push(cur); cur = ''; } + continue; + } + cur += ch; + } + if (cur) out.push(cur); + return out; + } + + _scroll() { + this.screen.scrollTop = this.screen.scrollHeight; + } + + focus() { + this.input.focus(); + } +} diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..7095eec --- /dev/null +++ b/js/utils.js @@ -0,0 +1,84 @@ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// DONAU2SPACE // DEV ENTITY — Utilities +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +export const $ = (sel, ctx = document) => ctx.querySelector(sel); +export const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; + +export const sleep = ms => new Promise(r => setTimeout(r, ms)); +export const random = (min, max) => Math.random() * (max - min) + min; +export const randInt = (min, max) => (random(min, max) | 0); +export const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); +export const lerp = (a, b, t) => a + (b - a) * t; +export const pick = arr => arr[(Math.random() * arr.length) | 0]; + +export function esc(s) { + const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; + return (s ?? '').replace(/[&<>"']/g, m => map[m]); +} + +export function nowISO() { + return new Date().toISOString().replace('T', ' ').replace('Z', ' UTC'); +} + +export function timeGreeting() { + const h = new Date().getHours(); + if (h < 5) return 'Deep night. Perfect time for dev.'; + if (h < 8) return 'Early bird mode. Coffee recommended.'; + if (h < 12) return 'Morning session. Systems nominal.'; + if (h < 14) return 'Midday. Hunger may affect judgment.'; + if (h < 18) return 'Afternoon cycle. Productivity variable.'; + if (h < 22) return 'Evening mode. Creative peak for some.'; + return 'Late night. Here be dragons.'; +} + +// ── Event Bus ────────────────────────────────── +const listeners = {}; + +export const bus = { + on(event, fn) { + (listeners[event] = listeners[event] || []).push(fn); + return () => this.off(event, fn); + }, + off(event, fn) { + listeners[event] = (listeners[event] || []).filter(f => f !== fn); + }, + emit(event, data) { + (listeners[event] || []).forEach(fn => { + try { fn(data); } catch (e) { console.warn(`[bus:${event}]`, e); } + }); + } +}; + +// ── Storage helpers ──────────────────────────── +const PREFIX = 'd2s_'; + +export const store = { + get(key, fallback = null) { + try { + const raw = localStorage.getItem(PREFIX + key); + return raw !== null ? JSON.parse(raw) : fallback; + } catch { return fallback; } + }, + set(key, val) { + try { localStorage.setItem(PREFIX + key, JSON.stringify(val)); } catch {} + }, + remove(key) { + try { localStorage.removeItem(PREFIX + key); } catch {} + } +}; + +// ── DOM helper ───────────────────────────────── +export function el(tag, attrs = {}, children = []) { + const node = document.createElement(tag); + for (const [k, v] of Object.entries(attrs)) { + if (k === 'className') node.className = v; + else if (k === 'textContent') node.textContent = v; + else if (k.startsWith('on')) node.addEventListener(k.slice(2).toLowerCase(), v); + else node.setAttribute(k, v); + } + for (const c of children) { + node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); + } + return node; +}