| <!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%; |
| } |
| |
| .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> |
|
|
| <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: [], |
| debris: [], |
| powerups: [], |
| homingMissiles: [], |
| enemyBullets: [], |
| effects: [], |
| particles: [], |
| lastStarTime: 0, |
| lastObstacleTime: 0, |
| lastCloudTime: 0, |
| lastPowerupTime: 0, |
| lastBossSpawnTime: 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'); |
| |
| |
| |
| |
| 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.debris = []; |
| gameState.powerups = []; |
| gameState.homingMissiles = []; |
| gameState.enemyBullets = []; |
| gameState.effects = []; |
| gameState.particles = []; |
| gameState.lastStarTime = 0; |
| gameState.lastObstacleTime = 0; |
| gameState.lastCloudTime = 0; |
| gameState.lastPowerupTime = 0; |
| gameState.lastBossSpawnTime = 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 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 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, |
| isBoss: false, |
| lastShotTime: 0 |
| }); |
| } |
| 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, |
| lastShotTime: 0 |
| }); |
| |
| 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 dist = Math.sqrt(dx*dx + dy*dy); |
| if (dist < minDistance) { |
| minDistance = dist; |
| 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 createEnemyBullet(obstacle) { |
| const angle = Math.atan2(gameState.plane.y - obstacle.y, gameState.plane.x - obstacle.x); |
| gameState.enemyBullets.push({ |
| x: obstacle.x, |
| y: obstacle.y, |
| size: obstacle.isBoss ? 10 : 6, |
| speed: obstacle.isBoss ? 5 : 4, |
| angle, |
| damage: obstacle.isBoss ? 2 : 1, |
| hit: false |
| }); |
| obstacle.lastShotTime = Date.now(); |
| } |
| |
| |
| |
| |
| 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 (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(p => { |
| p.x += p.speedX; |
| p.y += p.speedY; |
| p.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(d => { |
| d.x += d.speedX; |
| d.y += d.speedY; |
| d.rotation += d.rotationSpeed; |
| d.opacity -= 0.02; |
| }); |
| gameState.debris = gameState.debris.filter(d => d.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, |
| isBoss: false, |
| lastShotTime: 0 |
| }); |
| } |
| } |
| 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(m => m.x < canvas.width && m.x > 0 && m.y < canvas.height && m.y > 0 && !m.hit); |
| |
| |
| gameState.clouds.forEach(cloud => { |
| cloud.x -= cloud.speed * timeSlowFactor; |
| }); |
| gameState.clouds = gameState.clouds.filter(cloud => cloud.x+cloud.size>0); |
| |
| |
| gameState.powerups.forEach(pw => { |
| pw.x -= pw.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: pw.x - pw.size/2, |
| y: pw.y - pw.size/2, |
| width: pw.size, |
| height: pw.size |
| }; |
| if (checkCollision(planeRect, powerupRect) && !gameState.gameOver) { |
| pw.collected = true; |
| pw.type.effect(gameState); |
| updateUI(); |
| createParticles(pw.x, pw.y, 15, pw.type.color); |
| } |
| }); |
| gameState.powerups = gameState.powerups.filter(pw => pw.x+pw.size>0 && !pw.collected); |
| |
| |
| gameState.bullets.forEach(b => { |
| b.x += b.speed * timeSlowFactor; |
| }); |
| gameState.bullets = gameState.bullets.filter(b => b.x<canvas.width); |
| |
| |
| gameState.stars.forEach(s => { |
| s.x -= s.speed * timeSlowFactor; |
| s.rotation += s.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: s.x - s.size/2, |
| y: s.y - s.size/2, |
| width: s.size, |
| height: s.size |
| }; |
| if (checkCollision(planeRect, starRect) && !gameState.gameOver) { |
| s.collected = true; |
| const scoreBonus = 10; |
| gameState.score += (gameState.doubleScore> Date.now()? scoreBonus*2 : scoreBonus); |
| updateUI(); |
| createParticles(s.x, s.y, 10, 'gold'); |
| } |
| }); |
| gameState.stars = gameState.stars.filter(s => s.x + s.size>0 && !s.collected); |
| |
| |
| let comboCount = 0; |
| gameState.obstacles.forEach(ob => { |
| ob.x -= ob.speed * timeSlowFactor; |
| |
| if(!ob.hit && ob.x<canvas.width && ob.x>0){ |
| const shotCooldown = ob.isBoss ? 1500 : 3000; |
| if(Date.now() - ob.lastShotTime>shotCooldown){ |
| createEnemyBullet(ob); |
| } |
| } |
| |
| 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: ob.x - ob.collisionWidth/2, |
| y: ob.y - ob.collisionHeight/2, |
| width: ob.collisionWidth, |
| height: ob.collisionHeight |
| }; |
| if (checkCollision(planeRect, obstacleRect) && !gameState.gameOver) { |
| if (!gameState.plane.hasShield || gameState.plane.shieldDuration < Date.now()) { |
| ob.hit = true; |
| gameState.lives--; |
| updateUI(); |
| createExplosion(gameState.plane.x, gameState.plane.y); |
| if (gameState.lives<=0) { |
| endGame(); |
| } |
| } else { |
| ob.hit = true; |
| createExplosion(ob.x, ob.y); |
| createDebris(ob, 4); |
| } |
| } |
| |
| if(!ob.hit){ |
| const bulletHits = []; |
| gameState.bullets.forEach((bullet, idx) => { |
| const bulletRect = { |
| x: bullet.x - bullet.size/2, |
| y: bullet.y - bullet.size/2, |
| width: bullet.size, |
| height: bullet.size |
| }; |
| if(checkCollision(bulletRect, obstacleRect)){ |
| ob.health -= bullet.damage; |
| bulletHits.push(idx); |
| createExplosion(bullet.x, bullet.y); |
| if(ob.health<=0){ |
| ob.hit= true; |
| const scoreBonus = ob.isBoss? 500 : (ob.isLarge?30:15); |
| gameState.score += (gameState.doubleScore> Date.now()? scoreBonus*2 : scoreBonus); |
| updateUI(); |
| comboCount++; |
| if(ob.isLarge && !ob.isBoss){ |
| for(let i=0;i<3;i++){ |
| gameState.obstacles.push({ |
| x: ob.x+(Math.random()-0.5)*50, |
| y: ob.y+(Math.random()-0.5)*50, |
| width: ob.width/2, |
| height: ob.height/2, |
| collisionWidth: ob.collisionWidth/2, |
| collisionHeight: ob.collisionHeight/2, |
| speed: ob.speed*1.2, |
| type: ob.type, |
| health: 1, |
| maxHealth: 1, |
| isLarge: false, |
| isBoss: false, |
| lastShotTime: 0 |
| }); |
| } |
| } |
| if(ob.isBoss){ |
| gameState.bossActive= false; |
| gameState.achievements.bossSlayer= true; |
| } |
| createDebris(ob, ob.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(o => o.x + o.width>0 && !o.hit); |
| |
| |
| gameState.enemyBullets.forEach(bullet => { |
| bullet.x += Math.cos(bullet.angle) * bullet.speed * timeSlowFactor; |
| bullet.y += Math.sin(bullet.angle) * bullet.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 bulletRect= { |
| x: bullet.x- bullet.size/2, |
| y: bullet.y- bullet.size/2, |
| width: bullet.size, |
| height: bullet.size |
| }; |
| if(checkCollision(planeRect, bulletRect)){ |
| if(!gameState.plane.hasShield || gameState.plane.shieldDuration< Date.now()){ |
| gameState.lives -= bullet.damage; |
| createExplosion(bullet.x, bullet.y); |
| bullet.hit= true; |
| if(gameState.lives<=0){ |
| endGame(); |
| }else{ |
| updateUI(); |
| } |
| }else{ |
| createExplosion(bullet.x, bullet.y); |
| bullet.hit= true; |
| } |
| } |
| if(bullet.x<0||bullet.x>canvas.width|| bullet.y<0|| bullet.y>canvas.height){ |
| bullet.hit= true; |
| } |
| }); |
| gameState.enemyBullets= gameState.enemyBullets.filter(b => !b.hit); |
| |
| |
| gameState.explosions.forEach(ex => { |
| ex.size+= 2; |
| ex.alpha-=0.02; |
| }); |
| gameState.explosions= gameState.explosions.filter(e => e.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(p => { |
| ctx.beginPath(); |
| ctx.arc(p.x,p.y,p.size,0,Math.PI*2); |
| ctx.fillStyle= p.color; |
| ctx.globalAlpha= p.life/100; |
| ctx.fill(); |
| ctx.globalAlpha= 1; |
| }); |
| |
| |
| gameState.debris.forEach(d => { |
| ctx.save(); |
| ctx.translate(d.x,d.y); |
| ctx.rotate(d.rotation); |
| ctx.fillStyle= `rgba(100,100,100,${d.opacity})`; |
| ctx.fillRect(-d.width/2, -d.height/2, d.width, d.height); |
| ctx.restore(); |
| }); |
| |
| |
| gameState.powerups.forEach(pw => { |
| ctx.save(); |
| ctx.translate(pw.x,pw.y); |
| ctx.beginPath(); |
| ctx.arc(0,0, pw.size/2, 0, Math.PI*2); |
| const gradP= ctx.createRadialGradient(0,0,0,0,0,pw.size/2); |
| gradP.addColorStop(0, pw.type.color); |
| gradP.addColorStop(1, 'rgba(255,255,255,0)'); |
| ctx.fillStyle= gradP; |
| ctx.globalAlpha=0.3; |
| ctx.fill(); |
| ctx.globalAlpha=1; |
| |
| |
| ctx.fillStyle= pw.type.color; |
| ctx.beginPath(); |
| ctx.arc(0,0,pw.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(pw.type.icon),16)),0,1); |
| |
| ctx.restore(); |
| }); |
| |
| |
| gameState.homingMissiles.forEach(m => { |
| ctx.save(); |
| ctx.translate(m.x,m.y); |
| ctx.rotate(m.angle); |
| |
| ctx.fillStyle= 'red'; |
| ctx.beginPath(); |
| ctx.moveTo(m.size/2,0); |
| ctx.lineTo(-m.size/2, -m.size/3); |
| ctx.lineTo(-m.size/2, m.size/3); |
| ctx.closePath(); |
| ctx.fill(); |
| |
| ctx.fillStyle= 'orange'; |
| ctx.beginPath(); |
| ctx.moveTo(-m.size/2, -m.size/4); |
| ctx.lineTo(-m.size,0); |
| ctx.lineTo(-m.size/2,m.size/4); |
| ctx.closePath(); |
| ctx.fill(); |
| ctx.restore(); |
| }); |
| |
| |
| gameState.bullets.forEach(b => { |
| const grad= ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,b.size); |
| grad.addColorStop(0,'#ff0'); |
| grad.addColorStop(1,'#f80'); |
| ctx.beginPath(); |
| ctx.arc(b.x,b.y,b.size,0,Math.PI*2); |
| ctx.fillStyle= grad; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.moveTo(b.x- b.speed, b.y); |
| ctx.lineTo(b.x,b.y); |
| ctx.strokeStyle= 'rgba(255,200,0,0.8)'; |
| ctx.lineWidth= b.size/2; |
| ctx.stroke(); |
| }); |
| |
| |
| gameState.enemyBullets.forEach(b => { |
| ctx.save(); |
| ctx.translate(b.x,b.y); |
| ctx.beginPath(); |
| ctx.arc(0,0,b.size,0,Math.PI*2); |
| ctx.fillStyle= 'rgba(255,0,0,0.8)'; |
| ctx.fill(); |
| ctx.restore(); |
| }); |
| |
| |
| gameState.stars.forEach(s => { |
| ctx.save(); |
| ctx.translate(s.x,s.y); |
| ctx.rotate(s.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= s.size/2; |
| const innerRadius= s.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 gradStar= ctx.createRadialGradient(0,0,0,0,0, s.size/2); |
| gradStar.addColorStop(0,'gold'); |
| gradStar.addColorStop(1,'yellow'); |
| ctx.fillStyle= gradStar; |
| ctx.shadowColor= 'yellow'; |
| ctx.shadowBlur=10; |
| ctx.fill(); |
| ctx.restore(); |
| }); |
| |
| |
| gameState.obstacles.forEach(ob => { |
| ctx.save(); |
| ctx.translate(ob.x, ob.y); |
| if(ob.type==='rectangle'){ |
| if(ob.isBoss){ |
| |
| if(ob.health< ob.maxHealth){ |
| const w= 100; |
| ctx.fillStyle= 'red'; |
| ctx.fillRect(-w/2, -ob.height/2-15, w,5); |
| ctx.fillStyle= 'purple'; |
| ctx.fillRect(-w/2, -ob.height/2-15, w*(ob.health/ ob.maxHealth),5); |
| } |
| ctx.fillStyle= '#8B0000'; |
| ctx.fillRect(-ob.width/2, -ob.height/2, ob.width, ob.height); |
| ctx.fillStyle= '#600000'; |
| ctx.fillRect(-ob.width/2+5, -ob.height/2+5, ob.width-10, ob.height-10); |
| if(ob.health< ob.maxHealth){ |
| ctx.strokeStyle= 'rgba(0,0,0,0.5)'; |
| ctx.lineWidth=2; |
| for(let i=0;i<10;i++){ |
| ctx.beginPath(); |
| ctx.moveTo(-ob.width/2 + Math.random()* ob.width, -ob.height/2+ Math.random()*(ob.height/3)); |
| ctx.lineTo(-ob.width/2+ Math.random()* ob.width, ob.height/2- Math.random()*(ob.height/3)); |
| ctx.stroke(); |
| } |
| } |
| ctx.fillStyle= 'gold'; |
| ctx.font= 'bold 20px Arial'; |
| ctx.textAlign= 'center'; |
| ctx.textBaseline= 'middle'; |
| ctx.fillText('BOSS',0,0); |
| } else { |
| |
| if(ob.health< ob.maxHealth){ |
| const w= 20; |
| ctx.fillStyle= 'red'; |
| ctx.fillRect(-w/2, -ob.height/2-10, w,4); |
| ctx.fillStyle= 'lime'; |
| ctx.fillRect(-w/2, -ob.height/2-10, w*(ob.health/ ob.maxHealth),4); |
| } |
| ctx.save(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(20,0); |
| ctx.lineTo(-15,-12); |
| ctx.lineTo(-20,0); |
| ctx.lineTo(-15,12); |
| ctx.closePath(); |
| ctx.fillStyle= '#3498db'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.arc(5,0,4,0,Math.PI*2); |
| ctx.fillStyle= '#fff'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(3,0); |
| ctx.lineTo(-5,-15); |
| ctx.lineTo(-12,-15); |
| ctx.lineTo(-5,0); |
| ctx.closePath(); |
| ctx.fillStyle='#2980b9'; |
| ctx.fill(); |
| ctx.beginPath(); |
| ctx.moveTo(3,0); |
| ctx.lineTo(-5,15); |
| ctx.lineTo(-12,15); |
| ctx.lineTo(-5,0); |
| ctx.closePath(); |
| ctx.fillStyle='#2980b9'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(-15,0); |
| ctx.lineTo(-20,-8); |
| ctx.lineTo(-25,-8); |
| ctx.lineTo(-20,0); |
| ctx.closePath(); |
| ctx.fillStyle= '#1c5982'; |
| ctx.fill(); |
| ctx.beginPath(); |
| ctx.moveTo(-15,0); |
| ctx.lineTo(-20,8); |
| ctx.lineTo(-25,8); |
| ctx.lineTo(-20,0); |
| ctx.closePath(); |
| ctx.fillStyle= '#1c5982'; |
| ctx.fill(); |
| ctx.restore(); |
| } |
| } else { |
| |
| if(ob.health< ob.maxHealth){ |
| const w= 20; |
| ctx.fillStyle= 'red'; |
| ctx.fillRect(-w/2, -ob.height/2-10, w,4); |
| ctx.fillStyle= 'lime'; |
| ctx.fillRect(-w/2, -ob.height/2-10, w*(ob.health/ ob.maxHealth),4); |
| } |
| |
| ctx.beginPath(); |
| ctx.ellipse(0, -ob.height/6, ob.width/3, ob.height/4, 0, 0, Math.PI*2); |
| ctx.fillStyle= '#9b59b6'; |
| ctx.fill(); |
| |
| ctx.beginPath(); |
| ctx.ellipse(0,0, ob.width/2, ob.height/3,0,0,Math.PI*2); |
| ctx.fillStyle= '#8e44ad'; |
| ctx.fill(); |
| |
| for(let i=0;i<3;i++){ |
| const angle= (i/3)*Math.PI*2; |
| const rx= Math.cos(angle)*(ob.width/2.2); |
| const ry= Math.sin(angle)*(ob.height/3.2); |
| ctx.beginPath(); |
| ctx.arc(rx,ry,3,0,Math.PI*2); |
| ctx.fillStyle= '#ffdc00'; |
| ctx.fill(); |
| } |
| } |
| 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 gradS= ctx.createRadialGradient(0,0,0,0,0,45); |
| gradS.addColorStop(0,'rgba(0,204,255,0.2)'); |
| gradS.addColorStop(1,'rgba(0,204,255,0)'); |
| ctx.fillStyle= gradS; |
| 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(ex => { |
| ctx.save(); |
| ctx.translate(ex.x, ex.y); |
| const gradExp= ctx.createRadialGradient(0,0,0,0,0, ex.size); |
| gradExp.addColorStop(0, `rgba(255,100,0,${ex.alpha})`); |
| gradExp.addColorStop(0.5, `rgba(255,200,0,${ex.alpha*0.6})`); |
| gradExp.addColorStop(1, `rgba(255,255,255,0)`); |
| ctx.beginPath(); |
| ctx.arc(0,0, ex.size, 0, Math.PI*2); |
| ctx.fillStyle= gradExp; |
| 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, startY=0; |
| let handleX=0, 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 dist= Math.sqrt(handleX*handleX+ handleY*handleY); |
| if(dist> maxDistance){ |
| handleX= (handleX/dist)* maxDistance; |
| handleY= (handleY/dist)* maxDistance; |
| } |
| handle.style.transform= `translate(${handleX}px, ${handleY}px)`; |
| gameState.joystickAngle= Math.atan2(handleY, handleX); |
| gameState.joystickDistance= dist/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 dist= Math.sqrt(handleX*handleX+ handleY*handleY); |
| if(dist> maxDistance){ |
| handleX= (handleX/dist)* maxDistance; |
| handleY= (handleY/dist)* maxDistance; |
| } |
| handle.style.transform= `translate(${handleX}px, ${handleY}px)`; |
| gameState.joystickAngle= Math.atan2(handleY, handleX); |
| gameState.joystickDistance= dist/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> |
|
|