Spaces:
Running
Running
| class Game { | |
| constructor() { | |
| this.canvas = document.getElementById('gameCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.canvas.width = 900; | |
| this.canvas.height = 600; | |
| this.player = null; | |
| this.enemies = []; | |
| this.bullets = []; | |
| this.powerUps = []; | |
| this.particles = []; | |
| this.stars = []; | |
| this.score = 0; | |
| this.lives = 3; | |
| this.level = 1; | |
| this.powerLevel = 1; | |
| this.highScore = localStorage.getItem('highScore') || 0; | |
| this.gameState = 'menu'; | |
| this.difficulty = 'normal'; | |
| this.soundEnabled = true; | |
| this.isPaused = false; | |
| this.keys = {}; | |
| this.enemySpawnTimer = 0; | |
| this.powerUpSpawnTimer = 0; | |
| this.init(); | |
| } | |
| init() { | |
| this.setupEventListeners(); | |
| this.createStarfield(); | |
| this.updateHighScore(); | |
| } | |
| setupEventListeners() { | |
| // Menu buttons | |
| document.getElementById('startGameBtn').addEventListener('click', () => this.startGame()); | |
| document.getElementById('playBtn').addEventListener('click', () => this.startGame()); | |
| document.getElementById('restartBtn').addEventListener('click', () => this.startGame()); | |
| document.getElementById('nextLevelBtn').addEventListener('click', () => this.nextLevel()); | |
| document.getElementById('difficultyBtn').addEventListener('click', () => this.toggleDifficulty()); | |
| document.getElementById('soundBtn').addEventListener('click', () => this.toggleSound()); | |
| // Screen navigation | |
| document.getElementById('instructionsBtn').addEventListener('click', () => this.showScreen('instructionsScreen')); | |
| document.getElementById('leaderboardBtn').addEventListener('click', () => this.showScreen('leaderboardScreen')); | |
| document.getElementById('backFromInstructions').addEventListener('click', () => this.showScreen('startScreen')); | |
| document.getElementById('backFromLeaderboard').addEventListener('click', () => this.showScreen('startScreen')); | |
| // Keyboard controls | |
| window.addEventListener('keydown', (e) => { | |
| this.keys[e.key] = true; | |
| if (e.key === 'p' || e.key === 'P') { | |
| this.togglePause(); | |
| } | |
| if (e.key === 'm' || e.key === 'M') { | |
| this.toggleSound(); | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| this.keys[e.key] = false; | |
| }); | |
| } | |
| createStarfield() { | |
| for (let i = 0; i < 100; i++) { | |
| this.stars.push({ | |
| x: Math.random() * this.canvas.width, | |
| y: Math.random() * this.canvas.height, | |
| size: Math.random() * 2, | |
| speed: Math.random() * 0.5 + 0.1 | |
| }); | |
| } | |
| } | |
| startGame() { | |
| this.showScreen('gameScreen'); | |
| this.gameState = 'playing'; | |
| this.score = 0; | |
| this.lives = 3; | |
| this.level = 1; | |
| this.powerLevel = 1; | |
| this.enemies = []; | |
| this.bullets = []; | |
| this.powerUps = []; | |
| this.particles = []; | |
| this.player = new Player(this.canvas.width / 2, this.canvas.height - 100); | |
| this.updateUI(); | |
| this.gameLoop(); | |
| } | |
| nextLevel() { | |
| this.level++; | |
| this.enemies = []; | |
| this.powerUps = []; | |
| this.player.x = this.canvas.width / 2; | |
| this.player.y = this.canvas.height - 100; | |
| document.getElementById('levelCompleteOverlay').classList.add('hidden'); | |
| this.updateUI(); | |
| this.gameLoop(); | |
| } | |
| gameLoop() { | |
| if (this.gameState !== 'playing') return; | |
| if (!this.isPaused) { | |
| this.update(); | |
| this.render(); | |
| } | |
| requestAnimationFrame(() => this.gameLoop()); | |
| } | |
| update() { | |
| // Update stars | |
| this.stars.forEach(star => { | |
| star.y += star.speed; | |
| if (star.y > this.canvas.height) { | |
| star.y = 0; | |
| star.x = Math.random() * this.canvas.width; | |
| } | |
| }); | |
| // Player movement | |
| if (this.keys['ArrowLeft'] && this.player.x > 30) { | |
| this.player.x -= this.player.speed; | |
| } | |
| if (this.keys['ArrowRight'] && this.player.x < this.canvas.width - 30) { | |
| this.player.x += this.player.speed; | |
| } | |
| if (this.keys[' ']) { | |
| this.player.shoot(this.bullets, this.powerLevel); | |
| } | |
| // Update bullets | |
| this.bullets = this.bullets.filter(bullet => { | |
| bullet.y -= bullet.speed; | |
| return bullet.y > -10; | |
| }); | |
| // Spawn enemies | |
| this.enemySpawnTimer++; | |
| const spawnRate = Math.max(30, 60 - this.level * 5); | |
| if (this.enemySpawnTimer > spawnRate) { | |
| this.spawnEnemy(); | |
| this.enemySpawnTimer = 0; | |
| } | |
| // Update enemies | |
| this.enemies = this.enemies.filter(enemy => { | |
| enemy.y += enemy.speed; | |
| enemy.x += Math.sin(enemy.y * 0.02) * enemy.wobble; | |
| // Enemy shooting | |
| if (Math.random() < 0.005 * this.level) { | |
| enemy.shoot(this.bullets); | |
| } | |
| return enemy.y < this.canvas.height + 50; | |
| }); | |
| // Spawn power-ups | |
| this.powerUpSpawnTimer++; | |
| if (this.powerUpSpawnTimer > 500) { | |
| this.spawnPowerUp(); | |
| this.powerUpSpawnTimer = 0; | |
| } | |
| // Update power-ups | |
| this.powerUps = this.powerUps.filter(powerUp => { | |
| powerUp.y += powerUp.speed; | |
| powerUp.rotation += 0.05; | |
| return powerUp.y < this.canvas.height + 50; | |
| }); | |
| // Update particles | |
| this.particles = this.particles.filter(particle => { | |
| particle.x += particle.vx; | |
| particle.y += particle.vy; | |
| particle.life -= 0.02; | |
| return particle.life > 0; | |
| }); | |
| // Check collisions | |
| this.checkCollisions(); | |
| // Check level complete | |
| if (this.score >= this.level * 1000) { | |
| this.levelComplete(); | |
| } | |
| } | |
| spawnEnemy() { | |
| const types = ['basic', 'fast', 'tank']; | |
| const type = types[Math.floor(Math.random() * Math.min(types.length, this.level))]; | |
| this.enemies.push(new Enemy( | |
| Math.random() * (this.canvas.width - 60) + 30, | |
| -50, | |
| type | |
| )); | |
| } | |
| spawnPowerUp() { | |
| const types = ['rapidFire', 'tripleShot', 'shield', 'life']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| this.powerUps.push(new PowerUp( | |
| Math.random() * (this.canvas.width - 40) + 20, | |
| -30, | |
| type | |
| )); | |
| } | |
| checkCollisions() { | |
| // Bullet-Enemy collisions | |
| this.bullets.forEach((bullet, bulletIndex) => { | |
| if (bullet.isPlayer) { | |
| this.enemies.forEach((enemy, enemyIndex) => { | |
| if (this.isColliding(bullet, enemy)) { | |
| this.createExplosion(enemy.x, enemy.y); | |
| this.enemies.splice(enemyIndex, 1); | |
| this.bullets.splice(bulletIndex, 1); | |
| this.score += enemy.points; | |
| this.updateUI(); | |
| } | |
| }); | |
| } | |
| }); | |
| // Enemy-Player collisions | |
| this.enemies.forEach((enemy, index) => { | |
| if (this.isColliding(enemy, this.player)) { | |
| if (!this.player.shielded) { | |
| this.lives--; | |
| this.updateUI(); | |
| this.createExplosion(this.player.x, this.player.y); | |
| if (this.lives <= 0) { | |
| this.gameOver(); | |
| } | |
| } | |
| this.enemies.splice(index, 1); | |
| } | |
| }); | |
| // Bullet-Player collisions | |
| this.bullets.forEach((bullet, index) => { | |
| if (!bullet.isPlayer && this.isColliding(bullet, this.player)) { | |
| if (!this.player.shielded) { | |
| this.lives--; | |
| this.updateUI(); | |
| this.createExplosion(this.player.x, this.player.y); | |
| if (this.lives <= 0) { | |
| this.gameOver(); | |
| } | |
| } | |
| this.bullets.splice(index, 1); | |
| } | |
| }); | |
| // PowerUp-Player collisions | |
| this.powerUps.forEach((powerUp, index) => { | |
| if (this.isColliding(powerUp, this.player)) { | |
| this.applyPowerUp(powerUp.type); | |
| this.powerUps.splice(index, 1); | |
| } | |
| }); | |
| } | |
| isColliding(obj1, obj2) { | |
| const dist = Math.sqrt( | |
| Math.pow(obj1.x - obj2.x, 2) + | |
| Math.pow(obj1.y - obj2.y, 2) | |
| ); | |
| return dist < (obj1.radius || 20) + (obj2.radius || 20); | |
| } | |
| createExplosion(x, y) { | |
| for (let i = 0; i < 20; i++) { | |
| this.particles.push({ | |
| x: x, | |
| y: y, | |
| vx: (Math.random() - 0.5) * 8, | |
| vy: (Math.random() - 0.5) * 8, | |
| life: 1, | |
| color: `hsl(${Math.random() * 60}, 100%, 50%)` | |
| }); | |
| } | |
| } | |
| applyPowerUp(type) { | |
| switch(type) { | |
| case 'rapidFire': | |
| this.player.fireRate = 5; | |
| setTimeout(() => this.player.fireRate = 10, 5000); | |
| break; | |
| case 'tripleShot': | |
| this.powerLevel = 3; | |
| setTimeout(() => this.powerLevel = 1, 5000); | |
| break; | |
| case 'shield': | |
| this.player.shielded = true; | |
| setTimeout(() => this.player.shielded = false, 3000); | |
| break; | |
| case 'life': | |
| this.lives++; | |
| this.updateUI(); | |
| break; | |
| } | |
| } | |
| render() { | |
| // Clear canvas | |
| this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| // Draw stars | |
| this.ctx.fillStyle = '#fff'; | |
| this.stars.forEach(star => { | |
| this.ctx.globalAlpha = star.size / 2; | |
| this.ctx.fillRect(star.x, star.y, star.size, star.size); | |
| }); | |
| this.ctx.globalAlpha = 1; | |
| // Draw game objects | |
| this.player.draw(this.ctx); | |
| this.enemies.forEach(enemy => enemy.draw(this.ctx)); | |
| this.bullets.forEach(bullet => bullet.draw(this.ctx)); | |
| this.powerUps.forEach(powerUp => powerUp.draw(this.ctx)); | |
| this.particles.forEach(particle => { | |
| this.ctx.globalAlpha = particle.life; | |
| this.ctx.fillStyle = particle.color; | |
| this.ctx.fillRect(particle.x - 2, particle.y - 2, 4, 4); | |
| }); | |
| this.ctx.globalAlpha = 1; | |
| // Draw pause overlay | |
| if (this.isPaused) { | |
| this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.ctx.fillStyle = '#fff'; | |
| this.ctx.font = '48px Arial'; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText('PAUSED', this.canvas.width / 2, this.canvas.height / 2); | |
| } | |
| } | |
| togglePause() { | |
| if (this.gameState === 'playing') { | |
| this.isPaused = !this.isPaused; | |
| } | |
| } | |
| toggleDifficulty() { | |
| const difficulties = ['easy', 'normal', 'hard']; | |
| const currentIndex = difficulties.indexOf(this.difficulty); | |
| this.difficulty = difficulties[(currentIndex + 1) % difficulties.length]; | |
| document.getElementById('difficultyBtn').textContent = `Difficulty: ${this.difficulty.charAt(0).toUpperCase() + this.difficulty.slice(1)}`; | |
| } | |
| toggleSound() { | |
| this.soundEnabled = !this.soundEnabled; | |
| document.getElementById('soundBtn').textContent = `Sound: ${this.soundEnabled ? 'ON' : 'OFF'}`; | |
| } | |
| updateUI() { | |
| document.getElementById('score').textContent = this.score; | |
| document.getElementById('lives').textContent = this.lives; | |
| document.getElementById('level').textContent = this.level; | |
| document.getElementById('powerLevel').textContent = this.powerLevel; | |
| } | |
| updateHighScore() { | |
| document.getElementById('highScoreValue').textContent = this.highScore; | |
| } | |
| levelComplete() { | |
| this.gameState = 'levelComplete'; | |
| document.getElementById('levelScore').textContent = this.score; | |
| document.getElementById('levelCompleteOverlay').classList.remove('hidden'); | |
| } | |
| gameOver() { | |
| this.gameState = 'gameOver'; | |
| if (this.score > this.highScore) { | |
| this.highScore = this.score; | |
| localStorage.setItem('highScore', this.highScore); | |
| this.updateHighScore(); | |
| } | |
| this.saveToLeaderboard(); | |
| document.getElementById('finalScore').textContent = this.score; | |
| document.getElementById('gameOverOverlay').classList.remove('hidden'); | |
| } | |
| saveToLeaderboard() { | |
| let leaderboard = JSON.parse(localStorage.getItem('leaderboard')) || []; | |
| leaderboard.push({ | |
| score: this.score, | |
| level: this.level, | |
| date: new Date().toLocaleDateString() | |
| }); | |
| leaderboard.sort((a, b) => b.score - a.score); | |
| leaderboard = leaderboard.slice(0, 10); | |
| localStorage.setItem('leaderboard', JSON.stringify(leaderboard)); | |
| } | |
| showScreen(screenId) { | |
| document.querySelectorAll('.screen').forEach(screen => { | |
| screen.classList.add('hidden'); | |
| }); | |
| document.getElementById(screenId).classList.remove('hidden'); | |
| if (screenId === 'leaderboardScreen') { | |
| this.displayLeaderboard(); | |
| } | |
| } | |
| displayLeaderboard() { | |
| const leaderboard = JSON.parse(localStorage.getItem('leaderboard')) || []; | |
| const leaderboardList = document.getElementById('leaderboardList'); | |
| if (leaderboard.length === 0) { | |
| leaderboardList.innerHTML = '<p style="text-align: center; opacity: 0.6;">No scores yet. Be the first!</p>'; | |
| return; | |
| } | |
| leaderboardList.innerHTML = leaderboard.map((entry, index) => ` | |
| <div class="leaderboard-entry"> | |
| <span>${index + 1}. Level ${entry.level}</span> | |
| <span>${entry.score}</span> | |
| </div> | |
| `).join(''); | |
| } | |
| } | |
| class Player { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| this.speed = 5; | |
| this.fireRate = 10; | |
| this.lastShot = 0; | |
| this.shielded = false; | |
| } | |
| shoot(bullets, powerLevel) { | |
| const now = Date.now(); | |
| if (now - this.lastShot > 1000 / this.fireRate) { | |
| if (powerLevel === 3) { | |
| bullets.push(new Bullet(this.x - 15, this.y, -10, true)); | |
| bullets.push(new Bullet(this.x, this.y, -10, true)); | |
| bullets.push(new Bullet(this.x + 15, this.y, -10, true)); | |
| } else { | |
| bullets.push(new Bullet(this.x, this.y, -10, true)); | |
| } | |
| this.lastShot = now; | |
| } | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| // Draw shield if active | |
| if (this.shielded) { | |
| ctx.strokeStyle = 'rgba(0, 255, 136, 0.5)'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 35, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| } | |
| // Draw spaceship | |
| ctx.fillStyle = '#00ff88'; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -20); | |
| ctx.lineTo(-15, 20); | |
| ctx.lineTo(0, 10); | |
| ctx.lineTo(15, 20); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Draw cockpit | |
| ctx.fillStyle = '#00bbff'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| class Enemy { | |
| constructor(x, y, type) { | |
| this.x = x; | |
| this.y = y; | |
| this.type = type; | |
| this.lastShot = 0; | |
| switch(type) { | |
| case 'basic': | |
| this.speed = 2; | |
| this.health = 1; | |
| this.points = 10; | |
| this.color = '#ff006e'; | |
| this.wobble = 0; | |
| break; | |
| case 'fast': | |
| this.speed = 4; | |
| this.health = 1; | |
| this.points = 20; | |
| this.color = '#ff4458'; | |
| this.wobble = 2; | |
| break; | |
| case 'tank': | |
| this.speed = 1; | |
| this.health = 3; | |
| this.points = 50; | |
| this.color = '#8338ec'; | |
| this.wobble = 0; | |
| break; | |
| } | |
| } | |
| shoot(bullets) { | |
| const now = Date.now(); | |
| if (now - this.lastShot > 2000) { | |
| bullets.push(new Bullet(this.x, this.y + 20, 5, false)); | |
| this.lastShot = now; | |
| } | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, 20); | |
| ctx.lineTo(-15, -20); | |
| ctx.lineTo(0, -10); | |
| ctx.lineTo(15, -20); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| class Bullet { | |
| constructor(x, y, speed, isPlayer) { | |
| this.x = x; | |
| this.y = y; | |
| this.speed = speed; | |
| this.isPlayer = isPlayer; | |
| this.radius = 3; | |
| } | |
| draw(ctx) { | |
| ctx.fillStyle = this.isPlayer ? '#00ff88' : '#ff006e'; | |
| ctx.fillRect(this.x - 2, this.y - 5, 4, 10); | |
| } | |
| } | |
| class PowerUp { | |
| constructor(x, y, type) { | |
| this.x = x; | |
| this.y = y; | |
| this.type = type; | |
| this.speed = 1; | |
| this.rotation = 0; | |
| this.radius = 15; | |
| switch(type) { | |
| case 'rapidFire': | |
| this.color = '#ff006e'; | |
| break; | |
| case 'tripleShot': | |
| this.color = '#3a86ff'; | |
| break; | |
| case 'shield': | |
| this.color = '#00ff88'; | |
| break; | |
| case 'life': | |
| this.color = '#ff6b6b'; | |
| break; | |
| } | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.rotation); | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(-10, -10, 20, 20); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| const symbols = { | |
| rapidFire: '⚡', | |
| tripleShot: '⫸', | |
| shield: '🛡', | |
| life: '❤' | |
| }; | |
| ctx.fillText(symbols[this.type] || '?', 0, 0); | |
| ctx.restore(); | |
| } | |
| } | |
| // Initialize game when page loads | |
| window.addEventListener('DOMContentLoaded', () => { | |
| const game = new Game(); | |
| }); |