| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Breakout Game | 打砖块</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
| |
| body { |
| font-family: 'Press Start 2P', cursive; |
| overflow: hidden; |
| background: linear-gradient(135deg, #1a1a2e, #16213e); |
| } |
| |
| #gameCanvas { |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); |
| border-radius: 8px; |
| } |
| |
| .brick { |
| transition: all 0.2s ease; |
| } |
| |
| .brick:hover { |
| transform: scale(1.05); |
| } |
| |
| .power-up { |
| animation: float 2s ease-in-out infinite; |
| } |
| |
| @keyframes float { |
| 0%, 100% { transform: translateY(0); } |
| 50% { transform: translateY(-5px); } |
| } |
| |
| .game-over { |
| animation: pulse 1.5s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.05); } |
| 100% { transform: scale(1); } |
| } |
| </style> |
| </head> |
| <body class="min-h-screen flex flex-col items-center justify-center text-white"> |
| <div class="text-center mb-6"> |
| <h1 class="text-4xl md:text-5xl mb-4 text-cyan-400">BREAKOUT</h1> |
| <h2 class="text-xl md:text-2xl text-cyan-300">打砖块游戏</h2> |
| </div> |
| |
| <div class="relative"> |
| <canvas id="gameCanvas" width="800" height="500" class="bg-gray-900"></canvas> |
| |
| |
| <div id="startScreen" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-80 p-8 rounded-lg"> |
| <h3 class="text-3xl text-cyan-400 mb-6">BREAKOUT</h3> |
| <p class="text-lg mb-8 text-center">Use mouse or arrow keys to move the paddle.<br>Break all bricks to win!</p> |
| <button id="startButton" class="px-8 py-3 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-lg font-bold transition-all transform hover:scale-105"> |
| START GAME |
| </button> |
| <div class="mt-6 text-sm text-gray-400"> |
| <p>Controls: ← → or Mouse</p> |
| <p class="mt-2">Press P to pause</p> |
| </div> |
| </div> |
| |
| |
| <div id="gameOverScreen" class="absolute inset-0 hidden flex-col items-center justify-center bg-black bg-opacity-80 p-8 rounded-lg"> |
| <h3 class="text-3xl text-red-500 mb-4 game-over">GAME OVER</h3> |
| <p id="finalScore" class="text-xl mb-6">Score: 0</p> |
| <button id="restartButton" class="px-8 py-3 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-lg font-bold transition-all transform hover:scale-105"> |
| PLAY AGAIN |
| </button> |
| </div> |
| |
| |
| <div id="winScreen" class="absolute inset-0 hidden flex-col items-center justify-center bg-black bg-opacity-80 p-8 rounded-lg"> |
| <h3 class="text-3xl text-green-400 mb-4">YOU WIN!</h3> |
| <p id="winScore" class="text-xl mb-6">Score: 0</p> |
| <button id="nextLevelButton" class="px-8 py-3 bg-green-600 hover:bg-green-500 rounded-lg text-lg font-bold transition-all transform hover:scale-105 mb-4"> |
| NEXT LEVEL |
| </button> |
| <button id="menuButton" class="px-8 py-3 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-lg font-bold transition-all transform hover:scale-105"> |
| MAIN MENU |
| </button> |
| </div> |
| |
| |
| <div id="pauseScreen" class="absolute inset-0 hidden flex-col items-center justify-center bg-black bg-opacity-80 p-8 rounded-lg"> |
| <h3 class="text-3xl text-yellow-400 mb-6">PAUSED</h3> |
| <button id="resumeButton" class="px-8 py-3 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-lg font-bold transition-all transform hover:scale-105"> |
| RESUME |
| </button> |
| </div> |
| </div> |
| |
| <div class="mt-6 flex justify-between w-full max-w-2xl px-4"> |
| <div class="bg-gray-800 bg-opacity-70 px-6 py-3 rounded-lg"> |
| <span class="text-gray-400">Level:</span> |
| <span id="levelDisplay" class="ml-2 text-yellow-400">1</span> |
| </div> |
| <div class="bg-gray-800 bg-opacity-70 px-6 py-3 rounded-lg"> |
| <span class="text-gray-400">Score:</span> |
| <span id="scoreDisplay" class="ml-2 text-green-400">0</span> |
| </div> |
| <div class="bg-gray-800 bg-opacity-70 px-6 py-3 rounded-lg"> |
| <span class="text-gray-400">Lives:</span> |
| <span id="livesDisplay" class="ml-2 text-red-400">3</span> |
| </div> |
| </div> |
| |
| <script> |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const startScreen = document.getElementById('startScreen'); |
| const gameOverScreen = document.getElementById('gameOverScreen'); |
| const winScreen = document.getElementById('winScreen'); |
| const pauseScreen = document.getElementById('pauseScreen'); |
| const scoreDisplay = document.getElementById('scoreDisplay'); |
| const livesDisplay = document.getElementById('livesDisplay'); |
| const levelDisplay = document.getElementById('levelDisplay'); |
| const finalScore = document.getElementById('finalScore'); |
| const winScore = document.getElementById('winScore'); |
| |
| |
| let gameRunning = false; |
| let gamePaused = false; |
| let score = 0; |
| let lives = 3; |
| let level = 1; |
| |
| |
| const paddleHeight = 15; |
| const paddleWidth = 100; |
| let paddleX = (canvas.width - paddleWidth) / 2; |
| |
| |
| const ballRadius = 10; |
| let ballX = canvas.width / 2; |
| let ballY = canvas.height - 30; |
| let ballSpeedX = 5; |
| let ballSpeedY = -5; |
| |
| |
| const brickRowCount = 5; |
| const brickColumnCount = 9; |
| const brickWidth = 75; |
| const brickHeight = 20; |
| const brickPadding = 10; |
| const brickOffsetTop = 60; |
| const brickOffsetLeft = 30; |
| |
| let bricks = []; |
| |
| |
| const powerUps = []; |
| const powerUpTypes = ['extraLife', 'expandPaddle', 'slowBall', 'multiBall']; |
| |
| |
| function initBricks() { |
| bricks = []; |
| for (let c = 0; c < brickColumnCount; c++) { |
| bricks[c] = []; |
| for (let r = 0; r < brickRowCount; r++) { |
| const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft; |
| const brickY = r * (brickHeight + brickPadding) + brickOffsetTop; |
| |
| |
| let color; |
| switch(r) { |
| case 0: color = '#FF5252'; break; |
| case 1: color = '#FFD740'; break; |
| case 2: color = '#69F0AE'; break; |
| case 3: color = '#40C4FF'; break; |
| case 4: color = '#E040FB'; break; |
| } |
| |
| |
| const points = (brickRowCount - r) * 10; |
| |
| |
| const unbreakable = Math.random() < 0.1 && r === 0; |
| |
| bricks[c][r] = { x: brickX, y: brickY, width: brickWidth, height: brickHeight, |
| color: color, visible: true, points: points, unbreakable: unbreakable }; |
| } |
| } |
| } |
| |
| |
| document.addEventListener('keydown', keyDownHandler); |
| document.addEventListener('keyup', keyUpHandler); |
| document.addEventListener('mousemove', mouseMoveHandler); |
| |
| document.getElementById('startButton').addEventListener('click', startGame); |
| document.getElementById('restartButton').addEventListener('click', startGame); |
| document.getElementById('nextLevelButton').addEventListener('click', nextLevel); |
| document.getElementById('menuButton').addEventListener('click', showMenu); |
| document.getElementById('resumeButton').addEventListener('click', togglePause); |
| |
| |
| let rightPressed = false; |
| let leftPressed = false; |
| |
| function keyDownHandler(e) { |
| if (e.key === 'Right' || e.key === 'ArrowRight') { |
| rightPressed = true; |
| } else if (e.key === 'Left' || e.key === 'ArrowLeft') { |
| leftPressed = true; |
| } else if (e.key === 'p' || e.key === 'P') { |
| togglePause(); |
| } |
| } |
| |
| function keyUpHandler(e) { |
| if (e.key === 'Right' || e.key === 'ArrowRight') { |
| rightPressed = false; |
| } else if (e.key === 'Left' || e.key === 'ArrowLeft') { |
| leftPressed = false; |
| } |
| } |
| |
| |
| function mouseMoveHandler(e) { |
| if (!gameRunning || gamePaused) return; |
| |
| const relativeX = e.clientX - canvas.offsetLeft; |
| if (relativeX > 0 && relativeX < canvas.width) { |
| paddleX = relativeX - paddleWidth / 2; |
| |
| |
| if (paddleX < 0) { |
| paddleX = 0; |
| } else if (paddleX + paddleWidth > canvas.width) { |
| paddleX = canvas.width - paddleWidth; |
| } |
| } |
| } |
| |
| |
| function collisionDetection() { |
| for (let c = 0; c < brickColumnCount; c++) { |
| for (let r = 0; r < brickRowCount; r++) { |
| const brick = bricks[c][r]; |
| if (brick.visible && !brick.unbreakable) { |
| if (ballX > brick.x && ballX < brick.x + brickWidth && |
| ballY > brick.y && ballY < brick.y + brickHeight) { |
| |
| ballSpeedY = -ballSpeedY; |
| brick.visible = false; |
| score += brick.points; |
| scoreDisplay.textContent = score; |
| |
| |
| if (Math.random() < 0.2) { |
| spawnPowerUp(brick.x + brickWidth / 2, brick.y + brickHeight / 2); |
| } |
| |
| |
| if (checkWin()) { |
| showWinScreen(); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| |
| function spawnPowerUp(x, y) { |
| const type = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)]; |
| powerUps.push({ |
| x: x, |
| y: y, |
| width: 20, |
| height: 20, |
| type: type, |
| speed: 2, |
| active: false |
| }); |
| } |
| |
| function activatePowerUp(powerUp) { |
| switch(powerUp.type) { |
| case 'extraLife': |
| lives++; |
| livesDisplay.textContent = lives; |
| break; |
| case 'expandPaddle': |
| paddleWidth = 150; |
| setTimeout(() => { paddleWidth = 100; }, 10000); |
| break; |
| case 'slowBall': |
| ballSpeedX *= 0.7; |
| ballSpeedY *= 0.7; |
| setTimeout(() => { |
| ballSpeedX /= 0.7; |
| ballSpeedY /= 0.7; |
| }, 8000); |
| break; |
| case 'multiBall': |
| |
| score += 50; |
| scoreDisplay.textContent = score; |
| break; |
| } |
| } |
| |
| |
| function checkWin() { |
| for (let c = 0; c < brickColumnCount; c++) { |
| for (let r = 0; r < brickRowCount; r++) { |
| if (bricks[c][r].visible && !bricks[c][r].unbreakable) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| |
| function drawBall() { |
| ctx.beginPath(); |
| ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2); |
| ctx.fillStyle = '#FFFFFF'; |
| ctx.fill(); |
| ctx.closePath(); |
| } |
| |
| function drawPaddle() { |
| ctx.beginPath(); |
| ctx.roundRect(paddleX, canvas.height - paddleHeight - 10, paddleWidth, paddleHeight, 10); |
| ctx.fillStyle = '#40C4FF'; |
| ctx.fill(); |
| ctx.closePath(); |
| } |
| |
| function drawBricks() { |
| for (let c = 0; c < brickColumnCount; c++) { |
| for (let r = 0; r < brickRowCount; r++) { |
| if (bricks[c][r].visible) { |
| ctx.beginPath(); |
| ctx.roundRect(bricks[c][r].x, bricks[c][r].y, brickWidth, brickHeight, 4); |
| ctx.fillStyle = bricks[c][r].unbreakable ? '#555555' : bricks[c][r].color; |
| ctx.fill(); |
| ctx.strokeStyle = '#000000'; |
| ctx.stroke(); |
| ctx.closePath(); |
| |
| |
| if (bricks[c][r].unbreakable) { |
| ctx.beginPath(); |
| ctx.moveTo(bricks[c][r].x + 5, bricks[c][r].y + 5); |
| ctx.lineTo(bricks[c][r].x + brickWidth - 5, bricks[c][r].y + brickHeight - 5); |
| ctx.moveTo(bricks[c][r].x + brickWidth - 5, bricks[c][r].y + 5); |
| ctx.lineTo(bricks[c][r].x + 5, bricks[c][r].y + brickHeight - 5); |
| ctx.strokeStyle = '#333333'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| ctx.closePath(); |
| } |
| } |
| } |
| } |
| } |
| |
| function drawPowerUps() { |
| powerUps.forEach(powerUp => { |
| if (!powerUp.active) { |
| ctx.beginPath(); |
| ctx.roundRect(powerUp.x - powerUp.width / 2, powerUp.y - powerUp.height / 2, |
| powerUp.width, powerUp.height, 4); |
| |
| |
| switch(powerUp.type) { |
| case 'extraLife': ctx.fillStyle = '#FF5252'; break; |
| case 'expandPaddle': ctx.fillStyle = '#69F0AE'; break; |
| case 'slowBall': ctx.fillStyle = '#40C4FF'; break; |
| case 'multiBall': ctx.fillStyle = '#E040FB'; break; |
| } |
| |
| ctx.fill(); |
| ctx.strokeStyle = '#FFFFFF'; |
| ctx.stroke(); |
| ctx.closePath(); |
| |
| |
| ctx.font = '12px Arial'; |
| ctx.fillStyle = '#FFFFFF'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| |
| let symbol = ''; |
| switch(powerUp.type) { |
| case 'extraLife': symbol = '♥'; break; |
| case 'expandPaddle': symbol = '⇆'; break; |
| case 'slowBall': symbol = '⏱'; break; |
| case 'multiBall': symbol = '✦'; break; |
| } |
| |
| ctx.fillText(symbol, powerUp.x, powerUp.y); |
| } |
| }); |
| } |
| |
| |
| function startGame() { |
| gameRunning = true; |
| gamePaused = false; |
| score = 0; |
| lives = 3; |
| level = 1; |
| |
| scoreDisplay.textContent = score; |
| livesDisplay.textContent = lives; |
| levelDisplay.textContent = level; |
| |
| initBricks(); |
| resetBall(); |
| |
| startScreen.classList.add('hidden'); |
| gameOverScreen.classList.add('hidden'); |
| winScreen.classList.add('hidden'); |
| pauseScreen.classList.add('hidden'); |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| function nextLevel() { |
| level++; |
| levelDisplay.textContent = level; |
| |
| |
| ballSpeedX *= 1.1; |
| ballSpeedY *= 1.1; |
| |
| initBricks(); |
| resetBall(); |
| |
| winScreen.classList.add('hidden'); |
| requestAnimationFrame(gameLoop); |
| } |
| |
| function showMenu() { |
| winScreen.classList.add('hidden'); |
| startScreen.classList.remove('hidden'); |
| } |
| |
| function togglePause() { |
| if (!gameRunning) return; |
| |
| gamePaused = !gamePaused; |
| |
| if (gamePaused) { |
| pauseScreen.classList.remove('hidden'); |
| } else { |
| pauseScreen.classList.add('hidden'); |
| requestAnimationFrame(gameLoop); |
| } |
| } |
| |
| function showGameOver() { |
| gameRunning = false; |
| finalScore.textContent = `Score: ${score}`; |
| gameOverScreen.classList.remove('hidden'); |
| } |
| |
| function showWinScreen() { |
| gameRunning = false; |
| winScore.textContent = `Score: ${score}`; |
| winScreen.classList.remove('hidden'); |
| } |
| |
| function resetBall() { |
| ballX = canvas.width / 2; |
| ballY = canvas.height - 30; |
| ballSpeedX = 5 * (Math.random() > 0.5 ? 1 : -1); |
| ballSpeedY = -5; |
| paddleX = (canvas.width - paddleWidth) / 2; |
| } |
| |
| |
| function gameLoop() { |
| if (!gameRunning || gamePaused) return; |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| drawBricks(); |
| drawBall(); |
| drawPaddle(); |
| drawPowerUps(); |
| |
| |
| collisionDetection(); |
| |
| |
| ballX += ballSpeedX; |
| ballY += ballSpeedY; |
| |
| |
| if (ballX + ballSpeedX > canvas.width - ballRadius || ballX + ballSpeedX < ballRadius) { |
| ballSpeedX = -ballSpeedX; |
| } |
| |
| if (ballY + ballSpeedY < ballRadius) { |
| ballSpeedY = -ballSpeedY; |
| } else if (ballY + ballSpeedY > canvas.height - ballRadius) { |
| |
| if (ballX > paddleX && ballX < paddleX + paddleWidth) { |
| |
| const hitPosition = (ballX - (paddleX + paddleWidth / 2)) / (paddleWidth / 2); |
| const angle = hitPosition * Math.PI / 3; |
| |
| ballSpeedX = 5 * Math.sin(angle); |
| ballSpeedY = -5 * Math.cos(angle); |
| } else { |
| |
| lives--; |
| livesDisplay.textContent = lives; |
| |
| if (lives <= 0) { |
| showGameOver(); |
| } else { |
| resetBall(); |
| } |
| } |
| } |
| |
| |
| if (rightPressed && paddleX < canvas.width - paddleWidth) { |
| paddleX += 7; |
| } else if (leftPressed && paddleX > 0) { |
| paddleX -= 7; |
| } |
| |
| |
| for (let i = powerUps.length - 1; i >= 0; i--) { |
| const powerUp = powerUps[i]; |
| |
| if (!powerUp.active) { |
| powerUp.y += powerUp.speed; |
| |
| |
| if (powerUp.y + powerUp.height / 2 > canvas.height - paddleHeight - 10 && |
| powerUp.y - powerUp.height / 2 < canvas.height - 10 && |
| powerUp.x + powerUp.width / 2 > paddleX && |
| powerUp.x - powerUp.width / 2 < paddleX + paddleWidth) { |
| |
| activatePowerUp(powerUp); |
| powerUps.splice(i, 1); |
| } |
| |
| |
| if (powerUp.y > canvas.height) { |
| powerUps.splice(i, 1); |
| } |
| } |
| } |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| initBricks(); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=nkp2mr/test" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |