Dateien nach „js“ hochladen
This commit is contained in:
parent
ee23fa675b
commit
10fdc3aea1
4 changed files with 934 additions and 0 deletions
331
js/sequences.js
Normal file
331
js/sequences.js
Normal file
|
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
221
js/starfield.js
Normal file
221
js/starfield.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
298
js/terminal.js
Normal file
298
js/terminal.js
Normal file
|
|
@ -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 <span> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
84
js/utils.js
Normal file
84
js/utils.js
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue