|
<!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> |
|
|
|
|
|
<script> |
|
const meteorImage = new Image(); |
|
meteorImage.src = "https://via.placeholder.com/80x80.png?text=Meteor"; |
|
</script> |
|
|
|
<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: 2 |
|
}); |
|
|
|
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) { |
|
let bodyGradient = ctx.createLinearGradient(-obstacle.width/2, 0, obstacle.width/2, 0); |
|
if (obstacle.isBoss) { |
|
bodyGradient.addColorStop(0, '#ff4d4d'); |
|
bodyGradient.addColorStop(1, '#8B0000'); |
|
} else { |
|
bodyGradient.addColorStop(0, '#666'); |
|
bodyGradient.addColorStop(1, '#333'); |
|
} |
|
ctx.fillStyle = bodyGradient; |
|
ctx.shadowColor = 'rgba(0,0,0,0.4)'; |
|
ctx.shadowBlur = 4; |
|
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.shadowBlur = 0; |
|
|
|
let wingGradient = ctx.createLinearGradient(-obstacle.width/4, 0, obstacle.width/4, 0); |
|
wingGradient.addColorStop(0, obstacle.isBoss ? '#a83232' : '#444'); |
|
wingGradient.addColorStop(1, obstacle.isBoss ? '#600000' : '#222'); |
|
ctx.fillStyle = wingGradient; |
|
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.fillStyle = obstacle.isBoss ? '#800000' : '#555'; |
|
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(); |
|
|
|
let cockpitGradient = ctx.createRadialGradient(obstacle.width/4, 0, 0, obstacle.width/4, 0, obstacle.width/8); |
|
cockpitGradient.addColorStop(0, '#80dfff'); |
|
cockpitGradient.addColorStop(1, '#3498db'); |
|
ctx.fillStyle = cockpitGradient; |
|
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 16px Arial'; |
|
ctx.textAlign = 'center'; |
|
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 { |
|
|
|
if (meteorImage.complete) { |
|
ctx.drawImage(meteorImage, -obstacle.width / 2, -obstacle.height / 2, obstacle.width, obstacle.height); |
|
} else { |
|
|
|
ctx.beginPath(); |
|
ctx.arc(0, 0, obstacle.width / 2, 0, Math.PI * 2); |
|
ctx.fillStyle = '#555'; |
|
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 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(); |
|
} |
|
} |
|
} |
|
|
|
|
|
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> |
|
|