|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Asteroids Arcade</title> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap'); |
|
|
|
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
background-color: #000; |
|
|
color: #0f0; |
|
|
font-family: 'Orbitron', sans-serif; |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
height: 100vh; |
|
|
text-shadow: 0 0 5px #0f0; |
|
|
} |
|
|
|
|
|
#game-header { |
|
|
text-align: center; |
|
|
margin-bottom: 20px; |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { opacity: 0.8; } |
|
|
50% { opacity: 1; text-shadow: 0 0 15px #0f0; } |
|
|
100% { opacity: 0.8; } |
|
|
} |
|
|
|
|
|
#game-container { |
|
|
position: relative; |
|
|
width: 800px; |
|
|
height: 600px; |
|
|
border: 3px solid #0f0; |
|
|
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3), |
|
|
inset 0 0 20px rgba(0, 255, 0, 0.3); |
|
|
background-color: #111; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
canvas { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
#ui { |
|
|
position: absolute; |
|
|
top: 15px; |
|
|
left: 15px; |
|
|
font-size: 20px; |
|
|
display: flex; |
|
|
gap: 30px; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
#controls { |
|
|
position: absolute; |
|
|
bottom: 15px; |
|
|
left: 15px; |
|
|
font-size: 16px; |
|
|
color: #0f0; |
|
|
background-color: rgba(0, 0, 0, 0.5); |
|
|
padding: 10px; |
|
|
border-radius: 5px; |
|
|
border: 1px solid #0f0; |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.control-group { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.key { |
|
|
display: inline-block; |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
background-color: #222; |
|
|
border: 2px solid #0f0; |
|
|
border-radius: 5px; |
|
|
text-align: center; |
|
|
line-height: 26px; |
|
|
margin-right: 5px; |
|
|
} |
|
|
|
|
|
.key.space { |
|
|
width: 80px; |
|
|
} |
|
|
|
|
|
#game-over { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0, 0, 0, 0.8); |
|
|
display: none; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
z-index: 100; |
|
|
} |
|
|
|
|
|
#game-over h2 { |
|
|
font-size: 72px; |
|
|
color: #f00; |
|
|
text-shadow: 0 0 15px #f00; |
|
|
margin-bottom: 30px; |
|
|
animation: flicker 0.5s infinite alternate; |
|
|
} |
|
|
|
|
|
@keyframes flicker { |
|
|
0% { opacity: 0.7; } |
|
|
100% { opacity: 1; } |
|
|
} |
|
|
|
|
|
#final-score { |
|
|
font-size: 24px; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
#restart { |
|
|
padding: 12px 25px; |
|
|
font-family: 'Orbitron', sans-serif; |
|
|
font-size: 18px; |
|
|
background: linear-gradient(#222, #111); |
|
|
color: #0f0; |
|
|
border: 2px solid #0f0; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
#restart:hover { |
|
|
background: linear-gradient(#333, #222); |
|
|
box-shadow: 0 0 15px #0f0; |
|
|
transform: scale(1.05); |
|
|
} |
|
|
|
|
|
#mobile-controls { |
|
|
display: none; |
|
|
position: absolute; |
|
|
bottom: 80px; |
|
|
width: 100%; |
|
|
justify-content: center; |
|
|
gap: 20px; |
|
|
z-index: 10; |
|
|
} |
|
|
|
|
|
.mobile-btn { |
|
|
width: 70px; |
|
|
height: 70px; |
|
|
background-color: rgba(0, 255, 0, 0.2); |
|
|
border: 2px solid #0f0; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
color: #0f0; |
|
|
font-size: 24px; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
@media (max-width: 850px) { |
|
|
#game-container { |
|
|
width: 100vw; |
|
|
height: 60vh; |
|
|
border: none; |
|
|
border-bottom: 3px solid #0f0; |
|
|
} |
|
|
|
|
|
#mobile-controls { |
|
|
display: flex; |
|
|
} |
|
|
|
|
|
#controls { |
|
|
display: none; |
|
|
} |
|
|
} |
|
|
|
|
|
#start-screen { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0, 0, 0, 0.9); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
z-index: 200; |
|
|
} |
|
|
|
|
|
#start-screen h1 { |
|
|
font-size: 60px; |
|
|
margin-bottom: 30px; |
|
|
color: #0f0; |
|
|
text-shadow: 0 0 15px #0f0; |
|
|
} |
|
|
|
|
|
#start-screen p { |
|
|
font-size: 20px; |
|
|
margin-bottom: 40px; |
|
|
max-width: 600px; |
|
|
text-align: center; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
#start-btn { |
|
|
padding: 15px 40px; |
|
|
font-family: 'Orbitron', sans-serif; |
|
|
font-size: 24px; |
|
|
background: linear-gradient(#222, #111); |
|
|
color: #0f0; |
|
|
border: 2px solid #0f0; |
|
|
border-radius: 5px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
#start-btn:hover { |
|
|
background: linear-gradient(#333, #222); |
|
|
box-shadow: 0 0 15px #0f0; |
|
|
transform: scale(1.05); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="game-header"> |
|
|
<h1>ASTEROIDS</h1> |
|
|
</div> |
|
|
|
|
|
<div id="game-container"> |
|
|
<canvas id="game-canvas"></canvas> |
|
|
|
|
|
<div id="ui"> |
|
|
<div id="score-display">SCORE: <span id="score">0</span></div> |
|
|
<div id="lives-display">LIVES: <span id="lives">3</span></div> |
|
|
</div> |
|
|
|
|
|
<div id="controls"> |
|
|
<div class="control-group"> |
|
|
<div>SHIP CONTROLS:</div> |
|
|
<div><span class="key">←</span> Rotate Left</div> |
|
|
<div><span class="key">→</span> Rotate Right</div> |
|
|
<div><span class="key">↑</span> Thrust</div> |
|
|
</div> |
|
|
<div class="control-group"> |
|
|
<div>WEAPONS:</div> |
|
|
<div><span class="key space">SPACE</span> Fire</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="mobile-controls"> |
|
|
<div class="mobile-btn" id="left-btn">←</div> |
|
|
<div class="mobile-btn" id="up-btn">↑</div> |
|
|
<div class="mobile-btn" id="right-btn">→</div> |
|
|
<div class="mobile-btn" id="fire-btn">FIRE</div> |
|
|
</div> |
|
|
|
|
|
<div id="game-over"> |
|
|
<h2>GAME OVER</h2> |
|
|
<div id="final-score">FINAL SCORE: <span id="end-score">0</span></div> |
|
|
<button id="restart">PLAY AGAIN</button> |
|
|
</div> |
|
|
|
|
|
<div id="start-screen"> |
|
|
<h1>ASTEROIDS ARCADE</h1> |
|
|
<p>Navigate your spaceship through an asteroid field. Destroy asteroids to earn points, but be careful - hitting them will cost you a life!</p> |
|
|
<button id="start-btn">START GAME</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const canvas = document.getElementById('game-canvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const scoreElement = document.getElementById('score'); |
|
|
const livesElement = document.getElementById('lives'); |
|
|
const endScoreElement = document.getElementById('end-score'); |
|
|
const gameOverElement = document.getElementById('game-over'); |
|
|
const restartButton = document.getElementById('restart'); |
|
|
const startScreen = document.getElementById('start-screen'); |
|
|
const startButton = document.getElementById('start-btn'); |
|
|
|
|
|
|
|
|
const leftBtn = document.getElementById('left-btn'); |
|
|
const rightBtn = document.getElementById('right-btn'); |
|
|
const upBtn = document.getElementById('up-btn'); |
|
|
const fireBtn = document.getElementById('fire-btn'); |
|
|
|
|
|
|
|
|
function resizeCanvas() { |
|
|
canvas.width = canvas.parentElement.clientWidth; |
|
|
canvas.height = canvas.parentElement.clientHeight; |
|
|
} |
|
|
|
|
|
resizeCanvas(); |
|
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
|
|
|
let score = 0; |
|
|
let lives = 3; |
|
|
let gameOver = false; |
|
|
let gameStarted = false; |
|
|
let asteroids = []; |
|
|
let bullets = []; |
|
|
let particles = []; |
|
|
let keys = {}; |
|
|
|
|
|
|
|
|
const ship = { |
|
|
x: canvas.width / 2, |
|
|
y: canvas.height / 2, |
|
|
radius: 15, |
|
|
angle: -Math.PI/2, |
|
|
rotation: 0, |
|
|
thrusting: false, |
|
|
thrust: { |
|
|
x: 0, |
|
|
y: 0 |
|
|
}, |
|
|
velocity: { |
|
|
x: 0, |
|
|
y: 0 |
|
|
}, |
|
|
invulnerable: false, |
|
|
invulnerableTime: 0 |
|
|
}; |
|
|
|
|
|
|
|
|
window.addEventListener('keydown', (e) => { |
|
|
if (gameStarted) { |
|
|
keys[e.key] = true; |
|
|
|
|
|
|
|
|
if (e.key === ' ' && e.target === document.body) { |
|
|
e.preventDefault(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
window.addEventListener('keyup', (e) => { |
|
|
if (gameStarted) { |
|
|
keys[e.key] = false; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if ('ontouchstart' in window) { |
|
|
const handleMobileDown = (key) => { |
|
|
return () => { |
|
|
if (!gameStarted) return; |
|
|
|
|
|
if (key === 'ArrowLeft') keys['ArrowLeft'] = true; |
|
|
if (key === 'ArrowRight') keys['ArrowRight'] = true; |
|
|
if (key === 'ArrowUp') keys['ArrowUp'] = true; |
|
|
if (key === ' ') keys[' '] = true; |
|
|
}; |
|
|
}; |
|
|
|
|
|
const handleMobileUp = (key) => { |
|
|
return () => { |
|
|
if (!gameStarted) return; |
|
|
|
|
|
if (key === 'ArrowLeft') keys['ArrowLeft'] = false; |
|
|
if (key === 'ArrowRight') keys['ArrowRight'] = false; |
|
|
if (key === 'ArrowUp') keys['ArrowUp'] = false; |
|
|
if (key === ' ') keys[' '] = false; |
|
|
}; |
|
|
}; |
|
|
|
|
|
leftBtn.addEventListener('touchstart', handleMobileDown('ArrowLeft'), {passive: false}); |
|
|
leftBtn.addEventListener('touchend', handleMobileUp('ArrowLeft'), {passive: false}); |
|
|
rightBtn.addEventListener('touchstart', handleMobileDown('ArrowRight'), {passive: false}); |
|
|
rightBtn.addEventListener('touchend', handleMobileUp('ArrowRight'), {passive: false}); |
|
|
upBtn.addEventListener('touchstart', handleMobileDown('ArrowUp'), {passive: false}); |
|
|
upBtn.addEventListener('touchend', handleMobileUp('ArrowUp'), {passive: false}); |
|
|
fireBtn.addEventListener('touchstart', handleMobileDown(' '), {passive: false}); |
|
|
fireBtn.addEventListener('touchend', handleMobileUp(' '), {passive: false}); |
|
|
|
|
|
|
|
|
document.body.addEventListener('touchmove', (e) => { |
|
|
if (gameStarted) e.preventDefault(); |
|
|
}, { passive: false }); |
|
|
} |
|
|
|
|
|
|
|
|
startButton.addEventListener('click', startGame); |
|
|
|
|
|
|
|
|
restartButton.addEventListener('click', resetGame); |
|
|
|
|
|
function startGame() { |
|
|
gameStarted = true; |
|
|
startScreen.style.display = 'none'; |
|
|
resetGame(); |
|
|
gameLoop(); |
|
|
} |
|
|
|
|
|
function resetGame() { |
|
|
score = 0; |
|
|
lives = 3; |
|
|
gameOver = false; |
|
|
asteroids = []; |
|
|
bullets = []; |
|
|
particles = []; |
|
|
|
|
|
ship.x = canvas.width / 2; |
|
|
ship.y = canvas.height / 2; |
|
|
ship.angle = -Math.PI/2; |
|
|
ship.velocity.x = 0; |
|
|
ship.velocity.y = 0; |
|
|
ship.invulnerable = true; |
|
|
ship.invulnerableTime = 120; |
|
|
|
|
|
scoreElement.textContent = score; |
|
|
livesElement.textContent = lives; |
|
|
gameOverElement.style.display = 'none'; |
|
|
|
|
|
createAsteroids(4); |
|
|
} |
|
|
|
|
|
|
|
|
function createAsteroids(count) { |
|
|
for (let i = 0; i < count; i++) { |
|
|
const angle = Math.random() * Math.PI * 2; |
|
|
const distance = Math.max(canvas.width, canvas.height) * 0.4; |
|
|
|
|
|
const asteroid = createAsteroid( |
|
|
canvas.width/2 + Math.cos(angle) * distance, |
|
|
canvas.height/2 + Math.sin(angle) * distance, |
|
|
Math.random() * Math.PI * 2, |
|
|
1 |
|
|
); |
|
|
|
|
|
asteroids.push(asteroid); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function createAsteroid(x, y, angle, size) { |
|
|
let radius; |
|
|
let points; |
|
|
|
|
|
switch(size) { |
|
|
case 1: |
|
|
radius = 40; |
|
|
points = 20; |
|
|
break; |
|
|
case 2: |
|
|
radius = 25; |
|
|
points = 50; |
|
|
break; |
|
|
case 3: |
|
|
radius = 10; |
|
|
points = 100; |
|
|
break; |
|
|
} |
|
|
|
|
|
const speed = size === 1 ? 1 + Math.random() * 1 : |
|
|
size === 2 ? 1.5 + Math.random() * 1.5 : |
|
|
2 + Math.random() * 2; |
|
|
|
|
|
return { |
|
|
x, |
|
|
y, |
|
|
radius, |
|
|
size, |
|
|
points, |
|
|
vertices: Math.floor(Math.random() * 9) + 5, |
|
|
angle, |
|
|
speed, |
|
|
velocity: { |
|
|
x: Math.cos(angle) * speed, |
|
|
y: Math.sin(angle) * speed |
|
|
}, |
|
|
rotation: (Math.random() - 0.5) * 0.2 |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
function gameLoop() { |
|
|
if (!gameStarted || gameOver) return; |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#111'; |
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
|
drawStarfield(); |
|
|
|
|
|
|
|
|
updateShip(); |
|
|
drawShip(); |
|
|
|
|
|
updateBullets(); |
|
|
drawBullets(); |
|
|
|
|
|
updateAsteroids(); |
|
|
drawAsteroids(); |
|
|
|
|
|
updateParticles(); |
|
|
drawParticles(); |
|
|
|
|
|
checkCollisions(); |
|
|
|
|
|
spawnAsteroids(); |
|
|
|
|
|
|
|
|
if (ship.invulnerable) { |
|
|
ship.invulnerableTime--; |
|
|
if (ship.invulnerableTime <= 0) { |
|
|
ship.invulnerable = false; |
|
|
} |
|
|
} |
|
|
|
|
|
requestAnimationFrame(gameLoop); |
|
|
} |
|
|
|
|
|
function drawStarfield() { |
|
|
ctx.fillStyle = '#fff'; |
|
|
ctx.save(); |
|
|
|
|
|
for (let i = 0; i < 100; i++) { |
|
|
const x = Math.random() * canvas.width; |
|
|
const y = Math.random() * canvas.height; |
|
|
const size = Math.random() * 1.5; |
|
|
ctx.globalAlpha = Math.random(); |
|
|
ctx.beginPath(); |
|
|
ctx.arc(x, y, size, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
ctx.restore(); |
|
|
} |
|
|
|
|
|
function updateShip() { |
|
|
|
|
|
if (keys['ArrowLeft']) { |
|
|
ship.rotation = -0.1; |
|
|
} else if (keys['ArrowRight']) { |
|
|
ship.rotation = 0.1; |
|
|
} else { |
|
|
ship.rotation = 0; |
|
|
} |
|
|
|
|
|
|
|
|
ship.angle += ship.rotation; |
|
|
|
|
|
|
|
|
if (keys['ArrowUp']) { |
|
|
ship.thrusting = true; |
|
|
|
|
|
ship.thrust.x = Math.cos(ship.angle) * 0.1; |
|
|
ship.thrust.y = Math.sin(ship.angle) * 0.1; |
|
|
} else { |
|
|
ship.thrusting = false; |
|
|
ship.thrust.x = 0; |
|
|
ship.thrust.y = 0; |
|
|
} |
|
|
|
|
|
|
|
|
ship.velocity.x += ship.thrust.x; |
|
|
ship.velocity.y += ship.thrust.y; |
|
|
|
|
|
|
|
|
ship.velocity.x *= 0.98; |
|
|
ship.velocity.y *= 0.98; |
|
|
|
|
|
|
|
|
ship.x += ship.velocity.x; |
|
|
ship.y += ship.velocity.y; |
|
|
|
|
|
|
|
|
if (ship.x < 0 - ship.radius) { |
|
|
ship.x = canvas.width + ship.radius; |
|
|
} else if (ship.x > canvas.width + ship.radius) { |
|
|
ship.x = 0 - ship.radius; |
|
|
} |
|
|
|
|
|
if (ship.y < 0 - ship.radius) { |
|
|
ship.y = canvas.height + ship.radius; |
|
|
} else if (ship.y > canvas.height + ship.radius) { |
|
|
ship.y = 0 - ship.radius; |
|
|
} |
|
|
|
|
|
|
|
|
if (keys[' '] && !keys['_space']) { |
|
|
keys['_space'] = true; |
|
|
fireBullet(); |
|
|
} else if (!keys[' ']) { |
|
|
keys['_space'] = false; |
|
|
} |
|
|
} |
|
|
|
|
|
function drawShip() { |
|
|
ctx.save(); |
|
|
ctx.translate(ship.x, ship.y); |
|
|
ctx.rotate(ship.angle); |
|
|
|
|
|
|
|
|
if (ship.invulnerable) { |
|
|
ctx.strokeStyle = Date.now() % 200 > 100 ? '#0f0' : '#f80'; |
|
|
} else { |
|
|
ctx.strokeStyle = '#0f0'; |
|
|
} |
|
|
|
|
|
ctx.lineWidth = 2; |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(20, 0); |
|
|
ctx.lineTo(-10, -10); |
|
|
ctx.lineTo(-5, 0); |
|
|
ctx.lineTo(-10, 10); |
|
|
ctx.closePath(); |
|
|
ctx.stroke(); |
|
|
|
|
|
|
|
|
if (ship.thrusting) { |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(-15, 0); |
|
|
ctx.lineTo(-25, 0); |
|
|
ctx.strokeStyle = '#f80'; |
|
|
ctx.stroke(); |
|
|
} |
|
|
|
|
|
ctx.restore(); |
|
|
} |
|
|
|
|
|
function fireBullet() { |
|
|
if (bullets.length > 4) return; |
|
|
|
|
|
const bullet = { |
|
|
x: ship.x + Math.cos(ship.angle) * 20, |
|
|
y: ship.y + Math.sin(ship.angle) * 20, |
|
|
velocity: { |
|
|
x: Math.cos(ship.angle) * 7 + ship.velocity.x, |
|
|
y: Math.sin(ship.angle) * 7 + ship.velocity.y |
|
|
}, |
|
|
radius: 3, |
|
|
life: 60 |
|
|
}; |
|
|
|
|
|
bullets.push(bullet); |
|
|
|
|
|
|
|
|
playLaserSound(); |
|
|
} |
|
|
|
|
|
function playLaserSound() { |
|
|
const oscillator = new (AudioContext || webkitAudioContext)().createOscillator(); |
|
|
const gainNode = oscillator.context.createGain(); |
|
|
|
|
|
oscillator.type = 'square'; |
|
|
oscillator.frequency.value = 440; |
|
|
gainNode.gain.value = 0.1; |
|
|
|
|
|
oscillator.connect(gainNode); |
|
|
gainNode.connect(oscillator.context.destination); |
|
|
|
|
|
oscillator.start(); |
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, oscillator.context.currentTime + 0.2); |
|
|
oscillator.stop(oscillator.context.currentTime + 0.2); |
|
|
} |
|
|
|
|
|
function updateBullets() { |
|
|
for (let i = bullets.length - 1; i >= 0; i--) { |
|
|
bullets[i].x += bullets[i].velocity.x; |
|
|
bullets[i].y += bullets[i].velocity.y; |
|
|
bullets[i].life--; |
|
|
|
|
|
|
|
|
if (bullets[i].x < 0) bullets[i].x = canvas.width; |
|
|
else if (bullets[i].x > canvas.width) bullets[i].x = 0; |
|
|
|
|
|
if (bullets[i].y < 0) bullets[i].y = canvas.height; |
|
|
else if (bullets[i].y > canvas.height) bullets[i].y = 0; |
|
|
|
|
|
|
|
|
if (bullets[i].life <= 0) { |
|
|
bullets.splice(i, 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function drawBullets() { |
|
|
ctx.fillStyle = '#0f0'; |
|
|
for (let bullet of bullets) { |
|
|
ctx.beginPath(); |
|
|
ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateAsteroids() { |
|
|
for (let asteroid of asteroids) { |
|
|
asteroid.x += asteroid.velocity.x; |
|
|
asteroid.y += asteroid.velocity.y; |
|
|
asteroid.angle += asteroid.rotation; |
|
|
|
|
|
|
|
|
if (asteroid.x < 0 - asteroid.radius) { |
|
|
asteroid.x = canvas.width + asteroid.radius; |
|
|
} else if (asteroid.x > canvas.width + asteroid.radius) { |
|
|
asteroid.x = 0 - asteroid.radius; |
|
|
} |
|
|
|
|
|
if (asteroid.y < 0 - asteroid.radius) { |
|
|
asteroid.y = canvas.height + asteroid.radius; |
|
|
} else if (asteroid.y > canvas.height + asteroid.radius) { |
|
|
asteroid.y = 0 - asteroid.radius; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function drawAsteroids() { |
|
|
for (const asteroid of asteroids) { |
|
|
ctx.save(); |
|
|
ctx.translate(asteroid.x, asteroid.y); |
|
|
ctx.rotate(asteroid.angle); |
|
|
|
|
|
ctx.strokeStyle = '#fff'; |
|
|
ctx.lineWidth = 2; |
|
|
ctx.beginPath(); |
|
|
|
|
|
|
|
|
for (let i = 0; i < asteroid.vertices; i++) { |
|
|
const angle = (i / asteroid.vertices) * Math.PI * 2; |
|
|
const offset = asteroid.radius * (0.7 + Math.random() * 0.3); |
|
|
const x = Math.cos(angle) * offset; |
|
|
const y = Math.sin(angle) * offset; |
|
|
|
|
|
if (i === 0) { |
|
|
ctx.moveTo(x, y); |
|
|
} else { |
|
|
ctx.lineTo(x, y); |
|
|
} |
|
|
} |
|
|
|
|
|
ctx.closePath(); |
|
|
ctx.stroke(); |
|
|
ctx.restore(); |
|
|
} |
|
|
} |
|
|
|
|
|
function checkCollisions() { |
|
|
|
|
|
for (let i = asteroids.length - 1; i >= 0; i--) { |
|
|
const asteroid = asteroids[i]; |
|
|
|
|
|
for (let j = bullets.length - 1; j >= 0; j--) { |
|
|
const bullet = bullets[j]; |
|
|
|
|
|
const dx = asteroid.x - bullet.x; |
|
|
const dy = asteroid.y - bullet.y; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
if (distance < asteroid.radius + bullet.radius) { |
|
|
|
|
|
createParticles(asteroid); |
|
|
score += asteroid.points; |
|
|
scoreElement.textContent = score; |
|
|
|
|
|
|
|
|
if (asteroid.size < 3) { |
|
|
const numNew = 2; |
|
|
for (let n = 0; n < numNew; n++) { |
|
|
const angle = Math.random() * Math.PI * 2; |
|
|
const newAsteroid = createAsteroid( |
|
|
asteroid.x, |
|
|
asteroid.y, |
|
|
angle, |
|
|
asteroid.size + 1 |
|
|
); |
|
|
asteroids.push(newAsteroid); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
asteroids.splice(i, 1); |
|
|
bullets.splice(j, 1); |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!ship.invulnerable) { |
|
|
for (let i = 0; i < asteroids.length; i++) { |
|
|
const asteroid = asteroids[i]; |
|
|
const dx = ship.x - asteroid.x; |
|
|
const dy = ship.y - asteroid.y; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
if (distance < ship.radius + asteroid.radius) { |
|
|
|
|
|
lives--; |
|
|
livesElement.textContent = lives; |
|
|
createParticles(asteroid); |
|
|
|
|
|
if (lives <= 0) { |
|
|
gameOver = true; |
|
|
gameOverElement.style.display = 'flex'; |
|
|
endScoreElement.textContent = score; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
ship.x = canvas.width / 2; |
|
|
ship.y = canvas.height / 2; |
|
|
ship.velocity.x = 0; |
|
|
ship.velocity.y = 0; |
|
|
ship.angle = -Math.PI/2; |
|
|
ship.invulnerable = true; |
|
|
ship.invulnerableTime = 120; |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function createParticles(asteroid) { |
|
|
const particleCount = 15 + Math.floor(Math.random() * 10); |
|
|
const particleSizeBase = asteroid.radius / 10; |
|
|
|
|
|
for (let i = 0; i < particleCount; i++) { |
|
|
const size = particleSizeBase + Math.random() * 3; |
|
|
|
|
|
particles.push({ |
|
|
x: asteroid.x + (Math.random() * 20 - 10), |
|
|
y: asteroid.y + (Math.random() * 20 - 10), |
|
|
size: size, |
|
|
color: `hsl(${Math.random() * 60 + 30}, 100%, 50%)`, |
|
|
speed: 1 + Math.random() * 3 * (asteroid.radius / 30), |
|
|
angle: Math.random() * Math.PI * 2, |
|
|
life: 30 + Math.random() * 40, |
|
|
decay: 0.95 + Math.random() * 0.04 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
playExplosionSound(); |
|
|
} |
|
|
|
|
|
function playExplosionSound() { |
|
|
const oscillator = new (AudioContext || webkitAudioContext)().createOscillator(); |
|
|
const gainNode = oscillator.context.createGain(); |
|
|
|
|
|
oscillator.type = 'sawtooth'; |
|
|
oscillator.frequency.value = 100; |
|
|
gainNode.gain.value = 0.1; |
|
|
|
|
|
oscillator.connect(gainNode); |
|
|
gainNode.connect(oscillator.context.destination); |
|
|
|
|
|
oscillator.start(); |
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, oscillator.context.currentTime + 0.5); |
|
|
oscillator.stop(oscillator.context.currentTime + 0.5); |
|
|
} |
|
|
|
|
|
function updateParticles() { |
|
|
for (let i = particles.length - 1; i >= 0; i--) { |
|
|
const p = particles[i]; |
|
|
p.x += Math.cos(p.angle) * p.speed; |
|
|
p.y += Math.sin(p.angle) * p.speed; |
|
|
p.life--; |
|
|
p.speed *= p.decay; |
|
|
|
|
|
if (p.life <= 0) { |
|
|
particles.splice(i, 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function drawParticles() { |
|
|
for (const particle of particles) { |
|
|
ctx.fillStyle = particle.color; |
|
|
ctx.globalAlpha = Math.min(1, particle.life / 30); |
|
|
ctx.beginPath(); |
|
|
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
} |
|
|
ctx.globalAlpha = 1; |
|
|
} |
|
|
|
|
|
function spawnAsteroids() { |
|
|
|
|
|
if (asteroids.length <= 3 && Math.random() < 0.01) { |
|
|
const side = Math.floor(Math.random() * 4); |
|
|
let x, y; |
|
|
|
|
|
switch(side) { |
|
|
case 0: |
|
|
x = Math.random() * canvas.width; |
|
|
y = 0 - 50; |
|
|
break; |
|
|
case 1: |
|
|
x = canvas.width + 50; |
|
|
y = Math.random() * canvas.height; |
|
|
break; |
|
|
case 2: |
|
|
x = Math.random() * canvas.width; |
|
|
y = canvas.height + 50; |
|
|
break; |
|
|
case 3: |
|
|
x = 0 - 50; |
|
|
y = Math.random() * canvas.height; |
|
|
break; |
|
|
} |
|
|
|
|
|
const angle = Math.atan2( |
|
|
canvas.height/2 - y, |
|
|
canvas.width/2 - x |
|
|
) + (Math.random() - 0.5) * 0.5; |
|
|
|
|
|
asteroids.push(createAsteroid(x, y, angle, 1)); |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |