| |
| const canvas = document.getElementById('game-canvas'); |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width; |
| const H = canvas.height; |
|
|
| |
| const startScreen = document.getElementById('start-screen'); |
| const gameOverScreen = document.getElementById('game-over-screen'); |
| const pauseScreen = document.getElementById('pause-screen'); |
| const finalScoreEl = document.getElementById('final-score'); |
| const finalLevelEl = document.getElementById('final-level'); |
| const startBtn = document.getElementById('start-btn'); |
| const restartBtn = document.getElementById('restart-btn'); |
|
|
| |
| let gameState = 'menu'; |
| let score = 0; |
| let lives = 7; |
| let level = 1; |
| let highScore = 0; |
|
|
| |
| let audioCtx = null; |
|
|
| function initAudio() { |
| if (!audioCtx) { |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| if (audioCtx.state === 'suspended') audioCtx.resume(); |
| } |
|
|
| const sfx = { |
| |
| shoot(freq = 880) { |
| if (!audioCtx) return; |
| const o = audioCtx.createOscillator(); |
| const g = audioCtx.createGain(); |
| o.type = 'square'; |
| o.frequency.setValueAtTime(freq, audioCtx.currentTime); |
| o.frequency.exponentialRampToValueAtTime(freq * 0.35, audioCtx.currentTime + 0.1); |
| g.gain.setValueAtTime(0.08, audioCtx.currentTime); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.12); |
| o.connect(g).connect(audioCtx.destination); |
| o.start(); o.stop(audioCtx.currentTime + 0.12); |
| }, |
|
|
| |
| alienShot() { |
| if (!audioCtx) return; |
| const o = audioCtx.createOscillator(); |
| const g = audioCtx.createGain(); |
| o.type = 'sawtooth'; |
| o.frequency.setValueAtTime(220, audioCtx.currentTime); |
| o.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.15); |
| g.gain.setValueAtTime(0.04, audioCtx.currentTime); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15); |
| o.connect(g).connect(audioCtx.destination); |
| o.start(); o.stop(audioCtx.currentTime + 0.15); |
| }, |
|
|
| |
| explode() { |
| if (!audioCtx) return; |
| const len = audioCtx.sampleRate * 0.2; |
| const buf = audioCtx.createBuffer(1, len, audioCtx.sampleRate); |
| const d = buf.getChannelData(0); |
| for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len); |
| const src = audioCtx.createBufferSource(); |
| const g = audioCtx.createGain(); |
| const f = audioCtx.createBiquadFilter(); |
| f.type = 'lowpass'; f.frequency.value = 800; |
| src.buffer = buf; |
| g.gain.setValueAtTime(0.18, audioCtx.currentTime); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2); |
| src.connect(f).connect(g).connect(audioCtx.destination); |
| src.start(); |
| }, |
|
|
| |
| powerup() { |
| if (!audioCtx) return; |
| [523, 659, 784].forEach((f, i) => { |
| const o = audioCtx.createOscillator(); |
| const g = audioCtx.createGain(); |
| o.type = 'sine'; |
| o.frequency.value = f; |
| g.gain.setValueAtTime(0, audioCtx.currentTime + i * 0.07); |
| g.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + i * 0.07 + 0.02); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + i * 0.07 + 0.14); |
| o.connect(g).connect(audioCtx.destination); |
| o.start(audioCtx.currentTime + i * 0.07); |
| o.stop(audioCtx.currentTime + i * 0.07 + 0.14); |
| }); |
| }, |
|
|
| |
| hit() { |
| if (!audioCtx) return; |
| const o = audioCtx.createOscillator(); |
| const g = audioCtx.createGain(); |
| o.type = 'sine'; |
| o.frequency.setValueAtTime(120, audioCtx.currentTime); |
| o.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.3); |
| g.gain.setValueAtTime(0.15, audioCtx.currentTime); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); |
| o.connect(g).connect(audioCtx.destination); |
| o.start(); o.stop(audioCtx.currentTime + 0.3); |
| }, |
|
|
| |
| levelUp() { |
| if (!audioCtx) return; |
| [440, 554, 659, 880].forEach((f, i) => { |
| const o = audioCtx.createOscillator(); |
| const g = audioCtx.createGain(); |
| o.type = 'square'; |
| o.frequency.value = f; |
| g.gain.setValueAtTime(0, audioCtx.currentTime + i * 0.1); |
| g.gain.linearRampToValueAtTime(0.07, audioCtx.currentTime + i * 0.1 + 0.03); |
| g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + i * 0.1 + 0.2); |
| o.connect(g).connect(audioCtx.destination); |
| o.start(audioCtx.currentTime + i * 0.1); |
| o.stop(audioCtx.currentTime + i * 0.1 + 0.2); |
| }); |
| } |
| }; |
|
|
| |
| const stars = []; |
| for (let i = 0; i < 120; i++) { |
| stars.push({ |
| x: Math.random() * W, |
| y: Math.random() * H, |
| r: Math.random() * 1.5 + 0.3, |
| speed: Math.random() * 0.4 + 0.1, |
| twinkle: Math.random() * Math.PI * 2 |
| }); |
| } |
|
|
| |
| const player = { |
| x: W / 2, |
| y: H - 50, |
| w: 44, |
| h: 32, |
| speed: 5, |
| cooldown: 0, |
| invincible: 0, |
| thrust: 0 |
| }; |
|
|
| |
| const keys = {}; |
| window.addEventListener('keydown', e => { |
| keys[e.code] = true; |
| if (e.code === 'Space') e.preventDefault(); |
| if (e.code === 'KeyP' && gameState === 'playing') { |
| gameState = 'paused'; |
| pauseScreen.style.display = 'flex'; |
| } else if (e.code === 'KeyP' && gameState === 'paused') { |
| gameState = 'playing'; |
| pauseScreen.style.display = 'none'; |
| } |
| }); |
| window.addEventListener('keyup', e => { keys[e.code] = false; }); |
|
|
| startBtn.addEventListener('click', startGame); |
| restartBtn.addEventListener('click', startGame); |
|
|
| |
| let aliens = []; |
| let alienDir = 1; |
| let alienStepTimer = 0; |
| let alienStepInterval = 40; |
|
|
| |
| const ALIEN_TYPES = [ |
| { color: '#ff6b6b', points: 30 }, |
| { color: '#ffd93d', points: 20 }, |
| { color: '#6bff9e', points: 10 }, |
| ]; |
|
|
| function spawnAliens() { |
| aliens = []; |
| const rows = Math.min(3 + Math.floor(level / 3), 5); |
| const cols = Math.min(8 + Math.floor(level / 2), 14); |
| const spacingX = 44; |
| const spacingY = 40; |
| const offsetX = (W - cols * spacingX) / 2; |
| const offsetY = 60; |
|
|
| for (let r = 0; r < rows; r++) { |
| const type = r % ALIEN_TYPES.length; |
| for (let c = 0; c < cols; c++) { |
| aliens.push({ |
| x: offsetX + c * spacingX, |
| y: offsetY + r * spacingY, |
| w: 34, |
| h: 28, |
| alive: true, |
| type, |
| frame: 0 |
| }); |
| } |
| } |
|
|
| alienDir = 1; |
| alienStepTimer = 0; |
| alienStepInterval = Math.max(15, 50 - level * 3); |
| } |
|
|
| |
| let playerBullets = []; |
| let alienBullets = []; |
|
|
| |
| let particles = []; |
|
|
| |
| let scorePopups = []; |
|
|
| function addScorePopup(x, y, text, color = '#fff') { |
| scorePopups.push({ x, y, text, color, life: 1, decay: 0.02 }); |
| } |
|
|
| |
| let screenFlash = 0; |
|
|
| function flashScreen(color = 'rgba(255,255,255,0.15)', intensity = 1) { |
| screenFlash = { color, intensity, life: 1, decay: 0.06 }; |
| } |
|
|
| function explode(x, y, color, count = 18) { |
| for (let i = 0; i < count; i++) { |
| const angle = Math.random() * Math.PI * 2; |
| const speed = Math.random() * 4 + 1; |
| particles.push({ |
| x, y, |
| vx: Math.cos(angle) * speed, |
| vy: Math.sin(angle) * speed, |
| life: 1, |
| decay: Math.random() * 0.025 + 0.012, |
| r: Math.random() * 3.5 + 1.5, |
| color |
| }); |
| } |
| } |
|
|
| |
| let powerups = []; |
|
|
| function spawnPowerup(x, y) { |
| const types = ['rapid', 'shield', 'triple']; |
| const type = types[Math.floor(Math.random() * types.length)]; |
| powerups.push({ x, y, type, vy: 1.5, size: 14, w: 28, h: 28 }); |
| } |
|
|
| let rapidTimer = 0; |
| let shieldActive = false; |
| let shieldTimer = 0; |
| let tripleTimer = 0; |
|
|
| |
| function drawHUD() { |
| |
| ctx.fillStyle = '#fff'; |
| ctx.font = 'bold 18px "Segoe UI", sans-serif'; |
| ctx.textAlign = 'left'; |
| ctx.fillText(`SCORE ${score}`, 16, 30); |
|
|
| |
| ctx.textAlign = 'center'; |
| ctx.fillText(`LEVEL ${level}`, W / 2, 30); |
|
|
| |
| ctx.textAlign = 'right'; |
| for (let i = 0; i < lives; i++) { |
| const lx = W - 20 - i * 32; |
| drawMiniShip(lx, 24); |
| } |
| } |
|
|
| function drawMiniShip(x, y) { |
| ctx.fillStyle = '#5b9aff'; |
| ctx.beginPath(); |
| ctx.moveTo(x, y - 10); |
| ctx.lineTo(x - 10, y + 6); |
| ctx.lineTo(x + 10, y + 6); |
| ctx.closePath(); |
| ctx.fill(); |
| } |
|
|
| |
| function drawPlayer() { |
| if (player.invincible > 0 && Math.floor(player.invincible / 4) % 2 === 0) return; |
|
|
| const px = player.x; |
| const py = player.y; |
|
|
| |
| player.thrust += 0.15; |
| const flameH = 8 + Math.sin(player.thrust) * 4; |
| ctx.fillStyle = '#ff9933'; |
| ctx.beginPath(); |
| ctx.moveTo(px - 6, py + 16); |
| ctx.lineTo(px, py + 16 + flameH); |
| ctx.lineTo(px + 6, py + 16); |
| ctx.closePath(); |
| ctx.fill(); |
|
|
| |
| ctx.fillStyle = shieldActive ? '#33ffcc' : '#5b9aff'; |
| ctx.beginPath(); |
| ctx.moveTo(px, py - 16); |
| ctx.lineTo(px - 22, py + 16); |
| ctx.lineTo(px - 6, py + 12); |
| ctx.lineTo(px + 6, py + 12); |
| ctx.lineTo(px + 22, py + 16); |
| ctx.closePath(); |
| ctx.fill(); |
|
|
| |
| ctx.fillStyle = '#aaddff'; |
| ctx.beginPath(); |
| ctx.arc(px, py - 2, 4, 0, Math.PI * 2); |
| ctx.fill(); |
|
|
| |
| ctx.fillStyle = '#88bbee'; |
| ctx.beginPath(); |
| ctx.moveTo(px - 20, py + 10); |
| ctx.lineTo(px - 10, py); |
| ctx.lineTo(px - 14, py + 8); |
| ctx.closePath(); |
| ctx.fill(); |
| ctx.beginPath(); |
| ctx.moveTo(px + 20, py + 10); |
| ctx.lineTo(px + 10, py); |
| ctx.lineTo(px + 14, py + 8); |
| ctx.closePath(); |
| ctx.fill(); |
| } |
|
|
| function drawAlien(a) { |
| const t = ALIEN_TYPES[a.type]; |
| const cx = a.x; |
| const cy = a.y; |
|
|
| |
| const bob = Math.sin(Date.now() / 300 + a.x) * 1.5; |
|
|
| ctx.fillStyle = t.color; |
|
|
| |
| if (a.type === 0) { |
| |
| ctx.beginPath(); |
| ctx.arc(cx, cy + bob, 12, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#1a1a2e'; |
| ctx.fillRect(cx - 6, cy - 3 + bob, 4, 4); |
| ctx.fillRect(cx + 2, cy - 3 + bob, 4, 4); |
| |
| ctx.fillStyle = t.color; |
| ctx.fillRect(cx - 7, cy - 14 + bob, 2, 6); |
| ctx.fillRect(cx + 5, cy - 14 + bob, 2, 6); |
| } else if (a.type === 1) { |
| |
| ctx.beginPath(); |
| ctx.ellipse(cx, cy + bob, 14, 8, 0, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#1a1a2e'; |
| ctx.fillRect(cx - 5, cy - 3 + bob, 4, 4); |
| ctx.fillRect(cx + 1, cy - 3 + bob, 4, 4); |
| |
| ctx.fillStyle = t.color; |
| ctx.fillRect(cx - 18, cy - 2 + bob, 5, 4); |
| ctx.fillRect(cx + 13, cy - 2 + bob, 5, 4); |
| } else { |
| |
| ctx.beginPath(); |
| ctx.ellipse(cx, cy + bob, 14, 6, 0, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#aaffcc'; |
| ctx.beginPath(); |
| ctx.arc(cx, cy - 3 + bob, 7, Math.PI, 0); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#1a1a2e'; |
| ctx.fillRect(cx - 5, cy - 2 + bob, 3, 3); |
| ctx.fillRect(cx + 2, cy - 2 + bob, 3, 3); |
| } |
| } |
|
|
| function drawPowerup(p) { |
| const symbols = { rapid: 'R', shield: 'S', triple: 'T' }; |
| const colors = { rapid: '#ff6644', shield: '#33ffcc', triple: '#ffcc33' }; |
| const c = colors[p.type]; |
| ctx.fillStyle = c; |
| ctx.beginPath(); |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); |
| ctx.fill(); |
| ctx.fillStyle = '#0a0a2e'; |
| ctx.font = 'bold 12px sans-serif'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| ctx.fillText(symbols[p.type], p.x, p.y); |
| } |
|
|
| |
| function startGame() { |
| initAudio(); |
| score = 0; |
| lives = 7; |
| level = 1; |
| player.x = W / 2; |
| player.cooldown = 0; |
| player.invincible = 0; |
| rapidTimer = 0; |
| shieldTimer = 0; |
| tripleTimer = 0; |
| shieldActive = false; |
| playerBullets = []; |
| alienBullets = []; |
| particles = []; |
| powerups = []; |
|
|
| startScreen.style.display = 'none'; |
| gameOverScreen.style.display = 'none'; |
| pauseScreen.style.display = 'none'; |
|
|
| spawnAliens(); |
| gameState = 'playing'; |
| } |
|
|
| function gameOver() { |
| gameState = 'gameover'; |
| if (score > highScore) highScore = score; |
| finalScoreEl.textContent = `Score: ${score}`; |
| finalLevelEl.textContent = `Level: ${level}`; |
| gameOverScreen.style.display = 'flex'; |
| } |
|
|
| |
| function rectCollide(a, b) { |
| return ( |
| a.x - a.w / 2 < b.x + b.w / 2 && |
| a.x + a.w / 2 > b.x - b.w / 2 && |
| a.y - a.h / 2 < b.y + b.h / 2 && |
| a.y + a.h / 2 > b.y - b.h / 2 |
| ); |
| } |
|
|
| |
| function update() { |
| if (gameState !== 'playing') return; |
|
|
| |
| if (keys['ArrowLeft']) player.x -= player.speed; |
| if (keys['ArrowRight']) player.x += player.speed; |
| if (keys['ArrowUp']) player.y -= player.speed; |
| if (keys['ArrowDown']) player.y += player.speed; |
| |
| if (player.x < -player.w / 2) player.x = W + player.w / 2; |
| if (player.x > W + player.w / 2) player.x = -player.w / 2; |
| |
| player.y = Math.max(player.h / 2 + 10, Math.min(H - player.h / 2, player.y)); |
|
|
| |
| const shootRate = rapidTimer > 0 ? 6 : 14; |
| if (player.cooldown > 0) player.cooldown--; |
| if (keys['Space'] && player.cooldown === 0) { |
| player.cooldown = shootRate; |
| sfx.shoot(); |
| if (tripleTimer > 0) { |
| playerBullets.push({ x: player.x, y: player.y - 18, vx: 0, vy: -8, w: 3, h: 12 }); |
| playerBullets.push({ x: player.x, y: player.y - 14, vx: -2.5, vy: -8, w: 3, h: 12 }); |
| playerBullets.push({ x: player.x, y: player.y - 14, vx: 2.5, vy: -8, w: 3, h: 12 }); |
| } else { |
| playerBullets.push({ x: player.x, y: player.y - 18, vx: 0, vy: -8, w: 3, h: 12 }); |
| } |
| } |
|
|
| |
| if (player.invincible > 0) player.invincible--; |
|
|
| |
| if (rapidTimer > 0) rapidTimer--; |
| if (shieldTimer > 0) { shieldTimer--; if (shieldTimer === 0) shieldActive = false; } |
| if (tripleTimer > 0) tripleTimer--; |
|
|
| |
| playerBullets.forEach(b => { |
| b.x += b.vx; |
| b.y += b.vy; |
| }); |
| playerBullets = playerBullets.filter(b => b.y > -10 && b.x > -10 && b.x < W + 10); |
|
|
| |
| alienStepTimer++; |
| const aliveAliens = aliens.filter(a => a.alive); |
| |
| const aliveRatio = aliveAliens.length / Math.max(1, aliens.length); |
| const effectiveInterval = Math.max(10, Math.floor(alienStepInterval * aliveRatio)); |
|
|
| let hitEdge = false; |
| aliveAliens.forEach(a => { a.x += alienDir * 0.3; }); |
| aliveAliens.forEach(a => { |
| if (a.x < 20 || a.x > W - 20) hitEdge = true; |
| }); |
|
|
| if (hitEdge || alienStepTimer >= effectiveInterval) { |
| alienDir *= -1; |
| if (alienStepTimer >= effectiveInterval) alienStepTimer = 0; |
| aliveAliens.forEach(a => { |
| a.x += alienDir * 0.3; |
| a.y += 12; |
| a.frame = (a.frame + 1) % 2; |
| }); |
| } |
|
|
| |
| aliveAliens.forEach(a => { |
| if (Math.random() < 0.0005 + level * 0.00015) { |
| sfx.alienShot(); |
| alienBullets.push({ |
| x: a.x, y: a.y + 12, |
| vx: 0, |
| vy: 2.5, |
| w: 6, h: 6 |
| }); |
| } |
| }); |
|
|
| |
| alienBullets.forEach(b => { |
| b.x += b.vx; |
| b.y += b.vy; |
| }); |
| alienBullets = alienBullets.filter(b => b.y < H + 10); |
|
|
| |
| powerups.forEach(p => { p.y += p.vy; }); |
| powerups = powerups.filter(p => p.y < H + 20); |
|
|
| |
| |
| playerBullets.forEach(b => { |
| aliens.forEach(a => { |
| if (!a.alive) return; |
| if (rectCollide(b, a)) { |
| a.alive = false; |
| b.y = -999; |
| score += ALIEN_TYPES[a.type].points; |
| explode(a.x, a.y, ALIEN_TYPES[a.type].color, 22); |
| addScorePopup(a.x, a.y - 12, `+${ALIEN_TYPES[a.type].points}`, ALIEN_TYPES[a.type].color); |
| sfx.explode(); |
| |
| if (Math.random() < 0.15) spawnPowerup(a.x, a.y); |
| } |
| }); |
| }); |
|
|
| |
| if (player.invincible <= 0) { |
| alienBullets.forEach(b => { |
| if (rectCollide(b, player)) { |
| b.y = 9999; |
| if (shieldActive) { |
| shieldActive = false; |
| shieldTimer = 0; |
| explode(b.x, b.y, '#33ffcc', 10); |
| flashScreen('rgba(51,255,204,0.1)'); |
| } else { |
| lives--; |
| player.invincible = 150; |
| explode(player.x, player.y, '#ff4444', 30); |
| flashScreen('rgba(255,68,68,0.12)'); |
| sfx.hit(); |
| if (lives <= 0) gameOver(); |
| } |
| } |
| }); |
| } |
|
|
| |
| aliveAliens.forEach(a => { |
| if (a.y > H - 40) gameOver(); |
| }); |
|
|
| |
| powerups.forEach(p => { |
| if (rectCollide(p, player)) { |
| p.y = 9999; |
| if (p.type === 'rapid') rapidTimer = 300; |
| if (p.type === 'shield') { shieldActive = true; shieldTimer = 360; } |
| if (p.type === 'triple') tripleTimer = 240; |
| explode(p.x, p.y, '#ffffff', 8); |
| flashScreen('rgba(255,255,255,0.08)'); |
| sfx.powerup(); |
| } |
| }); |
|
|
| |
| particles.forEach(p => { |
| p.x += p.vx; |
| p.y += p.vy; |
| p.life -= p.decay; |
| p.vy += 0.04; |
| }); |
| particles = particles.filter(p => p.life > 0); |
|
|
| |
| if (aliens.every(a => !a.alive)) { |
| level++; |
| spawnAliens(); |
| flashScreen('rgba(100,180,255,0.12)'); |
| sfx.levelUp(); |
| |
| if (level % 2 === 0) lives = Math.min(lives + 1, 7); |
| } |
| } |
|
|
| |
| function render() { |
| |
| ctx.fillStyle = '#0a0a2e'; |
| ctx.fillRect(0, 0, W, H); |
|
|
| |
| stars.forEach(s => { |
| s.y += s.speed; |
| s.twinkle += 0.03; |
| if (s.y > H) { s.y = 0; s.x = Math.random() * W; } |
| const alpha = 0.5 + Math.sin(s.twinkle) * 0.3; |
| ctx.fillStyle = `rgba(200,210,255,${alpha})`; |
| ctx.beginPath(); |
| ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); |
| ctx.fill(); |
| }); |
|
|
| if (gameState === 'menu') return; |
|
|
| |
| drawPlayer(); |
|
|
| |
| aliens.filter(a => a.alive).forEach(drawAlien); |
|
|
| |
| ctx.fillStyle = '#66ccff'; |
| playerBullets.forEach(b => { |
| |
| const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + 16); |
| grad.addColorStop(0, 'rgba(100,200,255,0.8)'); |
| grad.addColorStop(1, 'rgba(100,200,255,0)'); |
| ctx.fillStyle = grad; |
| ctx.fillRect(b.x - 2, b.y + 2, 4, 16); |
|
|
| |
| ctx.fillStyle = '#aaddff'; |
| ctx.fillRect(b.x - 1.5, b.y - 6, 3, 12); |
| }); |
|
|
| |
| alienBullets.forEach(b => { |
| |
| ctx.fillStyle = 'rgba(255,85,85,0.25)'; |
| ctx.beginPath(); |
| ctx.arc(b.x, b.y, 8, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#ff5555'; |
| ctx.beginPath(); |
| ctx.arc(b.x, b.y, 3, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| ctx.fillStyle = '#ffaaaa'; |
| ctx.beginPath(); |
| ctx.arc(b.x, b.y, 1.5, 0, Math.PI * 2); |
| ctx.fill(); |
| }); |
|
|
| |
| powerups.forEach(drawPowerup); |
|
|
| |
| particles.forEach(p => { |
| ctx.globalAlpha = p.life; |
| ctx.fillStyle = p.color; |
| ctx.beginPath(); |
| ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); |
| ctx.fill(); |
| }); |
| ctx.globalAlpha = 1; |
|
|
| |
| let statusX = 16; |
| const statusY = H - 24; |
| ctx.font = '12px sans-serif'; |
| ctx.textAlign = 'left'; |
|
|
| if (rapidTimer > 0) { |
| ctx.fillStyle = '#ff6644'; |
| ctx.fillText(`RAPID ${Math.ceil(rapidTimer / 60)}`, statusX, statusY); |
| statusX += 80; |
| } |
| if (shieldActive) { |
| ctx.fillStyle = '#33ffcc'; |
| ctx.fillText(`SHIELD ${Math.ceil(shieldTimer / 60)}`, statusX, statusY); |
| statusX += 90; |
| } |
| if (tripleTimer > 0) { |
| ctx.fillStyle = '#ffcc33'; |
| ctx.fillText(`TRIPLE ${Math.ceil(tripleTimer / 60)}`, statusX, statusY); |
| } |
|
|
| |
| drawHUD(); |
|
|
| |
| if (screenFlash && screenFlash.life > 0) { |
| ctx.fillStyle = screenFlash.color; |
| ctx.globalAlpha = screenFlash.intensity * screenFlash.life; |
| ctx.fillRect(0, 0, W, H); |
| ctx.globalAlpha = 1; |
| screenFlash.life -= screenFlash.decay; |
| } |
|
|
| |
| scorePopups.forEach(p => { |
| ctx.globalAlpha = p.life; |
| ctx.fillStyle = p.color; |
| ctx.font = 'bold 14px "Orbitron", monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(p.text, p.x, p.y); |
| p.y -= 1; |
| p.life -= p.decay; |
| }); |
| scorePopups = scorePopups.filter(p => p.life > 0); |
| } |
|
|
| |
| function loop() { |
| update(); |
| render(); |
| requestAnimationFrame(loop); |
| } |
|
|
| loop(); |
|
|