Files
blackbird/fsys/public/games/game6.html
T
2026-08-08 19:09:36 +08:00

740 lines
21 KiB
HTML

<!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>