|
|
<!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> |