| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Sky Adventure - Plane Shooting Game</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 { |
| overflow: hidden; |
| touch-action: none; |
| margin: 0; |
| padding: 0; |
| } |
| #gameCanvas { |
| display: block; |
| background: linear-gradient(to bottom, #1e3c72 0%, #2a5298 100%); |
| } |
| .game-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| } |
| .cloud { |
| position: absolute; |
| background-color: rgba(255, 255, 255, 0.8); |
| border-radius: 50%; |
| } |
| @keyframes float { |
| 0% { transform: translateY(0px); } |
| 50% { transform: translateY(-10px); } |
| 100% { transform: translateY(0px); } |
| } |
| .plane { |
| animation: float 2s ease-in-out infinite; |
| } |
| .star { |
| position: absolute; |
| color: gold; |
| text-shadow: 0 0 10px yellow; |
| animation: twinkle 1s ease-in-out infinite alternate; |
| } |
| @keyframes twinkle { |
| from { opacity: 0.7; transform: scale(0.9); } |
| to { opacity: 1; transform: scale(1.1); } |
| } |
| .obstacle { |
| position: absolute; |
| background-color: #555; |
| border-radius: 5px; |
| } |
| .explosion { |
| position: absolute; |
| width: 60px; |
| height: 60px; |
| background: radial-gradient(circle, rgba(255,100,0,0.8) 0%, rgba(255,200,0,0.6) 50%, rgba(255,255,255,0) 70%); |
| border-radius: 50%; |
| animation: explode 0.5s ease-out forwards; |
| } |
| @keyframes explode { |
| 0% { transform: scale(0); opacity: 1; } |
| 100% { transform: scale(2); opacity: 0; } |
| } |
| .control-btn { |
| position: absolute; |
| width: 60px; |
| height: 60px; |
| background: rgba(255, 255, 255, 0.2); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 24px; |
| color: white; |
| pointer-events: auto; |
| user-select: none; |
| -webkit-tap-highlight-color: transparent; |
| } |
| .control-btn:active { |
| background: rgba(255, 255, 255, 0.4); |
| transform: scale(0.95); |
| } |
| #leftBtn { |
| bottom: 30px; |
| left: 30px; |
| } |
| #rightBtn { |
| bottom: 30px; |
| left: 110px; |
| } |
| #upBtn { |
| bottom: 100px; |
| right: 30px; |
| } |
| #downBtn { |
| bottom: 30px; |
| right: 30px; |
| } |
| #fireBtn { |
| bottom: 170px; |
| left: 30px; |
| } |
| .bullet { |
| position: absolute; |
| background: linear-gradient(to right, #ff0, #f80); |
| border-radius: 50%; |
| } |
| .enemy-bullet { |
| position: absolute; |
| background: linear-gradient(to right, #f00, #800); |
| border-radius: 50%; |
| } |
| .debris { |
| position: absolute; |
| background-color: #777; |
| border-radius: 2px; |
| } |
| .powerup { |
| position: absolute; |
| width: 40px; |
| height: 40px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 20px; |
| text-shadow: 0 0 5px white; |
| } |
| .shield { |
| position: absolute; |
| border-radius: 50%; |
| border: 3px solid rgba(0, 204, 255, 0.6); |
| pointer-events: none; |
| } |
| .homing-missile { |
| position: absolute; |
| background: linear-gradient(to bottom, #ff5f5f, #ff0000); |
| border-radius: 50% 50% 0 0; |
| transform-origin: center bottom; |
| } |
| @keyframes pulse { |
| 0% { transform: scale(1); opacity: 0.9; } |
| 50% { transform: scale(1.1); opacity: 1; } |
| 100% { transform: scale(1); opacity: 0.9; } |
| } |
| .powerup-effect { |
| position: absolute; |
| pointer-events: none; |
| animation: pulse 1.5s infinite; |
| } |
| .joystick { |
| position: absolute; |
| width: 100px; |
| height: 100px; |
| background: rgba(255, 255, 255, 0.2); |
| border-radius: 50%; |
| bottom: 30px; |
| left: 30px; |
| pointer-events: auto; |
| display: none; |
| } |
| .joystick-handle { |
| position: absolute; |
| width: 40px; |
| height: 40px; |
| background: rgba(255, 255, 255, 0.4); |
| border-radius: 50%; |
| top: 30px; |
| left: 30px; |
| } |
| .boss-health-bar { |
| position: absolute; |
| top: 10px; |
| left: 50%; |
| transform: translateX(-50%); |
| width: 200px; |
| height: 20px; |
| background: rgba(0, 0, 0, 0.5); |
| border-radius: 10px; |
| overflow: hidden; |
| } |
| .boss-health-fill { |
| height: 100%; |
| background: linear-gradient(to right, #ff0000, #ff9900); |
| width: 100%; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white flex flex-col items-center justify-center h-screen"> |
| <div class="relative w-full h-full"> |
| <canvas id="gameCanvas" class="w-full h-full"></canvas> |
| |
| |
| <div id="startScreen" class="game-overlay flex flex-col items-center justify-center bg-black bg-opacity-70"> |
| <h1 class="text-5xl font-bold mb-6 text-yellow-300">SKY ADVENTURE</h1> |
| <div class="plane text-6xl mb-8">✈️</div> |
| <p class="text-xl mb-8 text-center max-w-md px-4">控制飞机躲避障碍物<br>收集星星获得高分!<br>按射击按钮消灭障碍物!<br>收集道具获得特殊能力!</p> |
| <button id="startButton" class="bg-yellow-500 hover:bg-yellow-600 text-black font-bold py-3 px-8 rounded-full text-xl transition-all duration-300 transform hover:scale-105 pointer-events-auto"> |
| 开始游戏 |
| </button> |
| <div class="mt-8 grid grid-cols-3 gap-4 text-left max-w-md px-8"> |
| <div class="flex items-center"> |
| <div class="powerup bg-red-500 mr-2"><i class="fas fa-bolt"></i></div> |
| <span>火力增强</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-purple-500 mr-2"><i class="fas fa-rocket"></i></div> |
| <span>跟踪导弹</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-blue-500 mr-2"><i class="fas fa-shield-alt"></i></div> |
| <span>保护罩</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-green-500 mr-2"><i class="fas fa-heart"></i></div> |
| <span>恢复生命</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-cyan-500 mr-2"><i class="fas fa-clock"></i></div> |
| <span>时间减速</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-orange-500 mr-2"><i class="fas fa-bomb"></i></div> |
| <span>清屏炸弹</span> |
| </div> |
| <div class="flex items-center"> |
| <div class="powerup bg-pink-500 mr-2"><i class="fas fa-star"></i></div> |
| <span>双倍分数</span> |
| </div> |
| </div> |
| <div class="mt-4 text-sm text-gray-300"> |
| 最高分: <span id="highScoreDisplay">0</span> |
| </div> |
| </div> |
| |
| |
| <div id="gameUI" class="game-overlay hidden"> |
| <div class="absolute top-4 left-4 bg-black bg-opacity-50 px-4 py-2 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-star text-yellow-400 mr-2"></i> |
| <span id="scoreDisplay" class="text-xl">0</span> |
| </div> |
| </div> |
| <div class="absolute top-4 right-4 bg-black bg-opacity-50 px-4 py-2 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-heart text-red-500 mr-2"></i> |
| <span id="livesDisplay" class="text-xl">3</span> |
| </div> |
| </div> |
| <div class="absolute top-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 px-4 py-2 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-bolt text-yellow-400 mr-2"></i> |
| <span id="ammoDisplay" class="text-xl">∞</span> |
| </div> |
| </div> |
| <div class="absolute bottom-4 left-4 bg-black bg-opacity-50 px-4 py-2 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-tachometer-alt text-blue-400 mr-2"></i> |
| <span id="speedDisplay" class="text-xl">100</span> |
| <span class="ml-1">km/h</span> |
| </div> |
| </div> |
| |
| |
| <div id="powerupStatus" class="absolute bottom-24 right-4 flex gap-2"> |
| |
| </div> |
| |
| |
| <div id="bossHealthBar" class="boss-health-bar hidden"> |
| <div id="bossHealthFill" class="boss-health-fill"></div> |
| </div> |
| |
| |
| <div id="leftBtn" class="control-btn hidden"> |
| <i class="fas fa-arrow-left"></i> |
| </div> |
| <div id="rightBtn" class="control-btn hidden"> |
| <i class="fas fa-arrow-right"></i> |
| </div> |
| <div id="upBtn" class="control-btn hidden"> |
| <i class="fas fa-arrow-up"></i> |
| </div> |
| <div id="downBtn" class="control-btn hidden"> |
| <i class="fas fa-arrow-down"></i> |
| </div> |
| <div id="fireBtn" class="control-btn hidden"> |
| <i class="fas fa-bolt text-yellow-400"></i> |
| </div> |
| |
| |
| <div id="joystick" class="joystick hidden"> |
| <div id="joystickHandle" class="joystick-handle"></div> |
| </div> |
| </div> |
| |
| |
| <div id="gameOverScreen" class="game-overlay hidden flex flex-col items-center justify-center bg-black bg-opacity-70"> |
| <h1 class="text-5xl font-bold mb-6 text-red-500">GAME OVER</h1> |
| <div class="text-3xl mb-8"> |
| 得分: <span id="finalScore" class="text-yellow-400">0</span> |
| </div> |
| <div id="achievements" class="mb-4 text-center"> |
| |
| </div> |
| <button id="restartButton" class="bg-yellow-500 hover:bg-yellow-600 text-black font-bold py-3 px-8 rounded-full text-xl transition-all duration-300 transform hover:scale-105 pointer-events-auto"> |
| 再玩一次 |
| </button> |
| </div> |
| </div> |
|
|
| <audio id="shootSound" src="https://assets.mixkit.co/sfx/preview/mixkit-laser-weapon-shot-1680.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-achievement-bell-600.mp3" preload="auto"></audio> |
| <audio id="bgMusic" loop src="https://assets.mixkit.co/music/preview/mixkit-game-show-suspense-waiting-668.mp3" preload="auto"></audio> |
| <audio id="enemyShootSound" src="https://assets.mixkit.co/sfx/preview/mixkit-short-laser-gun-shot-1670.mp3" preload="auto"></audio> |
|
|
| <script> |
| |
| const gameState = { |
| started: false, |
| gameOver: false, |
| score: 0, |
| lives: 3, |
| ammo: Infinity, |
| speed: 100, |
| difficulty: 1, |
| timeSlow: 0, |
| doubleScore: 0, |
| bossActive: false, |
| bossHealth: 0, |
| bossMaxHealth: 0, |
| plane: { |
| x: 0, |
| y: 0, |
| width: 60, |
| height: 60, |
| velocity: 0, |
| verticalVelocity: 0, |
| rotation: 0, |
| lastFireTime: 0, |
| fireRate: 200, |
| bulletDamage: 1, |
| hasShield: false, |
| shieldDuration: 0, |
| powerups: { |
| rapidFire: 0, |
| homingMissiles: 0, |
| } |
| }, |
| stars: [], |
| obstacles: [], |
| clouds: [], |
| explosions: [], |
| bullets: [], |
| enemyBullets: [], |
| debris: [], |
| powerups: [], |
| homingMissiles: [], |
| effects: [], |
| particles: [], |
| lastStarTime: 0, |
| lastObstacleTime: 0, |
| lastCloudTime: 0, |
| lastPowerupTime: 0, |
| lastBossSpawnTime: 0, |
| lastEnemyShootTime: 0, |
| keys: { |
| ArrowUp: false, |
| ArrowDown: false, |
| ArrowLeft: false, |
| ArrowRight: false, |
| Space: false |
| }, |
| isMobile: false, |
| joystickActive: false, |
| joystickAngle: 0, |
| joystickDistance: 0, |
| achievements: { |
| firstBlood: false, |
| combo5: false, |
| noDamage: false, |
| bossSlayer: false |
| }, |
| highScore: localStorage.getItem('highScore') || 0 |
| }; |
| |
| |
| const POWERUP_TYPES = { |
| RAPID_FIRE: { |
| id: 'rapidFire', |
| icon: 'fas fa-bolt', |
| color: 'red', |
| duration: 10000, |
| effect: (game) => { |
| game.plane.fireRate = 100; |
| game.plane.powerups.rapidFire = Date.now() + POWERUP_TYPES.RAPID_FIRE.duration; |
| createEffect('火力增强!', 'red', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| HOMING_MISSILE: { |
| id: 'homingMissiles', |
| icon: 'fas fa-rocket', |
| color: 'purple', |
| duration: 10000, |
| effect: (game) => { |
| game.plane.powerups.homingMissiles = Date.now() + POWERUP_TYPES.HOMING_MISSILE.duration; |
| createEffect('跟踪导弹已激活!', 'purple', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| SHIELD: { |
| id: 'shield', |
| icon: 'fas fa-shield-alt', |
| color: 'blue', |
| duration: 8000, |
| effect: (game) => { |
| game.plane.hasShield = true; |
| game.plane.shieldDuration = Date.now() + POWERUP_TYPES.SHIELD.duration; |
| createEffect('保护罩已启用!', 'blue', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| HEALTH: { |
| id: 'health', |
| icon: 'fas fa-heart', |
| color: 'green', |
| effect: (game) => { |
| game.lives = Math.min(game.lives + 1, 5); |
| updateUI(); |
| createEffect('生命值恢复!', 'green', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| TIME_SLOW: { |
| id: 'timeSlow', |
| icon: 'fas fa-clock', |
| color: 'cyan', |
| duration: 8000, |
| effect: (game) => { |
| game.timeSlow = Date.now() + POWERUP_TYPES.TIME_SLOW.duration; |
| createEffect('时间减速!', 'cyan', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| CLEAR_SCREEN: { |
| id: 'clearScreen', |
| icon: 'fas fa-bomb', |
| color: 'orange', |
| effect: (game) => { |
| |
| game.obstacles.forEach(obstacle => { |
| createExplosion(obstacle.x, obstacle.y); |
| createDebris(obstacle, 8); |
| }); |
| game.obstacles = []; |
| createEffect('清屏炸弹!', 'orange', 1500); |
| playSound('powerupSound'); |
| } |
| }, |
| DOUBLE_SCORE: { |
| id: 'doubleScore', |
| icon: 'fas fa-star', |
| color: 'pink', |
| duration: 10000, |
| effect: (game) => { |
| game.doubleScore = Date.now() + POWERUP_TYPES.DOUBLE_SCORE.duration; |
| createEffect('双倍分数!', 'pink', 1500); |
| playSound('powerupSound'); |
| } |
| } |
| }; |
| |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const startScreen = document.getElementById('startScreen'); |
| const gameUI = document.getElementById('gameUI'); |
| const gameOverScreen = document.getElementById('gameOverScreen'); |
| const startButton = document.getElementById('startButton'); |
| const restartButton = document.getElementById('restartButton'); |
| const scoreDisplay = document.getElementById('scoreDisplay'); |
| const livesDisplay = document.getElementById('livesDisplay'); |
| const ammoDisplay = document.getElementById('ammoDisplay'); |
| const speedDisplay = document.getElementById('speedDisplay'); |
| const finalScore = document.getElementById('finalScore'); |
| const powerupStatus = document.getElementById('powerupStatus'); |
| const leftBtn = document.getElementById('leftBtn'); |
| const rightBtn = document.getElementById('rightBtn'); |
| const upBtn = document.getElementById('upBtn'); |
| const downBtn = document.getElementById('downBtn'); |
| const fireBtn = document.getElementById('fireBtn'); |
| const joystick = document.getElementById('joystick'); |
| const joystickHandle = document.getElementById('joystickHandle'); |
| const bossHealthBar = document.getElementById('bossHealthBar'); |
| const bossHealthFill = document.getElementById('bossHealthFill'); |
| const achievementsDisplay = document.getElementById('achievements'); |
| const highScoreDisplay = document.getElementById('highScoreDisplay'); |
| |
| |
| const shootSound = document.getElementById('shootSound'); |
| const explosionSound = document.getElementById('explosionSound'); |
| const powerupSound = document.getElementById('powerupSound'); |
| const bgMusic = document.getElementById('bgMusic'); |
| const enemyShootSound = document.getElementById('enemyShootSound'); |
| |
| |
| function playSound(soundElement) { |
| if (soundElement === 'bgMusic') { |
| bgMusic.currentTime = 0; |
| bgMusic.play().catch(e => console.log('Autoplay prevented:', e)); |
| } else { |
| const sound = document.getElementById(soundElement); |
| sound.currentTime = 0; |
| sound.play(); |
| } |
| } |
| |
| |
| function detectMobile() { |
| return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); |
| } |
| |
| |
| function resizeCanvas() { |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| if (gameState.started && !gameState.gameOver) { |
| gameState.plane.x = canvas.width / 2; |
| gameState.plane.y = canvas.height / 2; |
| } |
| } |
| |
| |
| function createEffect(text, color, duration) { |
| gameState.effects.push({ |
| text, |
| color, |
| x: gameState.plane.x, |
| y: gameState.plane.y - 50, |
| alpha: 1, |
| duration, |
| startTime: Date.now() |
| }); |
| } |
| |
| |
| function createParticles(x, y, count, color) { |
| for (let i = 0; i < count; i++) { |
| gameState.particles.push({ |
| x, |
| y, |
| size: Math.random() * 5 + 2, |
| speedX: (Math.random() - 0.5) * 4, |
| speedY: (Math.random() - 0.5) * 4, |
| color, |
| life: 100 |
| }); |
| } |
| } |
| |
| |
| function updateUI() { |
| scoreDisplay.textContent = gameState.score; |
| livesDisplay.textContent = gameState.lives; |
| speedDisplay.textContent = Math.floor(gameState.speed); |
| ammoDisplay.textContent = gameState.ammo === Infinity ? "∞" : gameState.ammo; |
| highScoreDisplay.textContent = gameState.highScore; |
| |
| |
| powerupStatus.innerHTML = ''; |
| |
| if (gameState.plane.powerups.rapidFire > Date.now()) { |
| const timeLeft = Math.ceil((gameState.plane.powerups.rapidFire - Date.now()) / 1000); |
| powerupStatus.innerHTML += ` |
| <div class="bg-black bg-opacity-50 px-3 py-1 rounded-lg flex items-center" title="火力增强 (${timeLeft}s)"> |
| <i class="fas fa-bolt text-red-500 mr-2"></i> |
| </div> |
| `; |
| } |
| |
| if (gameState.plane.powerups.homingMissiles > Date.now()) { |
| const timeLeft = Math.ceil((gameState.plane.powerups.homingMissiles - Date.now()) / 1000); |
| powerupStatus.innerHTML += ` |
| <div class="bg-black bg-opacity-50 px-3 py-1 rounded-lg flex items-center" title="跟踪导弹 (${timeLeft}s)"> |
| <i class="fas fa-rocket text-purple-500 mr-2"></i> |
| </div> |
| `; |
| } |
| |
| if (gameState.plane.hasShield && gameState.plane.shieldDuration > Date.now()) { |
| const timeLeft = Math.ceil((gameState.plane.shieldDuration - Date.now()) / 1000); |
| powerupStatus.innerHTML += ` |
| <div class="bg-black bg-opacity-50 px-3 py-1 rounded-lg flex items-center" title="保护罩 (${timeLeft}s)"> |
| <i class="fas fa-shield-alt text-blue-500 mr-2"></i> |
| </div> |
| `; |
| } |
| |
| if (gameState.timeSlow > Date.now()) { |
| const timeLeft = Math.ceil((gameState.timeSlow - Date.now()) / 1000); |
| powerupStatus.innerHTML += ` |
| <div class="bg-black bg-opacity-50 px-3 py-1 rounded-lg flex items-center" title="时间减速 (${timeLeft}s)"> |
| <i class="fas fa-clock text-cyan-500 mr-2"></i> |
| </div> |
| `; |
| } |
| |
| if (gameState.doubleScore > Date.now()) { |
| const timeLeft = Math.ceil((gameState.doubleScore - Date.now()) / 1000); |
| powerupStatus.innerHTML += ` |
| <div class="bg-black bg-opacity-50 px-3 py-1 rounded-lg flex items-center" title="双倍分数 (${timeLeft}s)"> |
| <i class="fas fa-star text-pink-500 mr-2"></i> |
| </div> |
| `; |
| } |
| |
| |
| if (gameState.bossActive) { |
| bossHealthBar.classList.remove('hidden'); |
| bossHealthFill.style.width = `${(gameState.bossHealth / gameState.bossMaxHealth) * 100}%`; |
| } else { |
| bossHealthBar.classList.add('hidden'); |
| } |
| } |
| |
| |
| function initGame() { |
| gameState.isMobile = detectMobile(); |
| resizeCanvas(); |
| gameState.started = true; |
| gameState.gameOver = false; |
| gameState.score = 0; |
| gameState.lives = 3; |
| gameState.ammo = Infinity; |
| gameState.speed = 100; |
| gameState.difficulty = 1; |
| gameState.timeSlow = 0; |
| gameState.doubleScore = 0; |
| gameState.bossActive = false; |
| gameState.bossHealth = 0; |
| gameState.bossMaxHealth = 0; |
| gameState.plane = { |
| x: canvas.width / 2, |
| y: canvas.height / 2, |
| width: 60, |
| height: 60, |
| velocity: 0, |
| verticalVelocity: 0, |
| rotation: 0, |
| lastFireTime: 0, |
| fireRate: 200, |
| bulletDamage: 1, |
| hasShield: false, |
| shieldDuration: 0, |
| powerups: { |
| rapidFire: 0, |
| homingMissiles: 0, |
| } |
| }; |
| gameState.stars = []; |
| gameState.obstacles = []; |
| gameState.clouds = []; |
| gameState.explosions = []; |
| gameState.bullets = []; |
| gameState.enemyBullets = []; |
| gameState.debris = []; |
| gameState.powerups = []; |
| gameState.homingMissiles = []; |
| gameState.effects = []; |
| gameState.particles = []; |
| gameState.lastStarTime = 0; |
| gameState.lastObstacleTime = 0; |
| gameState.lastCloudTime = 0; |
| gameState.lastPowerupTime = 0; |
| gameState.lastBossSpawnTime = 0; |
| gameState.lastEnemyShootTime = 0; |
| gameState.achievements = { |
| firstBlood: false, |
| combo5: false, |
| noDamage: false, |
| bossSlayer: false |
| }; |
| |
| startScreen.classList.add('hidden'); |
| gameOverScreen.classList.add('hidden'); |
| gameUI.classList.remove('hidden'); |
| |
| |
| if (gameState.isMobile) { |
| leftBtn.classList.add('hidden'); |
| rightBtn.classList.add('hidden'); |
| upBtn.classList.add('hidden'); |
| downBtn.classList.add('hidden'); |
| fireBtn.classList.remove('hidden'); |
| joystick.classList.remove('hidden'); |
| } |
| |
| updateUI(); |
| createInitialClouds(); |
| playSound('bgMusic'); |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| function createInitialClouds() { |
| for (let i = 0; i < 10; i++) { |
| createCloud(true); |
| } |
| } |
| |
| |
| function createCloud(initial = false) { |
| const size = Math.random() * 60 + 40; |
| const x = initial ? Math.random() * canvas.width : canvas.width + size; |
| const y = Math.random() * canvas.height; |
| const speed = Math.random() * 1 + 0.5; |
| |
| gameState.clouds.push({ |
| x, |
| y, |
| size, |
| speed, |
| parts: Array(3).fill().map(() => ({ |
| size: size * (Math.random() * 0.3 + 0.7), |
| offsetX: (Math.random() - 0.5) * size * 0.6, |
| offsetY: (Math.random() - 0.5) * size * 0.6 |
| })) |
| }); |
| } |
| |
| |
| function createStar() { |
| const size = Math.random() * 20 + 15; |
| const x = canvas.width + size; |
| const y = Math.random() * (canvas.height - size * 2) + size; |
| const speed = Math.random() * 3 + 3 + gameState.speed / 50; |
| |
| gameState.stars.push({ |
| x, |
| y, |
| size, |
| speed, |
| rotation: 0, |
| rotationSpeed: Math.random() * 0.1 - 0.05 |
| }); |
| } |
| |
| |
| function createEnemyBullet(x, y) { |
| const size = 8; |
| const speed = 5 + gameState.speed / 50; |
| const angle = Math.atan2( |
| gameState.plane.y - y, |
| gameState.plane.x - x |
| ); |
| |
| gameState.enemyBullets.push({ |
| x, |
| y, |
| size, |
| speed, |
| angle, |
| damage: 1 |
| }); |
| |
| playSound('enemyShootSound'); |
| } |
| |
| |
| function createObstacle() { |
| const width = Math.random() * 80 + 40; |
| const height = Math.random() * 80 + 40; |
| const x = canvas.width + width; |
| const y = Math.random() * (canvas.height - height); |
| const speed = Math.random() * 2 + 2 + gameState.speed / 50; |
| const type = Math.random() > 0.5 ? 'rectangle' : 'circle'; |
| const health = type === 'rectangle' ? (width > 80 ? 3 : 2) : 1; |
| const canShoot = Math.random() > 0.7; |
| |
| |
| const collisionWidth = width * 1.2; |
| const collisionHeight = height * 1.2; |
| |
| gameState.obstacles.push({ |
| x, |
| y, |
| width, |
| height, |
| collisionWidth, |
| collisionHeight, |
| speed, |
| type, |
| health, |
| maxHealth: health, |
| isLarge: width > 80, |
| canShoot, |
| lastShootTime: 0, |
| shootCooldown: Math.random() * 2000 + 1000 |
| }); |
| } |
| |
| |
| function createBoss() { |
| const width = 150; |
| const height = 150; |
| const x = canvas.width + width; |
| const y = canvas.height / 2 - height / 2; |
| const speed = 1 + gameState.speed / 100; |
| const health = 20 + Math.floor(gameState.score / 5000) * 5; |
| |
| gameState.bossActive = true; |
| gameState.bossHealth = health; |
| gameState.bossMaxHealth = health; |
| gameState.lastBossSpawnTime = Date.now(); |
| |
| gameState.obstacles.push({ |
| x, |
| y, |
| width, |
| height, |
| collisionWidth: width, |
| collisionHeight: height, |
| speed, |
| type: 'rectangle', |
| health, |
| maxHealth: health, |
| isLarge: true, |
| isBoss: true, |
| canShoot: true, |
| lastShootTime: 0, |
| shootCooldown: 500 |
| }); |
| |
| createEffect('BOSS出现!', 'red', 2000); |
| } |
| |
| |
| function createPowerup() { |
| const size = 40; |
| const x = canvas.width + size; |
| const y = Math.random() * (canvas.height - size * 2) + size; |
| const speed = Math.random() * 2 + 1; |
| |
| |
| const powerupKeys = Object.keys(POWERUP_TYPES); |
| const randomPowerup = POWERUP_TYPES[powerupKeys[Math.floor(Math.random() * powerupKeys.length)]]; |
| |
| gameState.powerups.push({ |
| x, |
| y, |
| size, |
| speed, |
| type: randomPowerup |
| }); |
| } |
| |
| |
| function createHomingMissile() { |
| if (gameState.obstacles.length === 0) return; |
| |
| |
| let closestObstacle = null; |
| let minDistance = Infinity; |
| |
| gameState.obstacles.forEach(obstacle => { |
| const dx = obstacle.x - gameState.plane.x; |
| const dy = obstacle.y - gameState.plane.y; |
| const distance = Math.sqrt(dx * dx + dy * dy); |
| |
| if (distance < minDistance) { |
| minDistance = distance; |
| closestObstacle = obstacle; |
| } |
| }); |
| |
| if (!closestObstacle) return; |
| |
| const size = 12; |
| gameState.homingMissiles.push({ |
| x: gameState.plane.x, |
| y: gameState.plane.y, |
| size, |
| speed: 8, |
| target: closestObstacle, |
| angle: Math.atan2( |
| closestObstacle.y - gameState.plane.y, |
| closestObstacle.x - gameState.plane.x |
| ) |
| }); |
| } |
| |
| |
| function createDebris(obstacle, count = 5) { |
| for (let i = 0; i < count; i++) { |
| gameState.debris.push({ |
| x: obstacle.x, |
| y: obstacle.y, |
| width: obstacle.width / 3, |
| height: obstacle.height / 3, |
| speedX: (Math.random() - 0.5) * 4, |
| speedY: (Math.random() - 0.5) * 4, |
| rotation: 0, |
| rotationSpeed: (Math.random() - 0.5) * 0.2, |
| opacity: 1 |
| }); |
| } |
| } |
| |
| |
| function createBullet() { |
| if (gameState.ammo <= 0) return false; |
| |
| const size = gameState.plane.powerups.rapidFire > Date.now() ? 10 : 8; |
| const damage = gameState.plane.powerups.rapidFire > Date.now() ? 2 : 1; |
| const speed = gameState.plane.powerups.rapidFire > Date.now() ? 18 : 15; |
| const x = gameState.plane.x + 30; |
| const y = gameState.plane.y; |
| |
| gameState.bullets.push({ |
| x, |
| y, |
| size, |
| speed, |
| damage |
| }); |
| |
| |
| if (gameState.plane.powerups.homingMissiles > Date.now() && |
| Math.random() > 0.7) { |
| requestAnimationFrame(createHomingMissile); |
| } |
| |
| |
| if (gameState.ammo !== Infinity) { |
| gameState.ammo--; |
| } |
| |
| playSound('shootSound'); |
| updateUI(); |
| |
| return true; |
| } |
| |
| |
| function createExplosion(x, y) { |
| gameState.explosions.push({ |
| x, |
| y, |
| size: 0, |
| maxSize: Math.random() * 40 + 40, |
| alpha: 1 |
| }); |
| createParticles(x, y, 20, '#ff6600'); |
| playSound('explosionSound'); |
| } |
| |
| |
| function checkCollision(rect1, rect2) { |
| return ( |
| rect1.x < rect2.x + rect2.width && |
| rect1.x + rect1.width > rect2.x && |
| rect1.y < rect2.y + rect2.height && |
| rect1.y + rect1.height > rect2.y |
| ); |
| } |
| |
| |
| function endGame() { |
| gameState.gameOver = true; |
| gameUI.classList.add('hidden'); |
| gameOverScreen.classList.remove('hidden'); |
| finalScore.textContent = gameState.score; |
| |
| |
| if (gameState.score > gameState.highScore) { |
| gameState.highScore = gameState.score; |
| localStorage.setItem('highScore', gameState.highScore); |
| achievementsDisplay.innerHTML += `<div class="text-yellow-400 mb-2">🎉 新纪录!</div>`; |
| } |
| |
| |
| if (!gameState.achievements.firstBlood) { |
| achievementsDisplay.innerHTML += `<div class="text-green-400 mb-2">🏆 首次击杀!</div>`; |
| } |
| if (!gameState.achievements.combo5) { |
| achievementsDisplay.innerHTML += `<div class="text-blue-400 mb-2">🔥 连续5次击杀!</div>`; |
| } |
| if (!gameState.achievements.noDamage && gameState.lives === 3) { |
| achievementsDisplay.innerHTML += `<div class="text-purple-400 mb-2">🛡️ 无伤通关!</div>`; |
| } |
| if (!gameState.achievements.bossSlayer && gameState.bossActive) { |
| achievementsDisplay.innerHTML += `<div class="text-red-400 mb-2">👹 Boss杀手!</div>`; |
| } |
| |
| |
| leftBtn.classList.add('hidden'); |
| rightBtn.classList.add('hidden'); |
| upBtn.classList.add('hidden'); |
| downBtn.classList.add('hidden'); |
| fireBtn.classList.add('hidden'); |
| joystick.classList.add('hidden'); |
| |
| bgMusic.pause(); |
| } |
| |
| |
| function gameLoop(timestamp) { |
| if (!gameState.started || gameState.gameOver) return; |
| |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| updateGame(timestamp); |
| |
| |
| drawGame(); |
| |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| function updateGame(timestamp) { |
| |
| gameState.difficulty = 1 + Math.min(gameState.score / 1000, 3); |
| |
| |
| if (!gameState.bossActive && |
| gameState.score > 0 && |
| gameState.score % 5000 === 0 && |
| timestamp - gameState.lastBossSpawnTime > 30000) { |
| createBoss(); |
| } |
| |
| |
| const timeSlowFactor = gameState.timeSlow > Date.now() ? 0.5 : 1; |
| |
| |
| if ((gameState.keys.Space || gameState.isFiring) && |
| timestamp - gameState.plane.lastFireTime > gameState.plane.fireRate) { |
| createBullet(); |
| gameState.plane.lastFireTime = timestamp; |
| } |
| |
| |
| if (timestamp - gameState.lastEnemyShootTime > 1000 / gameState.difficulty) { |
| gameState.obstacles.forEach(obstacle => { |
| if (obstacle.canShoot && timestamp - obstacle.lastShootTime > obstacle.shootCooldown) { |
| createEnemyBullet(obstacle.x - obstacle.width/2, obstacle.y); |
| obstacle.lastShootTime = timestamp; |
| } |
| }); |
| gameState.lastEnemyShootTime = timestamp; |
| } |
| |
| |
| gameState.enemyBullets.forEach(bullet => { |
| bullet.x += Math.cos(bullet.angle) * bullet.speed * timeSlowFactor; |
| bullet.y += Math.sin(bullet.angle) * bullet.speed * timeSlowFactor; |
| }); |
| gameState.enemyBullets = gameState.enemyBullets.filter(bullet => |
| bullet.x > 0 && bullet.x < canvas.width && |
| bullet.y > 0 && bullet.y < canvas.height |
| ); |
| |
| |
| if (gameState.plane.powerups.rapidFire > 0 && gameState.plane.powerups.rapidFire < Date.now()) { |
| gameState.plane.powerups.rapidFire = 0; |
| gameState.plane.fireRate = 200; |
| createEffect('火力增强结束', 'red', 1500); |
| } |
| |
| if (gameState.plane.powerups.homingMissiles > 0 && gameState.plane.powerups.homingMissiles < Date.now()) { |
| gameState.plane.powerups.homingMissiles = 0; |
| createEffect('跟踪导弹结束', 'purple', 1500); |
| } |
| |
| if (gameState.plane.hasShield && gameState.plane.shieldDuration < Date.now()) { |
| gameState.plane.hasShield = false; |
| createEffect('保护罩消失', 'blue', 1500); |
| } |
| |
| if (gameState.timeSlow > 0 && gameState.timeSlow < Date.now()) { |
| gameState.timeSlow = 0; |
| createEffect('时间恢复正常', 'cyan', 1500); |
| } |
| |
| if (gameState.doubleScore > 0 && gameState.doubleScore < Date.now()) { |
| gameState.doubleScore = 0; |
| createEffect('双倍分数结束', 'pink', 1500); |
| } |
| |
| |
| if (gameState.keys.ArrowUp || gameState.keys.ArrowDown) { |
| gameState.speed = Math.max(50, |
| Math.min(200, |
| gameState.speed + (gameState.keys.ArrowUp ? 0.5 : -0.5) |
| ) |
| ); |
| } |
| |
| |
| if (gameState.keys.ArrowLeft || gameState.joystickAngle < -Math.PI/4) { |
| gameState.plane.rotation = Math.max(gameState.plane.rotation - 2, -20); |
| gameState.plane.velocity = Math.max(gameState.plane.velocity - 0.5, -5); |
| } else if (gameState.keys.ArrowRight || gameState.joystickAngle > Math.PI/4) { |
| gameState.plane.rotation = Math.min(gameState.plane.rotation + 2, 20); |
| gameState.plane.velocity = Math.min(gameState.plane.velocity + 0.5, 5); |
| } else { |
| |
| gameState.plane.rotation *= 0.95; |
| gameState.plane.velocity *= 0.95; |
| if (Math.abs(gameState.plane.rotation) < 0.5) gameState.plane.rotation = 0; |
| if (Math.abs(gameState.plane.velocity) < 0.5) gameState.plane.velocity = 0; |
| } |
| |
| |
| if (gameState.keys.ArrowUp || (gameState.joystickActive && gameState.joystickAngle < -Math.PI/4 && gameState.joystickAngle > -3*Math.PI/4)) { |
| gameState.plane.verticalVelocity = Math.max(gameState.plane.verticalVelocity - 0.5, -5); |
| } else if (gameState.keys.ArrowDown || (gameState.joystickActive && gameState.joystickAngle > Math.PI/4 && gameState.joystickAngle < 3*Math.PI/4)) { |
| gameState.plane.verticalVelocity = Math.min(gameState.plane.verticalVelocity + 0.5, 5); |
| } else { |
| |
| gameState.plane.verticalVelocity *= 0.95; |
| if (Math.abs(gameState.plane.verticalVelocity) < 0.5) gameState.plane.verticalVelocity = 0; |
| } |
| |
| |
| gameState.plane.x += gameState.plane.velocity; |
| gameState.plane.y += gameState.plane.verticalVelocity; |
| |
| |
| gameState.plane.x = Math.max(gameState.plane.width / 2, Math.min(gameState.plane.x, canvas.width - gameState.plane.width / 2)); |
| gameState.plane.y = Math.max(gameState.plane.height / 2, Math.min(gameState.plane.y, canvas.height - gameState.plane.height / 2)); |
| |
| |
| if (timestamp - gameState.lastStarTime > 2000 / gameState.difficulty) { |
| createStar(); |
| gameState.lastStarTime = timestamp; |
| } |
| |
| |
| if (timestamp - gameState.lastObstacleTime > 1500 / gameState.difficulty * timeSlowFactor) { |
| createObstacle(); |
| gameState.lastObstacleTime = timestamp; |
| } |
| |
| |
| if (timestamp - gameState.lastCloudTime > 1000 * timeSlowFactor) { |
| createCloud(); |
| gameState.lastCloudTime = timestamp; |
| } |
| |
| |
| if (timestamp - gameState.lastPowerupTime > (Math.random() * 3000 + 5000) / gameState.difficulty * timeSlowFactor) { |
| createPowerup(); |
| gameState.lastPowerupTime = timestamp; |
| } |
| |
| |
| gameState.particles.forEach(particle => { |
| particle.x += particle.speedX; |
| particle.y += particle.speedY; |
| particle.life--; |
| }); |
| gameState.particles = gameState.particles.filter(p => p.life > 0); |
| |
| |
| gameState.effects = gameState.effects.filter(effect => |
| Date.now() - effect.startTime < effect.duration |
| ); |
| |
| |
| gameState.debris.forEach(debris => { |
| debris.x += debris.speedX; |
| debris.y += debris.speedY; |
| debris.rotation += debris.rotationSpeed; |
| debris.opacity -= 0.02; |
| }); |
| gameState.debris = gameState.debris.filter(debris => debris.opacity > 0); |
| |
| |
| gameState.homingMissiles.forEach(missile => { |
| if (!missile.target || missile.target.hit) { |
| |
| missile.x += Math.cos(missile.angle) * missile.speed; |
| missile.y += Math.sin(missile.angle) * missile.speed; |
| } else { |
| |
| const dx = missile.target.x - missile.x; |
| const dy = missile.target.y - missile.y; |
| const targetAngle = Math.atan2(dy, dx); |
| |
| |
| let angleDiff = targetAngle - missile.angle; |
| if (angleDiff > Math.PI) angleDiff -= Math.PI * 2; |
| if (angleDiff < -Math.PI) angleDiff += Math.PI * 2; |
| |
| missile.angle += angleDiff * 0.1; |
| missile.x += Math.cos(missile.angle) * missile.speed; |
| missile.y += Math.sin(missile.angle) * missile.speed; |
| |
| |
| const missileRect = { |
| x: missile.x - missile.size / 2, |
| y: missile.y - missile.size / 2, |
| width: missile.size, |
| height: missile.size |
| }; |
| |
| const targetRect = { |
| x: missile.target.x - missile.target.collisionWidth / 2, |
| y: missile.target.y - missile.target.collisionHeight / 2, |
| width: missile.target.collisionWidth, |
| height: missile.target.collisionHeight |
| }; |
| |
| if (checkCollision(missileRect, targetRect)) { |
| missile.hit = true; |
| missile.target.health -= 3; |
| |
| if (missile.target.health <= 0) { |
| missile.target.hit = true; |
| const scoreBonus = missile.target.isBoss ? 500 : (missile.target.isLarge ? 30 : 15); |
| gameState.score += gameState.doubleScore > Date.now() ? scoreBonus * 2 : scoreBonus; |
| updateUI(); |
| |
| |
| if (missile.target.isLarge && !missile.target.isBoss) { |
| for (let i = 0; i < 3; i++) { |
| gameState.obstacles.push({ |
| x: missile.target.x + (Math.random() - 0.5) * 50, |
| y: missile.target.y + (Math.random() - 0.5) * 50, |
| width: missile.target.width / 2, |
| height: missile.target.height / 2, |
| collisionWidth: missile.target.collisionWidth / 2, |
| collisionHeight: missile.target.collisionHeight / 2, |
| speed: missile.target.speed * 1.2, |
| type: missile.target.type, |
| health: 1, |
| maxHealth: 1, |
| isLarge: false |
| }); |
| } |
| } |
| |
| |
| if (missile.target.isBoss) { |
| gameState.bossActive = false; |
| gameState.achievements.bossSlayer = true; |
| } |
| |
| |
| createDebris(missile.target, missile.target.isBoss ? 30 : 12); |
| } |
| |
| |
| createExplosion(missile.x, missile.y); |
| } |
| } |
| }); |
| gameState.homingMissiles = gameState.homingMissiles.filter(missile => |
| missile.x < canvas.width && missile.x > 0 && |
| missile.y < canvas.height && missile.y > 0 && |
| !missile.hit |
| ); |
| |
| |
| gameState.clouds.forEach(cloud => { |
| cloud.x -= cloud.speed * timeSlowFactor; |
| }); |
| gameState.clouds = gameState.clouds.filter(cloud => cloud.x + cloud.size > 0); |
| |
| |
| gameState.powerups.forEach(powerup => { |
| powerup.x -= powerup.speed * timeSlowFactor; |
| |
| |
| const planeRect = { |
| x: gameState.plane.x - gameState.plane.width / 2, |
| y: gameState.plane.y - gameState.plane.height / 2, |
| width: gameState.plane.width, |
| height: gameState.plane.height |
| }; |
| |
| const powerupRect = { |
| x: powerup.x - powerup.size / 2, |
| y: powerup.y - powerup.size / 2, |
| width: powerup.size, |
| height: powerup.size |
| }; |
| |
| if (checkCollision(planeRect, powerupRect) && !gameState.gameOver) { |
| powerup.collected = true; |
| powerup.type.effect(gameState); |
| updateUI(); |
| createParticles(powerup.x, powerup.y, 15, powerup.type.color); |
| } |
| }); |
| gameState.powerups = gameState.powerups.filter(powerup => |
| powerup.x + powerup.size > 0 && !powerup.collected |
| ); |
| |
| |
| gameState.bullets.forEach(bullet => { |
| bullet.x += bullet.speed * timeSlowFactor; |
| }); |
| gameState.bullets = gameState.bullets.filter(bullet => bullet.x < canvas.width); |
| |
| |
| gameState.enemyBullets.forEach(bullet => { |
| const bulletRect = { |
| x: bullet.x - bullet.size / 2, |
| y: bullet.y - bullet.size / 2, |
| width: bullet.size, |
| height: bullet.size |
| }; |
| |
| const planeRect = { |
| x: gameState.plane.x - gameState.plane.width / 2, |
| y: gameState.plane.y - gameState.plane.height / 2, |
| width: gameState.plane.width, |
| height: gameState.plane.height |
| }; |
| |
| if (checkCollision(bulletRect, planeRect) { |
| bullet.hit = true; |
| if (!gameState.plane.hasShield || gameState.plane.shieldDuration < Date.now()) { |
| gameState.lives--; |
| updateUI(); |
| createExplosion(gameState.plane.x, gameState.plane.y); |
| |
| if (gameState.lives <= 0) { |
| endGame(); |
| } |
| } else { |
| |
| createExplosion(bullet.x, bullet.y); |
| } |
| } |
| }); |
| gameState.enemyBullets = gameState.enemyBullets.filter(bullet => !bullet.hit); |
| |
| |
| gameState.stars.forEach(star => { |
| star.x -= star.speed * timeSlowFactor; |
| star.rotation += star.rotationSpeed; |
| |
| |
| const planeRect = { |
| x: gameState.plane.x - gameState.plane.width / 2, |
| y: gameState.plane.y - gameState.plane.height / 2, |
| width: gameState.plane.width, |
| height: gameState.plane.height |
| }; |
| |
| const starRect = { |
| x: star.x - star.size / 2, |
| y: star.y - star.size / 2, |
| width: star.size, |
| height: star.size |
| }; |
| |
| if (checkCollision(planeRect, starRect) && !gameState.gameOver) { |
| star.collected = true; |
| const scoreBonus = 10; |
| gameState.score += gameState.doubleScore > Date.now() ? scoreBonus * 2 : scoreBonus; |
| updateUI(); |
| createParticles(star.x, star.y, 10, 'gold'); |
| } |
| }); |
| gameState.stars = gameState.stars.filter(star => star.x + star.size > 0 && !star.collected); |
| |
| |
| let comboCount = 0; |
| gameState.obstacles.forEach(obstacle => { |
| obstacle.x -= obstacle.speed * timeSlowFactor; |
| |
| |
| const planeRect = { |
| x: gameState.plane.x - gameState.plane.width / 2, |
| y: gameState.plane.y - gameState.plane.height / 2, |
| width: gameState.plane.width, |
| height: gameState.plane.height |
| }; |
| |
| const obstacleRect = { |
| x: obstacle.x - obstacle.collisionWidth / 2, |
| y: obstacle.y - obstacle.collisionHeight / 2, |
| width: obstacle.collisionWidth, |
| height: obstacle.collisionHeight |
| }; |
| |
| if (checkCollision(planeRect, obstacleRect) && !gameState.gameOver) { |
| |
| if (!gameState.plane.hasShield || gameState.plane.shieldDuration < Date.now()) { |
| obstacle.hit = true; |
| gameState.lives--; |
| updateUI(); |
| createExplosion(gameState.plane.x, gameState.plane.y); |
| |
| if (gameState.lives <= 0) { |
| endGame(); |
| } |
| } else { |
| |
| obstacle.hit = true; |
| createExplosion(obstacle.x, obstacle.y); |
| createDebris(obstacle, 4); |
| } |
| } |
| |
| |
| if (!obstacle.hit) { |
| const bulletHits = []; |
| |
| gameState.bullets.forEach((bullet, bulletIndex) => { |
| const bulletRect = { |
| x: bullet.x - bullet.size / 2, |
| y: bullet.y - bullet.size / 2, |
| width: bullet.size, |
| height: bullet.size |
| }; |
| |
| const obstacleCollisionRect = { |
| x: obstacle.x - obstacle.collisionWidth / 2, |
| y: obstacle.y - obstacle.collisionHeight / 2, |
| width: obstacle.collisionWidth, |
| height: obstacle.collisionHeight |
| }; |
| |
| if (checkCollision(bulletRect, obstacleCollisionRect)) { |
| obstacle.health -= bullet.damage; |
| bulletHits.push(bulletIndex); |
| createExplosion(bullet.x, bullet.y); |
| |
| if (obstacle.health <= 0) { |
| obstacle.hit = true; |
| const scoreBonus = obstacle.isBoss ? 500 : (obstacle.isLarge ? 30 : 15); |
| gameState.score += gameState.doubleScore > Date.now() ? scoreBonus * 2 : scoreBonus; |
| updateUI(); |
| comboCount++; |
| |
| |
| if (obstacle.isLarge && !obstacle.isBoss) { |
| for (let i = 0; i < 3; i++) { |
| gameState.obstacles.push({ |
| x: obstacle.x + (Math.random() - 0.5) * 50, |
| y: obstacle.y + (Math.random() - 0.5) * 50, |
| width: obstacle.width / 2, |
| height: obstacle.height / 2, |
| collisionWidth: obstacle.collisionWidth / 2, |
| collisionHeight: obstacle.collisionHeight / 2, |
| speed: obstacle.speed * 1.2, |
| type: obstacle.type, |
| health: 1, |
| maxHealth: 1, |
| isLarge: false |
| }); |
| } |
| } |
| |
| |
| if (obstacle.isBoss) { |
| gameState.bossActive = false; |
| gameState.achievements.bossSlayer = true; |
| } |
| |
| |
| createDebris(obstacle, obstacle.isBoss ? 30 : 8); |
| |
| |
| if (!gameState.achievements.firstBlood) { |
| gameState.achievements.firstBlood = true; |
| createEffect('首次击杀!', 'green', 2000); |
| } |
| } |
| } |
| }); |
| |
| |
| if (comboCount >= 5 && !gameState.achievements.combo5) { |
| gameState.achievements.combo5 = true; |
| createEffect('连续5次击杀!', 'blue', 2000); |
| } |
| |
| |
| for (let i = bulletHits.length - 1; i >= 0; i--) { |
| gameState.bullets.splice(bulletHits[i], 1); |
| } |
| } |
| }); |
| gameState.obstacles = gameState.obstacles.filter(obstacle => obstacle.x + obstacle.width > 0 && !obstacle.hit); |
| |
| |
| gameState.explosions.forEach(explosion => { |
| explosion.size += 2; |
| explosion.alpha -= 0.02; |
| }); |
| gameState.explosions = gameState.explosions.filter(explosion => explosion.alpha > 0); |
| } |
| |
| |
| function drawGame() { |
| |
| const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); |
| gradient.addColorStop(0, '#1e3c72'); |
| gradient.addColorStop(1, '#2a5298'); |
| ctx.fillStyle = gradient; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| |
| |
| gameState.clouds.forEach(cloud => { |
| cloud.parts.forEach(part => { |
| ctx.beginPath(); |
| ctx.arc( |
| cloud.x + part.offsetX, |
| cloud.y + part.offsetY, |
| part.size / 2, |
| 0, |
| Math.PI * 2 |
| ); |
| ctx.fillStyle = `rgba(255, 255, 255, ${0.7 + Math.random() * 0.3})`; |
| ctx.fill(); |
| }); |
| }); |
| |
| |
| gameState.particles.forEach(particle => { |
| ctx.beginPath(); |
| ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); |
| ctx.fillStyle = particle.color; |
| ctx.globalAlpha = particle.life / 100; |
| ctx.fill(); |
| ctx.globalAlpha = 1; |
| }); |
| |
| |
| gameState.debris.forEach(debris => { |
| ctx.save(); |
| ctx.translate(debris.x, debris.y); |
| ctx.rotate(debris.rotation); |
| |
| ctx.fillStyle = `rgba(100, 100, 100, ${debris.opacity})`; |
| ctx.fillRect( |
| -debris.width / 2, |
| -debris.height / 2, |
| debris.width, |
| debris.height |
| ); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.enemyBullets.forEach(bullet => { |
| const gradient = ctx.createRadialGradient( |
| bullet.x, bullet.y, 0, |
| bullet.x, bullet.y, bullet.size |
| ); |
| gradient.addColorStop(0, '#f00'); |
| gradient.addColorStop(1, '#800'); |
| |
| ctx.beginPath(); |
| ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); |
| ctx.fillStyle = gradient; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(bullet.x - Math.cos(bullet.angle) * 10, bullet.y - Math.sin(bullet.angle) * 10); |
| ctx.lineTo(bullet.x, bullet.y); |
| ctx.strokeStyle = 'rgba(255, 100, 100, 0.8)'; |
| ctx.lineWidth = bullet.size / 2; |
| ctx.stroke(); |
| }); |
| |
| |
| gameState.powerups.forEach(powerup => { |
| ctx.save(); |
| ctx.translate(powerup.x, powerup.y); |
| |
| |
| ctx.beginPath(); |
| ctx.arc(0, 0, powerup.size / 2, 0, Math.PI * 2); |
| const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, powerup.size / 2); |
| gradient.addColorStop(0, powerup.type.color); |
| gradient.addColorStop(1, 'rgba(255,255,255,0)'); |
| ctx.fillStyle = gradient; |
| ctx.globalAlpha = 0.3; |
| ctx.fill(); |
| ctx.globalAlpha = 1; |
| |
| |
| ctx.fillStyle = powerup.type.color; |
| ctx.beginPath(); |
| ctx.arc(0, 0, powerup.size / 2 - 3, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| ctx.strokeStyle = 'white'; |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| |
| |
| ctx.fillStyle = 'white'; |
| ctx.font = '20px FontAwesome'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| ctx.fillText(String.fromCharCode(parseInt(getIconCode(powerup.type.icon), 16)), 0, 1); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.homingMissiles.forEach(missile => { |
| ctx.save(); |
| ctx.translate(missile.x, missile.y); |
| ctx.rotate(missile.angle); |
| |
| |
| ctx.fillStyle = 'red'; |
| ctx.beginPath(); |
| ctx.moveTo(missile.size / 2, 0); |
| ctx.lineTo(-missile.size / 2, -missile.size / 3); |
| ctx.lineTo(-missile.size / 2, missile.size / 3); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| |
| ctx.fillStyle = 'orange'; |
| ctx.beginPath(); |
| ctx.moveTo(-missile.size / 2, -missile.size / 4); |
| ctx.lineTo(-missile.size, 0); |
| ctx.lineTo(-missile.size / 2, missile.size / 4); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.bullets.forEach(bullet => { |
| const gradient = ctx.createRadialGradient( |
| bullet.x, bullet.y, 0, |
| bullet.x, bullet.y, bullet.size |
| ); |
| gradient.addColorStop(0, '#ff0'); |
| gradient.addColorStop(1, '#f80'); |
| |
| ctx.beginPath(); |
| ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); |
| ctx.fillStyle = gradient; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(bullet.x - bullet.speed, bullet.y); |
| ctx.lineTo(bullet.x, bullet.y); |
| ctx.strokeStyle = 'rgba(255, 200, 0, 0.8)'; |
| ctx.lineWidth = bullet.size / 2; |
| ctx.stroke(); |
| }); |
| |
| |
| gameState.stars.forEach(star => { |
| ctx.save(); |
| ctx.translate(star.x, star.y); |
| ctx.rotate(star.rotation); |
| |
| ctx.beginPath(); |
| for (let i = 0; i < 5; i++) { |
| const angle = (i * 2 * Math.PI / 5) - Math.PI / 2; |
| const innerAngle = angle + Math.PI / 5; |
| const outerRadius = star.size / 2; |
| const innerRadius = star.size / 4; |
| |
| if (i === 0) { |
| ctx.moveTo( |
| Math.cos(angle) * outerRadius, |
| Math.sin(angle) * outerRadius |
| ); |
| } else { |
| ctx.lineTo( |
| Math.cos(angle) * outerRadius, |
| Math.sin(angle) * outerRadius |
| ); |
| } |
| |
| ctx.lineTo( |
| Math.cos(innerAngle) * innerRadius, |
| Math.sin(innerAngle) * innerRadius |
| ); |
| } |
| ctx.closePath(); |
| |
| const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, star.size / 2); |
| gradient.addColorStop(0, 'gold'); |
| gradient.addColorStop(1, 'yellow'); |
| ctx.fillStyle = gradient; |
| ctx.shadowColor = 'yellow'; |
| ctx.shadowBlur = 10; |
| ctx.fill(); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.obstacles.forEach(obstacle => { |
| ctx.save(); |
| ctx.translate(obstacle.x, obstacle.y); |
| |
| if (obstacle.type === 'rectangle') { |
| |
| if (obstacle.health < obstacle.maxHealth) { |
| const healthBarWidth = obstacle.isBoss ? 100 : 20; |
| ctx.fillStyle = 'red'; |
| ctx.fillRect( |
| -healthBarWidth / 2, |
| -obstacle.height / 2 - 15, |
| healthBarWidth, |
| 5 |
| ); |
| ctx.fillStyle = obstacle.isBoss ? 'purple' : 'lime'; |
| ctx.fillRect( |
| -healthBarWidth / 2, |
| -obstacle.height / 2 - 15, |
| healthBarWidth * (obstacle.health / obstacle.maxHealth), |
| 5 |
| ); |
| } |
| |
| |
| if (obstacle.canShoot) { |
| |
| ctx.fillStyle = obstacle.isBoss ? '#8B0000' : '#333'; |
| ctx.beginPath(); |
| ctx.moveTo(-obstacle.width/2, 0); |
| ctx.lineTo(obstacle.width/2, -obstacle.height/3); |
| ctx.lineTo(obstacle.width/2, obstacle.height/3); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| |
| ctx.fillStyle = obstacle.isBoss ? '#600000' : '#222'; |
| ctx.beginPath(); |
| ctx.moveTo(-obstacle.width/4, 0); |
| ctx.lineTo(obstacle.width/4, -obstacle.height/2); |
| ctx.lineTo(obstacle.width/2, -obstacle.height/3); |
| ctx.lineTo(obstacle.width/4, 0); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(-obstacle.width/4, 0); |
| ctx.lineTo(obstacle.width/4, obstacle.height/2); |
| ctx.lineTo(obstacle.width/2, obstacle.height/3); |
| ctx.lineTo(obstacle.width/4, 0); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(-obstacle.width/2, 0); |
| ctx.lineTo(-obstacle.width/3, -obstacle.height/4); |
| ctx.lineTo(-obstacle.width/4, -obstacle.height/4); |
| ctx.lineTo(-obstacle.width/3, 0); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(-obstacle.width/2, 0); |
| ctx.lineTo(-obstacle.width/3, obstacle.height/4); |
| ctx.lineTo(-obstacle.width/4, obstacle.height/4); |
| ctx.lineTo(-obstacle.width/3, 0); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| |
| ctx.fillStyle = '#3498db'; |
| ctx.beginPath(); |
| ctx.arc(obstacle.width/4, 0, obstacle.width/8, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| if (obstacle.isBoss) { |
| ctx.fillStyle = 'gold'; |
| ctx.font = 'bold 20px Arial'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| ctx.fillText('BOSS', 0, 0); |
| } |
| } else { |
| |
| ctx.fillStyle = obstacle.isBoss ? '#8B0000' : (obstacle.isLarge ? '#333' : '#555'); |
| ctx.fillRect( |
| -obstacle.width / 2, |
| -obstacle.height / 2, |
| obstacle.width, |
| obstacle.height |
| ); |
| |
| ctx.fillStyle = obstacle.isBoss ? '#600000' : (obstacle.isLarge ? '#222' : '#444'); |
| ctx.fillRect( |
| -obstacle.width / 2 + 5, |
| -obstacle.height / 2 + 5, |
| obstacle.width - 10, |
| obstacle.height - 10 |
| ); |
| |
| |
| if ((obstacle.isLarge || obstacle.isBoss) && obstacle.health < obstacle.maxHealth) { |
| ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; |
| ctx.lineWidth = 2; |
| for (let i = 0; i < (obstacle.isBoss ? 10 : 3); i++) { |
| ctx.beginPath(); |
| ctx.moveTo( |
| -obstacle.width / 2 + Math.random() * obstacle.width, |
| -obstacle.height / 2 + Math.random() * obstacle.height / 3 |
| ); |
| ctx.lineTo( |
| -obstacle.width / 2 + Math.random() * obstacle.width, |
| obstacle.height / 2 - Math.random() * obstacle.height / 3 |
| ); |
| ctx.stroke(); |
| } |
| } |
| |
| |
| if (obstacle.isBoss) { |
| ctx.fillStyle = 'gold'; |
| ctx.font = 'bold 20px Arial'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| ctx.fillText('BOSS', 0, 0); |
| } |
| } |
| } else { |
| |
| ctx.beginPath(); |
| ctx.arc(0, 0, obstacle.width / 2, 0, Math.PI * 2); |
| ctx.fillStyle = '#555'; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.arc(0, 0, obstacle.width / 2 - 5, 0, Math.PI * 2); |
| ctx.fillStyle = '#444'; |
| ctx.fill(); |
| |
| |
| if (obstacle.isLarge && obstacle.health < obstacle.maxHealth) { |
| ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; |
| ctx.lineWidth = 2; |
| for (let i = 0; i < 3; i++) { |
| ctx.beginPath(); |
| ctx.moveTo( |
| Math.cos(i * 2) * obstacle.width / 4, |
| Math.sin(i * 2) * obstacle.width / 4 |
| ); |
| ctx.lineTo( |
| Math.cos(i * 2 + 1) * obstacle.width / 3, |
| Math.sin(i * 2 + 1) * obstacle.width / 3 |
| ); |
| ctx.stroke(); |
| } |
| } |
| } |
| |
| ctx.restore(); |
| }); |
| |
| |
| ctx.save(); |
| ctx.translate(gameState.plane.x, gameState.plane.y); |
| ctx.rotate(gameState.plane.rotation * Math.PI / 180); |
| |
| |
| if (gameState.plane.hasShield && gameState.plane.shieldDuration > Date.now()) { |
| ctx.beginPath(); |
| ctx.arc(0, 0, 45, 0, Math.PI * 2); |
| ctx.strokeStyle = `rgba(0, 204, 255, ${0.3 + Math.sin(Date.now() / 200) * 0.3})`; |
| ctx.lineWidth = 3; |
| ctx.stroke(); |
| |
| |
| const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 45); |
| gradient.addColorStop(0, 'rgba(0, 204, 255, 0.2)'); |
| gradient.addColorStop(1, 'rgba(0, 204, 255, 0)'); |
| ctx.fillStyle = gradient; |
| ctx.fill(); |
| } |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(30, 0); |
| ctx.lineTo(-20, -15); |
| ctx.lineTo(-25, 0); |
| ctx.lineTo(-20, 15); |
| ctx.closePath(); |
| ctx.fillStyle = '#e74c3c'; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.arc(10, 0, 5, 0, Math.PI * 2); |
| ctx.fillStyle = '#3498db'; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(5, 0); |
| ctx.lineTo(-5, -20); |
| ctx.lineTo(-15, -20); |
| ctx.lineTo(-5, 0); |
| ctx.closePath(); |
| ctx.fillStyle = '#c0392b'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(5, 0); |
| ctx.lineTo(-5, 20); |
| ctx.lineTo(-15, 20); |
| ctx.lineTo(-5, 0); |
| ctx.closePath(); |
| ctx.fillStyle = '#c0392b'; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(-20, 0); |
| ctx.lineTo(-25, -10); |
| ctx.lineTo(-30, -10); |
| ctx.lineTo(-25, 0); |
| ctx.closePath(); |
| ctx.fillStyle = '#a5281b'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(-20, 0); |
| ctx.lineTo(-25, 10); |
| ctx.lineTo(-30, 10); |
| ctx.lineTo(-25, 0); |
| ctx.closePath(); |
| ctx.fillStyle = '#a5281b'; |
| ctx.fill(); |
| |
| ctx.restore(); |
| |
| |
| gameState.explosions.forEach(explosion => { |
| ctx.save(); |
| ctx.translate(explosion.x, explosion.y); |
| |
| const gradient = ctx.createRadialGradient( |
| 0, 0, 0, |
| 0, 0, explosion.size |
| ); |
| gradient.addColorStop(0, `rgba(255, 100, 0, ${explosion.alpha})`); |
| gradient.addColorStop(0.5, `rgba(255, 200, 0, ${explosion.alpha * 0.6})`); |
| gradient.addColorStop(1, `rgba(255, 255, 255, 0)`); |
| |
| ctx.beginPath(); |
| ctx.arc(0, 0, explosion.size, 0, Math.PI * 2); |
| ctx.fillStyle = gradient; |
| ctx.fill(); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.effects.forEach(effect => { |
| const timePassed = Date.now() - effect.startTime; |
| const progress = timePassed / effect.duration; |
| |
| ctx.save(); |
| ctx.translate(effect.x, effect.y - progress * 50); |
| ctx.globalAlpha = 1 - progress * 0.8; |
| |
| ctx.font = 'bold 20px Arial'; |
| ctx.fillStyle = effect.color; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| ctx.fillText(effect.text, 0, 0); |
| |
| ctx.restore(); |
| }); |
| |
| |
| if (gameState.speed > 120) { |
| for (let i = 0; i < 10; i++) { |
| const x = Math.random() * canvas.width; |
| const y = Math.random() * canvas.height; |
| const length = Math.random() * 20 + 10; |
| const angle = Math.atan2( |
| gameState.plane.y - y, |
| gameState.plane.x - x |
| ); |
| |
| ctx.save(); |
| ctx.translate(x, y); |
| ctx.rotate(angle); |
| |
| ctx.beginPath(); |
| ctx.moveTo(0, 0); |
| ctx.lineTo(length, 0); |
| ctx.strokeStyle = `rgba(255, 255, 255, ${Math.random() * 0.5 + 0.3})`; |
| ctx.lineWidth = 1; |
| ctx.stroke(); |
| |
| ctx.restore(); |
| } |
| } |
| |
| |
| if (gameState.difficulty > 1.5) { |
| ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; |
| ctx.font = '20px Arial'; |
| ctx.textAlign = 'right'; |
| ctx.fillText(`难度: ${gameState.difficulty.toFixed(1)}x`, canvas.width - 20, 30); |
| } |
| } |
| |
| |
| function getIconCode(iconClass) { |
| const icons = { |
| 'fas fa-bolt': 'f0e7', |
| 'fas fa-rocket': 'f135', |
| 'fas fa-shield-alt': 'f3ed', |
| 'fas fa-heart': 'f004', |
| 'fas fa-clock': 'f017', |
| 'fas fa-bomb': 'f1e2', |
| 'fas fa-star': 'f005' |
| }; |
| return icons[iconClass] || 'f128'; |
| } |
| |
| |
| function setupJoystick() { |
| const joystickArea = joystick; |
| const handle = joystickHandle; |
| let active = false; |
| let startX = 0; |
| let startY = 0; |
| let handleX = 0; |
| let handleY = 0; |
| const maxDistance = 40; |
| |
| joystickArea.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| const touch = e.touches[0]; |
| const rect = joystickArea.getBoundingClientRect(); |
| startX = rect.left + rect.width / 2; |
| startY = rect.top + rect.height / 2; |
| handleX = touch.clientX - startX; |
| handleY = touch.clientY - startY; |
| |
| |
| const distance = Math.sqrt(handleX * handleX + handleY * handleY); |
| if (distance > maxDistance) { |
| handleX = (handleX / distance) * maxDistance; |
| handleY = (handleY / distance) * maxDistance; |
| } |
| |
| handle.style.transform = `translate(${handleX}px, ${handleY}px)`; |
| |
| |
| gameState.joystickAngle = Math.atan2(handleY, handleX); |
| gameState.joystickDistance = distance / maxDistance; |
| gameState.joystickActive = true; |
| active = true; |
| }); |
| |
| joystickArea.addEventListener('touchmove', (e) => { |
| if (!active) return; |
| e.preventDefault(); |
| const touch = e.touches[0]; |
| handleX = touch.clientX - startX; |
| handleY = touch.clientY - startY; |
| |
| |
| const distance = Math.sqrt(handleX * handleX + handleY * handleY); |
| if (distance > maxDistance) { |
| handleX = (handleX / distance) * maxDistance; |
| handleY = (handleY / distance) * maxDistance; |
| } |
| |
| handle.style.transform = `translate(${handleX}px, ${handleY}px)`; |
| |
| |
| gameState.joystickAngle = Math.atan2(handleY, handleX); |
| gameState.joystickDistance = distance / maxDistance; |
| }); |
| |
| joystickArea.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| handle.style.transform = 'translate(0, 0)'; |
| gameState.joystickActive = false; |
| active = false; |
| }); |
| } |
| |
| |
| window.addEventListener('resize', resizeCanvas); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (gameState.keys.hasOwnProperty(e.key)) { |
| gameState.keys[e.key] = true; |
| e.preventDefault(); |
| } |
| |
| if (e.key === ' ' || e.key === 'Spacebar') { |
| gameState.keys.Space = true; |
| e.preventDefault(); |
| } |
| }); |
| |
| document.addEventListener('keyup', (e) => { |
| if (gameState.keys.hasOwnProperty(e.key)) { |
| gameState.keys[e.key] = false; |
| e.preventDefault(); |
| } |
| |
| if (e.key === ' ' || e.key === 'Spacebar') { |
| gameState.keys.Space = false; |
| e.preventDefault(); |
| } |
| }); |
| |
| |
| leftBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowLeft = true; |
| }); |
| leftBtn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowLeft = false; |
| }); |
| |
| rightBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowRight = true; |
| }); |
| rightBtn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowRight = false; |
| }); |
| |
| upBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowUp = true; |
| }); |
| upBtn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowUp = false; |
| }); |
| |
| downBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowDown = true; |
| }); |
| downBtn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowDown = false; |
| }); |
| |
| fireBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| gameState.isFiring = true; |
| }); |
| fireBtn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| gameState.isFiring = false; |
| }); |
| |
| |
| leftBtn.addEventListener('mousedown', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowLeft = true; |
| }); |
| leftBtn.addEventListener('mouseup', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowLeft = false; |
| }); |
| leftBtn.addEventListener('mouseleave', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowLeft = false; |
| }); |
| |
| rightBtn.addEventListener('mousedown', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowRight = true; |
| }); |
| rightBtn.addEventListener('mouseup', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowRight = false; |
| }); |
| rightBtn.addEventListener('mouseleave', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowRight = false; |
| }); |
| |
| upBtn.addEventListener('mousedown', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowUp = true; |
| }); |
| upBtn.addEventListener('mouseup', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowUp = false; |
| }); |
| upBtn.addEventListener('mouseleave', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowUp = false; |
| }); |
| |
| downBtn.addEventListener('mousedown', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowDown = true; |
| }); |
| downBtn.addEventListener('mouseup', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowDown = false; |
| }); |
| downBtn.addEventListener('mouseleave', (e) => { |
| e.preventDefault(); |
| gameState.keys.ArrowDown = false; |
| }); |
| |
| fireBtn.addEventListener('mousedown', (e) => { |
| e.preventDefault(); |
| gameState.isFiring = true; |
| }); |
| fireBtn.addEventListener('mouseup', (e) => { |
| e.preventDefault(); |
| gameState.isFiring = false; |
| }); |
| fireBtn.addEventListener('mouseleave', (e) => { |
| e.preventDefault(); |
| gameState.isFiring = false; |
| }); |
| |
| |
| startButton.addEventListener('click', initGame); |
| restartButton.addEventListener('click', initGame); |
| |
| |
| resizeCanvas(); |
| setupJoystick(); |
| |
| |
| highScoreDisplay.textContent = gameState.highScore; |
| </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=zdwalter/plane-fighter-2" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> </html> |
|
|
| |