Spaces:
Running
Running
| // ==================== STAR BACKGROUND ==================== | |
| (function createStars() { | |
| const container = document.getElementById('starsContainer'); | |
| const count = 120; | |
| for (let i = 0; i < count; i++) { | |
| const star = document.createElement('div'); | |
| star.classList.add('star'); | |
| const size = Math.random() * 3 + 1; | |
| star.style.width = size + 'px'; | |
| star.style.height = size + 'px'; | |
| star.style.left = Math.random() * 100 + '%'; | |
| star.style.top = Math.random() * 100 + '%'; | |
| star.style.setProperty('--duration', (Math.random() * 3 + 2) + 's'); | |
| star.style.setProperty('--delay', (Math.random() * 5) + 's'); | |
| container.appendChild(star); | |
| } | |
| })(); | |
| // ==================== HIGH SCORE MANAGEMENT ==================== | |
| function getHighScore(game) { | |
| return parseInt(localStorage.getItem('arcade_' + game + '_highscore')) || 0; | |
| } | |
| function setHighScore(game, score) { | |
| const current = getHighScore(game); | |
| if (score > current) { | |
| localStorage.setItem('arcade_' + game + '_highscore', score); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function updateHighScoreDisplay() { | |
| const games = ['snake', 'pong', 'breakout', 'memory']; | |
| const display = document.getElementById('highScoreDisplay'); | |
| if (!display) return; | |
| let html = '🏆 HIGH SCORES: '; | |
| games.forEach((g, i) => { | |
| const score = getHighScore(g); | |
| html += `${g.toUpperCase()}: ${score}`; | |
| if (i < games.length - 1) html += ' | '; | |
| }); | |
| display.innerHTML = html; | |
| } | |
| updateHighScoreDisplay(); | |
| // ==================== MODAL MANAGEMENT ==================== | |
| const modal = document.getElementById('gameModal'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const gameArea = document.getElementById('gameArea'); | |
| const gameScoreEl = document.getElementById('gameScore'); | |
| const mobileControls = document.getElementById('mobileControls'); | |
| const restartBtn = document.getElementById('restartGame'); | |
| const closeModalBtn = document.getElementById('closeModal'); | |
| let currentGame = null; | |
| let gameLoopId = null; | |
| let gameState = null; | |
| function openModal(gameName) { | |
| modal.classList.remove('hidden'); | |
| document.body.style.overflow = 'hidden'; | |
| currentGame = gameName; | |
| gameState = {}; | |
| mobileControls.classList.add('hidden'); | |
| restartBtn.classList.add('hidden'); | |
| gameScoreEl.textContent = ''; | |
| gameArea.innerHTML = ''; | |
| switch (gameName) { | |
| case 'snake': | |
| modalTitle.textContent = '🐍 SNAKE'; | |
| startSnake(); | |
| break; | |
| case 'pong': | |
| modalTitle.textContent = '🏓 PONG'; | |
| startPong(); | |
| break; | |
| case 'breakout': | |
| modalTitle.textContent = '🧱 BREAKOUT'; | |
| startBreakout(); | |
| break; | |
| case 'memory': | |
| modalTitle.textContent = '🧠 MEMORY MATCH'; | |
| startMemory(); | |
| break; | |
| } | |
| } | |
| function closeModal() { | |
| modal.classList.add('hidden'); | |
| document.body.style.overflow = ''; | |
| if (gameLoopId) { | |
| cancelAnimationFrame(gameLoopId); | |
| gameLoopId = null; | |
| } | |
| // Remove event listeners by cloning canvas | |
| if (gameArea.querySelector('canvas')) { | |
| const canvas = gameArea.querySelector('canvas'); | |
| const clone = canvas.cloneNode(true); | |
| canvas.parentNode.replaceChild(clone, canvas); | |
| } | |
| currentGame = null; | |
| gameState = null; | |
| gameArea.innerHTML = ''; | |
| updateHighScoreDisplay(); | |
| } | |
| closeModalBtn.addEventListener('click', closeModal); | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) closeModal(); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && currentGame) { | |
| closeModal(); | |
| } | |
| }); | |
| // ==================== GAME CARDS CLICK ==================== | |
| document.querySelectorAll('.game-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| const game = card.dataset.game; | |
| if (game) openModal(game); | |
| }); | |
| }); | |
| // ==================== SNAKE GAME ==================== | |
| function startSnake() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 400; | |
| canvas.height = 400; | |
| canvas.style.background = '#0a0a1a'; | |
| canvas.style.border = '2px solid var(--neon-green)'; | |
| canvas.style.boxShadow = '0 0 10px var(--neon-green)'; | |
| gameArea.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| const gridSize = 20; | |
| const cellSize = canvas.width / gridSize; | |
| let snake = [{ x: 10, y: 10 }]; | |
| let direction = { x: 1, y: 0 }; | |
| let nextDirection = { x: 1, y: 0 }; | |
| let food = spawnFood(); | |
| let score = 0; | |
| let speed = 120; | |
| let lastTime = 0; | |
| let gameOver = false; | |
| restartBtn.classList.remove('hidden'); | |
| restartBtn.onclick = () => { cancelAnimationFrame(gameLoopId); startSnake(); }; | |
| // Show mobile D-pad on touch devices | |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { | |
| mobileControls.classList.remove('hidden'); | |
| mobileControls.querySelectorAll('.mobile-btn').forEach(btn => { | |
| btn.onclick = (e) => { | |
| e.preventDefault(); | |
| const dir = btn.dataset.dir; | |
| changeDir(dir); | |
| }; | |
| }); | |
| } | |
| function spawnFood() { | |
| let pos; | |
| do { | |
| pos = { | |
| x: Math.floor(Math.random() * gridSize), | |
| y: Math.floor(Math.random() * gridSize) | |
| }; | |
| } while (snake.some(seg => seg.x === pos.x && seg.y === pos.y)); | |
| return pos; | |
| } | |
| function changeDir(dir) { | |
| if (gameOver) return; | |
| switch (dir) { | |
| case 'up': if (direction.y !== 1) nextDirection = { x: 0, y: -1 }; break; | |
| case 'down': if (direction.y !== -1) nextDirection = { x: 0, y: 1 }; break; | |
| case 'left': if (direction.x !== 1) nextDirection = { x: -1, y: 0 }; break; | |
| case 'right': if (direction.x !== -1) nextDirection = { x: 1, y: 0 }; break; | |
| } | |
| } | |
| document.addEventListener('keydown', handleKey); | |
| function handleKey(e) { | |
| if (currentGame !== 'snake') return; | |
| switch (e.key) { | |
| case 'ArrowUp': case 'w': case 'W': e.preventDefault(); changeDir('up'); break; | |
| case 'ArrowDown': case 's': case 'S': e.preventDefault(); changeDir('down'); break; | |
| case 'ArrowLeft': case 'a': case 'A': e.preventDefault(); changeDir('left'); break; | |
| case 'ArrowRight': case 'd': case 'D': e.preventDefault(); changeDir('right'); break; | |
| } | |
| } | |
| gameState.cleanup = () => document.removeEventListener('keydown', handleKey); | |
| function gameLoop(timestamp) { | |
| if (gameOver) return; | |
| if (timestamp - lastTime > speed) { | |
| lastTime = timestamp; | |
| direction = { ...nextDirection }; | |
| const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; | |
| // Wall collision | |
| if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) { | |
| endGame(); | |
| return; | |
| } | |
| // Self collision | |
| if (snake.some(seg => seg.x === head.x && seg.y === head.y)) { | |
| endGame(); | |
| return; | |
| } | |
| snake.unshift(head); | |
| // Food | |
| if (head.x === food.x && head.y === food.y) { | |
| score += 10; | |
| food = spawnFood(); | |
| speed = Math.max(50, speed - 2); | |
| gameScoreEl.textContent = 'Score: ' + score; | |
| } else { | |
| snake.pop(); | |
| } | |
| } | |
| // Draw | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Grid | |
| ctx.strokeStyle = 'rgba(57, 255, 20, 0.05)'; | |
| ctx.lineWidth = 0.5; | |
| for (let i = 0; i < gridSize; i++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(i * cellSize, 0); | |
| ctx.lineTo(i * cellSize, canvas.height); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, i * cellSize); | |
| ctx.lineTo(canvas.width, i * cellSize); | |
| ctx.stroke(); | |
| } | |
| // Snake body | |
| snake.forEach((seg, i) => { | |
| const gradient = ctx.createRadialGradient( | |
| seg.x * cellSize + cellSize / 2, seg.y * cellSize + cellSize / 2, 2, | |
| seg.x * cellSize + cellSize / 2, seg.y * cellSize + cellSize / 2, cellSize / 1.5 | |
| ); | |
| if (i === 0) { | |
| gradient.addColorStop(0, '#39ff14'); | |
| gradient.addColorStop(1, '#1a8a0a'); | |
| } else { | |
| gradient.addColorStop(0, '#2ad810'); | |
| gradient.addColorStop(1, '#0d5a05'); | |
| } | |
| ctx.fillStyle = gradient; | |
| ctx.shadowColor = '#39ff14'; | |
| ctx.shadowBlur = i === 0 ? 12 : 6; | |
| ctx.fillRect(seg.x * cellSize + 2, seg.y * cellSize + 2, cellSize - 4, cellSize - 4); | |
| ctx.shadowBlur = 0; | |
| }); | |
| // Food | |
| ctx.fillStyle = '#ff00e5'; | |
| ctx.shadowColor = '#ff00e5'; | |
| ctx.shadowBlur = 15; | |
| ctx.beginPath(); | |
| ctx.arc(food.x * cellSize + cellSize / 2, food.y * cellSize + cellSize / 2, cellSize / 2 - 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| function endGame() { | |
| gameOver = true; | |
| const isNew = setHighScore('snake', score); | |
| gameScoreEl.textContent = 'GAME OVER! Score: ' + score + (isNew ? ' 🏆 NEW HIGH!' : ''); | |
| gameScoreEl.classList.add('neon-text-pink'); | |
| // Draw game over overlay | |
| ctx.fillStyle = 'rgba(0,0,0,0.7)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.font = '24px "Press Start 2P", cursive'; | |
| ctx.fillStyle = '#ff00e5'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 10); | |
| ctx.font = '14px "Orbitron", sans-serif'; | |
| ctx.fillText('Score: ' + score, canvas.width / 2, canvas.height / 2 + 30); | |
| cancelAnimationFrame(gameLoopId); | |
| } | |
| gameScoreEl.textContent = 'Score: 0'; | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| // ==================== PONG GAME ==================== | |
| function startPong() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 500; | |
| canvas.height = 350; | |
| canvas.style.background = '#0a0a1a'; | |
| canvas.style.border = '2px solid var(--neon-pink)'; | |
| canvas.style.boxShadow = '0 0 10px var(--neon-pink)'; | |
| gameArea.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| const paddleW = 12; | |
| const paddleH = 70; | |
| const ballSize = 10; | |
| let playerY = canvas.height / 2 - paddleH / 2; | |
| let aiY = canvas.height / 2 - paddleH / 2; | |
| let ballX = canvas.width / 2; | |
| let ballY = canvas.height / 2; | |
| let ballVX = (Math.random() > 0.5 ? 1 : -1) * 4; | |
| let ballVY = (Math.random() * 6 - 3); | |
| let playerScore = 0; | |
| let aiScore = 0; | |
| const winScore = 7; | |
| let gameOver = false; | |
| let keys = {}; | |
| restartBtn.classList.remove('hidden'); | |
| restartBtn.onclick = () => { cancelAnimationFrame(gameLoopId); startPong(); }; | |
| document.addEventListener('keydown', (e) => { keys[e.key] = true; }); | |
| document.addEventListener('keyup', (e) => { keys[e.key] = false; }); | |
| // Mouse/touch support | |
| canvas.addEventListener('mousemove', (e) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleY = canvas.height / rect.height; | |
| playerY = (e.clientY - rect.top) * scaleY - paddleH / 2; | |
| playerY = Math.max(0, Math.min(canvas.height - paddleH, playerY)); | |
| }); | |
| canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleY = canvas.height / rect.height; | |
| playerY = (e.touches[0].clientY - rect.top) * scaleY - paddleH / 2; | |
| playerY = Math.max(0, Math.min(canvas.height - paddleH, playerY)); | |
| }, { passive: false }); | |
| gameState.cleanup = () => {}; | |
| function resetBall() { | |
| ballX = canvas.width / 2; | |
| ballY = canvas.height / 2; | |
| ballVX = (Math.random() > 0.5 ? 1 : -1) * 4; | |
| ballVY = (Math.random() * 6 - 3); | |
| } | |
| function gameLoop() { | |
| if (gameOver) return; | |
| // Player movement | |
| if (keys['ArrowUp'] || keys['w'] || keys['W']) playerY -= 6; | |
| if (keys['ArrowDown'] || keys['s'] || keys['S']) playerY += 6; | |
| playerY = Math.max(0, Math.min(canvas.height - paddleH, playerY)); | |
| // AI movement | |
| const aiCenter = aiY + paddleH / 2; | |
| if (aiCenter < ballY - 15) aiY += 3.5; | |
| else if (aiCenter > ballY + 15) aiY -= 3.5; | |
| aiY = Math.max(0, Math.min(canvas.height - paddleH, aiY)); | |
| // Ball movement | |
| ballX += ballVX; | |
| ballY += ballVY; | |
| // Top/bottom wall bounce | |
| if (ballY - ballSize / 2 <= 0 || ballY + ballSize / 2 >= canvas.height) { | |
| ballVY *= -1; | |
| ballY = Math.max(ballSize / 2, Math.min(canvas.height - ballSize / 2, ballY)); | |
| } | |
| // Player paddle collision | |
| if (ballX - ballSize / 2 <= 20 + paddleW && | |
| ballY > playerY && ballY < playerY + paddleH && | |
| ballVX < 0) { | |
| const hitPos = (ballY - playerY) / paddleH; | |
| ballVX *= -1.05; | |
| ballVY = (hitPos - 0.5) * 8; | |
| ballX = 20 + paddleW + ballSize / 2; | |
| } | |
| // AI paddle collision | |
| if (ballX + ballSize / 2 >= canvas.width - 20 - paddleW && | |
| ballY > aiY && ballY < aiY + paddleH && | |
| ballVX > 0) { | |
| const hitPos = (ballY - aiY) / paddleH; | |
| ballVX *= -1.05; | |
| ballVY = (hitPos - 0.5) * 8; | |
| ballX = canvas.width - 20 - paddleW - ballSize / 2; | |
| } | |
| // Scoring | |
| if (ballX < 0) { | |
| aiScore++; | |
| if (aiScore >= winScore) endPongGame(); | |
| else resetBall(); | |
| } | |
| if (ballX > canvas.width) { | |
| playerScore++; | |
| if (playerScore >= winScore) endPongGame(); | |
| else resetBall(); | |
| } | |
| // Clamp ball speed | |
| const speed = Math.sqrt(ballVX * ballVX + ballVY * ballVY); | |
| if (speed > 10) { | |
| ballVX = (ballVX / speed) * 10; | |
| ballVY = (ballVY / speed) * 10; | |
| } | |
| // Draw | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Center line | |
| ctx.setLineDash([8, 12]); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.1)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(canvas.width / 2, 0); | |
| ctx.lineTo(canvas.width / 2, canvas.height); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Paddles | |
| ctx.fillStyle = '#ff00e5'; | |
| ctx.shadowColor = '#ff00e5'; | |
| ctx.shadowBlur = 10; | |
| ctx.fillRect(20, playerY, paddleW, paddleH); | |
| ctx.fillStyle = '#00f0ff'; | |
| ctx.shadowColor = '#00f0ff'; | |
| ctx.fillRect(canvas.width - 20 - paddleW, aiY, paddleW, paddleH); | |
| // Ball | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.shadowColor = '#ffffff'; | |
| ctx.shadowBlur = 12; | |
| ctx.beginPath(); | |
| ctx.arc(ballX, ballY, ballSize / 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| // Scores | |
| ctx.font = '28px "Press Start 2P", cursive'; | |
| ctx.fillStyle = '#ff00e5'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(playerScore, canvas.width / 4, 45); | |
| ctx.fillStyle = '#00f0ff'; | |
| ctx.fillText(aiScore, (canvas.width / 4) * 3, 45); | |
| gameScoreEl.textContent = `YOU ${playerScore} - ${aiScore} AI | First to ${winScore}`; | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| function endPongGame() { | |
| gameOver = true; | |
| const winner = playerScore >= winScore ? 'YOU WIN!' : 'AI WINS!'; | |
| const isNew = setHighScore('pong', playerScore); | |
| gameScoreEl.textContent = winner + ' Final: ' + playerScore + '-' + aiScore + (isNew ? ' 🏆' : ''); | |
| ctx.fillStyle = 'rgba(0,0,0,0.7)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.font = '24px "Press Start 2P", cursive'; | |
| ctx.fillStyle = playerScore >= winScore ? '#39ff14' : '#ff00e5'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(winner, canvas.width / 2, canvas.height / 2); | |
| cancelAnimationFrame(gameLoopId); | |
| } | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| // ==================== BREAKOUT GAME ==================== | |
| function startBreakout() { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 480; | |
| canvas.height = 400; | |
| canvas.style.background = '#0a0a1a'; | |
| canvas.style.border = '2px solid var(--neon-cyan)'; | |
| canvas.style.boxShadow = '0 0 10px var(--neon-cyan)'; | |
| gameArea.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| const paddleW = 80; | |
| const paddleH = 12; | |
| const ballRadius = 7; | |
| const brickRows = 6; | |
| const brickCols = 8; | |
| const brickW = (canvas.width - 40) / brickCols; | |
| const brickH = 22; | |
| let paddleX = canvas.width / 2 - paddleW / 2; | |
| let ballX = canvas.width / 2; | |
| let ballY = canvas.height - 60; | |
| let ballVX = (Math.random() * 4 - 2); | |
| let ballVY = -5; | |
| let score = 0; | |
| let gameOver = false; | |
| let ballLaunched = false; | |
| const brickColors = ['#ff00e5', '#ff6600', '#ffff00', '#39ff14', '#00f0ff', '#8b5cf6']; | |
| let bricks = []; | |
| for (let row = 0; row < brickRows; row++) { | |
| bricks[row] = []; | |
| for (let col = 0; col < brickCols; col++) { | |
| bricks[row][col] = { alive: true, color: brickColors[row] }; | |
| } | |
| } | |
| restartBtn.classList.remove('hidden'); | |
| restartBtn.onclick = () => { cancelAnimationFrame(gameLoopId); startBreakout(); }; | |
| // Mouse/touch | |
| canvas.addEventListener('mousemove', (e) => { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| paddleX = (e.clientX - rect.left) * scaleX - paddleW / 2; | |
| paddleX = Math.max(0, Math.min(canvas.width - paddleW, paddleX)); | |
| if (!ballLaunched) ballX = paddleX + paddleW / 2; | |
| }); | |
| canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| paddleX = (e.touches[0].clientX - rect.left) * scaleX - paddleW / 2; | |
| paddleX = Math.max(0, Math.min(canvas.width - paddleW, paddleX)); | |
| if (!ballLaunched) ballX = paddleX + paddleW / 2; | |
| }, { passive: false }); | |
| canvas.addEventListener('click', () => { | |
| if (!ballLaunched && !gameOver) { | |
| ballLaunched = true; | |
| ballVY = -5; | |
| ballVX = (Math.random() * 4 - 2) || 2; | |
| } | |
| }); | |
| gameState.cleanup = () => {}; | |
| function gameLoop() { | |
| if (gameOver) return; | |
| // Ball movement | |
| if (ballLaunched) { | |
| ballX += ballVX; | |
| ballY += ballVY; | |
| // Wall collisions | |
| if (ballX - ballRadius <= 0 || ballX + ballRadius >= canvas.width) ballVX *= -1; | |
| if (ballY - ballRadius <= 0) ballVY *= -1; | |
| // Bottom (game over) | |
| if (ballY + ballRadius >= canvas.height) { | |
| endBreakout(false); | |
| return; | |
| } | |
| // Paddle collision | |
| if (ballY + ballRadius >= canvas.height - paddleH - 10 && | |
| ballY + ballRadius <= canvas.height - 5 && | |
| ballX > paddleX && ballX < paddleX + paddleW && | |
| ballVY > 0) { | |
| const hitPos = (ballX - paddleX) / paddleW; | |
| ballVY *= -1; | |
| ballVX = (hitPos - 0.5) * 7; | |
| ballY = canvas.height - paddleH - 10 - ballRadius; | |
| const speed = Math.sqrt(ballVX * ballVX + ballVY * ballVY); | |
| if (speed < 5) { | |
| ballVX = (ballVX / speed) * 5; | |
| ballVY = (ballVY / speed) * 5; | |
| } | |
| } | |
| // Brick collisions | |
| let allDestroyed = true; | |
| for (let row = 0; row < brickRows; row++) { | |
| for (let col = 0; col < brickCols; col++) { | |
| if (!bricks[row][col].alive) continue; | |
| allDestroyed = false; | |
| const bx = 20 + col * brickW; | |
| const by = 40 + row * brickH; | |
| if (ballX + ballRadius > bx && ballX - ballRadius < bx + brickW && | |
| ballY + ballRadius > by && ballY - ballRadius < by + brickH) { | |
| bricks[row][col].alive = false; | |
| score += 10; | |
| gameScoreEl.textContent = 'Score: ' + score; | |
| // Determine bounce direction | |
| const overlapLeft = (ballX + ballRadius) - bx; | |
| const overlapRight = (bx + brickW) - (ballX - ballRadius); | |
| const overlapTop = (ballY + ballRadius) - by; | |
| const overlapBottom = (by + brickH) - (ballY - ballRadius); | |
| const minOverlapX = Math.min(overlapLeft, overlapRight); | |
| const minOverlapY = Math.min(overlapTop, overlapBottom); | |
| if (minOverlapX < minOverlapY) ballVX *= -1; | |
| else ballVY *= -1; | |
| } | |
| } | |
| } | |
| if (allDestroyed) { | |
| endBreakout(true); | |
| return; | |
| } | |
| } | |
| // Clamp ball | |
| if (ballLaunched) { | |
| const speed = Math.sqrt(ballVX * ballVX + ballVY * ballVY); | |
| if (speed > 9) { | |
| ballVX = (ballVX / speed) * 9; | |
| ballVY = (ballVY / speed) * 9; | |
| } | |
| } | |
| // Draw | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Bricks | |
| for (let row = 0; row < brickRows; row++) { | |
| for (let col = 0; col < brickCols; col++) { | |
| if (!bricks[row][col].alive) continue; | |
| const bx = 20 + col * brickW; | |
| const by = 40 + row * brickH; | |
| ctx.fillStyle = bricks[row][col].color; | |
| ctx.shadowColor = bricks[row][col].color; | |
| ctx.shadowBlur = 4; | |
| ctx.fillRect(bx + 2, by + 2, brickW - 4, brickH - 4); | |
| ctx.shadowBlur = 0; | |
| } | |
| } | |
| // Paddle | |
| ctx.fillStyle = '#00f0ff'; | |
| ctx.shadowColor = '#00f0ff'; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.roundRect(paddleX, canvas.height - paddleH - 10, paddleW, paddleH, 6); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| // Ball | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.shadowColor = '#ffffff'; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| if (!ballLaunched && !gameOver) { | |
| ctx.font = '14px "Orbitron", sans-serif'; | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('CLICK TO LAUNCH', canvas.width / 2, canvas.height - 40); | |
| } | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| function endBreakout(won) { | |
| gameOver = true; | |
| const isNew = setHighScore('breakout', score); | |
| const msg = won ? 'YOU WIN! 🎉' : 'GAME OVER'; | |
| gameScoreEl.textContent = msg + ' Score: ' + score + (isNew ? ' 🏆 NEW HIGH!' : ''); | |
| ctx.fillStyle = 'rgba(0,0,0,0.7)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.font = '22px "Press Start 2P", cursive'; | |
| ctx.fillStyle = won ? '#39ff14' : '#ff00e5'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(won ? 'YOU WIN!' : 'GAME OVER', canvas.width / 2, canvas.height / 2 - 10); | |
| ctx.font = '14px "Orbitron", sans-serif'; | |
| ctx.fillText('Score: ' + score, canvas.width / 2, canvas.height / 2 + 30); | |
| cancelAnimationFrame(gameLoopId); | |
| } | |
| gameScoreEl.textContent = 'Score: 0'; | |
| gameLoopId = requestAnimationFrame(gameLoop); | |
| } | |
| // ==================== MEMORY MATCH GAME ==================== | |
| function startMemory() { | |
| const emojis = ['🚀', '🎮', '🕹️', '👾', '💎', '🔥', '⚡', '🌟']; | |
| const cards = [...emojis, ...emojis]; | |
| // Shuffle | |
| for (let i = cards.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [cards[i], cards[j]] = [cards[j], cards[i]]; | |
| } | |
| let flippedCards = []; | |
| let matchedCount = 0; | |
| let moves = 0; | |
| let lockBoard = false; | |
| let gameOver = false; | |
| const grid = document.createElement('div'); | |
| grid.style.display = 'grid'; | |
| grid.style.gridTemplateColumns = 'repeat(4, 1fr)'; | |
| grid.style.gap = '10px'; | |
| grid.style.maxWidth = '360px'; | |
| grid.style.width = '100%'; | |
| grid.style.margin = '0 auto'; | |
| gameArea.appendChild(grid); | |
| restartBtn.classList.remove('hidden'); | |
| restartBtn.onclick = () => { startMemory(); }; | |
| cards.forEach((emoji, index) => { | |
| const card = document.createElement('div'); | |
| card.classList.add('memory-card'); | |
| card.dataset.index = index; | |
| card.dataset.emoji = emoji; | |
| card.innerHTML = ` | |
| <div class="memory-card-inner"> | |
| <div class="memory-card-back">?</div> | |
| <div class="memory-card-front">${emoji}</div> | |
| </div> | |
| `; | |
| card.addEventListener('click', () => { | |
| if (lockBoard || gameOver) return; | |
| if (card.classList.contains('flipped') || card.classList.contains('matched')) return; | |
| if (flippedCards.length >= 2) return; | |
| card.classList.add('flipped'); | |
| flippedCards.push(card); | |
| if (flippedCards.length === 2) { | |
| moves++; | |
| gameScoreEl.textContent = 'Moves: ' + moves; | |
| lockBoard = true; | |
| const [card1, card2] = flippedCards; | |
| if (card1.dataset.emoji === card2.dataset.emoji) { | |
| // Match! | |
| setTimeout(() => { | |
| card1.classList.add('matched'); | |
| card2.classList.add('matched'); | |
| matchedCount++; | |
| flippedCards = []; | |
| lockBoard = false; | |
| if (matchedCount === emojis.length) { | |
| gameOver = true; | |
| const isNew = setHighScore('memory', moves); | |
| gameScoreEl.textContent = '🎉 ALL MATCHED! Moves: ' + moves + (isNew ? ' 🏆 BEST!' : ''); | |
| gameScoreEl.classList.add('neon-text-green'); | |
| } | |
| }, 400); | |
| } else { | |
| // No match | |
| setTimeout(() => { | |
| card1.classList.remove('flipped'); | |
| card2.classList.remove('flipped'); | |
| flippedCards = []; | |
| lockBoard = false; | |
| }, 700); | |
| } | |
| } | |
| }); | |
| grid.appendChild(card); | |
| }); | |
| gameScoreEl.textContent = 'Moves: 0'; | |
| gameState.cleanup = () => {}; | |
| } | |
| // ==================== INITIALIZATION ==================== | |
| console.log('🕹️ Neon Arcade Zone ready!'); | |
| console.log('🎮 Click a game card to play!'); | |
| console.log('🐍 Snake | 🏓 Pong | 🧱 Breakout | 🧠 Memory Match'); |