游戏
This commit is contained in:
1 parent
da12f288d2
commit
c83bc17cfc
12 files changed
+7109
No files matched your search
@@ -0,0 +1,739 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>末日生存 · 二维射击游戏</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(135deg, #0a1f1a 0%, #1a2f1f 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', 'Arial', sans-serif;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.game-container {
|
||||
background: #2d3a2c;
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.6), inset 0 0 15px rgba(0,0,0,0.4);
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 25px rgba(0, 180, 0, 0.3);
|
||||
background: #1e2b1c;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 800 / 600;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.info-panel {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #e0f0d8;
|
||||
font-weight: bold;
|
||||
background: #1e2b1c;
|
||||
border-radius: 40px;
|
||||
padding: 12px 25px;
|
||||
box-shadow: inset 0 2px 8px rgba(0,0,0,0.5);
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
.health-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.health-fill {
|
||||
width: 180px;
|
||||
height: 14px;
|
||||
background: #3a2e2e;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2px 5px rgba(0,0,0,0.7);
|
||||
}
|
||||
.fill-level {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #e74c3c, #f1c40f);
|
||||
border-radius: 20px;
|
||||
transition: width 0.15s ease;
|
||||
}
|
||||
.score-board {
|
||||
background: #0f1a0f;
|
||||
padding: 6px 20px;
|
||||
border-radius: 30px;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 1px;
|
||||
color: #d4ed5e;
|
||||
box-shadow: 0 0 12px #5a8f4a;
|
||||
}
|
||||
button {
|
||||
background: #d4b34c;
|
||||
border: none;
|
||||
color: #1e2b1c;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
padding: 10px 25px;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(180deg, #e9d584 0%, #b38b40 100%);
|
||||
box-shadow: 0 5px 0 #6b4e1d, 0 8px 15px rgba(0,0,0,0.4);
|
||||
transition: all 0.08s ease;
|
||||
border: 1px solid #f3e3a7;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(3px);
|
||||
box-shadow: 0 2px 0 #4b3a1a, 0 5px 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #b9d0a0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
.info-panel {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="800" height="600" aria-label="二维射击游戏地图"></canvas>
|
||||
<div class="info-panel">
|
||||
<div class="health-bar">
|
||||
<span style="color:#f1c40f;">❤️ 生命</span>
|
||||
<div class="health-fill">
|
||||
<div id="healthFillLevel" class="fill-level" style="width: 100%;"></div>
|
||||
</div>
|
||||
<span id="healthText" style="min-width: 50px;">100</span>
|
||||
</div>
|
||||
<div class="score-board">
|
||||
🎯 <span id="scoreDisplay">0</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<span>🖱️ 鼠标瞄准 | 左键射击</span>
|
||||
<button id="restartButton">↺ 重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color:#8aa87d; margin-top: 8px; font-size: 0.9rem;">
|
||||
⌨️ WASD / 方向键 移动人物
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI元素
|
||||
const healthFill = document.getElementById('healthFillLevel');
|
||||
const healthText = document.getElementById('healthText');
|
||||
const scoreSpan = document.getElementById('scoreDisplay');
|
||||
|
||||
// 固定尺寸
|
||||
const W = 800, H = 600;
|
||||
|
||||
// ----- 游戏状态 -----
|
||||
let player = {
|
||||
x: W / 2,
|
||||
y: H / 2,
|
||||
radius: 18,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
speed: 4.8,
|
||||
};
|
||||
|
||||
let monsters = [];
|
||||
let bullets = [];
|
||||
let score = 0;
|
||||
|
||||
// 射击冷却 (防止按住左键瞬间连射太夸张)
|
||||
const SHOOT_COOLDOWN_MAX = 18; // 帧数 (约60fps => 0.3秒)
|
||||
let shootCooldown = 0;
|
||||
|
||||
// 怪物生成控制
|
||||
let monsterSpawnTimer = 0;
|
||||
const SPAWN_DELAY = 45; // 大约每0.75秒生成一只 (60fps)
|
||||
const MAX_MONSTERS = 14;
|
||||
|
||||
// 控制标志
|
||||
let keys = {
|
||||
w: false, up: false,
|
||||
s: false, down: false,
|
||||
a: false, left: false,
|
||||
d: false, right: false
|
||||
};
|
||||
|
||||
// 鼠标坐标 (用于瞄准方向)
|
||||
let mouseX = player.x + 30;
|
||||
let mouseY = player.y;
|
||||
let mouseInside = true;
|
||||
|
||||
// 游戏循环控制
|
||||
let gameRunning = true;
|
||||
let animationFrameId = null;
|
||||
|
||||
// ----- 辅助函数:随机范围 -----
|
||||
const rand = (min, max) => Math.random() * (max - min) + min;
|
||||
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
// ----- 重置游戏 -----
|
||||
function restartGame() {
|
||||
player = {
|
||||
x: W / 2,
|
||||
y: H / 2,
|
||||
radius: 18,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
speed: 4.8,
|
||||
};
|
||||
monsters = [];
|
||||
bullets = [];
|
||||
score = 0;
|
||||
shootCooldown = 0;
|
||||
monsterSpawnTimer = 0;
|
||||
gameRunning = true;
|
||||
|
||||
// 重置鼠标位置到玩家附近
|
||||
mouseX = player.x + 20;
|
||||
mouseY = player.y;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// ----- 生成怪物 (在屏幕边缘附近) -----
|
||||
function spawnMonster() {
|
||||
if (monsters.length >= MAX_MONSTERS) return;
|
||||
|
||||
// 随机选择边缘:0=上, 1=下, 2=左, 3=右
|
||||
const edge = randInt(0, 3);
|
||||
let spawnX, spawnY;
|
||||
const padding = 25; // 略微在屏幕外生成,自然走入
|
||||
|
||||
switch (edge) {
|
||||
case 0: // 上
|
||||
spawnX = rand(padding, W - padding);
|
||||
spawnY = -padding;
|
||||
break;
|
||||
case 1: // 下
|
||||
spawnX = rand(padding, W - padding);
|
||||
spawnY = H + padding;
|
||||
break;
|
||||
case 2: // 左
|
||||
spawnX = -padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
break;
|
||||
case 3: // 右
|
||||
spawnX = W + padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
break;
|
||||
default:
|
||||
spawnX = -padding;
|
||||
spawnY = rand(padding, H - padding);
|
||||
}
|
||||
|
||||
// 怪物类型:基础速度 + 大小变化
|
||||
const type = Math.random();
|
||||
let radius, speed, health, color;
|
||||
if (type < 0.6) {
|
||||
// 普通僵尸
|
||||
radius = 16 + rand(0, 6);
|
||||
speed = 1.1 + rand(0, 0.9);
|
||||
health = 1;
|
||||
color = '#6b8c5c';
|
||||
} else if (type < 0.88) {
|
||||
// 快速怪物
|
||||
radius = 13 + rand(0, 5);
|
||||
speed = 2.0 + rand(0, 1.3);
|
||||
health = 1;
|
||||
color = '#b5876a';
|
||||
} else {
|
||||
// 大型怪物 (需要两枪)
|
||||
radius = 22 + rand(0, 7);
|
||||
speed = 0.7 + rand(0, 0.7);
|
||||
health = 2;
|
||||
color = '#8b3a3a';
|
||||
}
|
||||
|
||||
monsters.push({
|
||||
x: spawnX,
|
||||
y: spawnY,
|
||||
radius: radius,
|
||||
speed: speed,
|
||||
health: health,
|
||||
maxHealth: health,
|
||||
color: color,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 玩家移动 (基于键盘) -----
|
||||
function movePlayer() {
|
||||
if (!gameRunning) return;
|
||||
let dx = 0, dy = 0;
|
||||
|
||||
if (keys.w || keys.up) dy -= 1;
|
||||
if (keys.s || keys.down) dy += 1;
|
||||
if (keys.a || keys.left) dx -= 1;
|
||||
if (keys.d || keys.right) dx += 1;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
dx = (dx / len) * player.speed;
|
||||
dy = (dy / len) * player.speed;
|
||||
|
||||
let newX = player.x + dx;
|
||||
let newY = player.y + dy;
|
||||
|
||||
// 边界限制 (考虑玩家半径)
|
||||
const minBound = player.radius;
|
||||
const maxBoundX = W - player.radius;
|
||||
const maxBoundY = H - player.radius;
|
||||
|
||||
newX = Math.max(minBound, Math.min(maxBoundX, newX));
|
||||
newY = Math.max(minBound, Math.min(maxBoundY, newY));
|
||||
|
||||
player.x = newX;
|
||||
player.y = newY;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 怪物移动 (朝向玩家) -----
|
||||
function moveMonsters() {
|
||||
for (let m of monsters) {
|
||||
const dx = player.x - m.x;
|
||||
const dy = player.y - m.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 0.5) continue; // 已重叠
|
||||
|
||||
const moveX = (dx / dist) * m.speed;
|
||||
const moveY = (dy / dist) * m.speed;
|
||||
m.x += moveX;
|
||||
m.y += moveY;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 玩家射击 (生成子弹) -----
|
||||
function shootBullet(targetX, targetY) {
|
||||
if (!gameRunning) return;
|
||||
// 计算方向向量 (从玩家指向鼠标)
|
||||
const dx = targetX - player.x;
|
||||
const dy = targetY - player.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 0.001) return; // 避免零向量
|
||||
|
||||
const normX = dx / len;
|
||||
const normY = dy / len;
|
||||
|
||||
bullets.push({
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vx: normX * 7.5, // 子弹速度
|
||||
vy: normY * 7.5,
|
||||
radius: 4.5,
|
||||
active: true,
|
||||
damage: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 更新子弹位置与碰撞-----
|
||||
function updateBullets() {
|
||||
for (let i = bullets.length - 1; i >= 0; i--) {
|
||||
const b = bullets[i];
|
||||
b.x += b.vx;
|
||||
b.y += b.vy;
|
||||
|
||||
// 边界消除 (超出画布太远)
|
||||
if (b.x < -20 || b.x > W + 20 || b.y < -20 || b.y > H + 20) {
|
||||
bullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let bulletHit = false;
|
||||
|
||||
// 检测子弹与怪物碰撞
|
||||
for (let j = monsters.length - 1; j >= 0; j--) {
|
||||
const m = monsters[j];
|
||||
const d = Math.sqrt((b.x - m.x) ** 2 + (b.y - m.y) ** 2);
|
||||
if (d < m.radius + b.radius) {
|
||||
// 击中怪物
|
||||
m.health -= b.damage;
|
||||
bulletHit = true;
|
||||
|
||||
if (m.health <= 0) {
|
||||
// 击杀怪物,增加分数
|
||||
score += (m.maxHealth === 2 ? 15 : 10);
|
||||
monsters.splice(j, 1);
|
||||
}
|
||||
break; // 一颗子弹只击中一个怪物
|
||||
}
|
||||
}
|
||||
|
||||
if (bulletHit) {
|
||||
bullets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 怪物攻击玩家 (碰撞检测) -----
|
||||
function checkMonsterCollision() {
|
||||
if (!gameRunning) return;
|
||||
for (let i = monsters.length - 1; i >= 0; i--) {
|
||||
const m = monsters[i];
|
||||
const dist = Math.sqrt((m.x - player.x) ** 2 + (m.y - player.y) ** 2);
|
||||
if (dist < player.radius + m.radius) {
|
||||
// 怪物对玩家造成伤害 (每次碰撞扣血,并推开怪物防止连续帧多次伤害)
|
||||
const damageAmount = 8 + (m.maxHealth === 2 ? 4 : 0); // 大型怪物伤害更高
|
||||
player.health -= damageAmount;
|
||||
|
||||
// 推开怪物:沿连线方向移动怪物到刚好不重叠
|
||||
const angle = Math.atan2(m.y - player.y, m.x - player.x);
|
||||
const overlap = player.radius + m.radius - dist;
|
||||
if (overlap > 0) {
|
||||
m.x += Math.cos(angle) * overlap;
|
||||
m.y += Math.sin(angle) * overlap;
|
||||
}
|
||||
|
||||
// 检查玩家死亡
|
||||
if (player.health <= 0) {
|
||||
player.health = 0;
|
||||
gameRunning = false;
|
||||
updateUI();
|
||||
break; // 停止检测其他怪物
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再次确保玩家生命值不超限
|
||||
if (player.health <= 0) {
|
||||
player.health = 0;
|
||||
gameRunning = false;
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// ----- 更新UI元素-----
|
||||
function updateUI() {
|
||||
let healthPercent = Math.max(0, player.health) / player.maxHealth * 100;
|
||||
healthFill.style.width = `${healthPercent}%`;
|
||||
healthText.textContent = `${Math.max(0, player.health)}`;
|
||||
scoreSpan.textContent = score;
|
||||
}
|
||||
|
||||
// ----- 游戏主循环更新-----
|
||||
function update() {
|
||||
if (gameRunning) {
|
||||
// 减少射击冷却
|
||||
if (shootCooldown > 0) shootCooldown--;
|
||||
|
||||
// 移动玩家
|
||||
movePlayer();
|
||||
|
||||
// 怪物移动
|
||||
moveMonsters();
|
||||
|
||||
// 子弹更新与碰撞
|
||||
updateBullets();
|
||||
|
||||
// 怪物攻击玩家
|
||||
checkMonsterCollision();
|
||||
|
||||
// 怪物生成逻辑
|
||||
if (gameRunning) {
|
||||
monsterSpawnTimer--;
|
||||
if (monsterSpawnTimer <= 0 && monsters.length < MAX_MONSTERS) {
|
||||
spawnMonster();
|
||||
monsterSpawnTimer = SPAWN_DELAY + randInt(-8, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查玩家死亡 (可能由碰撞导致)
|
||||
if (player.health <= 0) {
|
||||
gameRunning = false;
|
||||
player.health = 0;
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制图形 (所有视觉元素) -----
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// 绘制网格/地面纹理
|
||||
ctx.strokeStyle = '#3a4a2e';
|
||||
ctx.lineWidth = 0.6;
|
||||
for (let i = 0; i < W; i += 40) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, H);
|
||||
ctx.strokeStyle = '#3a5535';
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let j = 0; j < H; j += 40) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, j);
|
||||
ctx.lineTo(W, j);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ----- 绘制怪物 -----
|
||||
for (let m of monsters) {
|
||||
// 怪物身体
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y, m.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = m.color;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#1a1a1a';
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.stroke();
|
||||
|
||||
// 怪物眼睛 (白色+瞳孔朝向玩家)
|
||||
const eyeOffset = m.radius * 0.5;
|
||||
const angleToPlayer = Math.atan2(player.y - m.y, player.x - m.x);
|
||||
const leftEyeX = m.x + Math.cos(angleToPlayer - 0.5) * eyeOffset;
|
||||
const leftEyeY = m.y + Math.sin(angleToPlayer - 0.5) * eyeOffset;
|
||||
const rightEyeX = m.x + Math.cos(angleToPlayer + 0.5) * eyeOffset;
|
||||
const rightEyeY = m.y + Math.sin(angleToPlayer + 0.5) * eyeOffset;
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(leftEyeX, leftEyeY, m.radius * 0.27, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(rightEyeX, rightEyeY, m.radius * 0.27, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.beginPath();
|
||||
ctx.arc(leftEyeX + Math.cos(angleToPlayer) * 1.8, leftEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(rightEyeX + Math.cos(angleToPlayer) * 1.8, rightEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// 如果生命值>1 显示护甲标记
|
||||
if (m.maxHealth === 2 && m.health === 2) {
|
||||
ctx.fillStyle = '#f0d050';
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
} else if (m.maxHealth === 2 && m.health === 1) {
|
||||
ctx.fillStyle = '#b5773a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 绘制子弹 -----
|
||||
for (let b of bullets) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(b.x, b.y, b.radius, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#f1e05a';
|
||||
ctx.shadowColor = '#ffbb00';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fill();
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = '#b38b20';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ----- 绘制玩家 (英雄) -----
|
||||
ctx.save();
|
||||
ctx.translate(player.x, player.y);
|
||||
// 玩家面向鼠标方向
|
||||
const aimAngle = Math.atan2(mouseY - player.y, mouseX - player.x);
|
||||
ctx.rotate(aimAngle);
|
||||
|
||||
// 身体
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, player.radius, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#3b7bb7';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#102840';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.stroke();
|
||||
|
||||
// 护甲/装备线条
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, player.radius - 3, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#c0daf5';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 武器/枪口
|
||||
ctx.fillStyle = '#4a4a5a';
|
||||
ctx.fillRect(8, -3.5, 15, 7);
|
||||
ctx.fillStyle = '#2b2b35';
|
||||
ctx.fillRect(19, -2.8, 7, 5.6);
|
||||
|
||||
// 眼睛
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, -7, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, 7, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0a14';
|
||||
ctx.beginPath();
|
||||
ctx.arc(7, -7, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(7, 7, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// 玩家血条 (头顶)
|
||||
if (gameRunning) {
|
||||
const healthPercent = player.health / player.maxHealth;
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44, 8);
|
||||
ctx.fillStyle = healthPercent > 0.6 ? '#6f9' : (healthPercent > 0.25 ? '#f4c542' : '#e03a3a');
|
||||
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44 * healthPercent, 8);
|
||||
}
|
||||
|
||||
// 游戏结束画面
|
||||
if (!gameRunning) {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#0f1a0f';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#e0d7b0';
|
||||
ctx.font = 'bold 38px "Segoe UI", Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('💀 你倒下了', W/2, H/2-20);
|
||||
ctx.font = '18px "Segoe UI"';
|
||||
ctx.fillText('点击"重新开始"继续战斗', W/2, H/2+50);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 动画循环 -----
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
animationFrameId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ----- 事件监听 -----
|
||||
function bindEvents() {
|
||||
// 键盘按下
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
// 防止页面滚动 / 默认行为
|
||||
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
|
||||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (key === 'w') keys.w = true;
|
||||
if (key === 'a') keys.a = true;
|
||||
if (key === 's') keys.s = true;
|
||||
if (key === 'd') keys.d = true;
|
||||
if (key === 'arrowup') keys.up = true;
|
||||
if (key === 'arrowdown') keys.down = true;
|
||||
if (key === 'arrowleft') keys.left = true;
|
||||
if (key === 'arrowright') keys.right = true;
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
|
||||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (key === 'w') keys.w = false;
|
||||
if (key === 'a') keys.a = false;
|
||||
if (key === 's') keys.s = false;
|
||||
if (key === 'd') keys.d = false;
|
||||
if (key === 'arrowup') keys.up = false;
|
||||
if (key === 'arrowdown') keys.down = false;
|
||||
if (key === 'arrowleft') keys.left = false;
|
||||
if (key === 'arrowright') keys.right = false;
|
||||
});
|
||||
|
||||
// 鼠标移动 (追踪瞄准位置)
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = W / rect.width; // canvas 物理像素比例
|
||||
const scaleY = H / rect.height;
|
||||
const relativeX = (e.clientX - rect.left) * scaleX;
|
||||
const relativeY = (e.clientY - rect.top) * scaleY;
|
||||
mouseX = Math.min(W, Math.max(0, relativeX));
|
||||
mouseY = Math.min(H, Math.max(0, relativeY));
|
||||
mouseInside = true;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
// 离开画布时瞄准点设为玩家右侧默认方向
|
||||
mouseX = player.x + 30;
|
||||
mouseY = player.y;
|
||||
mouseInside = false;
|
||||
});
|
||||
|
||||
// 射击:鼠标按下 (左键)
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
if (e.button === 0) { // 左键
|
||||
if (gameRunning && shootCooldown <= 0) {
|
||||
shootBullet(mouseX, mouseY);
|
||||
shootCooldown = SHOOT_COOLDOWN_MAX;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 防止右键菜单弹出
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
// 重新开始按钮
|
||||
document.getElementById('restartButton').addEventListener('click', () => {
|
||||
restartGame();
|
||||
});
|
||||
|
||||
// 窗口失去焦点时重置按键状态 (防止卡方向)
|
||||
window.addEventListener('blur', () => {
|
||||
keys.w = keys.a = keys.s = keys.d = false;
|
||||
keys.up = keys.down = keys.left = keys.right = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 启动一切
|
||||
bindEvents();
|
||||
restartGame(); // 初始状态
|
||||
gameLoop();
|
||||
|
||||
// 清理动画 (非必须,但良好实践)
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user