🎮ArcadeLab

经典大战 · 选人与决斗

by ApexFalcon66
834 lines25.4 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>经典大战 · 选人与决斗</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    background: radial-gradient(ellipse at 50% 40%, #0b1a24 0%, #05090d 55%, #010204 100%);
    font-family: -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
    color: #d7f6ff;
    overflow: hidden;
    user-select: none;
    -webkit-user-select: none;
  }

  .screen {
    position: fixed; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    transition: opacity .35s ease, transform .35s ease;
  }
  .screen.hidden {
    opacity: 0; pointer-events: none;
    transform: scale(1.02);
  }

  /* ---------- 开始界面 ---------- */
  #screen-start { gap: 28px; }
  .title-main {
    font-size: clamp(32px, 7vw, 64px);
    letter-spacing: 10px;
    font-weight: 900;
    color: #eafffd;
    text-shadow: 0 0 18px rgba(0,255,224,.6), 0 0 60px rgba(0,200,180,.35);
  }
  .title-sub {
    font-size: 13px; letter-spacing: 4px;
    color: rgba(0,255,224,.55);
  }
  .start-btn {
    margin-top: 8px;
    padding: 16px 64px;
    font-size: 18px; letter-spacing: 6px;
    color: #031412;
    background: linear-gradient(180deg, #9ffff2, #22e3c5);
    border: none; border-radius: 6px;
    box-shadow: 0 0 22px rgba(0,255,224,.45), inset 0 1px 0 rgba(255,255,255,.6);
    cursor: pointer;
    transition: transform .15s, box-shadow .15s;
  }
  .start-btn:hover { transform: translateY(-2px); box-shadow: 0 0 34px rgba(0,255,224,.7); }
  .start-btn:active { transform: translateY(0); }

  /* ---------- 选人界面 ---------- */
  #screen-select { gap: 16px; padding: 20px; }
  .select-title { font-size: 15px; letter-spacing: 5px; color: rgba(0,255,224,.7); }
  .select-hint  { font-size: 12px; color: rgba(215,246,255,.45); margin-top: -6px; }

  .roster {
    display: flex; gap: 10px;
    flex-wrap: wrap; justify-content: center;
    max-width: 760px;
  }

  .card {
    width: 128px;
    background: linear-gradient(170deg, rgba(14,32,44,.95), rgba(6,14,20,.98));
    border: 1px solid rgba(0,255,224,.16);
    border-radius: 8px;
    padding: 9px 10px 8px;
    cursor: pointer;
    transition: border-color .18s, box-shadow .18s, transform .18s;
    position: relative;
  }
  .card:hover { border-color: rgba(0,255,224,.45); transform: translateY(-2px); }
  .card.selected {
    border-color: #3dffe4;
    box-shadow: 0 0 0 1px rgba(61,255,228,.5), 0 0 18px rgba(0,255,224,.25);
  }
  .card.selected::after {
    content: "✓";
    position: absolute; top: 5px; right: 6px;
    width: 14px; height: 14px;
    font-size: 10px; line-height: 14px; text-align: center;
    color: #031412; background: #3dffe4;
    border-radius: 50%;
    font-weight: 700;
  }

  .char-name { font-size: 15px; font-weight: 800; letter-spacing: 1px; }
  .char-source { font-size: 9px; color: rgba(215,246,255,.4); margin-top: 1px; }

  .tag-row { display: flex; gap: 4px; margin: 6px 0 7px; }
  .tag {
    font-size: 9px; letter-spacing: 1px;
    padding: 1px 5px; border-radius: 3px;
    border: 1px solid rgba(0,255,224,.25);
    color: rgba(0,255,224,.8);
  }
  .tag.ranged { border-color: rgba(255,190,90,.35); color: rgba(255,200,120,.9); }

  .stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px 8px;
    font-size: 9px;
    color: rgba(215,246,255,.5);
  }
  .stats b { color: rgba(215,246,255,.85); font-weight: 600; margin-left: 3px; }

  .select-actions { display: flex; gap: 14px; margin-top: 6px; }
  .btn {
    padding: 12px 34px; font-size: 14px; letter-spacing: 3px;
    border-radius: 5px; border: 1px solid rgba(0,255,224,.3);
    background: rgba(0,255,224,.06);
    color: rgba(0,255,224,.8);
    cursor: pointer; transition: .18s;
  }
  .btn:hover:not(:disabled) { background: rgba(0,255,224,.14); }
  .btn:disabled { opacity: .28; cursor: not-allowed; }
  .btn-primary {
    background: linear-gradient(180deg, #9ffff2, #22e3c5);
    color: #031412; border: none; font-weight: 800;
  }
  .btn-primary:disabled { background: rgba(120,180,170,.2); color: rgba(215,246,255,.3); }

  /* ---------- 地图界面 ---------- */
  #screen-map { gap: 12px; }
  .map-hud {
    display: flex; gap: 40px; align-items: center;
    font-size: 12px; letter-spacing: 1px;
    color: rgba(215,246,255,.6);
  }
  .hud-name { font-weight: 700; color: #eafffd; font-size: 14px; }
  .hud-hp { color: #3dffe4; font-weight: 700; }
  .hud-vs { color: rgba(255,255,255,.2); font-size: 16px; letter-spacing: 4px; }
  canvas { display: block; touch-action: none; }

  .back-link {
    font-size: 11px; color: rgba(0,255,224,.4);
    cursor: pointer; letter-spacing: 2px;
    margin-top: 4px;
  }
  .back-link:hover { color: rgba(0,255,224,.8); }

  /* ---------- 获胜界面 ---------- */
  #screen-result {
    gap: 18px;
    background: radial-gradient(ellipse at 50% 50%, rgba(0,40,36,.85), rgba(1,2,4,.96) 70%);
  }
  .result-label {
    font-size: 13px; letter-spacing: 8px;
    color: rgba(0,255,224,.55);
    animation: fadeUp .5s ease;
  }
  .result-name {
    font-size: clamp(40px, 10vw, 76px);
    font-weight: 900;
    letter-spacing: 8px;
    color: #eafffd;
    text-shadow: 0 0 24px rgba(0,255,224,.75), 0 0 90px rgba(0,200,180,.45);
    animation: resultPop .55s cubic-bezier(.2,1.5,.5,1);
  }
  .result-sub {
    font-size: 12px; letter-spacing: 3px;
    color: rgba(215,246,255,.35);
    margin-bottom: 8px;
    animation: fadeUp .7s ease .1s both;
  }
  #btn-result-back {
    animation: fadeUp .7s ease .18s both;
  }
  @keyframes resultPop {
    0%   { transform: scale(.6); opacity: 0; }
    60%  { transform: scale(1.08); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
  @keyframes fadeUp {
    0%   { transform: translateY(10px); opacity: 0; }
    100% { transform: translateY(0); opacity: 1; }
  }
</style>
</head>
<body>

<section class="screen" id="screen-start">
  <div class="title-main">经典大战</div>
  <div class="title-sub">CLASSIC ARENA · 文本作证 · 规则统一</div>
  <button class="start-btn" id="btn-to-select">开 始 游 戏</button>
</section>

<section class="screen hidden" id="screen-select">
  <div class="select-title">选择参战角色</div>
  <div class="select-hint">至少选中 2 名角色,点击“开始决斗”进入地图</div>
  <div class="roster" id="roster"></div>
  <div class="select-actions">
    <button class="btn" id="btn-back-start">返回</button>
    <button class="btn btn-primary" id="btn-start-duel" disabled>开始决斗</button>
  </div>
</section>

<section class="screen hidden" id="screen-map">
  <div class="map-hud" id="map-hud"></div>
  <canvas id="game"></canvas>
  <div class="back-link" id="btn-back-select">← 返回选人</div>
</section>

<section class="screen hidden" id="screen-result">
  <div class="result-label">胜 者</div>
  <div class="result-name" id="result-name">—</div>
  <div class="result-sub" id="result-sub">经典大战</div>
  <button class="btn btn-primary" id="btn-result-back">返回重新选择</button>
</section>

<script>
(function () {
  'use strict';

  /* ============================================================
     0. 角色数据
     ============================================================ */
  const CHARACTERS = [
    {
      id: 'xiangzi', name: '祥子', source: '《骆驼祥子》',
      type: 'melee', typeLabel: '近战',
      hp: 100, sta: 100, mp: 0,
      speed: 1.6, atk: 9,
      atkRange: 1.4, detectRange: 5,
      atkCd: 0.7, color: '#3dffe4',
    },
    {
      id: 'ruifeng', name: '瑞丰', source: '《四世同堂》',
      type: 'ranged', typeLabel: '远程',
      hp: 72, sta: 80, mp: 100,
      speed: 1.15, atk: 6,
      atkRange: 6.5, detectRange: 0,
      atkCd: 1.1, color: '#ffd76a',
    },
    {
      id: 'fushan', name: '富善先生', source: '《四世同堂》',
      type: 'ranged', typeLabel: '远程',
      hp: 85, sta: 70, mp: 100,
      speed: 1.0, atk: 7,
      atkRange: 7.2, detectRange: 0,
      atkCd: 1.25, color: '#7ec8ff',
    },
  ];

  /* ============================================================
     1. 地图常量与物理
     ============================================================ */
  const COLS = 20, ROWS = 20, CELL = 32, WALL = 16;
  const GRID_W = COLS * CELL, GRID_H = ROWS * CELL;
  const WORLD_W = GRID_W + WALL * 2, WORLD_H = GRID_H + WALL * 2;

  const WALLS = [
    { x: 0, y: 0, w: WORLD_W, h: WALL },
    { x: 0, y: WORLD_H - WALL, w: WORLD_W, h: WALL },
    { x: 0, y: 0, w: WALL, h: WORLD_H },
    { x: WORLD_W - WALL, y: 0, w: WALL, h: WORLD_H },
  ];

  function aabbHit(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 moveBody(body, dx, dy) {
    body.x += dx;
    for (const wall of WALLS) {
      if (!aabbHit(body, wall)) continue;
      if (dx > 0) body.x = wall.x - body.w;
      else if (dx < 0) body.x = wall.x + wall.w;
    }
    body.y += dy;
    for (const wall of WALLS) {
      if (!aabbHit(body, wall)) continue;
      if (dy > 0) body.y = wall.y - body.h;
      else if (dy < 0) body.y = wall.y + wall.h;
    }
    return body;
  }
  function cellCenter(col, row) {
    return { x: WALL + col * CELL + CELL / 2, y: WALL + row * CELL + CELL / 2 };
  }

  /* ============================================================
     2. 选人界面
     ============================================================ */
  const rosterEl = document.getElementById('roster');
  const btnStartDuel = document.getElementById('btn-start-duel');
  const selected = new Set();

  function renderRoster() {
    rosterEl.innerHTML = CHARACTERS.map(c => {
      const typeClass = c.type === 'ranged' ? 'tag ranged' : 'tag';
      return `
        <div class="card" data-id="${c.id}">
          <div class="char-name">${c.name}</div>
          <div class="char-source">${c.source}</div>
          <div class="tag-row"><span class="${typeClass}">${c.typeLabel}</span></div>
          <div class="stats">
            <span>生命<b>${c.hp}</b></span>
            <span>攻击<b>${c.atk}</b></span>
            <span>速度<b>${c.speed}</b></span>
            <span>射程<b>${c.atkRange}</b></span>
          </div>
        </div>
      `;
    }).join('');

    rosterEl.querySelectorAll('.card').forEach(card => {
      card.addEventListener('click', () => {
        const id = card.dataset.id;
        if (selected.has(id)) { selected.delete(id); card.classList.remove('selected'); }
        else { selected.add(id); card.classList.add('selected'); }
        btnStartDuel.disabled = selected.size < 2;
      });
    });
  }

  /* ============================================================
     3. 界面切换
     ============================================================ */
  const screens = {
    start: document.getElementById('screen-start'),
    select: document.getElementById('screen-select'),
    map: document.getElementById('screen-map'),
    result: document.getElementById('screen-result'),
  };
  function show(name) {
    Object.values(screens).forEach(s => s.classList.add('hidden'));
    screens[name].classList.remove('hidden');
  }

  document.getElementById('btn-to-select').addEventListener('click', () => {
    renderRoster();
    selected.clear();
    btnStartDuel.disabled = true;
    show('select');
  });
  document.getElementById('btn-back-start').addEventListener('click', () => show('start'));
  document.getElementById('btn-back-select').addEventListener('click', () => {
    stopDuel();
    show('select');
  });

  /* ============================================================
     4. 地图绘制
     ============================================================ */
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const mapHud = document.getElementById('map-hud');

  function resizeCanvas() {
    const size = Math.min(window.innerWidth * 0.9, window.innerHeight * 0.72, 640);
    const dpr = window.devicePixelRatio || 1;
    canvas.style.width = size + 'px';
    canvas.style.height = size + 'px';
    canvas.width = Math.round(size * dpr);
    canvas.height = Math.round(size * dpr);
  }

  function drawArena(time) {
    const scale = canvas.width / WORLD_W;
    ctx.setTransform(scale, 0, 0, scale, 0, 0);
    ctx.clearRect(0, 0, WORLD_W, WORLD_H);

    const bg = ctx.createLinearGradient(WALL, WALL, WALL + GRID_W, WALL + GRID_H);
    bg.addColorStop(0, '#0b1927');
    bg.addColorStop(0.5, '#0d1e2e');
    bg.addColorStop(1, '#061018');
    ctx.fillStyle = bg;
    ctx.fillRect(WALL, WALL, GRID_W, GRID_H);

    ctx.lineWidth = 1;
    ctx.strokeStyle = 'rgba(80,200,255,.10)';
    ctx.beginPath();
    for (let c = 0; c <= COLS; c++) {
      const x = WALL + c * CELL;
      ctx.moveTo(x, WALL); ctx.lineTo(x, WALL + GRID_H);
    }
    for (let r = 0; r <= ROWS; r++) {
      const y = WALL + r * CELL;
      ctx.moveTo(WALL, y); ctx.lineTo(WALL + GRID_W, y);
    }
    ctx.stroke();

    const pulse = 0.5 + 0.5 * Math.sin(time * 0.002);
    ctx.save();
    ctx.shadowColor = 'rgba(0,255,224,.85)';
    ctx.shadowBlur = 8 + pulse * 16;
    ctx.beginPath();
    ctx.rect(0, 0, WORLD_W, WORLD_H);
    ctx.rect(WALL, WALL, GRID_W, GRID_H);
    ctx.fillStyle = '#00cdb2';
    ctx.fill('evenodd');
    ctx.restore();

    ctx.strokeStyle = `rgba(190,255,248,${0.4 + pulse * 0.45})`;
    ctx.lineWidth = 2;
    ctx.strokeRect(WALL + 1, WALL + 1, GRID_W - 2, GRID_H - 2);
  }

  /* ============================================================
     5. 决斗逻辑
     ============================================================ */
  let fighters = [];
  let projectiles = [];     // 远程投射物
  let duelActive = false;
  let rafId = null;
  let lastTime = 0;
  let duelSession = 0;      // 防止旧计时器误弹结算

  function assignWanderLeg(f) {
    f.wanderAngle = Math.random() * Math.PI * 2;
    f.wanderSpeedBoost = 1.15 + Math.random() * 1.05;
    f.wanderDistLeft = (2.5 + Math.random() * 5) * CELL;
  }

  function stopDuel() {
    duelActive = false;
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;
    fighters = [];
    projectiles = [];
  }

  function startDuel() {
    stopDuel();
    duelSession++;
    resizeCanvas();
    show('map');

    const chosen = CHARACTERS.filter(c => selected.has(c.id));
    const duo = chosen.slice(0, 2);

    const spawnA = cellCenter(3, 17);
    const spawnB = cellCenter(16, 2);

    fighters = duo.map((c, i) => {
      const f = {
        def: c,
        x: (i === 0 ? spawnA : spawnB).x - 13,
        y: (i === 0 ? spawnA : spawnB).y - 13,
        w: 26, h: 26,
        hp: c.hp, maxHp: c.hp,
        sta: c.sta, maxSta: c.sta,
        mp: c.mp, maxMp: c.mp,
        atkTimer: 0,
        shield: 0, shieldTimer: 0, shieldInit: false,
        marked: 0,
        alive: true,
        speedMult: 1,
        wanderAngle: 0,
        wanderSpeedBoost: 1,
        wanderDistLeft: 0,
        dashTimer: 0,
        dashCooldown: 0,
        hitFlash: 0,      // 受击闪白
        atkFlash: 0,      // 攻击闪光
      };
      assignWanderLeg(f);
      return f;
    });

    updateHud();
    duelActive = true;
    lastTime = performance.now();
    rafId = requestAnimationFrame(duelLoop);
  }

  function duelLoop(now) {
    if (!duelActive) return;
    const dt = Math.min((now - lastTime) / 1000, 0.05);
    lastTime = now;

    update(dt);
    render(now);

    if (duelActive) rafId = requestAnimationFrame(duelLoop);
  }

  /* ---- 发射投射物 ---- */
  function fireProjectile(owner, target, dmg, opts) {
    projectiles.push({
      owner,
      target,
      x: owner.x + owner.w / 2,
      y: owner.y + owner.h / 2,
      speed: opts.speed || 360,
      dmg,
      color: opts.color || '#fff',
      radius: opts.radius || 5,
      life: 3,
      isRuifeng: !!opts.isRuifeng,
      trail: [],
    });
    owner.atkFlash = 0.12;
  }

  /* ---- 统一伤害结算(技能在这里生效) ---- */
  function applyDamage(attacker, target, dmg, meta) {
    let finalDmg = dmg;

    // 瑞丰标记:对被标记目标增伤,并刷新标记
    if (meta.isRuifeng) {
      if (target.marked > 0) finalDmg *= 1.2;
      target.marked = 3;
    }

    // 富善护盾:抵挡 60%
    if (target.shield > 0) {
      finalDmg *= 0.4;
      target.shield = 0;
      target.shieldTimer = 0;
    }

    target.hp = Math.max(0, target.hp - finalDmg);
    target.hitFlash = 0.15;

    if (target.hp <= 0) {
      target.alive = false;
      endDuel(attacker);
      return;
    }
    updateHud();
  }

  function update(dt) {
    const aliveList = fighters.filter(f => f.alive);
    if (aliveList.length < 2) return;

    /* ---- 祥子:体力透支 ---- */
    const xz = fighters.find(f => f.def.id === 'xiangzi' && f.alive);
    if (xz) {
      if (xz.hp / xz.maxHp < 0.4 && xz.sta > 0) {
        xz.sta = Math.max(0, xz.sta - 2 * dt);
        xz.speedMult = xz.sta > 0 ? 1.3 : 0.6;
      } else {
        xz.speedMult = 1;
      }
    }

    /* ---- 富善先生:旧物屏障 ---- */
    const fs = fighters.find(f => f.def.id === 'fushan' && f.alive);
    if (fs) {
      if (!fs.shieldInit) { fs.shield = 1; fs.shieldInit = true; }
      fs.shieldTimer += dt;
      if (fs.shieldTimer >= 12) { fs.shieldTimer = 0; fs.shield = 1; }
    }

    /* ---- 冷却、标记、闪烁倒计时 ---- */
    for (const f of fighters) {
      if (!f.alive) continue;
      f.atkTimer = Math.max(0, f.atkTimer - dt);
      if (f.marked > 0) f.marked = Math.max(0, f.marked - dt);
      if (f.dashTimer > 0) f.dashTimer = Math.max(0, f.dashTimer - dt);
      if (f.dashCooldown > 0) f.dashCooldown = Math.max(0, f.dashCooldown - dt);
      if (f.hitFlash > 0) f.hitFlash = Math.max(0, f.hitFlash - dt);
      if (f.atkFlash > 0) f.atkFlash = Math.max(0, f.atkFlash - dt);
    }

    /* ---- 每个单位:移动 / 攻击 ---- */
    for (const f of fighters) {
      if (!f.alive) continue;
      const target = fighters.find(o => o !== f && o.alive);
      if (!target) continue;

      const dx = (target.x + target.w / 2) - (f.x + f.w / 2);
      const dy = (target.y + target.h / 2) - (f.y + f.h / 2);
      const targetDist = Math.hypot(dx, dy);

      let moveAngle = f.wanderAngle;
      let shouldMove = true;
      let tracking = false;

      if (f.def.type === 'melee' && targetDist <= f.def.detectRange * CELL) {
        moveAngle = Math.atan2(dy, dx);
        tracking = true;
        if (targetDist <= f.def.atkRange * CELL) shouldMove = false;
      }

      // 随机冲刺
      if (!tracking && f.dashCooldown <= 0 && Math.random() < 0.008) {
        f.dashTimer = 0.15 + Math.random() * 0.2;
        f.dashCooldown = 0.8 + Math.random() * 1.0;
      }

      // 移动
      if (shouldMove) {
        let speedMul = tracking ? 1.0 : f.wanderSpeedBoost;
        if (f.dashTimer > 0) speedMul *= 2.5;

        const speed = f.def.speed * CELL * (f.speedMult || 1) * speedMul * dt;
        const oldX = f.x, oldY = f.y;
        moveBody(f, Math.cos(moveAngle) * speed, Math.sin(moveAngle) * speed);

        const moved = Math.hypot(f.x - oldX, f.y - oldY);

        if (moved < speed * 0.4) {
          if (!tracking) {
            assignWanderLeg(f);
            f.dashTimer = 0;
          }
        } else if (!tracking) {
          f.wanderDistLeft -= moved;
          if (f.wanderDistLeft <= 0) assignWanderLeg(f);
        }
      }

      // 攻击
      if (targetDist <= f.def.atkRange * CELL && f.atkTimer <= 0) {
        f.atkTimer = f.def.atkCd;

        if (f.def.type === 'melee') {
          // 近战:立即命中
          f.atkFlash = 0.15;
          applyDamage(f, target, f.def.atk, { isRuifeng: false });
        } else {
          // 远程:发射投射物
          let dmg = f.def.atk;
          const isRuifeng = f.def.id === 'ruifeng';
          if (isRuifeng && targetDist < CELL * 2) dmg *= 0.65; // 被贴身削弱

          fireProjectile(f, target, dmg, {
            speed: 360,
            color: f.def.color,
            radius: 5,
            isRuifeng,
          });
        }
      }
    }

    /* ---- 投射物更新 ---- */
    for (let i = projectiles.length - 1; i >= 0; i--) {
      const p = projectiles[i];
      p.life -= dt;

      // 过期或目标已死
      if (p.life <= 0 || !p.target || !p.target.alive) {
        projectiles.splice(i, 1);
        continue;
      }

      const tx = p.target.x + p.target.w / 2;
      const ty = p.target.y + p.target.h / 2;
      const dx = tx - p.x;
      const dy = ty - p.y;
      const dist = Math.hypot(dx, dy);
      const hitR = p.target.w / 2 + p.radius;

      // 命中判定
      if (dist <= hitR) {
        applyDamage(p.owner, p.target, p.dmg, { isRuifeng: p.isRuifeng });
        projectiles.splice(i, 1);
        continue;
      }

      const step = p.speed * dt;
      if (step >= dist - hitR) {
        // 本帧会到达,直接命中
        applyDamage(p.owner, p.target, p.dmg, { isRuifeng: p.isRuifeng });
        projectiles.splice(i, 1);
        continue;
      }

      // 飞行
      p.x += (dx / dist) * step;
      p.y += (dy / dist) * step;

      // 拖尾
      p.trail.push({ x: p.x, y: p.y });
      if (p.trail.length > 7) p.trail.shift();
    }
  }

  function endDuel(winner) {
    duelActive = false;
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;

    const mySession = duelSession;
    setTimeout(() => {
      // 会话已切换,说明玩家已经返回重开,不再弹结算
      if (mySession !== duelSession) return;
      if (screens.map.classList.contains('hidden')) return;

      document.getElementById('result-name').textContent = winner.def.name;
      document.getElementById('result-sub').textContent = winner.def.source + ' · 经典大战';
      show('result');
    }, 550);
  }

  function updateHud() {
    if (fighters.length < 2) return;
    const [a, b] = fighters;
    mapHud.innerHTML = `
      <span><span class="hud-name">${a.def.name}</span> <span class="hud-hp">${Math.ceil(a.hp)}</span>/${a.maxHp}</span>
      <span class="hud-vs">VS</span>
      <span><span class="hud-name">${b.def.name}</span> <span class="hud-hp">${Math.ceil(b.hp)}</span>/${b.maxHp}</span>
    `;
  }

  /* ---- 渲染 ---- */
  function render(time) {
    drawArena(time);

    // 角色
    for (const f of fighters) {
      if (!f.alive) continue;
      const cx = f.x + f.w / 2;
      const cy = f.y + f.h / 2;

      // 近战探测范围
      if (f.def.type === 'melee' && f.def.detectRange > 0) {
        ctx.save();
        ctx.setLineDash([5, 5]);
        ctx.strokeStyle = 'rgba(61,255,228,.18)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(cx, cy, f.def.detectRange * CELL, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
      }

      // 身体
      const dashing = f.dashTimer > 0;
      const hit = f.hitFlash > 0;
      const atk = f.atkFlash > 0;

      ctx.save();
      ctx.shadowColor = f.def.color;
      ctx.shadowBlur = hit ? 28 : (atk ? 22 : (dashing ? 26 : 12));
      ctx.fillStyle = hit ? '#ffffff' : (atk ? '#ffffff' : (dashing ? '#ffffff' : f.def.color));
      ctx.beginPath();
      ctx.arc(cx, cy, f.w / 2, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      // 名字
      ctx.fillStyle = 'rgba(255,255,255,.85)';
      ctx.font = '11px -apple-system, "PingFang SC", sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(f.def.name, cx, f.y - 8);

      // 血条
      const barW = 34, barH = 4;
      const bx = cx - barW / 2, by = f.y - 20;
      ctx.fillStyle = 'rgba(0,0,0,.5)';
      ctx.fillRect(bx, by, barW, barH);
      ctx.fillStyle = f.def.color;
      ctx.fillRect(bx, by, barW * (f.hp / f.maxHp), barH);

      // 富善护盾
      if (f.shield > 0) {
        ctx.strokeStyle = 'rgba(126,200,255,.8)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(cx, cy, f.w / 2 + 5, 0, Math.PI * 2);
        ctx.stroke();
      }

      // 瑞丰标记
      if (f.marked > 0) {
        ctx.fillStyle = 'rgba(255,215,106,.9)';
        ctx.beginPath();
        ctx.moveTo(cx, f.y - 28);
        ctx.lineTo(cx - 4, f.y - 34);
        ctx.lineTo(cx + 4, f.y - 34);
        ctx.closePath();
        ctx.fill();
      }
    }

    // 投射物
    for (const p of projectiles) {
      // 拖尾
      for (let i = 0; i < p.trail.length; i++) {
        const t = p.trail[i];
        const k = (i + 1) / p.trail.length;
        ctx.globalAlpha = k * 0.45;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(t.x, t.y, p.radius * (0.25 + k * 0.75), 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;

      // 光晕主体
      ctx.save();
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 16;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();

      // 彩色外圈
      ctx.strokeStyle = p.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
      ctx.stroke();
    }
  }

  /* ============================================================
     6. 启动
     ============================================================ */
  document.getElementById('btn-start-duel').addEventListener('click', startDuel);

  document.getElementById('btn-result-back').addEventListener('click', () => {
    stopDuel();
    duelSession++;          // 让任何挂起的结算计时器失效
    renderRoster();
    selected.clear();
    btnStartDuel.disabled = true;
    show('select');
  });

  window.addEventListener('resize', () => {
    if (!screens.map.classList.contains('hidden')) resizeCanvas();
  });

  show('start');

})();
</script>
</body>
</html>

Game Source: 经典大战 · 选人与决斗

Creator: ApexFalcon66

Libraries: none

Complexity: complex (834 lines, 25.4 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-apexfalcon66-mvdey5al" to link back to the original. Then publish at arcadelab.ai/publish.