Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Galactic Defender</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #111; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| #gameCanvas { | |
| display: block; | |
| margin: 0 auto; | |
| background-color: #000; | |
| } | |
| #gameContainer { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #startScreen, #gameOverScreen, #pauseScreen { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| z-index: 10; | |
| } | |
| #gameOverScreen, #pauseScreen { | |
| display: none; | |
| } | |
| .btn { | |
| background: linear-gradient(135deg, #6e8efb, #a777e3); | |
| border: none; | |
| color: white; | |
| padding: 12px 24px; | |
| margin: 10px; | |
| border-radius: 30px; | |
| font-size: 18px; | |
| cursor: pointer; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .btn:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); | |
| } | |
| .btn:active { | |
| transform: translateY(1px); | |
| } | |
| .title { | |
| font-size: 48px; | |
| margin-bottom: 20px; | |
| background: linear-gradient(135deg, #6e8efb, #a777e3); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| .score-display { | |
| font-size: 24px; | |
| margin-bottom: 30px; | |
| } | |
| .powerup { | |
| position: absolute; | |
| font-size: 24px; | |
| animation: float 2s infinite ease-in-out; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .leaderboard { | |
| background: rgba(0, 0, 0, 0.6); | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .leaderboard-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 5px 10px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .difficulty-selector { | |
| margin: 20px 0; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .difficulty-btn { | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .difficulty-btn.active { | |
| background: linear-gradient(135deg, #6e8efb, #a777e3); | |
| box-shadow: 0 0 10px rgba(110, 142, 251, 0.5); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gameContainer"> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="startScreen"> | |
| <h1 class="title">GALACTIC DEFENDER</h1> | |
| <p class="text-white mb-8 text-lg">Defend the galaxy from alien invaders!</p> | |
| <div class="difficulty-selector"> | |
| <div class="difficulty-btn active" data-difficulty="easy">Easy</div> | |
| <div class="difficulty-btn" data-difficulty="medium">Medium</div> | |
| <div class="difficulty-btn" data-difficulty="hard">Hard</div> | |
| </div> | |
| <button id="startBtn" class="btn"> | |
| <i class="fas fa-play mr-2"></i> START GAME | |
| </button> | |
| <div class="mt-8 text-gray-400 text-center"> | |
| <p class="font-bold mb-2">Controls:</p> | |
| <p><i class="fas fa-arrow-left mr-2"></i> <i class="fas fa-arrow-right mr-2"></i> Move</p> | |
| <p><i class="fas fa-arrow-up mr-2"></i> <i class="fas fa-arrow-down mr-2"></i> Move</p> | |
| <p><i class="fas fa-space-shuttle mr-2"></i> Shoot</p> | |
| <p><i class="fas fa-pause mr-2"></i> Pause</p> | |
| </div> | |
| <div class="leaderboard mt-8 w-64"> | |
| <h3 class="text-center mb-2">TOP SCORES</h3> | |
| <div id="leaderboardList"> | |
| <!-- Leaderboard items will be added here --> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="gameOverScreen"> | |
| <h1 class="title">MISSION FAILED</h1> | |
| <p id="finalScore" class="score-display">Score: 0</p> | |
| <p id="highScore" class="text-gray-400 mb-4">High Score: 0</p> | |
| <button id="restartBtn" class="btn"> | |
| <i class="fas fa-redo mr-2"></i> TRY AGAIN | |
| </button> | |
| <button id="menuBtn" class="btn mt-4"> | |
| <i class="fas fa-home mr-2"></i> MAIN MENU | |
| </button> | |
| </div> | |
| <div id="pauseScreen"> | |
| <h1 class="title">GAME PAUSED</h1> | |
| <p class="text-white mb-8">Current Score: <span id="pauseScore">0</span></p> | |
| <button id="resumeBtn" class="btn"> | |
| <i class="fas fa-play mr-2"></i> RESUME | |
| </button> | |
| <button id="quitBtn" class="btn mt-4"> | |
| <i class="fas fa-sign-out-alt mr-2"></i> QUIT | |
| </button> | |
| </div> | |
| </div> | |
| <audio id="shootSound" src="https://assets.mixkit.co/sfx/preview/mixkit-laser-weapon-shot-1681.mp3" preload="auto"></audio> | |
| <audio id="explosionSound" src="https://assets.mixkit.co/sfx/preview/mixkit-explosion-impact-1684.mp3" preload="auto"></audio> | |
| <audio id="powerupSound" src="https://assets.mixkit.co/sfx/preview/mixkit-power-up-electricity-2580.mp3" preload="auto"></audio> | |
| <audio id="bgMusic" loop src="https://assets.mixkit.co/music/preview/mixkit-game-show-idea-229.mp3" preload="auto"></audio> | |
| <script> | |
| // Game variables | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const pauseScreen = document.getElementById('pauseScreen'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| const menuBtn = document.getElementById('menuBtn'); | |
| const resumeBtn = document.getElementById('resumeBtn'); | |
| const quitBtn = document.getElementById('quitBtn'); | |
| const finalScore = document.getElementById('finalScore'); | |
| const highScore = document.getElementById('highScore'); | |
| const pauseScore = document.getElementById('pauseScore'); | |
| const leaderboardList = document.getElementById('leaderboardList'); | |
| const difficultyBtns = document.querySelectorAll('.difficulty-btn'); | |
| // Sound elements | |
| const shootSound = document.getElementById('shootSound'); | |
| const explosionSound = document.getElementById('explosionSound'); | |
| const powerupSound = document.getElementById('powerupSound'); | |
| const bgMusic = document.getElementById('bgMusic'); | |
| // Set canvas size | |
| canvas.width = window.innerWidth * 0.9; | |
| canvas.height = window.innerHeight * 0.9; | |
| // Game state | |
| let gameRunning = false; | |
| let gamePaused = false; | |
| let score = 0; | |
| let lives = 3; | |
| let level = 1; | |
| let difficulty = 'easy'; | |
| let asteroids = []; | |
| let bullets = []; | |
| let explosions = []; | |
| let powerups = []; | |
| let enemies = []; | |
| let stars = []; | |
| let lastAsteroidTime = 0; | |
| let asteroidInterval = 2000; // ms | |
| let keys = {}; | |
| let leaderboard = JSON.parse(localStorage.getItem('spaceShooterLeaderboard')) || []; | |
| let playerPowerups = { | |
| rapidFire: false, | |
| tripleShot: false, | |
| shield: false | |
| }; | |
| let powerupEndTime = 0; | |
| // Player object | |
| const player = { | |
| x: canvas.width / 2, | |
| y: canvas.height - 60, | |
| width: 40, | |
| height: 60, | |
| speed: 5, | |
| color: '#6e8efb', | |
| lastShot: 0, | |
| shootDelay: 300, // ms | |
| draw() { | |
| ctx.save(); | |
| // Draw shield if active | |
| if (playerPowerups.shield) { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.width + 15, 0, Math.PI * 2); | |
| ctx.strokeStyle = 'rgba(100, 200, 255, 0.7)'; | |
| ctx.lineWidth = 3; | |
| ctx.stroke(); | |
| } | |
| // Draw spaceship (triangle) | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.x, this.y - this.height/2); | |
| ctx.lineTo(this.x - this.width/2, this.y + this.height/2); | |
| ctx.lineTo(this.x + this.width/2, this.y + this.height/2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Draw cockpit | |
| ctx.fillStyle = '#a777e3'; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y - 10, 10, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw engine glow | |
| if (keys['ArrowUp'] || keys['ArrowLeft'] || keys['ArrowRight'] || keys['ArrowDown']) { | |
| ctx.fillStyle = '#ff5555'; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.x - this.width/3, this.y + this.height/2); | |
| ctx.lineTo(this.x + this.width/3, this.y + this.height/2); | |
| ctx.lineTo(this.x, this.y + this.height/2 + 15); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| }, | |
| update() { | |
| if (keys['ArrowLeft'] && this.x > this.width/2) { | |
| this.x -= this.speed; | |
| } | |
| if (keys['ArrowRight'] && this.x < canvas.width - this.width/2) { | |
| this.x += this.speed; | |
| } | |
| if (keys['ArrowUp'] && this.y > this.height/2) { | |
| this.y -= this.speed; | |
| } | |
| if (keys['ArrowDown'] && this.y < canvas.height - this.height/2) { | |
| this.y += this.speed; | |
| } | |
| }, | |
| shoot() { | |
| const now = Date.now(); | |
| if (now - this.lastShot > (playerPowerups.rapidFire ? this.shootDelay / 2 : this.shootDelay)) { | |
| if (playerPowerups.tripleShot) { | |
| // Triple shot | |
| bullets.push({ | |
| x: this.x - 15, | |
| y: this.y - this.height/2, | |
| radius: 5, | |
| speed: 10, | |
| color: '#ff5555', | |
| angle: -0.2 | |
| }); | |
| bullets.push({ | |
| x: this.x, | |
| y: this.y - this.height/2, | |
| radius: 5, | |
| speed: 10, | |
| color: '#ff5555', | |
| angle: 0 | |
| }); | |
| bullets.push({ | |
| x: this.x + 15, | |
| y: this.y - this.height/2, | |
| radius: 5, | |
| speed: 10, | |
| color: '#ff5555', | |
| angle: 0.2 | |
| }); | |
| } else { | |
| // Single shot | |
| bullets.push({ | |
| x: this.x, | |
| y: this.y - this.height/2, | |
| radius: 5, | |
| speed: 10, | |
| color: '#ff5555', | |
| angle: 0 | |
| }); | |
| } | |
| this.lastShot = now; | |
| // Play shoot sound | |
| playSound('shoot'); | |
| } | |
| } | |
| }; | |
| // Create stars for background | |
| function createStars() { | |
| stars = []; | |
| for (let i = 0; i < 200; i++) { | |
| stars.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| radius: Math.random() * 2, | |
| speed: Math.random() * 0.5 + 0.1, | |
| alpha: Math.random() * 0.5 + 0.5 | |
| }); | |
| } | |
| } | |
| // Draw stars | |
| function drawStars() { | |
| ctx.save(); | |
| stars.forEach(star => { | |
| ctx.beginPath(); | |
| ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(255, 255, 255, ${star.alpha})`; | |
| ctx.fill(); | |
| // Move stars | |
| star.y += star.speed; | |
| if (star.y > canvas.height) { | |
| star.y = 0; | |
| star.x = Math.random() * canvas.width; | |
| } | |
| }); | |
| ctx.restore(); | |
| } | |
| // Create asteroid | |
| function createAsteroid() { | |
| const size = Math.random() * 30 + 20; | |
| const x = Math.random() * (canvas.width - size * 2) + size; | |
| asteroids.push({ | |
| x: x, | |
| y: -size, | |
| size: size, | |
| speed: Math.random() * 2 + 1 + level * 0.2, | |
| rotation: 0, | |
| rotationSpeed: Math.random() * 0.02 - 0.01, | |
| vertices: Math.floor(Math.random() * 5) + 5, | |
| offset: Array.from({length: Math.floor(Math.random() * 5) + 5}, () => Math.random() * 10 - 5), | |
| health: Math.floor(size / 10) | |
| }); | |
| } | |
| // Create enemy ship | |
| function createEnemyShip() { | |
| const types = ['basic', 'shooter', 'fast']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| const size = 40; | |
| const x = Math.random() * (canvas.width - size * 2) + size; | |
| let enemy = { | |
| x: x, | |
| y: -size, | |
| width: size, | |
| height: size, | |
| speed: 1 + level * 0.1, | |
| type: type, | |
| lastShot: 0, | |
| shootDelay: 2000, | |
| health: type === 'basic' ? 1 : (type === 'shooter' ? 2 : 1), | |
| color: type === 'basic' ? '#ff5555' : (type === 'shooter' ? '#55ff55' : '#5555ff') | |
| }; | |
| // Adjust properties based on type | |
| if (type === 'fast') { | |
| enemy.speed *= 2; | |
| } | |
| enemies.push(enemy); | |
| } | |
| // Create powerup | |
| function createPowerup(x, y) { | |
| const types = ['rapidFire', 'tripleShot', 'shield', 'extraLife']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| powerups.push({ | |
| x: x, | |
| y: y, | |
| type: type, | |
| radius: 15, | |
| speed: 2, | |
| color: getPowerupColor(type) | |
| }); | |
| } | |
| // Get powerup color by type | |
| function getPowerupColor(type) { | |
| switch(type) { | |
| case 'rapidFire': return '#ffcc00'; | |
| case 'tripleShot': return '#00ccff'; | |
| case 'shield': return '#00ffcc'; | |
| case 'extraLife': return '#ff00cc'; | |
| default: return '#ffffff'; | |
| } | |
| } | |
| // Get powerup icon by type | |
| function getPowerupIcon(type) { | |
| switch(type) { | |
| case 'rapidFire': return 'fa-bolt'; | |
| case 'tripleShot': return 'fa-crosshairs'; | |
| case 'shield': return 'fa-shield-alt'; | |
| case 'extraLife': return 'fa-heart'; | |
| default: return 'fa-star'; | |
| } | |
| } | |
| // Draw asteroid | |
| function drawAsteroid(asteroid) { | |
| ctx.save(); | |
| ctx.translate(asteroid.x, asteroid.y); | |
| ctx.rotate(asteroid.rotation); | |
| ctx.beginPath(); | |
| for (let i = 0; i < asteroid.vertices; i++) { | |
| const angle = (i / asteroid.vertices) * Math.PI * 2; | |
| const radius = asteroid.size + asteroid.offset[i % asteroid.offset.length]; | |
| const x = Math.cos(angle) * radius; | |
| const y = Math.sin(angle) * radius; | |
| if (i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| } | |
| ctx.closePath(); | |
| ctx.fillStyle = '#888'; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#555'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Draw health bar if asteroid has health > 1 | |
| if (asteroid.health > 1) { | |
| const healthPercent = asteroid.health / Math.floor(asteroid.size / 10); | |
| ctx.fillStyle = 'red'; | |
| ctx.fillRect(-asteroid.size/2, -asteroid.size/2 - 10, asteroid.size, 5); | |
| ctx.fillStyle = 'lime'; | |
| ctx.fillRect(-asteroid.size/2, -asteroid.size/2 - 10, asteroid.size * healthPercent, 5); | |
| } | |
| ctx.restore(); | |
| } | |
| // Draw enemy ship | |
| function drawEnemyShip(enemy) { | |
| ctx.save(); | |
| ctx.translate(enemy.x, enemy.y); | |
| // Draw different enemy types | |
| if (enemy.type === 'basic') { | |
| // Basic triangle enemy | |
| ctx.fillStyle = enemy.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -enemy.height/2); | |
| ctx.lineTo(-enemy.width/2, enemy.height/2); | |
| ctx.lineTo(enemy.width/2, enemy.height/2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Draw cockpit | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.arc(0, -5, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| else if (enemy.type === 'shooter') { | |
| // Shooter enemy (rectangle with turret) | |
| ctx.fillStyle = enemy.color; | |
| ctx.fillRect(-enemy.width/2, -enemy.height/2, enemy.width, enemy.height); | |
| // Draw turret | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(-5, -enemy.height/2 - 10, 10, 10); | |
| } | |
| else if (enemy.type === 'fast') { | |
| // Fast enemy (small and sleek) | |
| ctx.fillStyle = enemy.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -enemy.height/2); | |
| ctx.lineTo(-enemy.width/3, enemy.height/2); | |
| ctx.lineTo(enemy.width/3, enemy.height/2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Draw engine glow | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.moveTo(-enemy.width/4, enemy.height/2); | |
| ctx.lineTo(enemy.width/4, enemy.height/2); | |
| ctx.lineTo(0, enemy.height/2 + 8); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Draw health bar if enemy has health > 1 | |
| if (enemy.health > 1) { | |
| const healthPercent = enemy.type === 'shooter' ? enemy.health / 2 : enemy.health; | |
| ctx.fillStyle = 'red'; | |
| ctx.fillRect(-enemy.width/2, -enemy.height/2 - 10, enemy.width, 5); | |
| ctx.fillStyle = 'lime'; | |
| ctx.fillRect(-enemy.width/2, -enemy.height/2 - 10, enemy.width * healthPercent, 5); | |
| } | |
| ctx.restore(); | |
| } | |
| // Draw bullet | |
| function drawBullet(bullet) { | |
| ctx.save(); | |
| ctx.fillStyle = bullet.color; | |
| // If bullet has angle (for triple shot), adjust position | |
| if (bullet.angle) { | |
| bullet.x += Math.sin(bullet.angle) * 2; | |
| } | |
| ctx.beginPath(); | |
| ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| // Draw enemy bullet | |
| function drawEnemyBullet(bullet) { | |
| ctx.save(); | |
| ctx.fillStyle = '#ff00ff'; | |
| ctx.beginPath(); | |
| ctx.arc(bullet.x, bullet.y, bullet.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Add glow effect | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.beginPath(); | |
| ctx.arc(bullet.x, bullet.y, bullet.radius + 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| // Draw powerup | |
| function drawPowerup(powerup) { | |
| ctx.save(); | |
| // Draw outer circle | |
| ctx.beginPath(); | |
| ctx.arc(powerup.x, powerup.y, powerup.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = powerup.color; | |
| ctx.fill(); | |
| // Draw inner circle | |
| ctx.beginPath(); | |
| ctx.arc(powerup.x, powerup.y, powerup.radius - 5, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Draw icon using DOM element (since we can't draw Font Awesome icons on canvas) | |
| const icon = document.createElement('div'); | |
| icon.className = `powerup fas ${getPowerupIcon(powerup.type)}`; | |
| icon.style.color = 'white'; | |
| icon.style.left = `${powerup.x - 12}px`; | |
| icon.style.top = `${powerup.y - 12}px`; | |
| icon.style.zIndex = '5'; | |
| document.getElementById('gameContainer').appendChild(icon); | |
| // Remove icon after animation frame | |
| setTimeout(() => { | |
| if (icon.parentNode) { | |
| icon.parentNode.removeChild(icon); | |
| } | |
| }, 16); | |
| } | |
| // Draw explosion | |
| function drawExplosion(explosion) { | |
| ctx.save(); | |
| ctx.globalAlpha = explosion.alpha; | |
| ctx.fillStyle = explosion.color; | |
| for (let i = 0; i < explosion.particles; i++) { | |
| const angle = (i / explosion.particles) * Math.PI * 2; | |
| const distance = explosion.radius * (1 - explosion.progress); | |
| const x = explosion.x + Math.cos(angle) * distance; | |
| const y = explosion.y + Math.sin(angle) * distance; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, explosion.particleSize, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| // Check collision between two objects | |
| function checkCollision(obj1, obj2) { | |
| // Simple circle collision detection | |
| const dx = obj1.x - obj2.x; | |
| const dy = obj1.y - obj2.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (obj1.radius && obj2.radius) { | |
| return distance < obj1.radius + obj2.radius; | |
| } | |
| // For player (triangle) vs asteroid/enemy | |
| if (obj1 === player || obj2 === player) { | |
| const playerObj = obj1 === player ? obj1 : obj2; | |
| const otherObj = obj1 === player ? obj2 : obj1; | |
| // Simplified collision - check if object center is within player bounds | |
| return ( | |
| otherObj.x > playerObj.x - playerObj.width/2 && | |
| otherObj.x < playerObj.x + playerObj.width/2 && | |
| otherObj.y > playerObj.y - playerObj.height/2 && | |
| otherObj.y < playerObj.y + playerObj.height/2 | |
| ); | |
| } | |
| // For bullet vs asteroid/enemy | |
| if (obj1.radius && (obj2.size || obj2.width)) { | |
| const bullet = obj1.radius ? obj1 : obj2; | |
| const target = obj1.radius ? obj2 : obj1; | |
| if (target.size) { | |
| // Asteroid | |
| return distance < bullet.radius + target.size; | |
| } else { | |
| // Enemy ship | |
| return ( | |
| bullet.x > target.x - target.width/2 && | |
| bullet.x < target.x + target.width/2 && | |
| bullet.y > target.y - target.height/2 && | |
| bullet.y < target.y + target.height/2 | |
| ); | |
| } | |
| } | |
| return false; | |
| } | |
| // Create explosion | |
| function createExplosion(x, y, color = '#ff5555', size = 30) { | |
| explosions.push({ | |
| x: x, | |
| y: y, | |
| radius: size, | |
| particles: 12, | |
| particleSize: 3, | |
| color: color, | |
| alpha: 1, | |
| progress: 0, | |
| speed: 0.05 | |
| }); | |
| // Play explosion sound | |
| playSound('explosion'); | |
| } | |
| // Play sound | |
| function playSound(type) { | |
| try { | |
| switch(type) { | |
| case 'shoot': | |
| shootSound.currentTime = 0; | |
| shootSound.play(); | |
| break; | |
| case 'explosion': | |
| explosionSound.currentTime = 0; | |
| explosionSound.play(); | |
| break; | |
| case 'powerup': | |
| powerupSound.currentTime = 0; | |
| powerupSound.play(); | |
| break; | |
| } | |
| } catch(e) { | |
| console.log('Error playing sound:', e); | |
| } | |
| } | |
| // Draw HUD | |
| function drawHUD() { | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
| ctx.fillRect(10, 10, 200, 110); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = '20px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(`Score: ${score}`, 20, 40); | |
| ctx.fillText(`Lives: ${lives}`, 20, 70); | |
| ctx.fillText(`Level: ${level}`, 20, 100); | |
| // Draw powerup indicators | |
| if (playerPowerups.rapidFire || playerPowerups.tripleShot || playerPowerups.shield) { | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
| ctx.fillRect(canvas.width - 160, 10, 150, 50); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = '14px Arial'; | |
| ctx.textAlign = 'right'; | |
| const remainingTime = Math.max(0, Math.ceil((powerupEndTime - Date.now()) / 1000)); | |
| if (playerPowerups.rapidFire) { | |
| ctx.fillStyle = '#ffcc00'; | |
| ctx.fillText(`Rapid Fire: ${remainingTime}s`, canvas.width - 20, 30); | |
| } | |
| if (playerPowerups.tripleShot) { | |
| ctx.fillStyle = '#00ccff'; | |
| ctx.fillText(`Triple Shot: ${remainingTime}s`, canvas.width - 20, 50); | |
| } | |
| if (playerPowerups.shield) { | |
| ctx.fillStyle = '#00ffcc'; | |
| ctx.fillText(`Shield: ${remainingTime}s`, canvas.width - 20, 30); | |
| } | |
| } | |
| ctx.restore(); | |
| } | |
| // Update game state | |
| function update() { | |
| if (!gameRunning || gamePaused) return; | |
| // Update player | |
| player.update(); | |
| // Check powerup expiration | |
| if (playerPowerups.rapidFire || playerPowerups.tripleShot || playerPowerups.shield) { | |
| if (Date.now() > powerupEndTime) { | |
| playerPowerups.rapidFire = false; | |
| playerPowerups.tripleShot = false; | |
| playerPowerups.shield = false; | |
| } | |
| } | |
| // Create asteroids and enemies | |
| const now = Date.now(); | |
| if (now - lastAsteroidTime > asteroidInterval) { | |
| if (Math.random() < 0.7) { | |
| createAsteroid(); | |
| } else { | |
| createEnemyShip(); | |
| } | |
| lastAsteroidTime = now; | |
| // Decrease interval over time to make game harder | |
| asteroidInterval = Math.max(500, 2000 - level * 100); | |
| // Random chance to spawn powerup | |
| if (Math.random() < 0.1) { | |
| createPowerup(Math.random() * (canvas.width - 30) + 15, -30); | |
| } | |
| } | |
| // Update asteroids | |
| asteroids.forEach((asteroid, index) => { | |
| asteroid.y += asteroid.speed; | |
| asteroid.rotation += asteroid.rotationSpeed; | |
| // Check if asteroid is out of screen | |
| if (asteroid.y > canvas.height + asteroid.size) { | |
| asteroids.splice(index, 1); | |
| } | |
| // Check collision with player | |
| if (checkCollision(player, asteroid)) { | |
| if (playerPowerups.shield) { | |
| playerPowerups.shield = false; | |
| createExplosion(asteroid.x, asteroid.y, '#ffff55', asteroid.size); | |
| asteroids.splice(index, 1); | |
| } else { | |
| asteroids.splice(index, 1); | |
| lives--; | |
| createExplosion(asteroid.x, asteroid.y, '#ff5555', asteroid.size); | |
| if (lives <= 0) { | |
| gameOver(); | |
| } | |
| } | |
| } | |
| }); | |
| // Update enemies | |
| enemies.forEach((enemy, index) => { | |
| enemy.y += enemy.speed; | |
| // Enemy specific behavior | |
| if (enemy.type === 'shooter' && now - enemy.lastShot > enemy.shootDelay) { | |
| // Shoot at player | |
| bullets.push({ | |
| x: enemy.x, | |
| y: enemy.y + enemy.height/2, | |
| radius: 5, | |
| speed: -7, | |
| color: '#ff00ff', | |
| isEnemy: true | |
| }); | |
| enemy.lastShot = now; | |
| } | |
| // Check if enemy is out of screen | |
| if (enemy.y > canvas.height + enemy.height) { | |
| enemies.splice(index, 1); | |
| } | |
| // Check collision with player | |
| if (checkCollision(player, enemy)) { | |
| if (playerPowerups.shield) { | |
| playerPowerups.shield = false; | |
| createExplosion(enemy.x, enemy.y, '#ffff55', enemy.width); | |
| enemies.splice(index, 1); | |
| } else { | |
| enemies.splice(index, 1); | |
| lives--; | |
| createExplosion(enemy.x, enemy.y, '#ff5555', enemy.width); | |
| if (lives <= 0) { | |
| gameOver(); | |
| } | |
| } | |
| } | |
| }); | |
| // Update bullets | |
| bullets.forEach((bullet, index) => { | |
| if (bullet.isEnemy) { | |
| // Enemy bullets move down | |
| bullet.y -= bullet.speed; | |
| } else { | |
| // Player bullets move up | |
| bullet.y += bullet.speed; | |
| } | |
| // Check if bullet is out of screen | |
| if (bullet.y < 0 || bullet.y > canvas.height) { | |
| bullets.splice(index, 1); | |
| return; | |
| } | |
| // Check collision with asteroids | |
| asteroids.forEach((asteroid, aIndex) => { | |
| if (checkCollision(bullet, asteroid)) { | |
| // Reduce asteroid health | |
| asteroid.health--; | |
| if (asteroid.health <= 0) { | |
| // Remove asteroid | |
| asteroids.splice(aIndex, 1); | |
| // Add score | |
| score += Math.floor(asteroid.size); | |
| // Random chance to drop powerup | |
| if (Math.random() < 0.2) { | |
| createPowerup(asteroid.x, asteroid.y); | |
| } | |
| // Create explosion | |
| createExplosion(asteroid.x, asteroid.y, '#ffff55', asteroid.size); | |
| } | |
| // Remove bullet unless it's a piercing shot (future feature) | |
| bullets.splice(index, 1); | |
| } | |
| }); | |
| // Check collision with enemies | |
| enemies.forEach((enemy, eIndex) => { | |
| if (checkCollision(bullet, enemy)) { | |
| // Reduce enemy health | |
| enemy.health--; | |
| if (enemy.health <= 0) { | |
| // Remove enemy | |
| enemies.splice(eIndex, 1); | |
| // Add score based on enemy type | |
| score += enemy.type === 'basic' ? 50 : (enemy.type === 'shooter' ? 100 : 75); | |
| // Random chance to drop powerup | |
| if (Math.random() < 0.3) { | |
| createPowerup(enemy.x, enemy.y); | |
| } | |
| // Create explosion | |
| createExplosion(enemy.x, enemy.y, '#ffff55', enemy.width); | |
| } | |
| // Remove bullet | |
| bullets.splice(index, 1); | |
| } | |
| }); | |
| }); | |
| // Update powerups | |
| powerups.forEach((powerup, index) => { | |
| powerup.y += powerup.speed; | |
| // Check if powerup is out of screen | |
| if (powerup.y > canvas.height + powerup.radius) { | |
| powerups.splice(index, 1); | |
| return; | |
| } | |
| // Check collision with player | |
| if (checkCollision(player, powerup)) { | |
| // Apply powerup effect | |
| applyPowerup(powerup.type); | |
| powerups.splice(index, 1); | |
| // Play powerup sound | |
| playSound('powerup'); | |
| } | |
| }); | |
| // Update explosions | |
| explosions.forEach((explosion, index) => { | |
| explosion.progress += explosion.speed; | |
| explosion.alpha = 1 - explosion.progress; | |
| if (explosion.progress >= 1) { | |
| explosions.splice(index, 1); | |
| } | |
| }); | |
| // Level up every 500 points | |
| if (score > 0 && score % 500 === 0 && score / 500 >= level) { | |
| level++; | |
| createExplosion(canvas.width / 2, canvas.height / 2, '#ffffff', 100); | |
| } | |
| } | |
| // Apply powerup effect | |
| function applyPowerup(type) { | |
| // Set powerup duration (10 seconds) | |
| powerupEndTime = Date.now() + 10000; | |
| switch(type) { | |
| case 'rapidFire': | |
| playerPowerups.rapidFire = true; | |
| break; | |
| case 'tripleShot': | |
| playerPowerups.tripleShot = true; | |
| break; | |
| case 'shield': | |
| playerPowerups.shield = true; | |
| break; | |
| case 'extraLife': | |
| lives++; | |
| break; | |
| } | |
| } | |
| // Draw game | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw background | |
| drawStars(); | |
| // Draw game objects | |
| player.draw(); | |
| asteroids.forEach(drawAsteroid); | |
| enemies.forEach(drawEnemyShip); | |
| bullets.forEach(bullet => bullet.isEnemy ? drawEnemyBullet(bullet) : drawBullet(bullet)); | |
| powerups.forEach(drawPowerup); | |
| explosions.forEach(drawExplosion); | |
| // Draw HUD | |
| drawHUD(); | |
| } | |
| // Game loop | |
| function gameLoop() { | |
| update(); | |
| draw(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Update leaderboard | |
| function updateLeaderboard() { | |
| leaderboardList.innerHTML = ''; | |
| // Add current score to leaderboard | |
| if (score > 0) { | |
| leaderboard.push({ | |
| score: score, | |
| date: new Date().toLocaleDateString(), | |
| difficulty: difficulty | |
| }); | |
| } | |
| // Sort by score (descending) | |
| leaderboard.sort((a, b) => b.score - a.score); | |
| // Keep only top 10 scores | |
| leaderboard = leaderboard.slice(0, 10); | |
| // Save to localStorage | |
| localStorage.setItem('spaceShooterLeaderboard', JSON.stringify(leaderboard)); | |
| // Display leaderboard | |
| leaderboard.forEach((entry, index) => { | |
| const item = document.createElement('div'); | |
| item.className = 'leaderboard-item'; | |
| item.innerHTML = ` | |
| <span>${index + 1}. ${entry.score}</span> | |
| <span class="text-gray-400 text-sm">${entry.difficulty}</span> | |
| `; | |
| leaderboardList.appendChild(item); | |
| }); | |
| } | |
| // Start game | |
| function startGame() { | |
| // Reset game state | |
| score = 0; | |
| lives = difficulty === 'easy' ? 5 : (difficulty === 'medium' ? 3 : 1); | |
| level = 1; | |
| asteroids = []; | |
| bullets = []; | |
| explosions = []; | |
| powerups = []; | |
| enemies = []; | |
| playerPowerups = { | |
| rapidFire: false, | |
| tripleShot: false, | |
| shield: false | |
| }; | |
| // Reposition player | |
| player.x = canvas.width / 2; | |
| player.y = canvas.height - 60; | |
| // Set asteroid interval based on difficulty | |
| asteroidInterval = difficulty === 'easy' ? 2000 : (difficulty === 'medium' ? 1500 : 1000); | |
| // Hide screens | |
| startScreen.style.display = 'none'; | |
| gameOverScreen.style.display = 'none'; | |
| pauseScreen.style.display = 'none'; | |
| // Start game | |
| gameRunning = true; | |
| gamePaused = false; | |
| // Create stars | |
| createStars(); | |
| // Play background music | |
| try { | |
| bgMusic.volume = 0.3; | |
| bgMusic.currentTime = 0; | |
| bgMusic.play(); | |
| } catch(e) { | |
| console.log('Error playing music:', e); | |
| } | |
| // Start game loop | |
| gameLoop(); | |
| } | |
| // Game over | |
| function gameOver() { | |
| gameRunning = false; | |
| finalScore.textContent = `Score: ${score}`; | |
| // Update high score display | |
| const highScoreValue = leaderboard.length > 0 ? Math.max(...leaderboard.map(e => e.score)) : 0; | |
| highScore.textContent = `High Score: ${highScoreValue}`; | |
| // Update leaderboard | |
| updateLeaderboard(); | |
| // Show game over screen | |
| gameOverScreen.style.display = 'flex'; | |
| // Stop background music | |
| try { | |
| bgMusic.pause(); | |
| } catch(e) { | |
| console.log('Error stopping music:', e); | |
| } | |
| } | |
| // Pause game | |
| function pauseGame() { | |
| if (!gameRunning) return; | |
| gamePaused = !gamePaused; | |
| if (gamePaused) { | |
| pauseScreen.style.display = 'flex'; | |
| pauseScore.textContent = score; | |
| // Pause background music | |
| try { | |
| bgMusic.pause(); | |
| } catch(e) { | |
| console.log('Error pausing music:', e); | |
| } | |
| } else { | |
| pauseScreen.style.display = 'none'; | |
| // Resume background music | |
| try { | |
| bgMusic.play(); | |
| } catch(e) { | |
| console.log('Error resuming music:', e); | |
| } | |
| } | |
| } | |
| // Event listeners | |
| startBtn.addEventListener('click', startGame); | |
| restartBtn.addEventListener('click', startGame); | |
| menuBtn.addEventListener('click', () => { | |
| gameOverScreen.style.display = 'none'; | |
| startScreen.style.display = 'flex'; | |
| updateLeaderboard(); | |
| }); | |
| resumeBtn.addEventListener('click', pauseGame); | |
| quitBtn.addEventListener('click', () => { | |
| pauseScreen.style.display = 'none'; | |
| startScreen.style.display = 'flex'; | |
| gameRunning = false; | |
| updateLeaderboard(); | |
| }); | |
| // Difficulty selection | |
| difficultyBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| difficultyBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| difficulty = btn.dataset.difficulty; | |
| }); | |
| }); | |
| window.addEventListener('keydown', (e) => { | |
| keys[e.key] = true; | |
| if (e.key === ' ' && gameRunning && !gamePaused) { | |
| player.shoot(); | |
| } | |
| if (e.key === 'Escape') { | |
| pauseGame(); | |
| } | |
| // Prevent default for arrow keys and space | |
| if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) { | |
| e.preventDefault(); | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| keys[e.key] = false; | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth * 0.9; | |
| canvas.height = window.innerHeight * 0.9; | |
| // Reposition player | |
| player.x = canvas.width / 2; | |
| player.y = canvas.height - 60; | |
| // Recreate stars | |
| createStars(); | |
| }); | |
| // Initialize leaderboard | |
| updateLeaderboard(); | |
| // Start game loop (even when game not running to show start screen) | |
| gameLoop(); | |
| </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=SushilAI/space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |