diff --git a/_layouts/home.html b/_layouts/home.html new file mode 100644 index 0000000..7685b86 --- /dev/null +++ b/_layouts/home.html @@ -0,0 +1,40 @@ +--- +layout: default +--- + +{% include hud.html %} + +
+ + + {% if paginator.total_pages > 1 %} + + {% endif %} +
+ + + + diff --git a/assets/css/terminal.css b/assets/css/terminal.css new file mode 100644 index 0000000..1f34be8 --- /dev/null +++ b/assets/css/terminal.css @@ -0,0 +1,152 @@ +/* Terminal Overlay — output/game display only (input lives in HUD status bar) */ + +#terminal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.92); + z-index: 9999; + justify-content: center; + align-items: center; + padding: 20px; + box-sizing: border-box; + overflow: hidden; +} + +#terminal-overlay.active { + display: flex; +} + +body.terminal-open { + overflow: hidden !important; + position: fixed !important; + inset: 0 !important; + touch-action: none; +} + +.terminal-window { + width: 100%; + max-width: 700px; + height: 80vh; + max-height: 600px; + background: #0a0a0a; + border: 1px solid #00ff41; + box-shadow: 0 0 30px rgba(0, 255, 65, 0.2), inset 0 0 60px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + font-family: 'Share Tech Mono', 'Courier New', monospace; + position: relative; + box-sizing: border-box; +} + +.terminal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 14px; + border-bottom: 1px solid rgba(0, 255, 65, 0.3); + background: rgba(0, 255, 65, 0.05); + flex-shrink: 0; +} + +.terminal-title { + color: #00ff41; + font-size: 0.8em; + letter-spacing: 2px; + text-transform: uppercase; + text-shadow: 0 0 8px rgba(0, 255, 65, 0.5); +} + +.terminal-close { + color: #00aa00; + cursor: pointer; + font-size: 0.85em; + padding: 2px 6px; + transition: color 0.2s, text-shadow 0.2s; +} + +.terminal-close:hover { + color: #00ff41; + text-shadow: 0 0 10px rgba(0, 255, 65, 0.8); +} + +.terminal-body { + flex: 1; + overflow-y: auto; + padding: 12px 14px; + scrollbar-width: thin; + scrollbar-color: #00aa00 #0a0a0a; + min-height: 0; +} + +.terminal-body::-webkit-scrollbar { + width: 6px; +} + +.terminal-body::-webkit-scrollbar-track { + background: #0a0a0a; +} + +.terminal-body::-webkit-scrollbar-thumb { + background: #00aa00; + border-radius: 0; +} + +.terminal-line { + color: #00ff41; + font-size: 0.85em; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + +/* Breakout container */ +#breakout-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + min-height: 0; +} + +#breakout-canvas { + border: 1px solid rgba(0, 255, 65, 0.3); + max-width: 100%; + max-height: 100%; +} + +/* Mobile */ +@media (max-width: 768px) { + #terminal-overlay { + padding: 0; + } + + .terminal-window { + max-width: 100%; + width: 100%; + height: 100%; + max-height: 100%; + border: none; + box-sizing: border-box; + } + + .terminal-line { + font-size: 0.8em; + } + + #breakout-container { + padding: 5px; + } +} + +@media (max-width: 480px) { + .terminal-line { + font-size: 0.75em; + line-height: 1.5; + } + + .terminal-body { + padding: 8px 10px; + } +} diff --git a/assets/js/breakout.js b/assets/js/breakout.js new file mode 100644 index 0000000..9c35b92 --- /dev/null +++ b/assets/js/breakout.js @@ -0,0 +1,550 @@ +// Breakout — terminal-styled brick breaker +// Easter egg for 757btc.org +(function () { + 'use strict'; + + var GREEN = '#00ff41'; + var GREEN_DIM = '#00aa00'; + var GREEN_GLOW = 'rgba(0, 255, 65, 0.5)'; + var BLACK = '#0a0a0a'; + var WHITE = '#ffffff'; + + var canvas, ctx, container, exitCallback; + var animFrame = null; + var running = false; + + // Game state + var state = {}; + var HIGH_SCORE_KEY = '757btc_breakout_hi'; + + function defaultState() { + return { + // Paddle + paddleW: 80, + paddleH: 10, + paddleX: 0, + paddleSpeed: 6, + + // Ball + ballR: 5, + ballX: 0, + ballY: 0, + ballDX: 3, + ballDY: -3, + ballSpeed: 3, + + // Bricks + brickRows: 5, + brickCols: 8, + brickW: 0, + brickH: 18, + brickPad: 4, + brickOffsetTop: 50, + brickOffsetLeft: 0, + bricks: [], + + // Score + score: 0, + lives: 3, + level: 1, + highScore: parseInt(localStorage.getItem(HIGH_SCORE_KEY)) || 0, + + // Input + leftPressed: false, + rightPressed: false, + touchX: null, + + // State + paused: false, + gameOver: false, + started: false + }; + } + + function initBricks() { + var totalPad = state.brickPad * (state.brickCols + 1); + state.brickW = (canvas.width - totalPad) / state.brickCols; + state.brickOffsetLeft = state.brickPad; + + state.bricks = []; + for (var r = 0; r < state.brickRows; r++) { + state.bricks[r] = []; + for (var c = 0; c < state.brickCols; c++) { + state.bricks[r][c] = { alive: true, hits: r < 2 ? 2 : 1 }; + } + } + } + + function resetBall() { + state.ballX = canvas.width / 2; + state.ballY = canvas.height - 50; + var angle = (Math.random() * 0.8 + 0.6) * (Math.random() < 0.5 ? 1 : -1); + state.ballDX = state.ballSpeed * Math.sin(angle); + state.ballDY = -state.ballSpeed * Math.cos(angle); + state.started = false; + } + + function resizeCanvas() { + var rect = container.getBoundingClientRect(); + var w = Math.min(rect.width - 20, 600); + var h = Math.min(rect.height - 20, 500); + canvas.width = w; + canvas.height = h; + state.paddleX = (w - state.paddleW) / 2; + } + + // Drawing + function drawRect(x, y, w, h, color, glow) { + if (glow) { + ctx.shadowColor = GREEN_GLOW; + ctx.shadowBlur = 8; + } + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); + ctx.shadowBlur = 0; + } + + function drawBall() { + ctx.beginPath(); + ctx.arc(state.ballX, state.ballY, state.ballR, 0, Math.PI * 2); + ctx.fillStyle = GREEN; + ctx.shadowColor = GREEN_GLOW; + ctx.shadowBlur = 12; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.closePath(); + } + + function drawPaddle() { + drawRect( + state.paddleX, + canvas.height - state.paddleH - 10, + state.paddleW, + state.paddleH, + GREEN, + true + ); + } + + function drawBricks() { + for (var r = 0; r < state.brickRows; r++) { + for (var c = 0; c < state.brickCols; c++) { + var b = state.bricks[r][c]; + if (!b.alive) continue; + + var x = state.brickOffsetLeft + c * (state.brickW + state.brickPad); + var y = state.brickOffsetTop + r * (state.brickH + state.brickPad); + + var color = b.hits > 1 ? GREEN : GREEN_DIM; + drawRect(x, y, state.brickW, state.brickH, color, false); + + // Border + ctx.strokeStyle = GREEN; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, state.brickW, state.brickH); + } + } + } + + function drawHUD() { + ctx.font = '14px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = GREEN; + ctx.shadowColor = GREEN_GLOW; + ctx.shadowBlur = 4; + + ctx.textAlign = 'left'; + ctx.fillText('SCORE: ' + state.score, 10, 20); + + ctx.textAlign = 'center'; + ctx.fillText('LIVES: ' + state.lives, canvas.width / 2, 20); + + ctx.textAlign = 'right'; + ctx.fillText('HI: ' + state.highScore, canvas.width - 10, 20); + + ctx.shadowBlur = 0; + ctx.textAlign = 'left'; + + // Level indicator + ctx.font = '10px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = GREEN_DIM; + ctx.fillText('LVL ' + state.level, 10, 36); + } + + function drawStartScreen() { + ctx.font = '14px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = GREEN; + ctx.textAlign = 'center'; + ctx.shadowColor = GREEN_GLOW; + ctx.shadowBlur = 6; + ctx.fillText('[ PRESS SPACE OR TAP TO LAUNCH ]', canvas.width / 2, canvas.height - 70); + ctx.shadowBlur = 0; + ctx.textAlign = 'left'; + } + + function drawGameOver() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.font = '24px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = GREEN; + ctx.textAlign = 'center'; + ctx.shadowColor = GREEN_GLOW; + ctx.shadowBlur = 10; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 30); + + ctx.font = '14px "Share Tech Mono", "Courier New", monospace'; + ctx.fillText('SCORE: ' + state.score, canvas.width / 2, canvas.height / 2 + 10); + + if (state.score >= state.highScore && state.score > 0) { + ctx.fillStyle = WHITE; + ctx.fillText('★ NEW HIGH SCORE ★', canvas.width / 2, canvas.height / 2 + 35); + } + + ctx.fillStyle = GREEN_DIM; + ctx.font = '12px "Share Tech Mono", "Courier New", monospace'; + ctx.fillText('press SPACE to restart or ESC to exit', canvas.width / 2, canvas.height / 2 + 65); + + // NIP-07 leaderboard prompt + if (window.nostr && state.score > 0) { + ctx.fillStyle = GREEN; + ctx.fillText('press N to post score to nostr leaderboard', canvas.width / 2, canvas.height / 2 + 85); + } + + ctx.shadowBlur = 0; + ctx.textAlign = 'left'; + } + + // Collision + function brickCollision() { + for (var r = 0; r < state.brickRows; r++) { + for (var c = 0; c < state.brickCols; c++) { + var b = state.bricks[r][c]; + if (!b.alive) continue; + + var bx = state.brickOffsetLeft + c * (state.brickW + state.brickPad); + var by = state.brickOffsetTop + r * (state.brickH + state.brickPad); + + if ( + state.ballX + state.ballR > bx && + state.ballX - state.ballR < bx + state.brickW && + state.ballY + state.ballR > by && + state.ballY - state.ballR < by + state.brickH + ) { + state.ballDY = -state.ballDY; + b.hits--; + if (b.hits <= 0) { + b.alive = false; + state.score += (r < 2) ? 20 : 10; + } else { + state.score += 5; + } + return; + } + } + } + } + + function allBricksCleared() { + for (var r = 0; r < state.brickRows; r++) { + for (var c = 0; c < state.brickCols; c++) { + if (state.bricks[r][c].alive) return false; + } + } + return true; + } + + function nextLevel() { + state.level++; + state.ballSpeed += 0.5; + initBricks(); + resetBall(); + } + + // Update + function update() { + if (state.paused || state.gameOver || !state.started) return; + + // Paddle movement + if (state.leftPressed && state.paddleX > 0) { + state.paddleX -= state.paddleSpeed; + } + if (state.rightPressed && state.paddleX < canvas.width - state.paddleW) { + state.paddleX += state.paddleSpeed; + } + + // Touch + if (state.touchX !== null) { + var target = state.touchX - state.paddleW / 2; + state.paddleX += (target - state.paddleX) * 0.2; + state.paddleX = Math.max(0, Math.min(canvas.width - state.paddleW, state.paddleX)); + } + + // Ball + state.ballX += state.ballDX; + state.ballY += state.ballDY; + + // Wall collisions + if (state.ballX - state.ballR < 0 || state.ballX + state.ballR > canvas.width) { + state.ballDX = -state.ballDX; + state.ballX = Math.max(state.ballR, Math.min(canvas.width - state.ballR, state.ballX)); + } + if (state.ballY - state.ballR < 0) { + state.ballDY = -state.ballDY; + state.ballY = state.ballR; + } + + // Paddle collision + var paddleTop = canvas.height - state.paddleH - 10; + if ( + state.ballY + state.ballR >= paddleTop && + state.ballY + state.ballR <= paddleTop + state.paddleH + 4 && + state.ballX >= state.paddleX && + state.ballX <= state.paddleX + state.paddleW + ) { + // Angle based on where ball hits paddle + var hitPos = (state.ballX - state.paddleX) / state.paddleW; // 0-1 + var angle = (hitPos - 0.5) * Math.PI * 0.7; // -63° to +63° + var speed = Math.sqrt(state.ballDX * state.ballDX + state.ballDY * state.ballDY); + state.ballDX = speed * Math.sin(angle); + state.ballDY = -speed * Math.abs(Math.cos(angle)); + state.ballY = paddleTop - state.ballR; + } + + // Bottom — lose life + if (state.ballY + state.ballR > canvas.height) { + state.lives--; + if (state.lives <= 0) { + endGame(); + } else { + resetBall(); + } + } + + // Brick collision + brickCollision(); + + // Level clear + if (allBricksCleared()) { + nextLevel(); + } + } + + function endGame() { + state.gameOver = true; + if (state.score > state.highScore) { + state.highScore = state.score; + localStorage.setItem(HIGH_SCORE_KEY, state.score); + } + } + + // NIP-07 Nostr score posting + function postScoreToNostr() { + if (!window.nostr || state.score <= 0) return; + + var event = { + kind: 30069, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['d', '757btc-breakout'], + ['score', String(state.score)], + ['level', String(state.level)], + ['L', 'com.757btc.game'], + ['l', 'breakout', 'com.757btc.game'], + ['r', 'https://757btc.org'] + ], + content: 'Scored ' + state.score + ' points in 757btc Breakout (level ' + state.level + ') 🧱⚡' + }; + + window.nostr.signEvent(event).then(function (signed) { + // Publish to 757btc relay + var relays = ['wss://relay.757btc.org', 'wss://relay.damus.io']; + relays.forEach(function (url) { + try { + var ws = new WebSocket(url); + ws.onopen = function () { + ws.send(JSON.stringify(['EVENT', signed])); + setTimeout(function () { ws.close(); }, 3000); + }; + } catch (e) { /* silent */ } + }); + + // Show confirmation + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(canvas.width / 2 - 150, canvas.height / 2 + 95, 300, 30); + ctx.font = '12px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = GREEN; + ctx.textAlign = 'center'; + ctx.fillText('✓ score posted to nostr!', canvas.width / 2, canvas.height / 2 + 115); + ctx.textAlign = 'left'; + }).catch(function () { + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(canvas.width / 2 - 150, canvas.height / 2 + 95, 300, 30); + ctx.font = '12px "Share Tech Mono", "Courier New", monospace'; + ctx.fillStyle = '#ff4444'; + ctx.textAlign = 'center'; + ctx.fillText('✗ signing failed or cancelled', canvas.width / 2, canvas.height / 2 + 115); + ctx.textAlign = 'left'; + }); + } + + // Render + function draw() { + ctx.fillStyle = BLACK; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Subtle grid + ctx.strokeStyle = 'rgba(0, 255, 65, 0.03)'; + ctx.lineWidth = 0.5; + for (var x = 0; x < canvas.width; x += 40) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, canvas.height); + ctx.stroke(); + } + for (var y = 0; y < canvas.height; y += 40) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + } + + drawBricks(); + drawPaddle(); + if (!state.gameOver) drawBall(); + drawHUD(); + + if (!state.started && !state.gameOver) drawStartScreen(); + if (state.gameOver) drawGameOver(); + } + + function gameLoop() { + if (!running) return; + update(); + draw(); + animFrame = requestAnimationFrame(gameLoop); + } + + // Input handlers + function onKeyDown(e) { + if (e.key === 'ArrowLeft' || e.key === 'a') state.leftPressed = true; + if (e.key === 'ArrowRight' || e.key === 'd') state.rightPressed = true; + if (e.key === ' ') { + e.preventDefault(); + if (state.gameOver) { + restartGame(); + } else if (!state.started) { + state.started = true; + } + } + if (e.key === 'Escape') { + e.preventDefault(); + stop(); + if (exitCallback) exitCallback(); + } + if (e.key === 'n' || e.key === 'N') { + if (state.gameOver) postScoreToNostr(); + } + } + + function onKeyUp(e) { + if (e.key === 'ArrowLeft' || e.key === 'a') state.leftPressed = false; + if (e.key === 'ArrowRight' || e.key === 'd') state.rightPressed = false; + } + + function onTouchStart(e) { + e.preventDefault(); + if (state.gameOver) { + restartGame(); + return; + } + if (!state.started) { + state.started = true; + } + var rect = canvas.getBoundingClientRect(); + state.touchX = e.touches[0].clientX - rect.left; + } + + function onTouchMove(e) { + e.preventDefault(); + var rect = canvas.getBoundingClientRect(); + state.touchX = e.touches[0].clientX - rect.left; + } + + function onTouchEnd(e) { + e.preventDefault(); + state.touchX = null; + } + + function onMouseMove(e) { + var rect = canvas.getBoundingClientRect(); + var mouseX = e.clientX - rect.left; + state.paddleX = mouseX - state.paddleW / 2; + state.paddleX = Math.max(0, Math.min(canvas.width - state.paddleW, state.paddleX)); + } + + function onMouseClick(e) { + e.preventDefault(); + if (state.gameOver) { + restartGame(); + } else if (!state.started) { + state.started = true; + } + } + + function restartGame() { + state = defaultState(); + resizeCanvas(); + initBricks(); + resetBall(); + state.highScore = parseInt(localStorage.getItem(HIGH_SCORE_KEY)) || 0; + } + + function start(containerEl, onExit) { + container = containerEl; + exitCallback = onExit; + + canvas = document.createElement('canvas'); + canvas.id = 'breakout-canvas'; + canvas.style.display = 'block'; + canvas.style.margin = '0 auto'; + canvas.style.background = BLACK; + container.appendChild(canvas); + ctx = canvas.getContext('2d'); + + state = defaultState(); + resizeCanvas(); + initBricks(); + resetBall(); + state.highScore = parseInt(localStorage.getItem(HIGH_SCORE_KEY)) || 0; + + document.addEventListener('keydown', onKeyDown); + document.addEventListener('keyup', onKeyUp); + canvas.addEventListener('touchstart', onTouchStart, { passive: false }); + canvas.addEventListener('touchmove', onTouchMove, { passive: false }); + canvas.addEventListener('touchend', onTouchEnd, { passive: false }); + canvas.addEventListener('mousemove', onMouseMove); + canvas.addEventListener('click', onMouseClick); + + running = true; + gameLoop(); + } + + function stop() { + running = false; + if (animFrame) cancelAnimationFrame(animFrame); + document.removeEventListener('keydown', onKeyDown); + document.removeEventListener('keyup', onKeyUp); + if (canvas) { + canvas.removeEventListener('touchstart', onTouchStart); + canvas.removeEventListener('touchmove', onTouchMove); + canvas.removeEventListener('touchend', onTouchEnd); + canvas.removeEventListener('mousemove', onMouseMove); + canvas.removeEventListener('click', onMouseClick); + canvas.remove(); + } + canvas = null; + ctx = null; + } + + window.breakoutGame = { start: start, stop: stop }; +})(); diff --git a/assets/js/terminal.js b/assets/js/terminal.js new file mode 100644 index 0000000..d651cd3 --- /dev/null +++ b/assets/js/terminal.js @@ -0,0 +1,302 @@ +// Terminal — HUD prompt input + overlay for output/games +(function () { + 'use strict'; + + var overlay = null; + var outputEl = null; + var hudInput = null; + var history = []; + var historyIndex = -1; + var gameActive = false; + var posterData = null; + + // Load poster-data.json + function loadPosterData() { + fetch('/assets/data/poster-data.json') + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (data) { posterData = data; }) + .catch(function () { posterData = null; }); + } + + // --- Overlay (output only, no input) --- + + function createOverlay() { + overlay = document.createElement('div'); + overlay.id = 'terminal-overlay'; + overlay.innerHTML = + '
' + + '
' + + '757btc terminal' + + '[x]' + + '
' + + '
' + + '
'; + document.body.appendChild(overlay); + + outputEl = document.getElementById('terminal-output'); + + overlay.querySelector('.terminal-close').addEventListener('click', closeOverlay); + overlay.addEventListener('click', function (e) { + if (e.target === overlay) closeOverlay(); + }); + } + + function openOverlay() { + if (!overlay) createOverlay(); + outputEl.innerHTML = ''; + outputEl.style.display = ''; + overlay.classList.add('active'); + document.body.classList.add('terminal-open'); + // Dismiss mobile keyboard + if (hudInput) hudInput.blur(); + if (document.activeElement) document.activeElement.blur(); + } + + function closeOverlay() { + if (gameActive && window.breakoutGame) { + window.breakoutGame.stop(); + gameActive = false; + } + if (overlay) overlay.classList.remove('active'); + document.body.classList.remove('terminal-open'); + // Refocus the HUD input + if (hudInput) { + hudInput.value = ''; + hudInput.placeholder = 'system online'; + } + } + + function writeLine(text) { + if (!outputEl) return; + var line = document.createElement('div'); + line.className = 'terminal-line'; + line.textContent = text; + outputEl.appendChild(line); + outputEl.scrollTop = outputEl.scrollHeight; + } + + // --- HUD prompt input --- + + function handleHudInput(e) { + if (e.key === 'Enter') { + var cmd = hudInput.value.trim().toLowerCase(); + hudInput.value = ''; + if (cmd) { + history.push(cmd); + historyIndex = history.length; + processCommand(cmd); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (historyIndex > 0) { + historyIndex--; + hudInput.value = history[historyIndex]; + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (historyIndex < history.length - 1) { + historyIndex++; + hudInput.value = history[historyIndex]; + } else { + historyIndex = history.length; + hudInput.value = ''; + } + } else if (e.key === 'Escape') { + hudInput.value = ''; + hudInput.blur(); + } + // Stop propagation so page shortcuts don't fire + e.stopPropagation(); + } + + // --- Commands --- + + function processCommand(cmd) { + var commands = { + help: cmdHelp, + meetups: cmdMeetups, + links: cmdLinks, + about: cmdAbout, + clear: cmdClear, + // Easter eggs — not listed in help + breakout: cmdBreakout, + play: cmdBreakout, + matrix: cmdMatrix, + satoshi: cmdSatoshi, + '21m': cmd21m + }; + + var fn = commands[cmd]; + if (fn) { + fn(); + } else { + // Flash an error in the placeholder briefly + hudInput.placeholder = 'unknown command: ' + cmd; + setTimeout(function () { + hudInput.placeholder = 'system online'; + }, 2000); + } + } + + function cmdHelp() { + openOverlay(); + writeLine('757btc terminal v1.0'); + writeLine(''); + writeLine('available commands:'); + writeLine(' meetups — upcoming bitcoin meetups in the 757'); + writeLine(' links — useful links and resources'); + writeLine(' about — about 757btc'); + writeLine(' clear — close overlay'); + writeLine(' help — this message'); + writeLine(''); + writeLine('hint: there may be more commands than listed...'); + writeLine(''); + } + + function cmdMeetups() { + openOverlay(); + if (!posterData || !posterData.meetups) { + writeLine('meetup data unavailable'); + return; + } + writeLine(posterData.meetups.title); + writeLine(''); + posterData.meetups.list.forEach(function (m) { + var time = m.time ? ' @ ' + m.time : ''; + writeLine(' ▸ ' + m.frequency + (m.schedule ? ' ' + m.schedule : '') + time); + writeLine(' ' + m.location); + }); + writeLine(''); + } + + function cmdLinks() { + openOverlay(); + if (!posterData || !posterData.links) { + writeLine('link data unavailable'); + return; + } + posterData.links.forEach(function (l) { + writeLine(' ' + l.label); + writeLine(' → ' + l.url); + }); + writeLine(''); + } + + function cmdAbout() { + openOverlay(); + writeLine(''); + writeLine('757btc — Hampton Roads Bitcoin Community'); + writeLine('monthly meetups since February 2024'); + writeLine('we build, learn, and stack together'); + writeLine(''); + writeLine('"privacy is the power to selectively'); + writeLine(' reveal oneself to the world."'); + writeLine(''); + } + + function cmdClear() { + closeOverlay(); + } + + // Easter eggs + + function cmdBreakout() { + if (!window.breakoutGame) { + hudInput.placeholder = 'loading...'; + var script = document.createElement('script'); + script.src = '/assets/js/breakout.js'; + script.onload = function () { + startBreakout(); + }; + script.onerror = function () { + hudInput.placeholder = 'error: game not available'; + setTimeout(function () { hudInput.placeholder = 'system online'; }, 2000); + }; + document.head.appendChild(script); + } else { + startBreakout(); + } + } + + function startBreakout() { + openOverlay(); + gameActive = true; + + // Hide the text output, show game + var body = overlay.querySelector('.terminal-body'); + body.style.display = 'none'; + + var gameContainer = document.createElement('div'); + gameContainer.id = 'breakout-container'; + overlay.querySelector('.terminal-window').appendChild(gameContainer); + + window.breakoutGame.start(gameContainer, function () { + // Game exited + gameActive = false; + gameContainer.remove(); + body.style.display = ''; + closeOverlay(); + }); + } + + function cmdMatrix() { + openOverlay(); + writeLine(''); + writeLine('wake up, neo...'); + writeLine('the matrix has you...'); + writeLine('follow the white rabbit.'); + writeLine(''); + } + + function cmdSatoshi() { + openOverlay(); + writeLine(''); + writeLine('chancellor on brink of second bailout for banks'); + writeLine('— the times, 3 january 2009'); + writeLine(''); + } + + function cmd21m() { + openOverlay(); + writeLine(''); + writeLine('▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100%'); + writeLine('21,000,000 — no more, no less'); + writeLine(''); + } + + // --- Init --- + + function init() { + loadPosterData(); + + hudInput = document.getElementById('hud-input'); + if (!hudInput) return; + + hudInput.addEventListener('keydown', handleHudInput); + + // Close overlay on Escape anywhere + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && overlay && overlay.classList.contains('active')) { + closeOverlay(); + } + // Backtick focuses the HUD input + if (e.key === '`' && !gameActive && document.activeElement !== hudInput && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { + e.preventDefault(); + hudInput.focus(); + hudInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + window.terminal757 = { + open: openOverlay, + close: closeOverlay, + writeLine: writeLine + }; +})();