459 lines
13 KiB
HTML
459 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
|
|
<title>像素守卫 · 射击游戏</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
-webkit-tap-highlight-color: transparent;
|
|
user-select: none;
|
|
}
|
|
body {
|
|
background: linear-gradient(180deg, #1a3a1a 0%, #0d260d 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
padding: 10px;
|
|
margin: 0;
|
|
}
|
|
.game-container {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
background: radial-gradient(circle at 50% 30%, #3a6b3a, #1a331a);
|
|
border-radius: 48px;
|
|
box-shadow: 0 25px 50px rgba(0,0,0,0.7), inset 0 0 25px rgba(150,255,150,0.15);
|
|
padding: 16px 12px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
border: 1px solid #5a8a5a;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
border-radius: 32px;
|
|
background: radial-gradient(ellipse at 50% 20%, #4a7a4a 0%, #1a2e1a 85%);
|
|
box-shadow: inset 0 -8px 25px rgba(0,0,0,0.8), 0 12px 28px black;
|
|
touch-action: none;
|
|
}
|
|
.info-panel {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 14px;
|
|
padding: 0 10px;
|
|
color: #c0ffc0;
|
|
font-weight: bold;
|
|
text-shadow: 0 2px 8px black;
|
|
font-size: 1.3rem;
|
|
}
|
|
.score-board {
|
|
background: rgba(0,20,0,0.8);
|
|
backdrop-filter: blur(12px);
|
|
border-radius: 50px;
|
|
padding: 8px 22px;
|
|
letter-spacing: 1px;
|
|
box-shadow: 0 8px 18px rgba(0,0,0,0.6);
|
|
border: 1px solid #6aaa6a;
|
|
}
|
|
.restart-btn {
|
|
background: #4caf50;
|
|
border: none;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 1.2rem;
|
|
padding: 10px 25px;
|
|
border-radius: 40px;
|
|
background: linear-gradient(180deg, #6fbf6f 0%, #2e7d32 100%);
|
|
box-shadow: 0 8px 0 #1b5e20, 0 12px 22px rgba(0,0,0,0.5);
|
|
transition: all 0.08s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
cursor: pointer;
|
|
border: 1px solid #a5d6a7;
|
|
touch-action: manipulation;
|
|
}
|
|
.restart-btn:active {
|
|
transform: translateY(4px);
|
|
box-shadow: 0 4px 0 #1b5e20, 0 8px 14px rgba(0,0,0,0.6);
|
|
background: linear-gradient(180deg, #5aa55a 0%, #1f5a22 100%);
|
|
}
|
|
.hint {
|
|
color: #a0d0a0;
|
|
font-size: 0.8rem;
|
|
margin-top: 6px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="game-container">
|
|
<canvas id="gameCanvas" width="360" height="600" aria-label="像素守卫射击游戏"></canvas>
|
|
<div class="info-panel">
|
|
<div class="score-board" id="scoreDisplay">🗡️ 0</div>
|
|
<button class="restart-btn" id="restartButton">🏰 守城</button>
|
|
</div>
|
|
<div class="hint">👈 滑动瞄准 | 点击射击敌人</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const scoreSpan = document.getElementById('scoreDisplay');
|
|
|
|
const CW = 360;
|
|
const CH = 600;
|
|
canvas.width = CW;
|
|
canvas.height = CH;
|
|
|
|
// ----- 炮台设置 -----
|
|
const TOWER_X = CW / 2;
|
|
const TOWER_Y = CH - 55;
|
|
let aimAngle = -Math.PI / 2; // 默认向上
|
|
|
|
// 游戏状态
|
|
let gameActive = true;
|
|
let score = 0;
|
|
let health = 100;
|
|
const MAX_HEALTH = 100;
|
|
|
|
// 敌人数组
|
|
let enemies = [];
|
|
|
|
// 子弹数组
|
|
let bullets = [];
|
|
|
|
// 粒子效果
|
|
let particles = [];
|
|
|
|
// 生成控制
|
|
const ENEMY_SPAWN_INTERVAL = 40;
|
|
let frameCounter = 0;
|
|
let difficultyTimer = 0;
|
|
|
|
// ----- 辅助函数 -----
|
|
function spawnEnemy() {
|
|
const side = Math.random() < 0.5 ? 'left' : 'right';
|
|
const x = side === 'left' ? -20 : CW + 20;
|
|
const y = 40 + Math.random() * 250;
|
|
const speedX = side === 'left' ? 1.2 + Math.random() * 1.5 : -(1.2 + Math.random() * 1.5);
|
|
const speedY = (Math.random() - 0.5) * 1.2;
|
|
|
|
enemies.push({
|
|
x: x,
|
|
y: y,
|
|
vx: speedX,
|
|
vy: speedY,
|
|
radius: 14,
|
|
health: 1,
|
|
color: '#ff5555',
|
|
});
|
|
}
|
|
|
|
function restartGame() {
|
|
gameActive = true;
|
|
score = 0;
|
|
health = MAX_HEALTH;
|
|
enemies = [];
|
|
bullets = [];
|
|
particles = [];
|
|
frameCounter = 0;
|
|
difficultyTimer = 0;
|
|
aimAngle = -Math.PI / 2;
|
|
updateScoreDisplay();
|
|
}
|
|
|
|
function endGame() {
|
|
if (!gameActive) return;
|
|
gameActive = false;
|
|
if (window.navigator && window.navigator.vibrate) {
|
|
window.navigator.vibrate(60);
|
|
}
|
|
}
|
|
|
|
function updateScoreDisplay() {
|
|
scoreSpan.textContent = `🗡️ ${score} ❤️ ${health}`;
|
|
}
|
|
|
|
function damagePlayer(amount) {
|
|
health -= amount;
|
|
if (health <= 0) {
|
|
health = 0;
|
|
endGame();
|
|
}
|
|
updateScoreDisplay();
|
|
}
|
|
|
|
// 发射子弹
|
|
function shootBullet() {
|
|
if (!gameActive) return;
|
|
const bulletSpeed = 8;
|
|
const vx = Math.cos(aimAngle) * bulletSpeed;
|
|
const vy = Math.sin(aimAngle) * bulletSpeed;
|
|
bullets.push({
|
|
x: TOWER_X,
|
|
y: TOWER_Y,
|
|
vx: vx,
|
|
vy: vy,
|
|
radius: 5,
|
|
life: 80,
|
|
});
|
|
}
|
|
|
|
// ----- 更新逻辑 -----
|
|
function update() {
|
|
if (!gameActive) return;
|
|
|
|
// 更新子弹
|
|
for (let i = bullets.length - 1; i >= 0; i--) {
|
|
const b = bullets[i];
|
|
b.x += b.vx;
|
|
b.y += b.vy;
|
|
b.life--;
|
|
|
|
if (b.life <= 0 || b.x < -30 || b.x > CW + 30 || b.y < -30 || b.y > CH + 30) {
|
|
bullets.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
// 子弹与敌人碰撞
|
|
let bulletRemoved = false;
|
|
for (let j = enemies.length - 1; j >= 0; j--) {
|
|
const enemy = enemies[j];
|
|
const dist = Math.hypot(b.x - enemy.x, b.y - enemy.y);
|
|
if (dist < enemy.radius + b.radius) {
|
|
// 消灭敌人
|
|
enemies.splice(j, 1);
|
|
bulletRemoved = true;
|
|
score += 20;
|
|
updateScoreDisplay();
|
|
spawnParticles(enemy.x, enemy.y, '#ffaa00', 8);
|
|
break;
|
|
}
|
|
}
|
|
if (bulletRemoved) {
|
|
bullets.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// 更新敌人
|
|
for (let i = enemies.length - 1; i >= 0; i--) {
|
|
const enemy = enemies[i];
|
|
enemy.x += enemy.vx;
|
|
enemy.y += enemy.vy;
|
|
|
|
// 敌人与炮台碰撞
|
|
const distToTower = Math.hypot(enemy.x - TOWER_X, enemy.y - TOWER_Y);
|
|
if (distToTower < 35) {
|
|
enemies.splice(i, 1);
|
|
damagePlayer(12);
|
|
spawnParticles(TOWER_X, TOWER_Y, '#ff3333', 12);
|
|
continue;
|
|
}
|
|
|
|
// 移除离开屏幕的敌人
|
|
if (enemy.x < -60 || enemy.x > CW + 60 || enemy.y < -60 || enemy.y > CH + 60) {
|
|
enemies.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// 更新粒子
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const p = particles[i];
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
p.life -= 0.03;
|
|
if (p.life <= 0) particles.splice(i, 1);
|
|
}
|
|
|
|
// 生成敌人 (随难度增加)
|
|
if (gameActive) {
|
|
frameCounter++;
|
|
difficultyTimer++;
|
|
const adjustedInterval = Math.max(18, ENEMY_SPAWN_INTERVAL - Math.floor(difficultyTimer / 300));
|
|
if (frameCounter >= adjustedInterval) {
|
|
spawnEnemy();
|
|
frameCounter = 0;
|
|
if (Math.random() < 0.3) spawnEnemy(); // 偶尔双倍
|
|
}
|
|
}
|
|
}
|
|
|
|
function spawnParticles(x, y, color, count) {
|
|
for (let i = 0; i < count; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = 2 + Math.random() * 5;
|
|
particles.push({
|
|
x: x, y: y,
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
life: 1.0,
|
|
color: color,
|
|
size: 3 + Math.random() * 4,
|
|
});
|
|
}
|
|
}
|
|
|
|
// ----- 绘制图形 -----
|
|
function draw() {
|
|
ctx.clearRect(0, 0, CW, CH);
|
|
|
|
// 绘制地面网格
|
|
ctx.strokeStyle = '#2a4a2a';
|
|
ctx.lineWidth = 0.8;
|
|
for (let i = 0; i < CW; i += 30) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(i, 0);
|
|
ctx.lineTo(i, CH);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 绘制敌人
|
|
for (let enemy of enemies) {
|
|
ctx.save();
|
|
ctx.shadowColor = '#ff0000aa';
|
|
ctx.shadowBlur = 14;
|
|
ctx.fillStyle = enemy.color;
|
|
ctx.beginPath();
|
|
ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI*2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.arc(enemy.x-3, enemy.y-4, 4, 0, Math.PI*2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
// 绘制子弹
|
|
for (let b of bullets) {
|
|
ctx.fillStyle = '#ffff66';
|
|
ctx.shadowColor = '#ffaa00';
|
|
ctx.shadowBlur = 10;
|
|
ctx.beginPath();
|
|
ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// 绘制粒子
|
|
for (let p of particles) {
|
|
ctx.globalAlpha = p.life;
|
|
ctx.fillStyle = p.color;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
ctx.shadowBlur = 0;
|
|
|
|
// 绘制炮台
|
|
ctx.save();
|
|
ctx.translate(TOWER_X, TOWER_Y);
|
|
// 基座
|
|
ctx.fillStyle = '#5a5a7a';
|
|
ctx.shadowColor = '#000000';
|
|
ctx.shadowBlur = 10;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 18, 0, Math.PI*2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#7a7aaa';
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 12, 0, Math.PI*2);
|
|
ctx.fill();
|
|
|
|
// 炮管
|
|
ctx.rotate(aimAngle);
|
|
ctx.fillStyle = '#ccccdd';
|
|
ctx.fillRect(12, -5, 24, 10);
|
|
ctx.fillStyle = '#aaaacc';
|
|
ctx.fillRect(30, -3, 10, 6);
|
|
ctx.restore();
|
|
|
|
// 生命条
|
|
ctx.fillStyle = '#331111';
|
|
ctx.fillRect(15, CH-18, CW-30, 14);
|
|
const healthPercent = health / MAX_HEALTH;
|
|
const healthColor = healthPercent > 0.5 ? '#44cc44' : '#ff6644';
|
|
ctx.fillStyle = healthColor;
|
|
ctx.fillRect(15, CH-18, (CW-30) * healthPercent, 14);
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(15, CH-18, CW-30, 14);
|
|
|
|
// 游戏结束
|
|
if (!gameActive) {
|
|
ctx.globalAlpha = 0.8;
|
|
ctx.fillStyle = '#0a0f0a';
|
|
ctx.fillRect(0, 0, CW, CH);
|
|
ctx.globalAlpha = 1;
|
|
ctx.fillStyle = '#ffcccc';
|
|
ctx.font = 'bold 28px "Segoe UI"';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('💀 失守', CW/2, CH/2-20);
|
|
}
|
|
}
|
|
|
|
// ----- 游戏循环 -----
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// ----- 事件处理 -----
|
|
function handleAim(e) {
|
|
e.preventDefault();
|
|
if (!gameActive) return;
|
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scaleX = CW / rect.width;
|
|
const scaleY = CH / rect.height;
|
|
const canvasX = (clientX - rect.left) * scaleX;
|
|
const canvasY = (clientY - rect.top) * scaleY;
|
|
|
|
aimAngle = Math.atan2(canvasY - TOWER_Y, canvasX - TOWER_X);
|
|
}
|
|
|
|
function handleShoot(e) {
|
|
e.preventDefault();
|
|
if (!gameActive) return;
|
|
shootBullet();
|
|
}
|
|
|
|
function bindEvents() {
|
|
canvas.addEventListener('touchmove', handleAim, { passive: false });
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
handleAim(e);
|
|
handleShoot(e);
|
|
});
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
if (e.buttons === 1) handleAim(e);
|
|
});
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
handleAim(e);
|
|
handleShoot(e);
|
|
});
|
|
|
|
document.getElementById('restartButton').addEventListener('click', restartGame);
|
|
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
restartGame();
|
|
});
|
|
}
|
|
|
|
bindEvents();
|
|
restartGame();
|
|
gameLoop();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |