feat(chat-cli): add SSH, adventure, figlet, fortunes, achievements, leaderboard, boss key

This commit is contained in:
jae 2026-04-20 02:44:25 +00:00
parent df53d85d01
commit d91c0bed11
4 changed files with 681 additions and 5 deletions

View file

@ -1,6 +1,28 @@
{ {
"site": "jaeswift.xyz", "site": "jaeswift.xyz",
"entries": [ "entries": [
{
"version": "1.38.1",
"date": "20/04/2026",
"category": "FEATURE",
"title": "CLI Extensions: SSH, Adventure, Fortunes, Figlet, Achievements, Leaderboard, Boss Key",
"changes": [
"JAE-AI terminal (/) now has 7 new commands and 3 meta-features — type /help to see the categorised menu (SYSTEM / FUN / MODES / STATS)",
"/ssh <user>@<host> — fake SSH session with ED25519 handshake, MOTD, nested shell (ls/cat/cd/pwd/whoami/hostname/uname/date); exit via /exit-ssh or 'exit'",
"/adventure — short cyberpunk text adventure (DARK ALLEY → NEON ARCADE → HACKER DEN → LOW ROOFTOP → NEON ROOFTOP boss fight) with inventory, drone HP, /exit-adventure",
"/ttyper — typing-speed test with random programming quote, timer, WPM + accuracy calc, all-time best persisted in localStorage (jae-cli-ttyper-best-v1)",
"/cowsay <msg> — classic ASCII cow with auto-sized speech bubble",
"/figlet <text> — block-font ASCII banner (5-row UNICODE █ glyphs, full A-Z/0-9/punctuation, truncated to 20 chars)",
"/fortune -o — offensive mode with 30 crude operator-themed quips; /fortune alone returns 20 tame SITREP entries",
"/cmatrix — enhanced matrix rain: dual-colour green+cyan, denser (14px), speed ramps over 30s cycles, optional square-wave AudioContext ticks for typing-sound effect",
"/achievements — progress tracker showing X/24 commands discovered with ASCII progress bar [███░░░░░░░], list of unlocked commands, persists via localStorage (jae-cli-achievements-v1); toast pops for each new unlock",
"/leaderboard — session + all-time stats for this browser: command counts, unlocks, most-used command, top-5 session & all-time, military rank (recruit → general based on 20/50/100/200/500/1000/2000 thresholds)",
"Boss key — press Escape (while anything is 'hot') or Ctrl+Shift+B to instantly hide matrix rains, dev-mode badge, toasts, and clear chat input; shows 'NORMAL MODE' tooltip for ~2s",
"Nested mode routing — when inside /ssh, /adventure, /ttyper, plain (non-slash) chat input is routed to the nested handler instead of JAE-AI; chat.js updated with isInMode() check",
"Stats tracked in localStorage (jae-cli-stats-v1) and in-memory session counter; commands object grown from 17 → 24",
"Files: js/chat-cli.js (+632 lines → 1142 total), css/chat-cli.css (+21 lines), js/chat.js (+1 line — mode-aware CLI interception)"
]
},
{ {
"version": "1.38.0", "version": "1.38.0",
"date": "20/04/2026", "date": "20/04/2026",

View file

@ -40,3 +40,24 @@
.chat-msg-cli .chat-msg-body { font-size: 11px; } .chat-msg-cli .chat-msg-body { font-size: 11px; }
#devModeBadge { font-size: 9px !important; top: 62px !important; padding: 4px 9px !important; } #devModeBadge { font-size: 9px !important; top: 62px !important; padding: 4px 9px !important; }
} }
/* ─── Extensions v1.38.1: toasts, mode accents ─── */
.jae-cli-toast {
border-radius: 2px;
animation: jaeCliToastIn 0.28s ease-out;
}
@keyframes jaeCliToastIn {
from { transform: translateY(14px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* SSH & adventure nested mode user bubble tint */
.chat-msg-user-cli.chat-msg-user-cli-mode .chat-msg-body::before {
content: '> ';
color: #33ddff;
}
/* Figlet / cowsay bodies: ensure wide output doesn't wrap mid-block */
.chat-msg-cli .chat-msg-body {
overflow-x: auto;
}

View file

@ -11,6 +11,45 @@
let cmdHistory = []; let cmdHistory = [];
let devMode = false; let devMode = false;
let matrixActive = false; let matrixActive = false;
let sessionCmdCount = 0;
let sessionPerCmd = {};
let mode = null; // null | 'ssh' | 'adventure' | 'ttyper'
let modeState = {};
const ACH_KEY = 'jae-cli-achievements-v1';
const STATS_KEY = 'jae-cli-stats-v1';
const TTYPER_KEY = 'jae-cli-ttyper-best-v1';
let achievements = new Set();
let allTimeStats = { total: 0, perCmd: {} };
try {
const ar = localStorage.getItem(ACH_KEY);
if (ar) { const a = JSON.parse(ar); if (Array.isArray(a)) achievements = new Set(a); }
const sr = localStorage.getItem(STATS_KEY);
if (sr) allTimeStats = Object.assign({ total: 0, perCmd: {} }, JSON.parse(sr));
} catch (e) {}
function saveAch() { try { localStorage.setItem(ACH_KEY, JSON.stringify([...achievements])); } catch(e){} }
function saveStats() { try { localStorage.setItem(STATS_KEY, JSON.stringify(allTimeStats)); } catch(e){} }
function unlockAchievement(cmd) {
if (achievements.has(cmd)) return false;
achievements.add(cmd);
saveAch();
try {
const t = document.createElement('div');
t.className = 'jae-cli-toast';
t.textContent = '🏆 NEW: Discovered /' + cmd + ' command';
Object.assign(t.style, {
position: 'fixed', bottom: '24px', right: '24px', zIndex: '9999',
background: 'var(--bg-panel, #0d1810)', border: '1px solid var(--warning, #c9a227)',
color: 'var(--warning, #c9a227)', padding: '10px 16px',
fontFamily: 'var(--font-mono, monospace)', fontSize: '12px', letterSpacing: '1px',
boxShadow: '0 0 18px rgba(201,162,39,0.45)', transition: 'opacity 0.4s',
borderRadius: '2px', pointerEvents: 'none',
});
document.body.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; }, 3400);
setTimeout(() => { t.remove(); }, 4000);
} catch (e) {}
return true;
}
// Load history // Load history
try { try {
@ -122,6 +161,7 @@
const base = [ const base = [
'JAE-AI TERMINAL — available commands:', 'JAE-AI TERMINAL — available commands:',
'', '',
'— SYSTEM —',
' /help show this help', ' /help show this help',
' /ls list virtual files', ' /ls list virtual files',
' /cat <file> print file contents', ' /cat <file> print file contents',
@ -132,13 +172,27 @@
' /ping <host> fake ping sweep', ' /ping <host> fake ping sweep',
' /sudo <cmd> attempt privilege escalation', ' /sudo <cmd> attempt privilege escalation',
' /rm -rf / don\'t do it', ' /rm -rf / don\'t do it',
' /hack <target> hollywood hacking simulator',
' /matrix toggle matrix rain',
' /clear clear the chat', ' /clear clear the chat',
' /exit try to leave', ' /exit try to leave',
' /fortune random sitrep quip',
' /history show last commands', ' /history show last commands',
' /neofetch system summary ASCII', ' /neofetch system summary ASCII',
'',
'— FUN —',
' /hack <target> hollywood hacking simulator',
' /matrix toggle matrix rain',
' /cmatrix enhanced matrix (sound + ramp)',
' /fortune [-o] random quip (-o for offensive)',
' /cowsay <msg> a cow says something',
' /figlet <text> big ASCII banner (<=20 chars)',
'',
'— MODES —',
' /ssh <user>@<host> enter fake SSH session',
' /adventure cyberpunk text adventure',
' /ttyper typing speed test',
'',
'— STATS —',
' /achievements your unlocked commands',
' /leaderboard session & all-time stats',
]; ];
if (devMode) { if (devMode) {
base.push(''); base.push('');
@ -148,6 +202,7 @@
base.push(' /geo show GEO INTEL from telemetry'); base.push(' /geo show GEO INTEL from telemetry');
} }
base.push(''); base.push('');
base.push('Tip: Press Escape (or Ctrl+Shift+B) as a boss key to hide everything.');
base.push('Anything not prefixed with / goes to JAE-AI.'); base.push('Anything not prefixed with / goes to JAE-AI.');
return fmt(base); return fmt(base);
}; };
@ -401,6 +456,514 @@
} }
}; };
// ─── /cowsay ─────────────────────────────────────
commands.cowsay = function (args) {
const msg = args.join(' ') || 'moo.';
const text = msg.slice(0, 200);
const top = ' ' + '_'.repeat(text.length + 2);
const bot = ' ' + '-'.repeat(text.length + 2);
return fmt([
top,
'< ' + text + ' >',
bot,
' \\ ^__^',
' \\ (oo)\\_______',
' (__)\\ )\\/\\',
' ||----w |',
' || ||'
]);
};
// ─── /fortune (tame + offensive) ─────────────────
const FORTUNES_TAME = [
'SITREP: the compiler whispers. listen.',
'SITREP: your branch is ahead by one regret.',
'SITREP: staging is production if staging is where the users are.',
'SITREP: tests pass locally is not a deployment strategy.',
'SITREP: cache invalidation is a personality trait.',
'SITREP: the database is fine. the database is never fine.',
'SITREP: write it down. future-you is stupider than you think.',
'SITREP: uptime is measured in coffee.',
'SITREP: legacy code is just working code you hate.',
'SITREP: every bug is someone else\'s feature.',
'SITREP: the stack trace knows. trust the stack trace.',
'SITREP: container orchestration is a synonym for chaos.',
'SITREP: there are two kinds of backup: tested and imaginary.',
'SITREP: TODO comments age like milk.',
'SITREP: ship small, ship often, ship sober.',
'SITREP: rm is a lifestyle. ls is religion.',
'SITREP: root access is a personality disorder.',
'SITREP: the terminal is the only honest relationship.',
'SITREP: `it compiles` is not a vibe.',
'SITREP: operator status: caffeinated. proceed.'
];
const FORTUNES_OFFENSIVE = [
'SITREP: your code smells like a fucking landfill. grep deeper.',
'SITREP: whoever wrote this legacy shit owes the team a pint and a resignation letter.',
'SITREP: you can\'t centralise a garbage fire. you can only photograph it for post-mortem.',
'SITREP: touch grass, operator. the leaves are greener than your terminal.',
'SITREP: the sysadmin is tired. the sysadmin is always fucking tired.',
'SITREP: if it works, don\'t touch it. if it doesn\'t, blame the intern.',
'SITREP: your git history is a crime scene. get a lawyer.',
'SITREP: nginx config is 80% vibes and 20% stack overflow.',
'SITREP: the only thing more fragile than your ego is this regex.',
'SITREP: deploy on friday. embrace the chaos. live deliciously.',
'SITREP: your tests are flaky because YOU are flaky.',
'SITREP: merge conflicts are the universe\'s way of saying grow up.',
'SITREP: chmod 777 is a cry for help.',
'SITREP: whoever invented JavaScript owes humanity a bloody apology.',
'SITREP: your Docker image is 4GB because you don\'t know what you\'re doing. fix it.',
'SITREP: if you ssh into prod to "just check something", you are the incident.',
'SITREP: that variable name is an act of violence against future maintainers.',
'SITREP: you didn\'t break the build. the build broke you.',
'SITREP: 3am deploys are for masochists and idiots. which one are you?',
'SITREP: the documentation is a lie. it has always been a lie.',
'SITREP: `works on my machine` is a confession, not a defence.',
'SITREP: your monorepo is a cry for therapy.',
'SITREP: stop copy-pasting from stack overflow you absolute muppet.',
'SITREP: curl | bash is a religious experience and a mistake.',
'SITREP: kubernetes is just containers with extra steps and extra crying.',
'SITREP: the only good password is the one you forgot.',
'SITREP: your side project is a liability, not a portfolio.',
'SITREP: linters are for people who can\'t spell. spell better.',
'SITREP: shit deploys fast. good shit deploys slower. great shit doesn\'t deploy on fridays.',
'SITREP: if the server is smoking, that\'s your problem now.'
];
commands.fortune = function (args) {
const offensive = args && (args[0] === '-o' || args[0] === '--offensive');
const pool = offensive ? FORTUNES_OFFENSIVE : FORTUNES_TAME;
return pool[Math.floor(Math.random() * pool.length)];
};
// ─── /figlet — block-font ASCII banner ─<><E29480><EFBFBD>─────────
const FIGLET_FONT = {
'A': [' ██ ','█ █ ','████ ','█ █ ','█ █ '],
'B': ['███ ','█ █ ','███ ','█ █ ','███ '],
'C': [' ███ ','█ ','█ ','█ ',' ███ '],
'D': ['███ ','█ █ ','█ █ ','█ █ ','███ '],
'E': ['████ ','█ ','███ ','█ ','████ '],
'F': ['████ ','█ ','███ ','█ ','█ '],
'G': [' ███ ','█ ','█ ██ ','█ █ ',' ███ '],
'H': ['█ █ ','█ █ ','████ ','█ █ ','█ █ '],
'I': ['███ ',' █ ',' █ ',' █ ','███ '],
'J': [' ██ ',' █ ',' █ ','█ █ ',' ██ '],
'K': ['█ █ ','█ █ ','██ ','█ █ ','█ █ '],
'L': ['█ ','█ ','█ ','█ ','████ '],
'M': ['█ █','██ ██','█ █ █','█ █','█ █'],
'N': ['█ █ ','██ █ ','█ ██ ','█ █ ','█ █ '],
'O': [' ██ ','█ █ ','█ █ ','█ █ ',' ██ '],
'P': ['███ ','█ █ ','███ ','█ ','█ '],
'Q': [' ██ ','█ █ ','█ █ ','█ █ ',' █ █ '],
'R': ['███ ','█ █ ','███ ','█ █ ','█ █ '],
'S': [' ███ ','█ ',' ██ ',' █ ','███ '],
'T': ['█████',' █ ',' █ ',' █ ',' █ '],
'U': ['█ █ ','█ █ ','█ █ ','█ █ ',' ██ '],
'V': ['█ █ ','█ █ ','█ █ ',' ██ ',' █ '],
'W': ['█ █','█ █','█ █ █','██ ██','█ █'],
'X': ['█ █ ',' ██ ',' █ ',' ██ ','█ █ '],
'Y': ['█ █ ',' ██ ',' █ ',' █ ',' █ '],
'Z': ['████ ',' █ ',' █ ',' █ ','████ '],
'0': [' ██ ','█ █ ','█ █ ','█ █ ',' ██ '],
'1': [' █ ','██ ',' █ ',' █ ','███ '],
'2': [' ██ ','█ █ ',' █ ',' █ ','████ '],
'3': ['███ ',' █ ',' ██ ',' █ ','███ '],
'4': ['█ █ ','█ █ ','████ ',' █ ',' █ '],
'5': ['████ ','█ ','███ ',' █ ','███ '],
'6': [' ██ ','█ ','███ ','█ █ ',' ██ '],
'7': ['████ ',' █ ',' █ ',' █ ','█ '],
'8': [' ██ ','█ █ ',' ██ ','█ █ ',' ██ '],
'9': [' ██ ','█ █ ',' ███ ',' █ ',' ██ '],
' ': [' ',' ',' ',' ',' '],
'!': [' █ ',' █ ',' █ ',' ',' █ '],
'?': [' ██ ','█ █ ',' █ ',' ',' █ '],
'.': [' ',' ',' ',' ',' █ '],
'-': [' ',' ','████ ',' ',' '],
'_': [' ',' ',' ',' ','████ '],
'/': [' █ ',' █ ',' █ ','█ ',' '],
':': [' ',' █ ',' ',' █ ',' ']
};
commands.figlet = function (args) {
const text = (args.join(' ') || 'JAE').toUpperCase().slice(0, 20);
const rows = ['', '', '', '', ''];
for (const ch of text) {
const glyph = FIGLET_FONT[ch] || FIGLET_FONT['?'];
for (let i = 0; i < 5; i++) rows[i] += glyph[i] + ' ';
}
return rows.join('\n');
};
// ─── /ssh fake session ───────────────────────────
const SSH_FS = {
'/': ['home', 'etc', 'var', 'opt', 'tmp'],
'/home': ['operator', 'guest'],
'/etc': ['hostname', 'motd', 'shadow'],
'/var': ['log'],
'/var/log': ['auth.log', 'syslog'],
};
const SSH_FILES = {
'/etc/hostname': 'jaeswift-prod-01',
'/etc/motd': '\n WELCOME TO JAESWIFT PROD — authorised operators only.\n All activity logged. Press Ctrl+C to get off my lawn.\n',
'/etc/shadow': 'nice try. permission denied.',
'/home/operator/notes.txt': 'remember: the password is NOT hunter2.',
'/var/log/auth.log': '[' + new Date().toISOString() + '] sshd: Accepted publickey for operator from 127.0.0.1',
};
commands.ssh = function (args) {
const target = args[0] || '';
const m = target.match(/^([a-zA-Z0-9_-]+)@([a-zA-Z0-9.\-_]+)$/);
if (!m) return 'usage: /ssh <user>@<host> e.g. /ssh operator@jaeswift.xyz';
const user = m[1], host = m[2];
mode = 'ssh';
modeState = { user, host, cwd: '/home/' + user };
return fmt([
`Connecting to ${host} on port 22...`,
`The authenticity of host '${host}' can\'t be established.`,
`ED25519 key fingerprint is SHA256:J4Esw1FtX${Math.random().toString(36).slice(2,14)}.`,
`Warning: Permanently added '${host}' (ED25519) to the list of known hosts.`,
`${user}@${host}'s password: ****`,
`Authenticating...`,
`Last login: ${new Date().toUTCString()} from 10.0.0.1`,
``,
`Welcome to ${host} — type 'help' for commands, '/exit-ssh' to disconnect.`
]);
};
function sshHandle(line) {
const raw = line.trim();
if (!raw) return '';
if (raw === '/exit-ssh' || raw === 'exit' || raw === 'logout') {
const h = modeState.host;
mode = null; modeState = {};
return `Connection to ${h} closed.`;
}
const parts = raw.split(/\s+/);
const cmd = parts[0].toLowerCase();
const arg = parts.slice(1).join(' ');
switch (cmd) {
case 'help':
return fmt(['available: ls, cat <f>, pwd, cd <d>, whoami, hostname, uname, date, exit']);
case 'whoami': return modeState.user;
case 'hostname': return modeState.host;
case 'pwd': return modeState.cwd;
case 'uname': return 'Linux';
case 'date': return new Date().toString();
case 'ls': {
const dir = SSH_FS[modeState.cwd];
if (dir) return dir.join(' ');
if (modeState.cwd === '/home/' + modeState.user) return 'notes.txt .bashrc .ssh';
return '';
}
case 'cd': {
if (!arg || arg === '~') { modeState.cwd = '/home/' + modeState.user; return ''; }
if (arg.startsWith('/')) modeState.cwd = arg;
else modeState.cwd = (modeState.cwd + '/' + arg).replace(/\/+/g, '/');
return '';
}
case 'cat': {
if (!arg) return 'cat: missing operand';
const full = arg.startsWith('/') ? arg : (modeState.cwd + '/' + arg).replace(/\/+/g, '/');
if (SSH_FILES[full] != null) return SSH_FILES[full];
return `cat: ${arg}: No such file or directory`;
}
default:
return `${cmd}: command not found`;
}
}
// ─── /adventure — cyberpunk text adventure ───────
const ADV_ROOMS = {
'alley': { name: 'DARK ALLEY', desc: 'Rain slicks the neon puddles. A fire-escape ladder climbs north to a rooftop. East is a buzzing arcade door.', exits: { n: 'rooftop_low', e: 'arcade' }, items: ['rusty knife'] },
'arcade': { name: 'NEON ARCADE', desc: 'Machines scream colour. A bouncer eyes you. A back door leads south to a hacker den. West back to the alley.', exits: { w: 'alley', s: 'hacker_den' }, items: ['token'] },
'hacker_den': { name: 'HACKER DEN', desc: 'Ten monitors, one cat. A hacker types without looking. They might talk. North back to arcade.', exits: { n: 'arcade' }, items: [], npc: 'hacker' },
'rooftop_low': { name: 'LOW ROOFTOP', desc: 'Gravel, pigeons, a distant drone. A ladder climbs east to the high neon rooftop.', exits: { s: 'alley', e: 'rooftop_high' }, items: [] },
'rooftop_high': { name: 'NEON ROOFTOP', desc: 'The city spreads below. A corporate drone blocks the datalink pylon. BOSS FIGHT.', exits: { w: 'rooftop_low' }, items: [], boss: 'drone' }
};
commands.adventure = function () {
mode = 'adventure';
modeState = { room: 'alley', inv: [], hp: 100, bossHp: 60, talked: false, victory: false };
return fmt([
'== NEON/NULL — a very short cyberpunk — ==',
'',
'You are an operator looking to upload the encryption key to the city.',
'Commands: look, n/s/e/w (or north/south/east/west), take <item>,',
' use <item>, inventory (i), attack, talk, /exit-adventure',
'',
advDescribe()
]);
};
function advDescribe() {
const r = ADV_ROOMS[modeState.room];
const out = [`[${r.name}]`, r.desc];
if (r.items && r.items.length) out.push('You see: ' + r.items.join(', '));
if (r.boss && modeState.bossHp > 0) out.push('A CORPORATE DRONE is here. HP: ' + modeState.bossHp);
const exits = Object.keys(r.exits).map(k => ({n:'north',s:'south',e:'east',w:'west'})[k]).join(', ');
out.push('Exits: ' + exits);
return out.join('\n');
}
function adventureHandle(line) {
const raw = line.trim().toLowerCase();
if (!raw) return '';
if (raw === '/exit-adventure' || raw === 'exit-adventure' || raw === 'quit') {
mode = null; modeState = {};
return 'You jack out. Back to JAE-AI.';
}
const dirMap = { n: 'n', north: 'n', s: 's', south: 's', e: 'e', east: 'e', w: 'w', west: 'w' };
if (dirMap[raw]) {
const r = ADV_ROOMS[modeState.room];
const d = dirMap[raw];
if (r.boss && modeState.bossHp > 0) return 'The drone blocks your path. ATTACK it first.';
if (!r.exits[d]) return 'You can\'t go that way.';
modeState.room = r.exits[d];
return advDescribe();
}
if (raw === 'look' || raw === 'l') return advDescribe();
if (raw === 'inventory' || raw === 'i' || raw === 'inv') {
return modeState.inv.length ? 'You carry: ' + modeState.inv.join(', ') : 'Your pockets are empty.';
}
if (raw.startsWith('take ')) {
const item = raw.slice(5).trim();
const r = ADV_ROOMS[modeState.room];
const idx = r.items.indexOf(item);
if (idx === -1) return `There is no ${item} here.`;
r.items.splice(idx, 1);
modeState.inv.push(item);
return `You take the ${item}.`;
}
if (raw.startsWith('use ')) {
const item = raw.slice(4).trim();
if (!modeState.inv.includes(item)) return `You don't have a ${item}.`;
if (item === 'token' && modeState.room === 'arcade') {
modeState.inv.push('encryption key');
return 'The arcade machine clunks. A secret panel opens — you palm an ENCRYPTION KEY.';
}
if (item === 'encryption key' && modeState.room === 'rooftop_high' && modeState.bossHp <= 0) {
modeState.victory = true;
mode = null; modeState = {};
return fmt([
'You jack the ENCRYPTION KEY into the pylon.',
'The city\'s surveillance grid goes dark for 11 seconds.',
'That\'s all you needed.',
'',
'>> YOU WIN. Mission complete. Exiting adventure. <<'
]);
}
return 'Nothing happens.';
}
if (raw === 'talk') {
const r = ADV_ROOMS[modeState.room];
if (r.npc === 'hacker' && !modeState.talked) {
modeState.talked = true;
if (!modeState.inv.includes('token')) return 'The hacker: "Arcade machine. Back one. Use the token. Trust me."';
if (!modeState.inv.includes('encryption key')) return 'The hacker: "Use what you have. The arcade remembers."';
return 'The hacker: "Rooftop. Pylon. Key. Go."';
}
return 'No one to talk to.';
}
if (raw === 'attack') {
const r = ADV_ROOMS[modeState.room];
if (!r.boss || modeState.bossHp <= 0) return 'Nothing to attack.';
const weapon = modeState.inv.includes('rusty knife') ? 'knife' : 'fist';
const dmg = weapon === 'knife' ? (8 + Math.floor(Math.random()*8)) : (3 + Math.floor(Math.random()*4));
modeState.bossHp -= dmg;
modeState.hp -= (3 + Math.floor(Math.random()*6));
let out = `You attack with your ${weapon} for ${dmg} damage. Drone HP: ${Math.max(0,modeState.bossHp)}.`;
if (modeState.hp <= 0) {
mode = null; modeState = {};
return out + '\nThe drone flatlines you. GAME OVER.';
}
if (modeState.bossHp <= 0) out += '\nThe drone crashes to the gravel. The pylon is exposed.';
out += ` Your HP: ${modeState.hp}.`;
return out;
}
return `Unknown command: '${raw}'. Try look, directions, take, use, attack, talk.`;
}
// ─── /ttyper — typing speed test ─────────────────
const TTYPER_QUOTES = [
'the quick brown fox jumps over the lazy dog while the sysadmin sips cold coffee and watches nginx logs scroll endlessly',
'code is read many more times than it is written so leave comments that future you will not curse at',
'in the terminal no one can hear you scream but the bash history remembers every single thing you have ever typed',
'deploy on friday and spend the weekend rolling back changes like a very unfortunate software archaeologist',
'the best debugger is still a print statement followed by staring at the output for twenty minutes in silence'
];
commands.ttyper = function () {
const q = TTYPER_QUOTES[Math.floor(Math.random() * TTYPER_QUOTES.length)];
mode = 'ttyper';
modeState = { quote: q, start: 0 };
return fmt([
'=== TTYPER — typing speed test ===',
'Type the quote below exactly as shown, then press Enter.',
'Type "/exit-ttyper" to cancel.',
'',
'> ' + q,
'',
'(timer starts on your first submission)'
]);
};
function ttyperHandle(line) {
const raw = line.trim();
if (raw === '/exit-ttyper' || raw === 'exit-ttyper' || raw === 'quit') {
mode = null; modeState = {};
return 'TTYPER cancelled.';
}
if (!modeState.start) modeState.start = Date.now();
const elapsed = (Date.now() - modeState.start) / 1000;
const target = modeState.quote;
const a = raw, b = target;
const maxLen = Math.max(a.length, b.length);
let correct = 0;
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] === b[i]) correct++;
const accuracy = maxLen === 0 ? 100 : Math.round((correct / maxLen) * 100);
const words = target.split(/\s+/).length;
const wpm = elapsed > 0 ? Math.round((words / elapsed) * 60) : 0;
let best = null;
try {
const raw2 = localStorage.getItem(TTYPER_KEY);
if (raw2) best = JSON.parse(raw2);
} catch (e) {}
let bestNote = '';
if (!best || (wpm > (best.wpm || 0) && accuracy >= 90)) {
const rec = { wpm, accuracy, ts: Date.now() };
try { localStorage.setItem(TTYPER_KEY, JSON.stringify(rec)); } catch(e){}
bestNote = '\n*** NEW PERSONAL BEST ***';
best = rec;
}
mode = null; modeState = {};
return fmt([
'=== TTYPER RESULT ===',
`time: ${elapsed.toFixed(2)}s`,
`WPM: ${wpm}`,
`accuracy: ${accuracy}%`,
best ? `all-time best: ${best.wpm} WPM @ ${best.accuracy}%` : '',
bestNote
].filter(Boolean));
}
// ─── /cmatrix — enhanced matrix rain ─────────────
let cmatrixCanvas = null, cmatrixRAF = null, cmatrixAudio = null, cmatrixStart = 0;
function toggleCMatrix(on) {
if (on) {
cmatrixCanvas = document.createElement('canvas');
cmatrixCanvas.id = 'cmatrixRain';
Object.assign(cmatrixCanvas.style, {
position: 'fixed', inset: '0', width: '100%', height: '100%',
pointerEvents: 'none', zIndex: '8', opacity: '0.32',
});
document.body.appendChild(cmatrixCanvas);
const ctx = cmatrixCanvas.getContext('2d');
function resize() { cmatrixCanvas.width = innerWidth; cmatrixCanvas.height = innerHeight; }
resize();
window.addEventListener('resize', resize);
const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネ01JAESWIFT░▒▓█'.split('');
const size = 14;
const cols = Math.floor(cmatrixCanvas.width / size);
const drops = new Array(cols).fill(0).map(() => Math.random() * 40);
cmatrixStart = Date.now();
try {
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) cmatrixAudio = new AC();
} catch (e) {}
let tick = 0;
function draw() {
const t = (Date.now() - cmatrixStart) / 1000;
const phase = (t % 30) / 30;
const speed = 1 + phase * 2.5;
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
ctx.fillRect(0, 0, cmatrixCanvas.width, cmatrixCanvas.height);
ctx.font = size + 'px JetBrains Mono, monospace';
for (let i = 0; i < drops.length; i++) {
const ch = chars[Math.floor(Math.random() * chars.length)];
ctx.fillStyle = (i % 2 === 0) ? '#00ff66' : '#33ddff';
ctx.fillText(ch, i * size, drops[i] * size);
if (drops[i] * size > cmatrixCanvas.height && Math.random() > 0.97) drops[i] = 0;
drops[i] += speed;
}
tick++;
if (cmatrixAudio && tick % 4 === 0) {
try {
const osc = cmatrixAudio.createOscillator();
const g = cmatrixAudio.createGain();
osc.type = 'square';
osc.frequency.value = 1600 + Math.random() * 800;
g.gain.value = 0.0035;
osc.connect(g); g.connect(cmatrixAudio.destination);
osc.start();
osc.stop(cmatrixAudio.currentTime + 0.02);
} catch (e) {}
}
cmatrixRAF = requestAnimationFrame(draw);
}
draw();
} else {
if (cmatrixRAF) cancelAnimationFrame(cmatrixRAF);
if (cmatrixCanvas) cmatrixCanvas.remove();
if (cmatrixAudio) { try { cmatrixAudio.close(); } catch(e){} }
cmatrixCanvas = null; cmatrixAudio = null;
}
}
let cmatrixActive = false;
commands.cmatrix = function () {
cmatrixActive = !cmatrixActive;
toggleCMatrix(cmatrixActive);
return cmatrixActive
? 'CMATRIX engaged. Dual-colour rain with audio. Toggle off with /cmatrix.'
: 'CMATRIX disengaged.';
};
// ─── /achievements ───────────────────────────────
const TOTAL_COMMANDS = 24;
commands.achievements = function () {
const unlocked = [...achievements].sort();
const n = unlocked.length;
const pct = Math.round((n / TOTAL_COMMANDS) * 100);
const filled = Math.min(10, Math.round((n / TOTAL_COMMANDS) * 10));
const bar = '[' + '█'.repeat(filled) + '░'.repeat(10 - filled) + ']';
const out = [
'=== ACHIEVEMENTS ===',
'',
`Progress: ${bar} ${n}/${TOTAL_COMMANDS} (${pct}%)`,
''
];
if (n === 0) out.push('No commands discovered yet. Try /help to start.');
else {
out.push('Unlocked:');
unlocked.forEach(c => out.push(' ✓ /' + c));
}
out.push('');
out.push('Keep exploring, operator.');
return fmt(out);
};
// ─── /leaderboard ────────────────────────────────
function rankFor(total) {
if (total >= 2000) return 'general';
if (total >= 1000) return 'colonel';
if (total >= 500) return 'major';
if (total >= 200) return 'captain';
if (total >= 100) return 'lieutenant';
if (total >= 50) return 'sergeant';
if (total >= 20) return 'corporal';
return 'recruit';
}
commands.leaderboard = function () {
const entries = Object.entries(allTimeStats.perCmd || {}).sort((a,b) => b[1] - a[1]);
const mostUsed = entries.length ? entries[0][0] : '—';
const sessionEntries = Object.entries(sessionPerCmd).sort((a,b) => b[1] - a[1]);
return fmt([
'=== LEADERBOARD (this browser) ===',
'',
`Session commands: ${sessionCmdCount}`,
`All-time commands: ${allTimeStats.total || 0}`,
`Commands unlocked: ${achievements.size}/${TOTAL_COMMANDS}`,
`Most-used (all-time): /${mostUsed}`,
`Rank: ${rankFor(allTimeStats.total || 0).toUpperCase()}`,
'',
'Session top 5:',
...sessionEntries.slice(0, 5).map(([c, n]) => ` /${c} ×${n}`),
'',
'All-time top 5:',
...entries.slice(0, 5).map(([c, n]) => ` /${c} ×${n}`)
]);
};
// ─── Matrix rain overlay ───────────────────────── // ─── Matrix rain overlay ─────────────────────────
let matrixCanvas = null; let matrixCanvas = null;
let matrixRAF = null; let matrixRAF = null;
@ -476,7 +1039,24 @@
// ─── Entry point — handle a /-command ─────────── // ─── Entry point — handle a /-command ───────────
async function handle(raw) { async function handle(raw) {
const trimmed = raw.trim(); const trimmed = (raw || '').trim();
if (!trimmed) return { handled: false };
// If we are inside a nested mode, route input there regardless of '/' prefix,
// except for an explicit /exit-<mode> escape (handled inside each).
if (mode === 'ssh') {
try { const out = sshHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
catch (e) { return { handled: true, output: 'ssh error: ' + e.message }; }
}
if (mode === 'adventure') {
try { const out = adventureHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
catch (e) { return { handled: true, output: 'adventure error: ' + e.message }; }
}
if (mode === 'ttyper') {
try { const out = ttyperHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
catch (e) { return { handled: true, output: 'ttyper error: ' + e.message }; }
}
if (!trimmed.startsWith('/')) return { handled: false }; if (!trimmed.startsWith('/')) return { handled: false };
const parts = trimmed.slice(1).split(/\s+/); const parts = trimmed.slice(1).split(/\s+/);
@ -494,6 +1074,17 @@
return { handled: true, output: `command not found: /${cmd}. type /help for available commands.` }; return { handled: true, output: `command not found: /${cmd}. type /help for available commands.` };
} }
// Track stats + unlock achievements for known commands
try {
sessionCmdCount++;
sessionPerCmd[cmd] = (sessionPerCmd[cmd] || 0) + 1;
allTimeStats.total = (allTimeStats.total || 0) + 1;
allTimeStats.perCmd = allTimeStats.perCmd || {};
allTimeStats.perCmd[cmd] = (allTimeStats.perCmd[cmd] || 0) + 1;
saveStats();
unlockAchievement(cmd);
} catch (e) {}
try { try {
const result = await fn(args); const result = await fn(args);
return { handled: true, output: (result == null ? '' : String(result)) }; return { handled: true, output: (result == null ? '' : String(result)) };
@ -502,9 +1093,50 @@
} }
} }
// ─── Boss key — Escape / Ctrl+Shift+B ────────────
function bossKeyActivate() {
try {
if (matrixActive) { matrixActive = false; toggleMatrix(false); }
if (cmatrixActive) { cmatrixActive = false; toggleCMatrix(false); }
const badge = document.getElementById('devModeBadge');
if (badge) badge.style.display = 'none';
const toasts = document.querySelectorAll('.jae-cli-toast');
toasts.forEach(t => t.remove());
const ci = document.getElementById('chatInput');
if (ci) { ci.value = ''; try { ci.blur(); } catch(e){} }
const tip = document.createElement('div');
tip.textContent = 'NORMAL MODE';
Object.assign(tip.style, {
position: 'fixed', top: '12px', left: '50%', transform: 'translateX(-50%)',
background: '#111', color: '#9aa', border: '1px solid #333',
padding: '4px 10px', fontFamily: 'monospace', fontSize: '10px',
letterSpacing: '2px', zIndex: '99999', opacity: '0.85',
borderRadius: '2px', pointerEvents: 'none',
});
document.body.appendChild(tip);
setTimeout(() => { tip.style.transition = 'opacity 0.5s'; tip.style.opacity = '0'; }, 1500);
setTimeout(() => tip.remove(), 2200);
} catch (e) {}
}
document.addEventListener('keydown', function (e) {
const isEsc = (e.key === 'Escape');
const isCtrlShiftB = (e.ctrlKey && e.shiftKey && (e.key === 'B' || e.key === 'b'));
if (isEsc || isCtrlShiftB) {
// Don't nuke on Escape while user is typing in chat unless something is actually "hot"
const hot = matrixActive || cmatrixActive || document.getElementById('devModeBadge') || document.querySelector('.jae-cli-toast');
if (isCtrlShiftB || (isEsc && hot)) {
bossKeyActivate();
if (isCtrlShiftB) e.preventDefault();
}
}
});
window.__jaeCLI = { window.__jaeCLI = {
handle: handle, handle: handle,
commands: commands, commands: commands,
isDev: function () { return devMode; }, isDev: function () { return devMode; },
isInMode: function () { return mode !== null; },
currentMode: function () { return mode; },
bossKey: bossKeyActivate,
}; };
})(); })();

View file

@ -128,7 +128,8 @@
if (!text || isWaiting) return; if (!text || isWaiting) return;
// ─── CLI interception ─── // ─── CLI interception ───
if (text.startsWith('/') && window.__jaeCLI && typeof window.__jaeCLI.handle === 'function') { const inCliMode = window.__jaeCLI && typeof window.__jaeCLI.isInMode === 'function' && window.__jaeCLI.isInMode();
if ((text.startsWith('/') || inCliMode) && window.__jaeCLI && typeof window.__jaeCLI.handle === 'function') {
isWaiting = true; isWaiting = true;
chatInput.value = ''; chatInput.value = '';
const userBubble = addMessage('user', text); const userBubble = addMessage('user', text);