🎮ArcadeLab

血染红旗

by EpicCoder88
1065 lines31.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>
  html, body {
    margin: 0; height: 100%;
    background: #0a0806;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
  }
  canvas {
    width: min(97vw, 175vh);
    height: auto;
    display: block;
    border-radius: 10px;
    box-shadow: 0 0 90px rgba(180, 30, 30, 0.3), 0 0 0 1px rgba(120, 40, 40, 0.3);
    touch-action: none;
  }
  #touch {
    position: fixed; inset: 0;
    pointer-events: none;
    display: none;
  }
  #touch button {
    pointer-events: auto;
    position: absolute;
    bottom: 24px;
    width: 76px; height: 76px;
    border-radius: 50%;
    border: 2px solid rgba(200, 80, 60, 0.5);
    background: rgba(80, 20, 20, 0.5);
    color: #ffd0b0;
    font-size: 24px;
    backdrop-filter: blur(6px);
  }
  #touch button:active { background: rgba(180, 40, 30, 0.7); }
  #btnL { left: 20px; }
  #btnR { left: 114px; }
  #btnF { right: 20px; }
  #btnRl { right: 114px; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="touch">
  <button id="btnL">◀</button>
  <button id="btnR">▶</button>
  <button id="btnRl">R</button>
  <button id="btnF">开火</button>
</div>
<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);

  // ==================== 音效 ====================
  let audioCtx = null;
  function initAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  }
  function tone(freq, dur, type = 'square', vol = 0.05, slideTo = null) {
    if (!audioCtx) return;
    const t = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, t);
    if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
    gain.gain.setValueAtTime(0.0001, t);
    gain.gain.exponentialRampToValueAtTime(vol, t + 0.008);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(t);
    osc.stop(t + dur + 0.03);
  }
  function noiseBurst(dur, vol) {
    if (!audioCtx) return;
    const bufferSize = audioCtx.sampleRate * dur;
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
    const source = audioCtx.createBufferSource();
    source.buffer = buffer;
    const gain = audioCtx.createGain();
    gain.gain.value = vol;
    source.connect(gain);
    gain.connect(audioCtx.destination);
    source.start();
  }

  // ==================== 常量 ====================
  const FLAG = { x: 480, y: 270, r: 26, hp: 100, maxHp: 100 };
  const PLAYER_SPEED = 2.6;
  const MAG_SIZE = 5;
  const RELOAD_TIME = 48;
  const FIRE_COOLDOWN = 30;
  const BULLET_SPEED = 12;
  const BULLET_DAMAGE = 60;

  // ==================== 状态 ====================
  let player, zombies, bullets, particles, bloodStains, pickups;
  let wave, waveTimer, enemiesToSpawn, spawnTimer, state, frame, shake;
  let score, kills, message, messageTimer;
  const keys = {};
  let mouse = { x: W / 2, y: H / 2, down: false };

  // ==================== 地图障碍 ====================
  const obstacles = [
    { x: 160, y: 120, w: 60, h: 60 },
    { x: 740, y: 120, w: 60, h: 60 },
    { x: 160, y: 360, w: 60, h: 60 },
    { x: 740, y: 360, w: 60, h: 60 },
    { x: 420, y: 80, w: 120, h: 40 },
    { x: 420, y: 420, w: 120, h: 40 },
    { x: 80, y: 240, w: 40, h: 80 },
    { x: 840, y: 240, w: 40, h: 80 },
  ];

  // ==================== 工具 ====================
  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 dist(ax, ay, bx, by) {
    const dx = ax - bx, dy = ay - by;
    return Math.sqrt(dx * dx + dy * dy);
  }
  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  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 = 4, 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 resetPlayer() {
    player = {
      x: FLAG.x, y: FLAG.y + 80,
      w: 18, h: 24,
      vx: 0, vy: 0,
      hp: 100, maxHp: 100,
      ammo: MAG_SIZE,
      reloading: false,
      reloadTimer: 0,
      fireCooldown: 0,
      facing: -Math.PI / 2,
      hurtTimer: 0,
      down: false,
      downTimer: 0,
    };
  }

  function startWave(n) {
    wave = n;
    // 每波敌人数量递增
    enemiesToSpawn = 3 + n * 2;
    spawnTimer = 0;
    // 每 3 波出一个重装僵尸
    message = n === 1 ? '第一波 · 守住军旗' : `第 ${n} 波`;
    messageTimer = 100;
  }

  function init() {
    zombies = [];
    bullets = [];
    particles = [];
    bloodStains = [];
    pickups = [];
    frame = 0;
    shake = 0;
    score = 0;
    kills = 0;
    state = 'playing';
    message = '湘江之后 · 血染红旗';
    messageTimer = 160;
    resetPlayer();
    FLAG.hp = FLAG.maxHp;
    startWave(1);
  }

  // ==================== 生成僵尸 ====================
  function spawnZombie() {
    const side = Math.floor(Math.random() * 4);
    let x, y;
    const m = 40;
    if (side === 0) { x = Math.random() * W; y = -m; }
    else if (side === 1) { x = W + m; y = Math.random() * H; }
    else if (side === 2) { x = Math.random() * W; y = H + m; }
    else { x = -m; y = Math.random() * H; }

    // 类型:0普通 1冲锋 2重装
    let type = 0;
    const r = Math.random();
    if (wave >= 3 && r < 0.2) type = 2;
    else if (wave >= 2 && r < 0.45) type = 1;

    const base = {
      x, y, w: 18, h: 22,
      vx: 0, vy: 0,
      type,
      hp: type === 2 ? 160 : 60,
      maxHp: type === 2 ? 160 : 60,
      speed: type === 1 ? 1.5 : type === 2 ? 0.65 : 0.95,
      damage: type === 2 ? 22 : 12,
      attackTimer: 0,
      hitTimer: 0,
      wanderAngle: Math.random() * Math.PI * 2,
      wanderTimer: 0,
    };
    zombies.push(base);
    enemiesToSpawn--;
  }

  // ==================== 玩家射击 ====================
  function tryFire() {
    if (player.down || player.reloading || player.fireCooldown > 0) return;
    if (player.ammo <= 0) {
      startReload();
      return;
    }
    player.ammo--;
    player.fireCooldown = FIRE_COOLDOWN;
    const a = player.facing;
    const sx = player.x + Math.cos(a) * 18;
    const sy = player.y + Math.sin(a) * 18;
    bullets.push({
      x: sx, y: sy,
      vx: Math.cos(a) * BULLET_SPEED,
      vy: Math.sin(a) * BULLET_SPEED,
      life: 50,
    });
    // 枪口火焰
    burst(sx, sy, '#ffcc66', 4, 2.5, 1.6);
    tone(180, 0.08, 'sawtooth', 0.08, 60);
    noiseBurst(0.06, 0.04);
    shake = Math.max(shake, 5);

    if (player.ammo <= 0) startReload();
  }

  function startReload() {
    if (player.reloading || player.ammo === MAG_SIZE) return;
    player.reloading = true;
    player.reloadTimer = RELOAD_TIME;
    tone(320, 0.06, 'square', 0.03);
  }

  function finishReload() {
    player.reloading = false;
    player.ammo = MAG_SIZE;
    tone(480, 0.08, 'square', 0.04, 640);
  }

  // ==================== 伤害 ====================
  function damagePlayer(amount) {
    if (player.down) return;
    player.hp -= amount;
    player.hurtTimer = 12;
    shake = Math.max(shake, 6);
    burst(player.x, player.y, '#ff3b3b', 8, 3, 2);
    tone(160, 0.12, 'sawtooth', 0.06, 60);
    if (player.hp <= 0) {
      player.hp = 0;
      player.down = true;
      player.downTimer = 180; // 3秒倒地
      burst(player.x, player.y, '#8a1010', 18, 4, 3);
    }
  }

  function damageZombie(z, amount) {
    z.hp -= amount;
    z.hitTimer = 8;
    burst(z.x, z.y, '#8a1010', 5, 2.5, 2);
    if (z.hp <= 0) {
      kills++;
      score += z.type === 2 ? 30 : z.type === 1 ? 15 : 10;
      burst(z.x, z.y, '#6a0808', 14, 4, 2.5);
      bloodStains.push({ x: z.x, y: z.y, r: 12 + Math.random() * 10, a: 0.5 });
      tone(90, 0.14, 'sawtooth', 0.05, 40);
      const idx = zombies.indexOf(z);
      if (idx >= 0) zombies.splice(idx, 1);
    }
  }

  // ==================== 更新 ====================
  function update() {
    frame++;
    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);
    }

    // 血迹渐隐
    for (let i = bloodStains.length - 1; i >= 0; i--) {
      bloodStains[i].a -= 0.0006;
      if (bloodStains[i].a <= 0) bloodStains.splice(i, 1);
    }

    if (messageTimer > 0) messageTimer--;

    if (state !== 'playing') return;

    // 玩家倒地处理
    if (player.down) {
      player.downTimer--;
      if (player.downTimer <= 0) {
        state = 'lost';
        message = '军旗未倒 · 战士已逝';
        messageTimer = 99999;
      }
      return;
    }

    // ===== 玩家移动 =====
    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 * PLAYER_SPEED;
    player.vy = my * PLAYER_SPEED;

    // X 轴碰撞
    player.x += player.vx;
    for (const o of obstacles) 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 (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);

    // ===== 瞄准 =====
    const pdx = mouse.x - player.x;
    const pdy = mouse.y - player.y;
    player.facing = Math.atan2(pdy, pdx);

    // 射击冷却
    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 (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) < 16) {
            damageZombie(z, BULLET_DAMAGE);
            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);
      }
    }

    // ===== 僵尸 AI =====
    for (const z of zombies) {
      if (z.hitTimer > 0) z.hitTimer--;

      // 目标选择:优先玩家(如果玩家活着且在附近),否则军旗
      let tx = FLAG.x, ty = FLAG.y;
      let targetIsPlayer = false;
      if (!player.down) {
        const dPlayer = dist(z.x, z.y, player.x, player.y);
        const dFlag = dist(z.x, z.y, FLAG.x, FLAG.y);
        if (dPlayer < 180 || dPlayer < dFlag) {
          tx = player.x; ty = player.y;
          targetIsPlayer = true;
        }
      }

      const dx = tx - z.x, dy = ty - z.y;
      const d = Math.sqrt(dx * dx + dy * dy) || 1;
      // 加入一点游荡,避免完全直线
      z.wanderTimer--;
      if (z.wanderTimer <= 0) {
        z.wanderTimer = 30 + Math.random() * 40;
        z.wanderAngle = Math.random() * Math.PI * 2;
      }
      const wanderX = Math.cos(z.wanderAngle) * 0.3;
      const wanderY = Math.sin(z.wanderAngle) * 0.3;

      z.vx = (dx / d) * z.speed + wanderX;
      z.vy = (dy / d) * z.speed + wanderY;

      // 碰撞
      z.x += z.vx;
      for (const o of obstacles) 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.y += z.vy;
      for (const o of obstacles) 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.attackTimer--;
      if (targetIsPlayer && dist(z.x, z.y, player.x, player.y) < 22) {
        if (z.attackTimer <= 0) {
          z.attackTimer = 36;
          damagePlayer(z.damage);
        }
      } else if (!targetIsPlayer && dist(z.x, z.y, FLAG.x, FLAG.y) < 30) {
        if (z.attackTimer <= 0) {
          z.attackTimer = 50;
          FLAG.hp -= z.damage * 0.6;
          burst(FLAG.x, FLAG.y, '#cc2222', 6, 2.5, 1.8);
          tone(100, 0.1, 'sawtooth', 0.04, 50);
          if (FLAG.hp <= 0) {
            FLAG.hp = 0;
            state = 'lost';
            message = '军旗倒下 · 阵地失守';
            messageTimer = 99999;
            burst(FLAG.x, FLAG.y, '#ff2a2a', 30, 6, 3);
          }
        }
      }
    }

    // ===== 波次逻辑 =====
    if (enemiesToSpawn > 0) {
      spawnTimer--;
      if (spawnTimer <= 0) {
        spawnZombie();
        spawnTimer = Math.max(14, 40 - wave * 2);
      }
    } else if (zombies.length === 0) {
      // 波次结束
      if (wave < 5) {
        startWave(wave + 1);
        // 奖励:回复一些生命,补满弹药
        player.hp = Math.min(player.maxHp, player.hp + 25);
        player.ammo = MAG_SIZE;
        player.reloading = false;
      } else {
        state = 'won';
        message = '血染红旗 · 阵地仍在';
        messageTimer = 99999;
        score += player.hp * 5 + FLAG.hp * 3;
      }
    }

    // 站在旗边缓慢回血
    if (!player.down && dist(player.x, player.y, FLAG.x, FLAG.y) < FLAG.r + 30) {
      player.hp = Math.min(player.maxHp, player.hp + 0.04);
    }
  }

  // ==================== 绘制 ====================
  function drawBackground() {
    // 夜色与血色交织
    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, '#0d0808');
    g.addColorStop(0.5, '#1a0d0a');
    g.addColorStop(1, '#120806');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    // 远处血色雾气
    const waveIntensity = Math.min(1, wave / 5);
    const g2 = ctx.createRadialGradient(W / 2, H / 2, 60, W / 2, H / 2, 620);
    g2.addColorStop(0, `rgba(120, 20, 15, ${0.12 + waveIntensity * 0.15})`);
    g2.addColorStop(1, 'rgba(120, 20, 15, 0)');
    ctx.fillStyle = g2;
    ctx.fillRect(0, 0, W, H);

    // 地面纹理点
    ctx.fillStyle = 'rgba(60, 30, 20, 0.25)';
    for (let i = 0; i < 80; i++) {
      const x = (i * 137.5) % W;
      const y = (i * 89.7) % H;
      ctx.fillRect(x, y, 2, 2);
    }
  }

  function drawBloodStains() {
    for (const s of bloodStains) {
      ctx.globalAlpha = s.a;
      ctx.fillStyle = '#5a0808';
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawObstacles() {
    for (const o of obstacles) {
      ctx.fillStyle = '#2a1a14';
      roundRect(o.x, o.y, o.w, o.h, 5);
      ctx.fill();
      ctx.fillStyle = '#3d261c';
      roundRect(o.x + 3, o.y + 3, o.w - 6, 4, 2);
      ctx.fill();
      ctx.strokeStyle = 'rgba(80, 40, 30, 0.5)';
      ctx.lineWidth = 1;
      roundRect(o.x, o.y, o.w, o.h, 5);
      ctx.stroke();
    }
  }

  function drawFlag() {
    const { x, y } = FLAG;
    const hpRatio = FLAG.hp / FLAG.maxHp;

    // 旗杆
    ctx.strokeStyle = '#6b4a2a';
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.moveTo(x, y + 30);
    ctx.lineTo(x, y - 55);
    ctx.stroke();

    // 旗面(动态飘动)
    const t = frame * 0.06;
    const wave1 = Math.sin(t) * 4;
    const wave2 = Math.sin(t + 1.2) * 3;
    ctx.save();
    ctx.globalAlpha = 0.9;
    const flagGrad = ctx.createLinearGradient(x, y - 55, x + 48, y - 5);
    flagGrad.addColorStop(0, hpRatio > 0.4 ? '#c41e1e' : '#6a0e0e');
    flagGrad.addColorStop(1, hpRatio > 0.4 ? '#8a1010' : '#3a0606');
    ctx.fillStyle = flagGrad;
    ctx.beginPath();
    ctx.moveTo(x, y - 55);
    ctx.quadraticCurveTo(x + 24, y - 58 + wave1, x + 48, y - 50 + wave2);
    ctx.lineTo(x + 48, y - 12 + wave2);
    ctx.quadraticCurveTo(x + 24, y - 10 + wave1, x, y - 5);
    ctx.closePath();
    ctx.fill();

    // 五角星
    ctx.fillStyle = '#ffd93d';
    ctx.save();
    ctx.translate(x + 22, y - 32 + wave1 * 0.5);
    ctx.beginPath();
    for (let i = 0; i < 10; i++) {
      const r = i % 2 === 0 ? 9 : 4;
      const a = -Math.PI / 2 + i * Math.PI / 5;
      const px = Math.cos(a) * r;
      const py = Math.sin(a) * r;
      i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
    }
    ctx.closePath();
    ctx.fill();
    ctx.restore();
    ctx.restore();

    // 旗帜血条
    const barW = 70, barH = 6;
    const bx = x - barW / 2, by = y + 38;
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    roundRect(bx, by, barW, barH, 3);
    ctx.fill();
    ctx.fillStyle = hpRatio > 0.5 ? '#e03030' : hpRatio > 0.25 ? '#d08020' : '#a01010';
    roundRect(bx, by, barW * hpRatio, barH, 3);
    ctx.fill();
    ctx.strokeStyle = 'rgba(200, 80, 60, 0.6)';
    ctx.lineWidth = 1;
    roundRect(bx, by, barW, barH, 3);
    ctx.stroke();
  }

  function drawPlayer() {
    if (player.down) {
      // 倒地状态
      ctx.save();
      ctx.globalAlpha = 0.7;
      ctx.fillStyle = '#5a3030';
      roundRect(player.x - 14, player.y - 8, 28, 16, 6);
      ctx.fill();
      ctx.restore();
      // 倒地计时圈
      const ratio = player.downTimer / 180;
      ctx.strokeStyle = `rgba(255, ${100 + 100 * (1 - ratio)}, 50, 0.9)`;
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(player.x, player.y, 22, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * ratio);
      ctx.stroke();
      return;
    }

    // 阴影
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    ctx.beginPath();
    ctx.ellipse(player.x, player.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.6;
    ctx.fillStyle = '#5a6a4a';
    roundRect(player.x - player.w / 2, player.y - player.h / 2, player.w, player.h, 5);
    ctx.fill();

    // 军帽
    ctx.fillStyle = '#3d4a30';
    roundRect(player.x - 10, player.y - 16, 20, 8, 3);
    ctx.fill();
    // 红星
    ctx.fillStyle = '#d02020';
    ctx.beginPath();
    ctx.arc(player.x, player.y - 12, 2.5, 0, Math.PI * 2);
    ctx.fill();

    // 枪
    ctx.save();
    ctx.translate(player.x, player.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();

    // 准星线
    ctx.save();
    ctx.globalAlpha = 0.25;
    ctx.strokeStyle = '#ffcc66';
    ctx.lineWidth = 1;
    ctx.setLineDash([6, 8]);
    ctx.beginPath();
    ctx.moveTo(player.x, player.y);
    ctx.lineTo(mouse.x, mouse.y);
    ctx.stroke();
    ctx.restore();

    // 玩家血条
    const hpRatio = player.hp / player.maxHp;
    const barW = 36, barH = 4;
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    roundRect(player.x - barW / 2, player.y - 28, barW, barH, 2);
    ctx.fill();
    ctx.fillStyle = hpRatio > 0.5 ? '#50c050' : hpRatio > 0.25 ? '#d0a020' : '#d03030';
    roundRect(player.x - barW / 2, player.y - 28, barW * hpRatio, barH, 2);
    ctx.fill();

    // 换弹进度
    if (player.reloading) {
      const r = 1 - player.reloadTimer / RELOAD_TIME;
      ctx.strokeStyle = '#ffcc66';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(player.x, player.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.7;

      // 身体颜色
      let bodyColor = '#4a5a3a';
      let headColor = '#5a6a4a';
      if (z.type === 1) { bodyColor = '#6a3a2a'; headColor = '#7a4a3a'; }
      if (z.type === 2) { bodyColor = '#3a3a3a'; headColor = '#4a4a4a'; }

      // 身体
      ctx.fillStyle = bodyColor;
      roundRect(z.x - z.w / 2, z.y - z.h / 2, z.w, z.h, 4);
      ctx.fill();

      // 头
      ctx.fillStyle = headColor;
      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.fillStyle = 'rgba(100, 10, 10, 0.6)';
      ctx.beginPath();
      ctx.arc(z.x + 3, z.y + 3, 3, 0, Math.PI * 2);
      ctx.fill();

      // 重装僵尸有护甲标记
      if (z.type === 2) {
        ctx.strokeStyle = '#888';
        ctx.lineWidth = 1.5;
        roundRect(z.x - z.w / 2 - 1, z.y - z.h / 2 - 1, z.w + 2, z.h + 2, 5);
        ctx.stroke();
      }
      ctx.restore();

      // 血条
      if (z.hp < z.maxHp) {
        const hpRatio = z.hp / z.maxHp;
        const barW = 26, barH = 3;
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.fillRect(z.x - barW / 2, z.y - z.h / 2 - 14, barW, barH);
        ctx.fillStyle = z.type === 2 ? '#a06060' : '#d03030';
        ctx.fillRect(z.x - barW / 2, z.y - z.h / 2 - 14, barW * hpRatio, barH);
      }
    }
  }

  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() {
    ctx.save();
    ctx.textBaseline = 'middle';

    // 左上:波次
    ctx.fillStyle = '#ffd0b0';
    ctx.font = 'bold 20px system-ui, -apple-system, sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText(`第 ${wave} 波`, 24, 32);

    // 弹药
    const ammoStr = player.reloading ? '装填中…' : `${player.ammo} / ${MAG_SIZE}`;
    ctx.fillStyle = player.ammo === 0 && !player.reloading ? '#ff5050' : '#e8d8c0';
    ctx.font = 'bold 18px system-ui, -apple-system, sans-serif';
    ctx.fillText(ammoStr, 24, 62);

    // 换弹提示
    if (player.ammo < MAG_SIZE && !player.reloading) {
      ctx.fillStyle = 'rgba(255, 200, 100, 0.6)';
      ctx.font = '14px system-ui, -apple-system, sans-serif';
      ctx.fillText('按 R 装填', 24, 86);
    }

    // 右上:击杀/得分
    ctx.textAlign = 'right';
    ctx.fillStyle = 'rgba(230, 210, 190, 0.85)';
    ctx.font = 'bold 18px system-ui, -apple-system, sans-serif';
    ctx.fillText(`击杀 ${kills}`, W - 24, 32);
    ctx.fillStyle = 'rgba(230, 210, 190, 0.5)';
    ctx.font = '15px system-ui, -apple-system, sans-serif';
    ctx.fillText(`得分 ${score}`, W - 24, 58);

    // 玩家血量
    const hpRatio = player.hp / player.maxHp;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(W - 200, H - 40, 176, 16, 8);
    ctx.fill();
    ctx.fillStyle = hpRatio > 0.5 ? '#50c050' : hpRatio > 0.25 ? '#d0a020' : '#d03030';
    roundRect(W - 200, H - 40, 176 * hpRatio, 16, 8);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 12px system-ui, -apple-system, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`生命 ${Math.ceil(player.hp)} / ${player.maxHp}`, W - 112, H - 32);

    ctx.restore();

    // 中央消息
    if (messageTimer > 0 && message) {
      ctx.save();
      const a = Math.min(1, messageTimer / 40) * 0.9;
      ctx.globalAlpha = a;
      ctx.textAlign = 'center';
      ctx.font = 'bold 32px system-ui, -apple-system, sans-serif';
      ctx.fillStyle = '#ffd0b0';
      ctx.shadowColor = '#8a1010';
      ctx.shadowBlur = 16;
      ctx.fillText(message, W / 2, H / 2 - 80);
      ctx.restore();
    }

    // 操作提示(前几秒)
    if (frame < 240 && state === 'playing') {
      ctx.save();
      ctx.globalAlpha = Math.max(0, 1 - frame / 240) * 0.55;
      ctx.textAlign = 'center';
      ctx.fillStyle = '#d0b8a0';
      ctx.font = '15px system-ui, -apple-system, sans-serif';
      ctx.fillText('WASD / 方向键移动 鼠标瞄准 左键开火 R 装填', W / 2, H - 24);
      ctx.restore();
    }
  }

  function drawEndScreen() {
    if (state !== 'won' && state !== 'lost') return;
    ctx.save();
    ctx.fillStyle = state === 'won' ? 'rgba(10, 15, 8, 0.8)' : 'rgba(15, 5, 5, 0.82)';
    ctx.fillRect(0, 0, W, H);

    ctx.textAlign = 'center';

    if (state === 'won') {
      ctx.shadowColor = '#ffd93d';
      ctx.shadowBlur = 26;
      ctx.fillStyle = '#ffd93d';
      ctx.font = 'bold 56px system-ui, -apple-system, sans-serif';
      ctx.fillText('阵地仍在', W / 2, H / 2 - 70);
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#e8d8c0';
      ctx.font = 'bold 26px system-ui, -apple-system, sans-serif';
      ctx.fillText(`击杀 ${kills} 得分 ${score}`, W / 2, H / 2 + 5);
      ctx.fillStyle = 'rgba(230, 210, 190, 0.7)';
      ctx.font = '18px system-ui, -apple-system, sans-serif';
      ctx.fillText('大部队已经归来', W / 2, H / 2 + 55);
    } else {
      ctx.shadowColor = '#8a1010';
      ctx.shadowBlur = 26;
      ctx.fillStyle = '#d02020';
      ctx.font = 'bold 56px system-ui, -apple-system, sans-serif';
      ctx.fillText('红旗不倒', W / 2, H / 2 - 70);
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#e8d8c0';
      ctx.font = 'bold 26px system-ui, -apple-system, sans-serif';
      ctx.fillText(`坚守至第 ${wave} 波 击杀 ${kills}`, W / 2, H / 2 + 5);
      ctx.fillStyle = 'rgba(230, 210, 190, 0.7)';
      ctx.font = '18px system-ui, -apple-system, sans-serif';
      ctx.fillText('但火种已经留下', W / 2, H / 2 + 55);
    }

    const a = 0.5 + Math.sin(frame * 0.08) * 0.4;
    ctx.fillStyle = `rgba(200, 180, 160, ${a})`;
    ctx.font = 'bold 18px system-ui, -apple-system, sans-serif';
    ctx.fillText('按 R 重新开始', W / 2, H / 2 + 120);
    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();
    drawBloodStains();
    drawObstacles();
    drawFlag();
    drawZombies();
    drawBullets();
    drawPlayer();
    drawParticles();
    drawHUD();
    drawEndScreen();

    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 => {
    initAudio();
    if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();
    if (!e.repeat) {
      if (e.code === 'KeyR') {
        if (state === 'won' || state === 'lost') init();
        else startReload();
      }
    }
    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 => {
    initAudio();
    if (e.button === 0) mouse.down = true;
  });
  window.addEventListener('mouseup', () => { mouse.down = false; });
  canvas.addEventListener('contextmenu', e => e.preventDefault());

  // 触屏
  const touchBar = document.getElementById('touch');
  if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
    touchBar.style.display = 'block';
    const bind = (id, code, isFire, isReload) => {
      const el = document.getElementById(id);
      const on = ev => {
        ev.preventDefault();
        initAudio();
        if (isFire) mouse.down = true;
        else if (isReload) {
          if (state === 'won' || state === 'lost') init();
          else startReload();
        } else keys[code] = true;
      };
      const off = ev => {
        ev.preventDefault();
        if (isFire) mouse.down = false;
        else keys[code] = false;
      };
      el.addEventListener('pointerdown', on);
      el.addEventListener('pointerup', off);
      el.addEventListener('pointercancel', off);
      el.addEventListener('pointerleave', off);
    };
    bind('btnL', 'ArrowLeft', false, false);
    bind('btnR', 'ArrowRight', false, false);
    bind('btnRl', null, false, true);
    bind('btnF', null, true, false);

    // 触屏瞄准:滑动屏幕
    canvas.addEventListener('touchmove', e => {
      e.preventDefault();
      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);
    }, { passive: false });
    canvas.addEventListener('touchstart', e => {
      initAudio();
      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);
    }, { passive: true });
  }

  // ==================== 启动 ====================
  init();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: 血染红旗

Creator: EpicCoder88

Libraries: none

Complexity: complex (1065 lines, 31.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-murutl7p" to link back to the original. Then publish at arcadelab.ai/publish.