Spaces:
Running
Running
<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> | |
<!-- 游戏UI --> | |
<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"> | |
<!-- 这里会被JavaScript动态填充 --> | |
</div> | |
<!-- Boss血条 --> | |
<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, // Boss是否激活 | |
bossHealth: 0, // Boss当前血量 | |
bossMaxHealth: 0, // Boss最大血量 | |
plane: { | |
x: 0, | |
y: 0, | |
width: 60, | |
height: 60, | |
velocity: 0, // 左右方向速度 | |
verticalVelocity: 0, // 上下方向速度 | |
rotation: 0, | |
lastFireTime: 0, | |
fireRate: 200, // 射击间隔(ms) | |
bulletDamage: 1, // 子弹伤害 | |
hasShield: false, | |
shieldDuration: 0, | |
powerups: { | |
rapidFire: 0, // 火力增强 | |
homingMissiles: 0, // 跟踪导弹 | |
} | |
}, | |
stars: [], | |
obstacles: [], | |
clouds: [], | |
explosions: [], | |
bullets: [], | |
debris: [], | |
powerups: [], // 道具 | |
homingMissiles: [], // 跟踪导弹 | |
effects: [], // 文字特效 | |
particles: [], // 粒子效果 | |
enemyBullets: [], // 敌人发射的子弹 | |
lastStarTime: 0, | |
lastObstacleTime: 0, | |
lastCloudTime: 0, | |
lastPowerupTime: 0, | |
lastBossSpawnTime: 0, // 上次生成Boss时间 | |
keys: { | |
ArrowUp: false, | |
ArrowDown: false, | |
ArrowLeft: false, | |
ArrowRight: false, | |
Space: false | |
}, | |
isMobile: false, | |
joystickActive: false, | |
joystickAngle: 0, | |
joystickDistance: 0, | |
achievements: { | |
firstBlood: false, // 第一次击杀 | |
combo5: false, // 连续5次击杀 | |
noDamage: false, // 无伤通关 | |
bossSlayer: false // 击败Boss | |
}, | |
highScore: localStorage.getItem('highScore') || 0 | |
}; | |
// 道具类型 | |
const POWERUP_TYPES = { | |
RAPID_FIRE: { | |
id: 'rapidFire', | |
icon: 'fas fa-bolt', | |
color: 'red', | |
duration: 10000, // 10秒 | |
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, // 10秒 | |
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, // 8秒 | |
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); // 最多5条命 | |
updateUI(); | |
createEffect('生命值恢复!', 'green', 1500); | |
playSound('powerupSound'); | |
} | |
}, | |
TIME_SLOW: { | |
id: 'timeSlow', | |
icon: 'fas fa-clock', | |
color: 'cyan', | |
duration: 8000, // 8秒 | |
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, // 10秒 | |
effect: (game) => { | |
game.doubleScore = Date.now() + POWERUP_TYPES.DOUBLE_SCORE.duration; | |
createEffect('双倍分数!', 'pink', 1500); | |
playSound('powerupSound'); | |
} | |
} | |
}; | |
// 获取DOM元素 | |
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 | |
}); | |
} | |
} | |
// 更新UI | |
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> | |
`; | |
} | |
// 更新Boss血条 | |
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; | |
// 碰撞体积比实际显示大20% | |
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 // 用于控制敌人射击间隔 | |
}); | |
} | |
// 创建Boss障碍物 | |
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 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) { // 70%概率发射跟踪导弹 | |
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); | |
// 检查Boss生成条件 | |
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; | |
} | |
// 生成新道具 (每5-8秒) | |
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(); | |
// 如果是大型障碍物但不是Boss,分裂成小型障碍物 | |
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 | |
}); | |
} | |
} | |
// 如果是Boss,标记Boss已击败 | |
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.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; | |
// 敌人射击逻辑:当在屏幕内时按间隔发射子弹 | |
if (!obstacle.hit && obstacle.x < canvas.width && obstacle.x > 0) { | |
const shotCooldown = obstacle.isBoss ? 1500 : 3000; | |
if (Date.now() - obstacle.lastShotTime > shotCooldown) { | |
createEnemyBullet(obstacle); | |
} | |
} | |
// 检测与飞机的碰撞 | |
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 | |
}; | |
if (checkCollision(bulletRect, obstacleRect)) { | |
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++; | |
// 如果是大型障碍物但不是Boss,分裂成小型障碍物 | |
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, | |
isBoss: false, | |
lastShotTime: 0 | |
}); | |
} | |
} | |
// 如果是Boss,标记Boss已击败 | |
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.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(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.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 gradP = ctx.createRadialGradient(0, 0, 0, 0, 0, powerup.size / 2); | |
gradP.addColorStop(0, powerup.type.color); | |
gradP.addColorStop(1, 'rgba(255,255,255,0)'); | |
ctx.fillStyle = gradP; | |
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 grad = ctx.createRadialGradient( | |
bullet.x, bullet.y, 0, | |
bullet.x, bullet.y, bullet.size | |
); | |
grad.addColorStop(0, '#ff0'); | |
grad.addColorStop(1, '#f80'); | |
ctx.beginPath(); | |
ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); | |
ctx.fillStyle = grad; | |
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.enemyBullets.forEach(bullet => { | |
ctx.save(); | |
ctx.translate(bullet.x, bullet.y); | |
ctx.beginPath(); | |
ctx.arc(0, 0, bullet.size, 0, Math.PI * 2); | |
ctx.fillStyle = 'rgba(255,0,0,0.8)'; | |
ctx.fill(); | |
ctx.restore(); | |
}); | |
// 绘制星星 | |
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 gradStar = ctx.createRadialGradient(0, 0, 0, 0, 0, star.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(obstacle => { | |
ctx.save(); | |
ctx.translate(obstacle.x, obstacle.y); | |
if (obstacle.type === 'rectangle') { | |
// 如果是Boss | |
if (obstacle.isBoss) { | |
// 画Boss的矩形身体 | |
// 绘制健康条 | |
if (obstacle.health < obstacle.maxHealth) { | |
const healthBarWidth = 100; | |
ctx.fillStyle = 'red'; | |
ctx.fillRect( | |
-healthBarWidth / 2, | |
-obstacle.height / 2 - 15, | |
healthBarWidth, | |
5 | |
); | |
ctx.fillStyle = 'purple'; | |
ctx.fillRect( | |
-healthBarWidth / 2, | |
-obstacle.height / 2 - 15, | |
healthBarWidth * (obstacle.health / obstacle.maxHealth), | |
5 | |
); | |
} | |
// 主体 | |
ctx.fillStyle = '#8B0000'; | |
ctx.fillRect( | |
-obstacle.width / 2, | |
-obstacle.height / 2, | |
obstacle.width, | |
obstacle.height | |
); | |
ctx.fillStyle = '#600000'; | |
ctx.fillRect( | |
-obstacle.width / 2 + 5, | |
-obstacle.height / 2 + 5, | |
obstacle.width - 10, | |
obstacle.height - 10 | |
); | |
// 裂缝效果 | |
if (obstacle.health < obstacle.maxHealth) { | |
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; | |
ctx.lineWidth = 2; | |
for (let i = 0; i < 10; 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(); | |
} | |
} | |
// BOSS标记 | |
ctx.fillStyle = 'gold'; | |
ctx.font = 'bold 20px Arial'; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillText('BOSS', 0, 0); | |
} | |
else { | |
// 普通矩形敌人 -> 用小飞机造型来表示 | |
// 先画一个简化小飞机 (与玩家外观类似,但颜色不同) | |
ctx.save(); | |
// 如果受损,可能需要画个小血条 | |
if (obstacle.health < obstacle.maxHealth) { | |
const healthBarWidth = 20; | |
ctx.fillStyle = 'red'; | |
ctx.fillRect(-healthBarWidth / 2, -obstacle.height / 2 - 10, healthBarWidth, 4); | |
ctx.fillStyle = 'lime'; | |
ctx.fillRect( | |
-healthBarWidth / 2, | |
-obstacle.height / 2 - 10, | |
healthBarWidth * (obstacle.health / obstacle.maxHealth), | |
4 | |
); | |
} | |
// 敌机主体(蓝色机身) | |
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 { | |
// 圆形敌人 -> 绘制UFO形状 | |
// 小血条 | |
if (obstacle.health < obstacle.maxHealth) { | |
const healthBarWidth = 20; | |
ctx.fillStyle = 'red'; | |
ctx.fillRect(-healthBarWidth / 2, -obstacle.height / 2 - 10, healthBarWidth, 4); | |
ctx.fillStyle = 'lime'; | |
ctx.fillRect( | |
-healthBarWidth / 2, | |
-obstacle.height / 2 - 10, | |
healthBarWidth * (obstacle.health / obstacle.maxHealth), | |
4 | |
); | |
} | |
// UFO顶部圆 dome | |
ctx.beginPath(); | |
ctx.ellipse(0, -obstacle.height / 6, obstacle.width / 3, obstacle.height / 4, 0, 0, Math.PI * 2); | |
ctx.fillStyle = '#9b59b6'; | |
ctx.fill(); | |
// UFO主体 | |
ctx.beginPath(); | |
ctx.ellipse(0, 0, obstacle.width / 2, obstacle.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) * (obstacle.width / 2.2); | |
const ry = Math.sin(angle) * (obstacle.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(explosion => { | |
ctx.save(); | |
ctx.translate(explosion.x, explosion.y); | |
const gradExp = ctx.createRadialGradient( | |
0, 0, 0, | |
0, 0, explosion.size | |
); | |
gradExp.addColorStop(0, `rgba(255, 100, 0, ${explosion.alpha})`); | |
gradExp.addColorStop(0.5, `rgba(255, 200, 0, ${explosion.alpha * 0.6})`); | |
gradExp.addColorStop(1, `rgba(255, 255, 255, 0)`); | |
ctx.beginPath(); | |
ctx.arc(0, 0, explosion.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); | |
} | |
} | |
// 辅助函数: 获取FontAwesome图标的Unicode | |
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> | |