🎮ArcadeLab

血染红旗 · 测试场地

by EpicCoder88
922 lines25.2 KB
▶ Play
<!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.