血染红旗 · 测试场地
by EpicCoder88922 lines25.2 KB
<!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; }
html, body {
height: 100%;
background: #0d0a08;
overflow: hidden;
font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.stage {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
}
canvas {
width: min(100vw, 177.78vh);
height: auto;
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
touch-action: none;
cursor: crosshair;
}
.hud-toggle {
position: fixed; top: 14px; right: 14px;
z-index: 10;
padding: 8px 16px;
border-radius: 6px;
border: 1px solid rgba(160, 70, 50, 0.4);
background: rgba(30, 15, 10, 0.75);
color: #c8a890;
font-size: 12px;
letter-spacing: 0.1em;
cursor: pointer;
backdrop-filter: blur(6px);
font-family: inherit;
transition: all 0.15s;
}
.hud-toggle:hover {
background: rgba(120, 30, 20, 0.7);
color: #fff;
}
</style>
</head>
<body>
<div class="stage">
<canvas id="game" width="960" height="540"></canvas>
</div>
<button class="hud-toggle" id="hudToggle">隐藏数据</button>
<script>
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 960, H = 540;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.scale(dpr, dpr);
const hudToggle = document.getElementById('hudToggle');
let showHUD = true;
hudToggle.addEventListener('click', () => {
showHUD = !showHUD;
hudToggle.textContent = showHUD ? '隐藏数据' : '显示数据';
});
// ================================================================
// 配置(所有可调参数集中在这里)
// ================================================================
const CFG = {
player: {
speed: 2.6,
w: 18, h: 24,
hp: 100,
radius: 10, // 碰撞半径(用于绘制参考圆)
},
gun: {
magSize: 5,
reloadTime: 48, // 帧
fireCooldown: 30, // 帧
bulletSpeed: 12,
bulletDamage: 60,
bulletLife: 50,
spread: 0, // 散布角度(弧度),0 = 精准
},
zombie: {
speed: 0.95,
w: 18, h: 22,
hp: 60,
damage: 12,
attackRange: 22,
attackCooldown: 36,
},
debug: {
showFPS: true,
showCoords: true,
showCollision: false,
showHitbox: false,
showAimLine: true,
showGrid: false,
}
};
// ================================================================
// 状态
// ================================================================
let player, bullets, particles, zombies, targets;
let keys = {};
let mouse = { x: W / 2, y: H / 2, down: false };
let frame = 0, shake = 0;
let fps = 60, fpsAcc = 0, fpsCount = 0, fpsTimer = 0;
// 测试场地障碍物
const obstacles = [
// 边界墙(视觉参考)
{ x: 0, y: 0, w: W, h: 2, type: 'wall' },
{ x: 0, y: H - 2, w: W, h: 2, type: 'wall' },
{ x: 0, y: 0, w: 2, h: H, type: 'wall' },
{ x: W - 2, y: 0, w: 2, h: H, type: 'wall' },
// 测试用掩体
{ x: 200, y: 120, w: 80, h: 30, type: 'cover' },
{ x: 680, y: 120, w: 80, h: 30, type: 'cover' },
{ x: 200, y: 390, w: 80, h: 30, type: 'cover' },
{ x: 680, y: 390, w: 80, h: 30, type: 'cover' },
{ x: 440, y: 240, w: 80, h: 60, type: 'cover' },
{ x: 100, y: 240, w: 30, h: 60, type: 'cover' },
{ x: 830, y: 240, w: 30, h: 60, type: 'cover' },
// 柱子
{ x: 320, y: 180, w: 20, h: 20, type: 'pillar' },
{ x: 620, y: 180, w: 20, h: 20, type: 'pillar' },
{ x: 320, y: 340, w: 20, h: 20, type: 'pillar' },
{ x: 620, y: 340, w: 20, h: 20, type: 'pillar' },
];
// ================================================================
// 工具
// ================================================================
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(a, b) { return a + Math.random() * (b - a); }
function dist(ax, ay, bx, by) {
const dx = ax - bx, dy = ay - by;
return Math.sqrt(dx * dx + dy * dy);
}
function rectsOverlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function roundRect(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function burst(x, y, color, n, power, size) {
power = power || 4; size = size || 2;
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 0.6 + Math.random() * power;
particles.push({
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 0.8,
life: 1,
decay: 0.018 + Math.random() * 0.025,
color,
size: size * (0.6 + Math.random() * 0.8)
});
}
}
// ================================================================
// 初始化
// ================================================================
function initPlayer() {
player = {
x: W / 2, y: H / 2,
w: CFG.player.w, h: CFG.player.h,
vx: 0, vy: 0,
hp: CFG.player.hp,
maxHp: CFG.player.hp,
ammo: CFG.gun.magSize,
reloading: false,
reloadTimer: 0,
fireCooldown: 0,
facing: -Math.PI / 2,
hurtTimer: 0,
};
}
function initZombies() {
zombies = [];
const spawns = [
{ x: 100, y: 100 }, { x: 860, y: 100 },
{ x: 100, y: 440 }, { x: 860, y: 440 },
{ x: 480, y: 80 },
];
for (const s of spawns) {
zombies.push({
x: s.x, y: s.y,
w: CFG.zombie.w, h: CFG.zombie.h,
vx: 0, vy: 0,
hp: CFG.zombie.hp, maxHp: CFG.zombie.hp,
attackTimer: 0,
hitTimer: 0,
wanderAngle: Math.random() * Math.PI * 2,
wanderTimer: 0,
});
}
}
function initTargets() {
targets = [];
// 静止靶子,用来测试射击
const positions = [
{ x: 150, y: 180 }, { x: 810, y: 180 },
{ x: 150, y: 360 }, { x: 810, y: 360 },
{ x: 480, y: 150 }, { x: 480, y: 420 },
];
for (const p of positions) {
targets.push({ x: p.x, y: p.y, r: 14, hp: 1, hit: false, hitTimer: 0 });
}
}
function init() {
initPlayer();
initZombies();
initTargets();
bullets = [];
particles = [];
frame = 0;
shake = 0;
}
// ================================================================
// 射击
// ================================================================
function tryFire() {
if (player.reloading || player.fireCooldown > 0) return;
if (player.ammo <= 0) { startReload(); return; }
player.ammo--;
player.fireCooldown = CFG.gun.fireCooldown;
const a = player.facing + (Math.random() - 0.5) * CFG.gun.spread;
const sx = player.x + Math.cos(a) * 16;
const sy = player.y + Math.sin(a) * 16;
bullets.push({
x: sx, y: sy,
vx: Math.cos(a) * CFG.gun.bulletSpeed,
vy: Math.sin(a) * CFG.gun.bulletSpeed,
life: CFG.gun.bulletLife,
});
burst(sx, sy, '#ffcc66', 4, 2.5, 1.6);
shake = Math.max(shake, 3);
}
function startReload() {
if (player.reloading || player.ammo === CFG.gun.magSize) return;
player.reloading = true;
player.reloadTimer = CFG.gun.reloadTime;
}
function finishReload() {
player.reloading = false;
player.ammo = CFG.gun.magSize;
}
// ================================================================
// 更新
// ================================================================
function update() {
frame++;
// FPS 计算
fpsAcc += 1;
fpsTimer++;
if (fpsTimer >= 30) {
fps = Math.round(fpsAcc / (fpsTimer / 60));
fpsAcc = 0;
fpsTimer = 0;
}
if (shake > 0.4) shake *= 0.86; else shake = 0;
// 粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.vy += 0.12; p.vx *= 0.97;
p.x += p.vx; p.y += p.vy;
p.life -= p.decay;
if (p.life <= 0) particles.splice(i, 1);
}
// 移动
const left = keys['ArrowLeft'] || keys['KeyA'];
const right = keys['ArrowRight'] || keys['KeyD'];
const up = keys['ArrowUp'] || keys['KeyW'];
const down = keys['ArrowDown'] || keys['KeyS'];
let mx = 0, my = 0;
if (left) mx -= 1;
if (right) mx += 1;
if (up) my -= 1;
if (down) my += 1;
if (mx !== 0 && my !== 0) { mx *= 0.7071; my *= 0.7071; }
player.vx = mx * CFG.player.speed;
player.vy = my * CFG.player.speed;
// X 轴碰撞
player.x += player.vx;
for (const o of obstacles) {
if (o.type === 'wall') continue;
if (rectsOverlap(player, o)) {
if (player.vx > 0) player.x = o.x - player.w;
else if (player.vx < 0) player.x = o.x + o.w;
}
}
player.x = clamp(player.x, player.w / 2, W - player.w / 2);
// Y 轴碰撞
player.y += player.vy;
for (const o of obstacles) {
if (o.type === 'wall') continue;
if (rectsOverlap(player, o)) {
if (player.vy > 0) player.y = o.y - player.h;
else if (player.vy < 0) player.y = o.y + o.h;
}
}
player.y = clamp(player.y, player.h / 2, H - player.h / 2);
// 瞄准
player.facing = Math.atan2(mouse.y - player.y, mouse.x - player.x);
// 射击
if (player.fireCooldown > 0) player.fireCooldown--;
if (player.hurtTimer > 0) player.hurtTimer--;
if (player.reloading) {
player.reloadTimer--;
if (player.reloadTimer <= 0) finishReload();
}
if (mouse.down) tryFire();
// 子弹
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx; b.y += b.vy; b.life--;
let hit = false;
// 撞墙
for (const o of obstacles) {
if (o.type === 'wall') continue;
if (b.x > o.x && b.x < o.x + o.w && b.y > o.y && b.y < o.y + o.h) {
hit = true;
break;
}
}
// 撞僵尸
if (!hit) {
for (const z of zombies) {
if (dist(b.x, b.y, z.x, z.y) < 14) {
z.hp -= CFG.gun.bulletDamage;
z.hitTimer = 8;
burst(z.x, z.y, '#8a1010', 5, 2.5, 2);
hit = true;
if (z.hp <= 0) {
burst(z.x, z.y, '#6a0808', 14, 4, 2.5);
const idx = zombies.indexOf(z);
if (idx >= 0) zombies.splice(idx, 1);
}
break;
}
}
}
// 撞靶子
if (!hit) {
for (const t of targets) {
if (t.hit) continue;
if (dist(b.x, b.y, t.x, t.y) < t.r) {
t.hit = true;
t.hitTimer = 30;
burst(t.x, t.y, '#ffd93d', 10, 3, 2);
hit = true;
break;
}
}
}
if (hit || b.life <= 0 || b.x < 0 || b.x > W || b.y < 0 || b.y > H) {
if (hit) burst(b.x, b.y, '#ffaa55', 3, 2, 1.4);
bullets.splice(i, 1);
}
}
// 靶子恢复
for (const t of targets) {
if (t.hitTimer > 0) {
t.hitTimer--;
if (t.hitTimer <= 0) t.hit = false;
}
}
// 僵尸 AI
for (const z of zombies) {
if (z.hitTimer > 0) z.hitTimer--;
const dx = player.x - z.x;
const dy = player.y - z.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
// 游荡
z.wanderTimer--;
if (z.wanderTimer <= 0) {
z.wanderTimer = 40 + Math.random() * 60;
z.wanderAngle = Math.random() * Math.PI * 2;
}
const wx = Math.cos(z.wanderAngle) * 0.2;
const wy = Math.sin(z.wanderAngle) * 0.2;
// 如果距离远,加一点游荡;如果近,直冲
const chase = d < 300 ? 1 : 0.6;
z.vx = (dx / d) * CFG.zombie.speed * chase + wx;
z.vy = (dy / d) * CFG.zombie.speed * chase + wy;
// 碰撞
z.x += z.vx;
for (const o of obstacles) {
if (o.type === 'wall') continue;
if (rectsOverlap(z, o)) {
if (z.vx > 0) z.x = o.x - z.w;
else if (z.vx < 0) z.x = o.x + o.w;
}
}
z.x = clamp(z.x, z.w / 2, W - z.w / 2);
z.y += z.vy;
for (const o of obstacles) {
if (o.type === 'wall') continue;
if (rectsOverlap(z, o)) {
if (z.vy > 0) z.y = o.y - z.h;
else if (z.vy < 0) z.y = o.y + o.h;
}
}
z.y = clamp(z.y, z.h / 2, H - z.h / 2);
// 攻击玩家
z.attackTimer--;
if (d < CFG.zombie.attackRange && z.attackTimer <= 0) {
z.attackTimer = CFG.zombie.attackCooldown;
player.hp = Math.max(0, player.hp - CFG.zombie.damage);
player.hurtTimer = 12;
shake = Math.max(shake, 6);
burst(player.x, player.y, '#ff3b3b', 6, 2.5, 2);
}
}
// 玩家死亡自动复活
if (player.hp <= 0) {
player.hp = player.maxHp;
player.x = W / 2;
player.y = H / 2;
burst(player.x, player.y, '#d83020', 20, 5, 3);
}
}
// ================================================================
// 绘制
// ================================================================
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#0d0a08');
g.addColorStop(0.5, '#14100d');
g.addColorStop(1, '#0d0a08');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
if (CFG.debug.showGrid) {
ctx.strokeStyle = 'rgba(80, 50, 40, 0.15)';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
}
}
function drawObstacles() {
for (const o of obstacles) {
if (o.type === 'wall') {
// 边界墙
ctx.fillStyle = 'rgba(160, 70, 50, 0.25)';
ctx.fillRect(o.x, o.y, o.w, o.h);
continue;
}
// 掩体
const g = ctx.createLinearGradient(0, o.y, 0, o.y + o.h);
g.addColorStop(0, '#3a2820');
g.addColorStop(1, '#221610');
ctx.fillStyle = g;
roundRect(o.x, o.y, o.w, o.h, 4);
ctx.fill();
// 顶部高光
ctx.fillStyle = 'rgba(120, 80, 60, 0.4)';
roundRect(o.x + 2, o.y + 2, o.w - 4, 3, 2);
ctx.fill();
// 描边
ctx.strokeStyle = 'rgba(100, 50, 35, 0.6)';
ctx.lineWidth = 1;
roundRect(o.x, o.y, o.w, o.h, 4);
ctx.stroke();
// 碰撞箱
if (CFG.debug.showCollision) {
ctx.strokeStyle = 'rgba(0, 255, 100, 0.4)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.strokeRect(o.x, o.y, o.w, o.h);
ctx.setLineDash([]);
}
}
}
function drawTargets() {
for (const t of targets) {
ctx.save();
ctx.translate(t.x, t.y);
if (t.hit) {
// 命中状态
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#ffd93d';
ctx.beginPath();
ctx.arc(0, 0, t.r, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = '#ffd93d';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, t.r + 4, 0, Math.PI * 2);
ctx.stroke();
} else {
// 正常靶子
ctx.fillStyle = '#2a1a14';
ctx.beginPath();
ctx.arc(0, 0, t.r, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#8a5030';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, t.r - 2, 0, Math.PI * 2);
ctx.stroke();
// 靶心
ctx.fillStyle = '#d83020';
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
function drawPlayer() {
const { x, y, w, h } = player;
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(x, y + 14, 12, 5, 0, 0, Math.PI * 2);
ctx.fill();
// 受伤闪烁
const hurt = player.hurtTimer > 0 && Math.floor(player.hurtTimer / 3) % 2 === 0;
ctx.save();
if (hurt) ctx.globalAlpha = 0.5;
// 身体
ctx.fillStyle = '#5a6a4a';
roundRect(x - w / 2, y - h / 2, w, h, 5);
ctx.fill();
// 军帽
ctx.fillStyle = '#3d4a30';
roundRect(x - 10, y - 16, 20, 8, 3);
ctx.fill();
// 红星
ctx.fillStyle = '#d02020';
ctx.beginPath();
ctx.arc(x, y - 12, 2.5, 0, Math.PI * 2);
ctx.fill();
// 枪
ctx.save();
ctx.translate(x, y);
ctx.rotate(player.facing);
ctx.fillStyle = '#3a2818';
ctx.fillRect(6, -2, 22, 4);
ctx.fillStyle = '#6a4a28';
ctx.fillRect(6, -1.5, 8, 3);
ctx.restore();
ctx.restore();
// 瞄准线
if (CFG.debug.showAimLine) {
ctx.save();
ctx.globalAlpha = 0.2;
ctx.strokeStyle = '#ffcc66';
ctx.lineWidth = 1;
ctx.setLineDash([6, 8]);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.restore();
}
// 碰撞圆
if (CFG.debug.showHitbox) {
ctx.save();
ctx.strokeStyle = 'rgba(0, 255, 100, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(x, y, CFG.player.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
ctx.strokeRect(x - w / 2, y - h / 2, w, h);
ctx.restore();
}
// 血条
const hpR = player.hp / player.maxHp;
const bw = 36, bh = 4;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
roundRect(x - bw / 2, y - 28, bw, bh, 2);
ctx.fill();
ctx.fillStyle = hpR > 0.5 ? '#50c050' : hpR > 0.25 ? '#d0a020' : '#d03030';
roundRect(x - bw / 2, y - 28, bw * hpR, bh, 2);
ctx.fill();
// 换弹环
if (player.reloading) {
const r = 1 - player.reloadTimer / CFG.gun.reloadTime;
ctx.strokeStyle = '#ffcc66';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, 24, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * r);
ctx.stroke();
}
}
function drawZombies() {
for (const z of zombies) {
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.ellipse(z.x, z.y + 12, 10, 4, 0, 0, Math.PI * 2);
ctx.fill();
const hit = z.hitTimer > 0;
ctx.save();
if (hit) ctx.globalAlpha = 0.6;
// 身体
ctx.fillStyle = '#4a5a3a';
roundRect(z.x - z.w / 2, z.y - z.h / 2, z.w, z.h, 4);
ctx.fill();
// 头
ctx.fillStyle = '#5a6a4a';
ctx.beginPath();
ctx.arc(z.x, z.y - z.h / 2 - 2, 7, 0, Math.PI * 2);
ctx.fill();
// 红眼
ctx.fillStyle = '#ff2020';
ctx.beginPath();
ctx.arc(z.x - 2.5, z.y - z.h / 2 - 3, 1.8, 0, Math.PI * 2);
ctx.arc(z.x + 2.5, z.y - z.h / 2 - 3, 1.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 血条
if (z.hp < z.maxHp) {
const hr = z.hp / z.maxHp;
const bw = 26, bh = 3;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(z.x - bw / 2, z.y - z.h / 2 - 14, bw, bh);
ctx.fillStyle = '#d03030';
ctx.fillRect(z.x - bw / 2, z.y - z.h / 2 - 14, bw * hr, bh);
}
// 碰撞箱
if (CFG.debug.showHitbox) {
ctx.strokeStyle = 'rgba(255, 100, 100, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(z.x - z.w / 2, z.y - z.h / 2, z.w, z.h);
}
}
}
function drawBullets() {
for (const b of bullets) {
ctx.save();
ctx.shadowColor = '#ffcc66';
ctx.shadowBlur = 8;
ctx.fillStyle = '#ffe080';
ctx.beginPath();
ctx.arc(b.x, b.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 拖尾
ctx.strokeStyle = 'rgba(255, 200, 100, 0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(b.x, b.y);
ctx.lineTo(b.x - b.vx * 1.5, b.y - b.vy * 1.5);
ctx.stroke();
}
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawHUD() {
if (!showHUD) return;
ctx.save();
ctx.textBaseline = 'middle';
// 左上:帧率 + 坐标
ctx.fillStyle = 'rgba(200, 180, 160, 0.7)';
ctx.font = 'bold 13px monospace';
ctx.textAlign = 'left';
let y = 20;
if (CFG.debug.showFPS) {
ctx.fillText(`FPS: ${fps}`, 16, y);
y += 18;
}
if (CFG.debug.showCoords) {
ctx.fillText(`P: (${Math.round(player.x)}, ${Math.round(player.y)})`, 16, y);
y += 18;
ctx.fillText(`A: ${player.facing.toFixed(2)} rad`, 16, y);
y += 18;
ctx.fillText(`M: (${Math.round(mouse.x)}, ${Math.round(mouse.y)})`, 16, y);
y += 18;
}
// 子弹 / 换弹
ctx.fillStyle = player.reloading ? '#ffcc66' : player.ammo === 0 ? '#ff5050' : '#e8d8c0';
ctx.font = 'bold 16px monospace';
ctx.fillText(
player.reloading ? `换弹中 ${Math.ceil((1 - player.reloadTimer / CFG.gun.reloadTime) * 100)}%` : `弹药 ${player.ammo} / ${CFG.gun.magSize}`,
16, y + 4
);
// 右上:实体数量
ctx.textAlign = 'right';
ctx.fillStyle = 'rgba(200, 180, 160, 0.5)';
ctx.font = '13px monospace';
ctx.fillText(`走尸: ${zombies.length}`, W - 16, 20);
ctx.fillText(`子弹: ${bullets.length}`, W - 16, 38);
ctx.fillText(`粒子: ${particles.length}`, W - 16, 56);
// 底部:操作提示
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(200, 180, 160, 0.35)';
ctx.font = '13px system-ui, sans-serif';
ctx.fillText('WASD 移动 · 鼠标瞄准 · 左键射击 · R 装填 · F 切换调试', W / 2, H - 20);
ctx.restore();
}
function render() {
ctx.save();
ctx.clearRect(0, 0, W, H);
if (shake > 0.4) {
ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
}
drawBackground();
drawObstacles();
drawTargets();
drawZombies();
drawBullets();
drawPlayer();
drawParticles();
drawHUD();
ctx.restore();
}
// ================================================================
// 主循环
// ================================================================
const STEP = 1000 / 60;
let lastTime = performance.now();
let acc = 0;
function loop(now) {
requestAnimationFrame(loop);
let delta = now - lastTime;
lastTime = now;
if (delta > 200) delta = 200;
acc += delta;
let guard = 0;
while (acc >= STEP && guard++ < 5) {
update();
acc -= STEP;
}
render();
}
// ================================================================
// 输入
// ================================================================
window.addEventListener('keydown', e => {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();
if (!e.repeat) {
if (e.code === 'KeyR') startReload();
if (e.code === 'KeyF') {
// 循环调试显示
const modes = ['showHitbox', 'showCollision', 'showGrid'];
const names = ['碰撞箱', '碰撞范围', '网格'];
// 简单轮流切换
if (CFG.debug.showHitbox) { CFG.debug.showHitbox = false; CFG.debug.showCollision = true; }
else if (CFG.debug.showCollision) { CFG.debug.showCollision = false; CFG.debug.showGrid = true; }
else if (CFG.debug.showGrid) { CFG.debug.showGrid = false; }
else { CFG.debug.showHitbox = true; }
}
}
keys[e.code] = true;
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
mouse.x = (e.clientX - r.left) * (W / r.width);
mouse.y = (e.clientY - r.top) * (H / r.height);
});
canvas.addEventListener('mousedown', e => {
if (e.button === 0) mouse.down = true;
});
window.addEventListener('mouseup', () => { mouse.down = false; });
canvas.addEventListener('contextmenu', e => e.preventDefault());
// 触屏
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
canvas.addEventListener('touchstart', e => {
const t = e.touches[0];
const r = canvas.getBoundingClientRect();
mouse.x = (t.clientX - r.left) * (W / r.width);
mouse.y = (t.clientY - r.top) * (H / r.height);
mouse.down = true;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchmove', e => {
const t = e.touches[0];
const r = canvas.getBoundingClientRect();
mouse.x = (t.clientX - r.left) * (W / r.width);
mouse.y = (t.clientY - r.top) * (H / r.height);
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchend', e => {
mouse.down = false;
e.preventDefault();
}, { passive: false });
}
// ================================================================
// 启动
// ================================================================
init();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 血染红旗 · 测试场地
Creator: EpicCoder88
Libraries: none
Complexity: complex (922 lines, 25.2 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-epiccoder88-murvtdw0" to link back to the original. Then publish at arcadelab.ai/publish.