// ─── Star Defender ─────────────────────────────────────────────── const canvas = document.getElementById('game-canvas'); const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; // ─── DOM refs ──────────────────────────────────────────────────── 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'); // ─── Game state ────────────────────────────────────────────────── let gameState = 'menu'; // menu | playing | paused | gameover let score = 0; let lives = 7; let level = 1; let highScore = 0; // ─── Sound effects (Web Audio API) ─────────────────────────────── let audioCtx = null; function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtx.state === 'suspended') audioCtx.resume(); } const sfx = { // High-pitched "pew" shot 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); }, // Alien shot — lower pitch 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); }, // Explosion — noise burst 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(); }, // Power-up pickup — ascending chirp 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); }); }, // Player hit — low rumble 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); }, // Level up — ascending arpeggio 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); }); } }; // ─── Star background ──────────────────────────────────────────── 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 }); } // ─── Player ────────────────────────────────────────────────────── const player = { x: W / 2, y: H - 50, w: 44, h: 32, speed: 5, cooldown: 0, invincible: 0, thrust: 0 // visual oscillation }; // ─── Input ─────────────────────────────────────────────────────── 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); // ─── Aliens ────────────────────────────────────────────────────── let aliens = []; let alienDir = 1; let alienStepTimer = 0; let alienStepInterval = 40; // frames between row shifts // Alien types with pixel-art style colors const ALIEN_TYPES = [ { color: '#ff6b6b', points: 30 }, // row 0 — red, high value { color: '#ffd93d', points: 20 }, // row 1 — yellow { color: '#6bff9e', points: 10 }, // row 2 — green ]; 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 // animation frame }); } } alienDir = 1; alienStepTimer = 0; alienStepInterval = Math.max(15, 50 - level * 3); } // ─── Bullets ───────────────────────────────────────────────────── let playerBullets = []; let alienBullets = []; // ─── Particles ─────────────────────────────────────────────────── let particles = []; // ─── Score pop-ups ─────────────────────────────────────────────── let scorePopups = []; function addScorePopup(x, y, text, color = '#fff') { scorePopups.push({ x, y, text, color, life: 1, decay: 0.02 }); } // ─── Screen flash ──────────────────────────────────────────────── 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 }); } } // ─── Power-ups ─────────────────────────────────────────────────── 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; // ─── UI drawing ────────────────────────────────────────────────── function drawHUD() { // Score ctx.fillStyle = '#fff'; ctx.font = 'bold 18px "Segoe UI", sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`SCORE ${score}`, 16, 30); // Level ctx.textAlign = 'center'; ctx.fillText(`LEVEL ${level}`, W / 2, 30); // Lives (draw ships) 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(); } // ─── Drawing helpers ───────────────────────────────────────────── function drawPlayer() { if (player.invincible > 0 && Math.floor(player.invincible / 4) % 2 === 0) return; const px = player.x; const py = player.y; // Thrust flame 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(); // Body 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(); // Cockpit ctx.fillStyle = '#aaddff'; ctx.beginPath(); ctx.arc(px, py - 2, 4, 0, Math.PI * 2); ctx.fill(); // Wing highlights 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; // Gentle bob const bob = Math.sin(Date.now() / 300 + a.x) * 1.5; ctx.fillStyle = t.color; // Body shape varies by type if (a.type === 0) { // Red — bulky with antennae ctx.beginPath(); ctx.arc(cx, cy + bob, 12, 0, Math.PI * 2); ctx.fill(); // Eyes ctx.fillStyle = '#1a1a2e'; ctx.fillRect(cx - 6, cy - 3 + bob, 4, 4); ctx.fillRect(cx + 2, cy - 3 + bob, 4, 4); // Antennae 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) { // Yellow — crab-like ctx.beginPath(); ctx.ellipse(cx, cy + bob, 14, 8, 0, 0, Math.PI * 2); ctx.fill(); // Eyes ctx.fillStyle = '#1a1a2e'; ctx.fillRect(cx - 5, cy - 3 + bob, 4, 4); ctx.fillRect(cx + 1, cy - 3 + bob, 4, 4); // Claws ctx.fillStyle = t.color; ctx.fillRect(cx - 18, cy - 2 + bob, 5, 4); ctx.fillRect(cx + 13, cy - 2 + bob, 5, 4); } else { // Green — saucer-like ctx.beginPath(); ctx.ellipse(cx, cy + bob, 14, 6, 0, 0, Math.PI * 2); ctx.fill(); // Dome ctx.fillStyle = '#aaffcc'; ctx.beginPath(); ctx.arc(cx, cy - 3 + bob, 7, Math.PI, 0); ctx.fill(); // Eyes 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); } // ─── Start / restart ───────────────────────────────────────────── 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'; } // ─── Collision ─────────────────────────────────────────────────── 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 ); } // ─── Update ────────────────────────────────────────────────────── function update() { if (gameState !== 'playing') return; // Player movement 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; // Horizontal wrap-around if (player.x < -player.w / 2) player.x = W + player.w / 2; if (player.x > W + player.w / 2) player.x = -player.w / 2; // Clamp vertical player.y = Math.max(player.h / 2 + 10, Math.min(H - player.h / 2, player.y)); // Shooting 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 }); } } // Invincibility timer if (player.invincible > 0) player.invincible--; // Power-up timers if (rapidTimer > 0) rapidTimer--; if (shieldTimer > 0) { shieldTimer--; if (shieldTimer === 0) shieldActive = false; } if (tripleTimer > 0) tripleTimer--; // Player bullets 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); // Alien movement (step-down) alienStepTimer++; const aliveAliens = aliens.filter(a => a.alive); // Speed up slightly as aliens die 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; // flip and move a.y += 12; a.frame = (a.frame + 1) % 2; }); } // Alien shooting 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 }); } }); // Alien bullets alienBullets.forEach(b => { b.x += b.vx; b.y += b.vy; }); alienBullets = alienBullets.filter(b => b.y < H + 10); // Power-ups powerups.forEach(p => { p.y += p.vy; }); powerups = powerups.filter(p => p.y < H + 20); // ─── Collisions ─── // Player bullets vs aliens playerBullets.forEach(b => { aliens.forEach(a => { if (!a.alive) return; if (rectCollide(b, a)) { a.alive = false; b.y = -999; // kill bullet 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(); // Chance to drop power-up if (Math.random() < 0.15) spawnPowerup(a.x, a.y); } }); }); // Alien bullets vs player if (player.invincible <= 0) { alienBullets.forEach(b => { if (rectCollide(b, player)) { b.y = 9999; // kill bullet 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(); } } }); } // Aliens reaching player aliveAliens.forEach(a => { if (a.y > H - 40) gameOver(); }); // Power-ups vs player 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 ─── particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life -= p.decay; p.vy += 0.04; // gravity }); particles = particles.filter(p => p.life > 0); // Next level if (aliens.every(a => !a.alive)) { level++; spawnAliens(); flashScreen('rgba(100,180,255,0.12)'); sfx.levelUp(); // Bonus life every 2 levels if (level % 2 === 0) lives = Math.min(lives + 1, 7); } } // ─── Render ────────────────────────────────────────────────────── function render() { // Background ctx.fillStyle = '#0a0a2e'; ctx.fillRect(0, 0, W, H); // Stars 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; // Player drawPlayer(); // Aliens aliens.filter(a => a.alive).forEach(drawAlien); // Player bullets ctx.fillStyle = '#66ccff'; playerBullets.forEach(b => { // Glow trail 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); // Bullet core ctx.fillStyle = '#aaddff'; ctx.fillRect(b.x - 1.5, b.y - 6, 3, 12); }); // Alien bullets alienBullets.forEach(b => { // Glow ctx.fillStyle = 'rgba(255,85,85,0.25)'; ctx.beginPath(); ctx.arc(b.x, b.y, 8, 0, Math.PI * 2); ctx.fill(); // Core ctx.fillStyle = '#ff5555'; ctx.beginPath(); ctx.arc(b.x, b.y, 3, 0, Math.PI * 2); ctx.fill(); // Bright center ctx.fillStyle = '#ffaaaa'; ctx.beginPath(); ctx.arc(b.x, b.y, 1.5, 0, Math.PI * 2); ctx.fill(); }); // Power-ups powerups.forEach(drawPowerup); // Particles 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; // Power-up status indicators 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); } // HUD drawHUD(); // Screen flash overlay 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; } // Score popups 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); } // ─── Game loop ─────────────────────────────────────────────────── function loop() { update(); render(); requestAnimationFrame(loop); } loop();