const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const cysCanvas = document.getElementById('cystinstein-canvas'); const cysCtx = cysCanvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const CX = canvas.width / 2; const CY = canvas.height / 2; // ─── Leaderboard ───────────────────────────────────────────────────────────── const LEADERBOARD_KEY = 'sysdigger_leaderboard'; const LEADERBOARD_MAX = 10; function loadLeaderboard() { try { return JSON.parse(localStorage.getItem(LEADERBOARD_KEY)) || []; } catch { return []; } } function saveLeaderboard(board) { localStorage.setItem(LEADERBOARD_KEY, JSON.stringify(board)); } function addEntry(name, score) { const board = loadLeaderboard(); board.push({ name: name.trim() || 'Anonymous', score }); board.sort((a, b) => b.score - a.score); const trimmed = board.slice(0, LEADERBOARD_MAX); saveLeaderboard(trimmed); return trimmed; } function renderLeaderboard(highlightEntry) { const board = loadLeaderboard(); const tbody = document.getElementById('leaderboard-rows'); tbody.innerHTML = ''; if (board.length === 0) { const tr = document.createElement('tr'); tr.innerHTML = `No scores yet`; tbody.appendChild(tr); return; } board.forEach((entry, i) => { const tr = document.createElement('tr'); const isHighlight = highlightEntry && entry.name === highlightEntry.name && entry.score === highlightEntry.score; if (isHighlight) tr.classList.add('highlight'); else if (i === 0) tr.classList.add('rank-1'); else if (i === 1) tr.classList.add('rank-2'); else if (i === 2) tr.classList.add('rank-3'); const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : i + 1; tr.innerHTML = ` ${medal} ${escapeHtml(entry.name)} ${entry.score}`; tbody.appendChild(tr); }); } function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } function topScore() { const board = loadLeaderboard(); return board.length > 0 ? board[0].score : null; } // ─── State ─────────────────────────────────────────────────────────────────── let score = 0; let docks = 0; let gameOver = false; let gameStarted = false; let incomingAtoms = []; let anchorPoints = []; let particles = []; let clickEffects = []; let lastSpawn = 0; let spawnInterval = SPAWN_INTERVAL_START; const stars = Array.from({ length: STAR_COUNT }, () => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * STAR_RADIUS_MAX, a: STAR_ALPHA_MIN + Math.random() * STAR_ALPHA_RANGE })); // ─── Anchors ───────────────────────────────────────────────────────────────── function initAnchors() { anchorPoints = []; for (let i = 0; i < NUM_ANCHORS; i++) { const angle = (i / NUM_ANCHORS) * Math.PI * 2 - Math.PI / 2; anchorPoints.push({ x: CX + Math.cos(angle) * ANCHOR_RADIUS, y: CY + Math.sin(angle) * ANCHOR_RADIUS, angle, active: false, blockTimer: 0, pulsePhase: Math.random() * Math.PI * 2 }); } } // ─── Spawn ─────────────────────────────────────────────────────────────────── function spawnAtom() { const available = anchorPoints.map((_, i) => i).filter(i => !anchorPoints[i].active); if (available.length === 0) return; const idx = available[Math.floor(Math.random() * available.length)]; incomingAtoms.push(new IncomingAtom(idx)); } // ─── Input ─────────────────────────────────────────────────────────────────── canvas.addEventListener('click', e => { if (gameOver) return; const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; let hit = false; anchorPoints.forEach((ap, i) => { if (!ap.active) return; if (Math.hypot(mx - ap.x, my - ap.y) > CLICK_RADIUS) return; const atom = incomingAtoms.find(a => a.anchorIndex === i && !a.dead); if (atom) { atom.dead = true; ap.active = false; ap.blockTimer = BLOCK_TIMER_FRAMES; score++; document.getElementById('score').textContent = `Score: ${score}`; burst(ap.x, ap.y, '#00ff88'); clickEffects.push({ x: ap.x, y: ap.y, radius: 5, life: 1 }); hit = true; } }); if (!hit) clickEffects.push({ x: mx, y: my, radius: 5, life: 0.5 }); }); // ─── Game flow ─────────────────────────────────────────────────────────────── function triggerGameOver() { gameOver = true; const best = topScore(); const isNewRecord = best === null || score > best; document.getElementById('final-score').textContent = `Score: ${score}`; document.getElementById('new-record').classList.toggle('visible', isNewRecord); const input = document.getElementById('player-name'); input.value = ''; setTimeout(() => input.focus(), 50); renderLeaderboard(null); document.getElementById('overlay').classList.add('active'); } function submitScore() { const name = document.getElementById('player-name').value.trim() || 'Anonymous'; const entry = { name, score }; addEntry(name, score); renderLeaderboard(entry); const best = topScore(); updateHudBest(best); document.getElementById('name-form').style.display = 'none'; } function restartGame() { score = docks = 0; gameOver = false; gameStarted = false; incomingAtoms = []; particles = []; clickEffects = []; lastSpawn = 0; spawnInterval = SPAWN_INTERVAL_START; document.getElementById('score').textContent = 'Score: 0'; document.getElementById('docks-count').textContent = `0 / ${MAX_DOCKS} docks`; document.getElementById('name-form').style.display = 'flex'; document.getElementById('overlay').classList.remove('active'); document.getElementById('intro').classList.remove('hidden'); startIdleTimer(); initAnchors(); updateCystinstein(); } // ─── Attract mode ──────────────────────────────────────────────────────────── const ATTRACT_DELAY = 30_000; let idleTimer = null; function startIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(showAttract, ATTRACT_DELAY); } function showAttract() { document.getElementById('intro').classList.add('hidden'); renderAttractLeaderboard(); document.getElementById('attract').classList.remove('hidden'); } function showIntro() { document.getElementById('attract').classList.add('hidden'); document.getElementById('intro').classList.remove('hidden'); startIdleTimer(); } function renderAttractLeaderboard() { const board = loadLeaderboard(); const tbody = document.getElementById('attract-rows'); tbody.innerHTML = ''; if (board.length === 0) { tbody.innerHTML = `No scores yet — be the first!`; return; } board.forEach((entry, i) => { const tr = document.createElement('tr'); if (i === 0) tr.classList.add('rank-1'); else if (i === 1) tr.classList.add('rank-2'); else if (i === 2) tr.classList.add('rank-3'); const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : i + 1; tr.innerHTML = ` ${medal} ${escapeHtml(entry.name)} ${entry.score}`; tbody.appendChild(tr); }); } function dismissAttract() { if (!document.getElementById('attract').classList.contains('hidden')) { showIntro(); } } document.addEventListener('keydown', dismissAttract); document.addEventListener('click', dismissAttract); function startGame() { clearTimeout(idleTimer); gameStarted = true; document.getElementById('intro').classList.add('hidden'); } function updateHudBest(best) { document.getElementById('highscore').textContent = best !== null ? `Best: ${best}` : 'Best: —'; } // ─── Enter key on name input ────────────────────────────────────────────────── document.getElementById('player-name').addEventListener('keydown', e => { if (e.key === 'Enter') submitScore(); }); // ─── Main loop ─────────────────────────────────────────────────────────────── function gameLoop(ts) { drawBackground(); if (gameStarted && !gameOver && ts - lastSpawn > spawnInterval) { spawnAtom(); lastSpawn = ts; spawnInterval = Math.max(SPAWN_INTERVAL_MIN, spawnInterval - SPAWN_DIFFICULTY_STEP); } incomingAtoms = incomingAtoms.filter(a => !a.dead); incomingAtoms.forEach(a => { a.update(); a.draw(); }); drawCentralMolecule(); drawAnchorPoints(); drawParticles(); drawClickEffects(); requestAnimationFrame(gameLoop); } // ─── Boot ──────────────────────────────────────────────────────────────────── document.getElementById('intro').innerHTML = document.getElementById('intro').innerHTML.replace('${MAX_DOCKS}', MAX_DOCKS); updateHudBest(topScore()); startIdleTimer(); initAnchors(); updateCystinstein(); requestAnimationFrame(gameLoop);