Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Space Defender</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| #gameCanvas { | |
| display: block; | |
| background: linear-gradient(to bottom, #000000 0%, #000033 100%); | |
| cursor: crosshair; | |
| } | |
| #gameUI { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| color: white; | |
| font-size: 18px; | |
| text-shadow: 0 0 5px #00ffff; | |
| } | |
| #startScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| z-index: 10; | |
| } | |
| #gameOverScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: none; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| z-index: 10; | |
| } | |
| h1 { | |
| font-size: 48px; | |
| margin-bottom: 20px; | |
| color: #00ffff; | |
| text-shadow: 0 0 10px #00ffff; | |
| } | |
| button { | |
| padding: 15px 30px; | |
| font-size: 20px; | |
| background: linear-gradient(to bottom, #00aaaa, #008888); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| margin-top: 20px; | |
| transition: all 0.3s; | |
| } | |
| button:hover { | |
| background: linear-gradient(to bottom, #00cccc, #00aaaa); | |
| transform: scale(1.05); | |
| box-shadow: 0 0 15px #00ffff; | |
| } | |
| .instructions { | |
| margin-top: 30px; | |
| text-align: center; | |
| max-width: 500px; | |
| line-height: 1.6; | |
| } | |
| .highlight { | |
| color: #00ffff; | |
| font-weight: bold; | |
| } | |
| .powerBar { | |
| margin-top: 10px; | |
| height: 10px; | |
| background: #333; | |
| border-radius: 5px; | |
| overflow: hidden; | |
| } | |
| .powerFill { | |
| height: 100%; | |
| background: linear-gradient(to right, #00aaff, #00ffff); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| /* Ability Icons */ | |
| .abilities-container { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| display: flex; | |
| gap: 15px; | |
| z-index: 5; | |
| } | |
| .ability { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 10px; | |
| background: rgba(0, 0, 0, 0.5); | |
| border: 2px solid rgba(255, 255, 255, 0.2); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .ability:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
| } | |
| .ability-icon { | |
| font-size: 24px; | |
| margin-bottom: 5px; | |
| } | |
| .ability-key { | |
| position: absolute; | |
| top: 5px; | |
| right: 5px; | |
| font-size: 12px; | |
| background: rgba(0, 0, 0, 0.5); | |
| border-radius: 3px; | |
| padding: 2px 4px; | |
| } | |
| .ability-cooldown { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 0%; | |
| background: rgba(0, 0, 0, 0.7); | |
| border-radius: 0 0 8px 8px; | |
| transition: height 0.1s; | |
| } | |
| .ability.active { | |
| border-color: #00ffff; | |
| box-shadow: 0 0 15px #00ffff; | |
| } | |
| .ability.on-cooldown { | |
| opacity: 0.6; | |
| } | |
| .ability-timer { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 14px; | |
| font-weight: bold; | |
| color: white; | |
| text-shadow: 0 0 3px black; | |
| display: none; | |
| } | |
| .ability.active .ability-timer { | |
| display: block; | |
| } | |
| /* Specific ability styles */ | |
| #bulletModeAbility { | |
| border-color: #aa00aa; | |
| } | |
| #bulletModeAbility.active { | |
| border-color: #ff00ff; | |
| box-shadow: 0 0 15px #ff00ff; | |
| } | |
| #rocketAbility { | |
| border-color: #ff5500; | |
| } | |
| #rocketAbility.active { | |
| border-color: #ffaa00; | |
| box-shadow: 0 0 15px #ffaa00; | |
| } | |
| #wingmanAbility { | |
| border-color: #55aa00; | |
| } | |
| #wingmanAbility.active { | |
| border-color: #55ff00; | |
| box-shadow: 0 0 15px #55ff00; | |
| } | |
| #miniShipAbility { | |
| border-color: #0055ff; | |
| } | |
| #miniShipAbility.active { | |
| border-color: #00aaff; | |
| box-shadow: 0 0 15px #00aaff; | |
| } | |
| .wingman-timer { | |
| position: absolute; | |
| bottom: 100px; | |
| right: 20px; | |
| color: #55ff00; | |
| font-size: 14px; | |
| text-shadow: 0 0 5px #55ff00; | |
| z-index: 5; | |
| display: none; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| } | |
| .mini-ship-timer { | |
| position: absolute; | |
| bottom: 130px; | |
| right: 20px; | |
| color: #00aaff; | |
| font-size: 14px; | |
| text-shadow: 0 0 5px #00aaff; | |
| z-index: 5; | |
| display: none; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| } | |
| </style> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="gameUI"> | |
| <div>Score: <span id="score">0</span></div> | |
| <div>Lives: <span id="lives">3</span></div> | |
| <div>Level: <span id="level">1</span></div> | |
| <div>Bullet Power: <span id="bulletPower">1</span>/5</div> | |
| <div class="powerBar"> | |
| <div class="powerFill" id="powerFill"></div> | |
| </div> | |
| </div> | |
| <div id="startScreen"> | |
| <h1>SPACE DEFENDER</h1> | |
| <p class="instructions"> | |
| Defend your sector from the alien invasion!<br> | |
| Use <span class="highlight">mouse</span> to move your ship and <span class="highlight">click</span> to shoot.<br> | |
| Press <span class="highlight">SPACE</span> or click the icon to switch bullet modes.<br> | |
| Press <span class="highlight">R</span> or click the icon to fire a powerful rocket (cooldown: 5s).<br> | |
| Press <span class="highlight">W</span> or click the icon to deploy wingman drones (cooldown: 10s).<br> | |
| Press <span class="highlight">M</span> or click the icon to summon mini ships (cooldown: 15s).<br> | |
| Destroy aliens to increase your bullet power!<br> | |
| <span class="highlight">Every 5 kills</span> gives you an additional bullet (max 5)! | |
| </p> | |
| <button id="startButton">START MISSION</button> | |
| </div> | |
| <div id="gameOverScreen"> | |
| <h1>MISSION FAILED</h1> | |
| <p>Your final score: <span id="finalScore">0</span></p> | |
| <p>Max bullet power: <span id="finalPower">1</span>/5</p> | |
| <button id="restartButton">TRY AGAIN</button> | |
| </div> | |
| <!-- Ability Icons --> | |
| <div class="abilities-container"> | |
| <div class="ability" id="bulletModeAbility" title="Switch Bullet Mode (SPACE)"> | |
| <div class="ability-icon"><i class="fas fa-arrows-alt-h"></i></div> | |
| <div class="ability-key">SPACE</div> | |
| <div class="ability-cooldown"></div> | |
| <div class="ability-timer"></div> | |
| </div> | |
| <div class="ability" id="rocketAbility" title="Fire Rocket (R)"> | |
| <div class="ability-icon"><i class="fas fa-rocket"></i></div> | |
| <div class="ability-key">R</div> | |
| <div class="ability-cooldown"></div> | |
| <div class="ability-timer"></div> | |
| </div> | |
| <div class="ability" id="wingmanAbility" title="Deploy Wingmen (W)"> | |
| <div class="ability-icon"><i class="fas fa-fighter-jet"></i></div> | |
| <div class="ability-key">W</div> | |
| <div class="ability-cooldown"></div> | |
| <div class="ability-timer"></div> | |
| </div> | |
| <div class="ability" id="miniShipAbility" title="Summon Mini Ships (M)"> | |
| <div class="ability-icon"><i class="fas fa-space-shuttle"></i></div> | |
| <div class="ability-key">M</div> | |
| <div class="ability-cooldown"></div> | |
| <div class="ability-timer"></div> | |
| </div> | |
| </div> | |
| <div class="wingman-timer" id="wingmanTimer">Wingmen: 10s</div> | |
| <div class="mini-ship-timer" id="miniShipTimer">Mini Ships: 10s</div> | |
| <script> | |
| // Game canvas setup | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Game state | |
| let gameRunning = false; | |
| let score = 0; | |
| let lives = 3; | |
| let level = 1; | |
| let aliens = []; | |
| let bullets = []; | |
| let rockets = []; | |
| let wingmen = []; | |
| let miniShips = []; | |
| let particles = []; | |
| let lastAlienSpawn = 0; | |
| let alienSpawnInterval = 2000; | |
| let alienSpeed = 1; | |
| let alienHealth = 1; | |
| let kills = 0; | |
| let bulletPower = 1; | |
| let maxBulletPower = 1; | |
| let bulletMode = 'spread'; // 'spread' or 'parallel' | |
| const MAX_BULLETS = 5; | |
| // Ability cooldowns | |
| const abilities = { | |
| bulletMode: { | |
| cooldown: 0, | |
| maxCooldown: 0, | |
| ready: true, | |
| element: document.getElementById('bulletModeAbility') | |
| }, | |
| rocket: { | |
| cooldown: 0, | |
| maxCooldown: 5000, // 5 seconds | |
| ready: true, | |
| element: document.getElementById('rocketAbility') | |
| }, | |
| wingman: { | |
| cooldown: 0, | |
| maxCooldown: 10000, // 10 seconds | |
| ready: true, | |
| element: document.getElementById('wingmanAbility'), | |
| active: false, | |
| duration: 0, | |
| maxDuration: 10000 // 10 seconds | |
| }, | |
| miniShip: { | |
| cooldown: 0, | |
| maxCooldown: 15000, // 15 seconds | |
| ready: true, | |
| element: document.getElementById('miniShipAbility'), | |
| active: false, | |
| duration: 0, | |
| maxDuration: 10000 // 10 seconds | |
| } | |
| }; | |
| // Player spaceship | |
| const player = { | |
| x: canvas.width / 2, | |
| y: canvas.height - 50, | |
| width: 40, | |
| height: 60, | |
| speed: 8, | |
| color: '#00ffff', | |
| lastShot: 0, | |
| shootDelay: 300 | |
| }; | |
| // UI elements | |
| const scoreElement = document.getElementById('score'); | |
| const livesElement = document.getElementById('lives'); | |
| const levelElement = document.getElementById('level'); | |
| const bulletPowerElement = document.getElementById('bulletPower'); | |
| const powerFillElement = document.getElementById('powerFill'); | |
| const finalScoreElement = document.getElementById('finalScore'); | |
| const finalPowerElement = document.getElementById('finalPower'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const startButton = document.getElementById('startButton'); | |
| const restartButton = document.getElementById('restartButton'); | |
| const wingmanTimerElement = document.getElementById('wingmanTimer'); | |
| const miniShipTimerElement = document.getElementById('miniShipTimer'); | |
| // Event listeners | |
| startButton.addEventListener('click', startGame); | |
| restartButton.addEventListener('click', startGame); | |
| canvas.addEventListener('mousemove', movePlayer); | |
| canvas.addEventListener('click', shoot); | |
| // Ability click handlers | |
| document.getElementById('bulletModeAbility').addEventListener('click', toggleBulletMode); | |
| document.getElementById('rocketAbility').addEventListener('click', fireRocket); | |
| document.getElementById('wingmanAbility').addEventListener('click', deployWingmen); | |
| document.getElementById('miniShipAbility').addEventListener('click', summonMiniShips); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space') { | |
| e.preventDefault(); | |
| toggleBulletMode(); | |
| } else if (e.code === 'KeyR' && abilities.rocket.ready) { | |
| e.preventDefault(); | |
| fireRocket(); | |
| } else if (e.code === 'KeyW' && abilities.wingman.ready) { | |
| e.preventDefault(); | |
| deployWingmen(); | |
| } else if (e.code === 'KeyM' && abilities.miniShip.ready) { | |
| e.preventDefault(); | |
| summonMiniShips(); | |
| } | |
| }); | |
| // Resize handler | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| player.y = canvas.height - 50; | |
| }); | |
| // Start game function | |
| function startGame() { | |
| gameRunning = true; | |
| score = 0; | |
| lives = 3; | |
| level = 1; | |
| aliens = []; | |
| bullets = []; | |
| rockets = []; | |
| wingmen = []; | |
| miniShips = []; | |
| particles = []; | |
| lastAlienSpawn = 0; | |
| alienSpawnInterval = 2000; | |
| alienSpeed = 1; | |
| alienHealth = 1; | |
| kills = 0; | |
| bulletPower = 1; | |
| maxBulletPower = 1; | |
| bulletMode = 'spread'; | |
| // Reset abilities | |
| for (const ability in abilities) { | |
| abilities[ability].cooldown = 0; | |
| abilities[ability].ready = true; | |
| abilities[ability].active = false; | |
| abilities[ability].duration = 0; | |
| abilities[ability].element.classList.remove('active', 'on-cooldown'); | |
| abilities[ability].element.querySelector('.ability-cooldown').style.height = '0%'; | |
| abilities[ability].element.querySelector('.ability-timer').style.display = 'none'; | |
| } | |
| scoreElement.textContent = score; | |
| livesElement.textContent = lives; | |
| levelElement.textContent = level; | |
| bulletPowerElement.textContent = `${bulletPower}/${MAX_BULLETS}`; | |
| powerFillElement.style.width = '0%'; | |
| wingmanTimerElement.style.display = 'none'; | |
| miniShipTimerElement.style.display = 'none'; | |
| startScreen.style.display = 'none'; | |
| gameOverScreen.style.display = 'none'; | |
| // Start game loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Toggle bullet mode | |
| function toggleBulletMode() { | |
| if (!gameRunning) return; | |
| bulletMode = bulletMode === 'spread' ? 'parallel' : 'spread'; | |
| // Visual feedback | |
| abilities.bulletMode.element.classList.add('active'); | |
| setTimeout(() => { | |
| abilities.bulletMode.element.classList.remove('active'); | |
| }, 300); | |
| if (bulletMode === 'spread') { | |
| abilities.bulletMode.element.style.borderColor = '#aa00aa'; | |
| } else { | |
| abilities.bulletMode.element.style.borderColor = '#00aa00'; | |
| } | |
| // Visual effect | |
| for (let i = 0; i < 20; i++) { | |
| particles.push({ | |
| x: player.x, | |
| y: player.y, | |
| radius: Math.random() * 8 + 2, | |
| color: bulletMode === 'spread' ? '#ff00ff' : '#00ff00', | |
| speedX: Math.random() * 6 - 3, | |
| speedY: Math.random() * 6 - 3, | |
| life: Math.random() * 30 + 20, | |
| decay: Math.random() * 0.1 + 0.9 | |
| }); | |
| } | |
| } | |
| // Fire rocket | |
| function fireRocket() { | |
| if (!gameRunning || !abilities.rocket.ready) return; | |
| // Create rocket | |
| rockets.push({ | |
| x: player.x, | |
| y: player.y - player.height / 2, | |
| width: 15, | |
| height: 30, | |
| speedY: -15, | |
| color: '#ff5500', | |
| explosionRadius: 100, | |
| damage: 5 | |
| }); | |
| // Rocket exhaust effect | |
| for (let i = 0; i < 20; i++) { | |
| particles.push({ | |
| x: player.x, | |
| y: player.y - player.height / 2 + 15, | |
| radius: Math.random() * 6 + 3, | |
| color: '#ffaa00', | |
| speedX: (Math.random() - 0.5) * 3, | |
| speedY: Math.random() * 5, | |
| life: 30, | |
| decay: 0.9 | |
| }); | |
| } | |
| // Start cooldown | |
| abilities.rocket.ready = false; | |
| abilities.rocket.cooldown = abilities.rocket.maxCooldown; | |
| abilities.rocket.element.classList.add('on-cooldown'); | |
| // Visual feedback | |
| abilities.rocket.element.classList.add('active'); | |
| setTimeout(() => { | |
| abilities.rocket.element.classList.remove('active'); | |
| }, 300); | |
| } | |
| // Deploy wingmen drones | |
| function deployWingmen() { | |
| if (!gameRunning || !abilities.wingman.ready) return; | |
| // Create two wingmen drones | |
| wingmen.push({ | |
| x: player.x - 60, | |
| y: player.y, | |
| width: 25, | |
| height: 40, | |
| offsetX: -60, | |
| offsetY: 0, | |
| targetX: player.x - 60, | |
| targetY: player.y, | |
| color: '#55ff00', | |
| lastShot: 0, | |
| shootDelay: 800 | |
| }); | |
| wingmen.push({ | |
| x: player.x + 60, | |
| y: player.y, | |
| width: 25, | |
| height: 40, | |
| offsetX: 60, | |
| offsetY: 0, | |
| targetX: player.x + 60, | |
| targetY: player.y, | |
| color: '#55ff00', | |
| lastShot: 0, | |
| shootDelay: 800 | |
| }); | |
| // Wingman deployment effect | |
| for (let i = 0; i < 30; i++) { | |
| particles.push({ | |
| x: player.x - 60, | |
| y: player.y, | |
| radius: Math.random() * 5 + 2, | |
| color: '#55ff00', | |
| speedX: (Math.random() - 0.5) * 4, | |
| speedY: (Math.random() - 0.5) * 4, | |
| life: Math.random() * 30 + 20, | |
| decay: 0.9 | |
| }); | |
| particles.push({ | |
| x: player.x + 60, | |
| y: player.y, | |
| radius: Math.random() * 5 + 2, | |
| color: '#55ff00', | |
| speedX: (Math.random() - 0.5) * 4, | |
| speedY: (Math.random() - 0.5) * 4, | |
| life: Math.random() * 30 + 20, | |
| decay: 0.9 | |
| }); | |
| } | |
| // Start wingman duration | |
| abilities.wingman.active = true; | |
| abilities.wingman.duration = abilities.wingman.maxDuration; | |
| wingmanTimerElement.style.display = 'block'; | |
| wingmanTimerElement.textContent = `Wingmen: ${Math.ceil(abilities.wingman.duration/1000)}s`; | |
| // Start cooldown | |
| abilities.wingman.ready = false; | |
| abilities.wingman.cooldown = abilities.wingman.maxCooldown; | |
| abilities.wingman.element.classList.add('on-cooldown'); | |
| // Visual feedback | |
| abilities.wingman.element.classList.add('active'); | |
| setTimeout(() => { | |
| abilities.wingman.element.classList.remove('active'); | |
| }, 300); | |
| } | |
| // Summon mini ships | |
| function summonMiniShips() { | |
| if (!gameRunning || !abilities.miniShip.ready) return; | |
| // Create three mini ships | |
| for (let i = 0; i < 3; i++) { | |
| miniShips.push({ | |
| x: player.x + (i - 1) * 40, | |
| y: player.y, | |
| width: 20, | |
| height: 30, | |
| color: '#00aaff', | |
| lastShot: 0, | |
| shootDelay: 500, | |
| targetAlien: null | |
| }); | |
| } | |
| // Mini ship summon effect | |
| for (let i = 0; i < 40; i++) { | |
| particles.push({ | |
| x: player.x, | |
| y: player.y, | |
| radius: Math.random() * 6 + 3, | |
| color: '#00aaff', | |
| speedX: (Math.random() - 0.5) * 6, | |
| speedY: (Math.random() - 0.5) * 6, | |
| life: Math.random() * 30 + 20, | |
| decay: 0.9 | |
| }); | |
| } | |
| // Start mini ship duration | |
| abilities.miniShip.active = true; | |
| abilities.miniShip.duration = abilities.miniShip.maxDuration; | |
| miniShipTimerElement.style.display = 'block'; | |
| miniShipTimerElement.textContent = `Mini Ships: ${Math.ceil(abilities.miniShip.duration/1000)}s`; | |
| // Start cooldown | |
| abilities.miniShip.ready = false; | |
| abilities.miniShip.cooldown = abilities.miniShip.maxCooldown; | |
| abilities.miniShip.element.classList.add('on-cooldown'); | |
| // Visual feedback | |
| abilities.miniShip.element.classList.add('active'); | |
| setTimeout(() => { | |
| abilities.miniShip.element.classList.remove('active'); | |
| }, 300); | |
| } | |
| // Update wingmen | |
| function updateWingmen(deltaTime) { | |
| if (!abilities.wingman.active) return; | |
| // Update wingman duration | |
| abilities.wingman.duration -= deltaTime; | |
| wingmanTimerElement.textContent = `Wingmen: ${Math.ceil(abilities.wingman.duration/1000)}s`; | |
| if (abilities.wingman.duration <= 0) { | |
| abilities.wingman.active = false; | |
| wingmen = []; | |
| wingmanTimerElement.style.display = 'none'; | |
| } | |
| // Update wingmen positions to follow player with slight delay | |
| for (let i = 0; i < wingmen.length; i++) { | |
| const wingman = wingmen[i]; | |
| // Update target position relative to player | |
| wingman.targetX = player.x + wingman.offsetX; | |
| wingman.targetY = player.y + wingman.offsetY; | |
| // Move towards target with easing | |
| wingman.x += (wingman.targetX - wingman.x) * 0.1; | |
| wingman.y += (wingman.targetY - wingman.y) * 0.1; | |
| // Draw wingman | |
| ctx.fillStyle = wingman.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(wingman.x, wingman.y - wingman.height / 2); | |
| ctx.lineTo(wingman.x - wingman.width / 2, wingman.y + wingman.height / 2); | |
| ctx.lineTo(wingman.x + wingman.width / 2, wingman.y + wingman.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Cockpit glow | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; | |
| ctx.beginPath(); | |
| ctx.arc(wingman.x, wingman.y - wingman.height / 4, wingman.width / 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Engine glow | |
| ctx.fillStyle = 'rgba(150, 255, 100, 0.7)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(wingman.x - wingman.width / 3, wingman.y + wingman.height / 2); | |
| ctx.lineTo(wingman.x, wingman.y + wingman.height / 2 + 10); | |
| ctx.lineTo(wingman.x + wingman.width / 3, wingman.y + wingman.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Wingmen shoot at aliens | |
| const now = Date.now(); | |
| if (now - wingman.lastShot >= wingman.shootDelay && aliens.length > 0) { | |
| wingman.lastShot = now; | |
| // Find closest alien | |
| let closestAlien = null; | |
| let minDistance = Infinity; | |
| for (const alien of aliens) { | |
| const distance = Math.sqrt( | |
| Math.pow(wingman.x - (alien.x + alien.width / 2), 2) + | |
| Math.pow(wingman.y - (alien.y + alien.height / 2), 2) | |
| ); | |
| if (distance < minDistance) { | |
| minDistance = distance; | |
| closestAlien = alien; | |
| } | |
| } | |
| if (closestAlien) { | |
| // Calculate direction to alien | |
| const angle = Math.atan2( | |
| closestAlien.y + closestAlien.height / 2 - wingman.y, | |
| closestAlien.x + closestAlien.width / 2 - wingman.x | |
| ); | |
| // Create bullet | |
| bullets.push({ | |
| x: wingman.x, | |
| y: wingman.y - wingman.height / 2, | |
| width: 3, | |
| height: 15, | |
| speedX: Math.cos(angle) * 7, | |
| speedY: Math.sin(angle) * 7, | |
| color: '#55ff00' | |
| }); | |
| // Muzzle flash effect | |
| for (let j = 0; j < 5; j++) { | |
| particles.push({ | |
| x: wingman.x, | |
| y: wingman.y - wingman.height / 2, | |
| radius: Math.random() * 3 + 1, | |
| color: '#55ff00', | |
| speedX: Math.cos(angle) * (Math.random() * 3 + 2), | |
| speedY: Math.sin(angle) * (Math.random() * 3 + 2), | |
| life: 15, | |
| decay: 0.9 | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Update mini ships | |
| function updateMiniShips(deltaTime) { | |
| if (!abilities.miniShip.active) return; | |
| // Update mini ship duration | |
| abilities.miniShip.duration -= deltaTime; | |
| miniShipTimerElement.textContent = `Mini Ships: ${Math.ceil(abilities.miniShip.duration/1000)}s`; | |
| if (abilities.miniShip.duration <= 0) { | |
| abilities.miniShip.active = false; | |
| miniShips = []; | |
| miniShipTimerElement.style.display = 'none'; | |
| } | |
| // Update mini ships | |
| for (let i = miniShips.length - 1; i >= 0; i--) { | |
| const miniShip = miniShips[i]; | |
| // Find the furthest alien if not already targeting one | |
| if (!miniShip.targetAlien || miniShip.targetAlien.health <= 0) { | |
| let furthestAlien = null; | |
| let maxDistance = 0; | |
| for (const alien of aliens) { | |
| const distance = Math.sqrt( | |
| Math.pow(miniShip.x - (alien.x + alien.width / 2), 2) + | |
| Math.pow(miniShip.y - (alien.y + alien.height / 2), 2) | |
| ); | |
| if (distance > maxDistance) { | |
| maxDistance = distance; | |
| furthestAlien = alien; | |
| } | |
| } | |
| miniShip.targetAlien = furthestAlien; | |
| } | |
| // Move towards target alien if exists | |
| if (miniShip.targetAlien) { | |
| const targetX = miniShip.targetAlien.x + miniShip.targetAlien.width / 2; | |
| const targetY = miniShip.targetAlien.y + miniShip.targetAlien.height / 2; | |
| // Move with easing | |
| miniShip.x += (targetX - miniShip.x) * 0.05; | |
| miniShip.y += (targetY - miniShip.y) * 0.05; | |
| // Shoot at target | |
| const now = Date.now(); | |
| if (now - miniShip.lastShot >= miniShip.shootDelay) { | |
| miniShip.lastShot = now; | |
| // Calculate direction to target | |
| const angle = Math.atan2( | |
| targetY - miniShip.y, | |
| targetX - miniShip.x | |
| ); | |
| // Create bullet | |
| bullets.push({ | |
| x: miniShip.x, | |
| y: miniShip.y, | |
| width: 3, | |
| height: 10, | |
| speedX: Math.cos(angle) * 8, | |
| speedY: Math.sin(angle) * 8, | |
| color: '#00aaff' | |
| }); | |
| // Muzzle flash effect | |
| for (let j = 0; j < 3; j++) { | |
| particles.push({ | |
| x: miniShip.x, | |
| y: miniShip.y, | |
| radius: Math.random() * 2 + 1, | |
| color: '#00aaff', | |
| speedX: Math.cos(angle) * (Math.random() * 2 + 1), | |
| speedY: Math.sin(angle) * (Math.random() * 2 + 1), | |
| life: 10, | |
| decay: 0.9 | |
| }); | |
| } | |
| } | |
| } else { | |
| // No target, return to player | |
| miniShip.x += (player.x - miniShip.x) * 0.05; | |
| miniShip.y += (player.y - 50 - miniShip.y) * 0.05; | |
| } | |
| // Draw mini ship | |
| ctx.fillStyle = miniShip.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(miniShip.x, miniShip.y - miniShip.height / 2); | |
| ctx.lineTo(miniShip.x - miniShip.width / 2, miniShip.y + miniShip.height / 2); | |
| ctx.lineTo(miniShip.x + miniShip.width / 2, miniShip.y + miniShip.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Cockpit glow | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; | |
| ctx.beginPath(); | |
| ctx.arc(miniShip.x, miniShip.y - miniShip.height / 4, miniShip.width / 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Engine glow | |
| ctx.fillStyle = 'rgba(100, 200, 255, 0.7)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(miniShip.x - miniShip.width / 3, miniShip.y + miniShip.height / 2); | |
| ctx.lineTo(miniShip.x, miniShip.y + miniShip.height / 2 + 8); | |
| ctx.lineTo(miniShip.x + miniShip.width / 3, miniShip.y + miniShip.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| } | |
| // Update cooldowns | |
| function updateCooldowns(deltaTime) { | |
| for (const ability in abilities) { | |
| if (!abilities[ability].ready) { | |
| abilities[ability].cooldown -= deltaTime; | |
| // Update UI | |
| const cooldownPercent = (1 - abilities[ability].cooldown / abilities[ability].maxCooldown) * 100; | |
| abilities[ability].element.querySelector('.ability-cooldown').style.height = `${100 - cooldownPercent}%`; | |
| // Update timer for active abilities | |
| if (abilities[ability].active) { | |
| const timer = abilities[ability].element.querySelector('.ability-timer'); | |
| timer.textContent = Math.ceil(abilities[ability].duration / 1000); | |
| } | |
| if (abilities[ability].cooldown <= 0) { | |
| abilities[ability].ready = true; | |
| abilities[ability].cooldown = 0; | |
| abilities[ability].element.classList.remove('on-cooldown'); | |
| } | |
| } | |
| } | |
| } | |
| // Game loop | |
| let lastTime = 0; | |
| function gameLoop(timestamp) { | |
| if (!gameRunning) return; | |
| const deltaTime = timestamp - lastTime; | |
| lastTime = timestamp; | |
| // Clear canvas | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw stars | |
| drawStars(); | |
| // Spawn aliens | |
| if (timestamp - lastAlienSpawn > alienSpawnInterval) { | |
| spawnAlien(); | |
| lastAlienSpawn = timestamp; | |
| } | |
| // Update and draw player | |
| drawPlayer(); | |
| // Update and draw wingmen | |
| updateWingmen(deltaTime); | |
| // Update and draw mini ships | |
| updateMiniShips(deltaTime); | |
| // Update and draw bullets | |
| updateBullets(); | |
| // Update and draw rockets | |
| updateRockets(); | |
| // Update and draw aliens | |
| updateAliens(); | |
| // Update and draw particles | |
| updateParticles(); | |
| // Update cooldowns | |
| updateCooldowns(deltaTime); | |
| // Check for level up | |
| if (score >= level * 1000) { | |
| levelUp(); | |
| } | |
| // Continue game loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Draw stars background | |
| function drawStars() { | |
| ctx.fillStyle = 'white'; | |
| for (let i = 0; i < 100; i++) { | |
| const x = Math.sin(i * 100) * canvas.width / 2 + canvas.width / 2; | |
| const y = Math.cos(i * 100) * canvas.height / 2 + canvas.height / 2; | |
| const size = Math.random() * 2; | |
| ctx.globalAlpha = Math.random(); | |
| ctx.fillRect(x, y, size, size); | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |
| // Move player with mouse | |
| function movePlayer(e) { | |
| player.x = e.clientX; | |
| if (player.x < player.width / 2) player.x = player.width / 2; | |
| if (player.x > canvas.width - player.width / 2) player.x = canvas.width - player.width / 2; | |
| } | |
| // Draw player spaceship | |
| function drawPlayer() { | |
| // Ship body | |
| ctx.fillStyle = player.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(player.x, player.y - player.height / 2); | |
| ctx.lineTo(player.x - player.width / 2, player.y + player.height / 2); | |
| ctx.lineTo(player.x + player.width / 2, player.y + player.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Cockpit glow | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; | |
| ctx.beginPath(); | |
| ctx.arc(player.x, player.y - player.height / 4, player.width / 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Engine glow | |
| ctx.fillStyle = 'rgba(255, 100, 0, 0.7)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(player.x - player.width / 3, player.y + player.height / 2); | |
| ctx.lineTo(player.x, player.y + player.height / 2 + 15); | |
| ctx.lineTo(player.x + player.width / 3, player.y + player.height / 2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Player shoot function | |
| function shoot(e) { | |
| if (!gameRunning) return; | |
| const now = Date.now(); | |
| if (now - player.lastShot < player.shootDelay) return; | |
| player.lastShot = now; | |
| // Create bullets based on current bullet power and mode | |
| if (bulletMode === 'spread') { | |
| // Spread mode - bullets fan out in an arc | |
| for (let i = 0; i < bulletPower; i++) { | |
| // Calculate angle offset for each bullet | |
| const angleOffset = (i - (bulletPower - 1) / 2) * 0.3; // Spread angle | |
| bullets.push({ | |
| x: player.x, | |
| y: player.y - player.height / 2, | |
| width: 3, | |
| height: 15, | |
| speedX: Math.sin(angleOffset) * 3, // Horizontal speed for spread | |
| speedY: -10, // Base upward speed | |
| color: '#00ffff' | |
| }); | |
| } | |
| } else { | |
| // Parallel mode - bullets move straight up in parallel lines | |
| for (let i = 0; i < bulletPower; i++) { | |
| // Calculate horizontal offset for each bullet | |
| const xOffset = (i - (bulletPower - 1) / 2) * 15; | |
| bullets.push({ | |
| x: player.x + xOffset, | |
| y: player.y - player.height / 2, | |
| width: 3, | |
| height: 15, | |
| speedX: 0, // No horizontal movement | |
| speedY: -10, // Upward speed | |
| color: '#00ff00' // Different color for parallel mode | |
| }); | |
| } | |
| } | |
| // Muzzle flash effect | |
| for (let j = 0; j < 10; j++) { | |
| particles.push({ | |
| x: player.x, | |
| y: player.y - player.height / 2, | |
| radius: Math.random() * 5 + 2, | |
| color: bulletMode === 'spread' ? '#00ffff' : '#00ff00', | |
| speedX: (Math.random() - 0.5) * 4, | |
| speedY: Math.random() * -5, | |
| life: 20, | |
| decay: Math.random() * 0.2 + 0.8 | |
| }); | |
| } | |
| } | |
| // Update bullets | |
| function updateBullets() { | |
| for (let i = bullets.length - 1; i >= 0; i--) { | |
| const bullet = bullets[i]; | |
| // Update position based on speed | |
| bullet.x += bullet.speedX || 0; | |
| bullet.y += bullet.speedY; | |
| // Draw bullet | |
| ctx.fillStyle = bullet.color; | |
| ctx.fillRect(bullet.x - bullet.width / 2, bullet.y, bullet.width, bullet.height); | |
| // Add bullet trail | |
| particles.push({ | |
| x: bullet.x, | |
| y: bullet.y + bullet.height, | |
| radius: Math.random() * 2 + 1, | |
| color: bullet.color, | |
| speedX: 0, | |
| speedY: 0, | |
| life: 10, | |
| decay: 0.9 | |
| }); | |
| // Remove bullets that are off screen | |
| if (bullet.y + bullet.height < 0 || | |
| bullet.x < 0 || | |
| bullet.x > canvas.width) { | |
| bullets.splice(i, 1); | |
| } | |
| } | |
| } | |
| // Update rockets | |
| function updateRockets() { | |
| for (let i = rockets.length - 1; i >= 0; i--) { | |
| const rocket = rockets[i]; | |
| // Update position | |
| rocket.y += rocket.speedY; | |
| // Draw rocket | |
| ctx.fillStyle = rocket.color; | |
| ctx.beginPath(); | |
| ctx.moveTo(rocket.x, rocket.y); | |
| ctx.lineTo(rocket.x - rocket.width / 2, rocket.y + rocket.height); | |
| ctx.lineTo(rocket.x + rocket.width / 2, rocket.y + rocket.height); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Rocket flame effect | |
| if (Math.random() > 0.3) { | |
| particles.push({ | |
| x: rocket.x + (Math.random() - 0.5) * rocket.width / 2, | |
| y: rocket.y + rocket.height, | |
| radius: Math.random() * 5 + 2, | |
| color: '#ffaa00', | |
| speedX: (Math.random() - 0.5) * 2, | |
| speedY: Math.random() * 5, | |
| life: 15, | |
| decay: 0.9 | |
| }); | |
| } | |
| // Check for collisions with aliens | |
| for (let j = aliens.length - 1; j >= 0; j--) { | |
| const alien = aliens[j]; | |
| const distance = Math.sqrt( | |
| Math.pow(rocket.x - (alien.x + alien.width / 2), 2) + | |
| Math.pow(rocket.y - (alien.y + alien.height / 2), 2) | |
| ); | |
| if (distance < rocket.explosionRadius) { | |
| // Rocket hit alien | |
| alien.health -= rocket.damage; | |
| if (alien.health <= 0) { | |
| // Alien destroyed | |
| createExplosion( | |
| alien.x + alien.width / 2, | |
| alien.y + alien.height / 2, | |
| alien.color, | |
| 25 | |
| ); | |
| score += alien.points; | |
| kills++; | |
| scoreElement.textContent = score; | |
| aliens.splice(j, 1); | |
| // Check for bullet power increase | |
| updateBulletPower(); | |
| } | |
| } | |
| } | |
| // Create explosion when rocket reaches top or hits alien | |
| if (rocket.y + rocket.height < 0) { | |
| // Rocket reached top of screen - explode | |
| createExplosion( | |
| rocket.x, | |
| rocket.y, | |
| rocket.color, | |
| 30 | |
| ); | |
| // Damage all aliens in explosion radius | |
| for (let j = aliens.length - 1; j >= 0; j--) { | |
| const alien = aliens[j]; | |
| const distance = Math.sqrt( | |
| Math.pow(rocket.x - (alien.x + alien.width / 2), 2) + | |
| Math.pow(rocket.y - (alien.y + alien.height / 2), 2) | |
| ); | |
| if (distance < rocket.explosionRadius) { | |
| alien.health -= rocket.damage; | |
| if (alien.health <= 0) { | |
| score += alien.points; | |
| kills++; | |
| scoreElement.textContent = score; | |
| aliens.splice(j, 1); | |
| updateBulletPower(); | |
| } | |
| } | |
| } | |
| rockets.splice(i, 1); | |
| } | |
| } | |
| } | |
| // Spawn alien | |
| function spawnAlien() { | |
| const size = Math.random() * 30 + 20; | |
| const health = Math.floor(Math.random() * level) + alienHealth; | |
| aliens.push({ | |
| x: Math.random() * (canvas.width - size), | |
| y: -size, | |
| width: size, | |
| height: size, | |
| speed: Math.random() * alienSpeed + alienSpeed, | |
| color: `hsl(${Math.random() * 60 + 300}, 70%, 50%)`, | |
| health: health, | |
| maxHealth: health, | |
| points: health * 100 | |
| }); | |
| } | |
| // Update aliens | |
| function updateAliens() { | |
| for (let i = aliens.length - 1; i >= 0; i--) { | |
| const alien = aliens[i]; | |
| alien.y += alien.speed; | |
| // Draw alien | |
| ctx.fillStyle = alien.color; | |
| ctx.beginPath(); | |
| ctx.arc(alien.x + alien.width / 2, alien.y + alien.height / 2, alien.width / 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw alien details | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.beginPath(); | |
| ctx.arc(alien.x + alien.width / 2, alien.y + alien.height / 3, alien.width / 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw health bar | |
| const healthPercent = alien.health / alien.maxHealth; | |
| ctx.fillStyle = 'red'; | |
| ctx.fillRect(alien.x, alien.y - 10, alien.width, 5); | |
| ctx.fillStyle = 'lime'; | |
| ctx.fillRect(alien.x, alien.y - 10, alien.width * healthPercent, 5); | |
| // Check for bullet collisions | |
| for (let j = bullets.length - 1; j >= 0; j--) { | |
| const bullet = bullets[j]; | |
| if ( | |
| bullet.x + bullet.width / 2 > alien.x && | |
| bullet.x - bullet.width / 2 < alien.x + alien.width && | |
| bullet.y < alien.y + alien.height && | |
| bullet.y + bullet.height > alien.y | |
| ) { | |
| // Hit effect | |
| createExplosion( | |
| bullet.x, | |
| bullet.y, | |
| alien.color, | |
| 5 | |
| ); | |
| alien.health--; | |
| bullets.splice(j, 1); | |
| if (alien.health <= 0) { | |
| // Alien destroyed | |
| createExplosion( | |
| alien.x + alien.width / 2, | |
| alien.y + alien.height / 2, | |
| alien.color, | |
| 15 | |
| ); | |
| score += alien.points; | |
| kills++; | |
| scoreElement.textContent = score; | |
| aliens.splice(i, 1); | |
| // Check for bullet power increase | |
| updateBulletPower(); | |
| break; | |
| } | |
| } | |
| } | |
| // Check if alien reached bottom | |
| if (alien.y > canvas.height) { | |
| lives--; | |
| livesElement.textContent = lives; | |
| aliens.splice(i, 1); | |
| if (lives <= 0) { | |
| gameOver(); | |
| } | |
| } | |
| } | |
| } | |
| // Update bullet power based on kills | |
| function updateBulletPower() { | |
| // Every 5 kills increases bullet power (max 5) | |
| const newPower = Math.min(Math.floor(kills / 5) + 1, MAX_BULLETS); | |
| if (newPower > bulletPower) { | |
| bulletPower = newPower; | |
| maxBulletPower = Math.max(maxBulletPower, bulletPower); | |
| bulletPowerElement.textContent = `${bulletPower}/${MAX_BULLETS}`; | |
| // Power up effect | |
| for (let i = 0; i < 30; i++) { | |
| particles.push({ | |
| x: player.x, | |
| y: player.y, | |
| radius: Math.random() * 10 + 5, | |
| color: bulletMode === 'spread' ? '#00ffff' : '#00ff00', | |
| speedX: Math.random() * 6 - 3, | |
| speedY: Math.random() * 6 - 3, | |
| life: Math.random() * 30 + 20, | |
| decay: Math.random() * 0.1 + 0.9 | |
| }); | |
| } | |
| } | |
| // Update power bar | |
| const powerProgress = (kills % 5) / 5 * 100; | |
| powerFillElement.style.width = powerProgress + '%'; | |
| } | |
| // Create explosion effect | |
| function createExplosion(x, y, color, particleCount) { | |
| for (let i = 0; i < particleCount; i++) { | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| radius: Math.random() * 5 + 2, | |
| color: color, | |
| speedX: Math.random() * 6 - 3, | |
| speedY: Math.random() * 6 - 3, | |
| life: Math.random() * 30 + 20, | |
| decay: Math.random() * 0.1 + 0.9 | |
| }); | |
| } | |
| // Add shockwave effect for larger explosions | |
| if (particleCount > 15) { | |
| for (let i = 0; i < 10; i++) { | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| radius: Math.random() * 15 + 5, | |
| color: '#ffffff', | |
| speedX: (Math.random() - 0.5) * 10, | |
| speedY: (Math.random() - 0.5) * 10, | |
| life: 40, | |
| decay: 0.85 | |
| }); | |
| } | |
| } | |
| } | |
| // Update particles | |
| function updateParticles() { | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.x += p.speedX || 0; | |
| p.y += p.speedY || 0; | |
| p.life *= p.decay; | |
| ctx.globalAlpha = p.life / 100; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| if (p.life < 1) { | |
| particles.splice(i, 1); | |
| } | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |
| // Level up | |
| function levelUp() { | |
| level++; | |
| levelElement.textContent = level; | |
| // Increase difficulty | |
| alienSpawnInterval *= 0.9; | |
| alienSpeed += 0.2; | |
| alienHealth += 0.5; | |
| // Level up effect | |
| ctx.fillStyle = 'rgba(0, 255, 255, 0.5)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Level up message | |
| ctx.fillStyle = 'white'; | |
| ctx.font = '48px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(`LEVEL ${level}`, canvas.width / 2, canvas.height / 2); | |
| ctx.textAlign = 'left'; | |
| } | |
| // Game over | |
| function gameOver() { | |
| gameRunning = false; | |
| finalScoreElement.textContent = score; | |
| finalPowerElement.textContent = `${maxBulletPower}/${MAX_BULLETS}`; | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| </script> | |
| </body> | |
| </html> |