🎮ArcadeLab

矛盾升级:皇室战争

by HyperShark77
2145 lines85.5 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>矛盾升级:皇室战争</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #050810;
    display: flex; justify-content: center; align-items: center;
    min-height: 100vh; font-family: 'Microsoft YaHei', sans-serif;
    color: #fff; overflow: hidden;
  }
  #gameContainer {
    position: relative;
    box-shadow: 0 0 60px rgba(0, 200, 255, 0.35);
    border-radius: 10px; overflow: hidden;
  }
  canvas { display: block; cursor: pointer; position: relative; z-index: 1; }
  #ui {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    pointer-events: none; z-index: 10;
  }
  .screen {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    display: flex; flex-direction: column;
    justify-content: center; align-items: center;
    background: rgba(5, 10, 25, 0.96);
    pointer-events: all;
    text-align: center; padding: 20px; z-index: 20;
    overflow-y: auto;
  }
  .screen.hidden { display: none; pointer-events: none; }
  h1 {
    font-size: 38px; letter-spacing: 4px;
    background: linear-gradient(90deg, #ff3d71, #ffaa00, #00e5ff);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent;
    margin-bottom: 8px;
  }
  h2 { font-size: 17px; color: #00e5ff; margin-bottom: 14px; letter-spacing: 4px; }
  p { font-size: 14px; color: #a0b0c0; margin: 5px 0; line-height: 1.6; }
  button {
    margin-top: 10px; padding: 12px 32px; font-size: 15px;
    font-family: inherit; font-weight: bold; letter-spacing: 1px;
    color: #001018; border: none; border-radius: 8px; cursor: pointer;
    background: linear-gradient(90deg, #00e5ff, #00ffa0);
    transition: transform 0.15s, box-shadow 0.15s;
    pointer-events: all; position: relative; z-index: 30;
  }
  button:hover:not(:disabled) { transform: scale(1.06); box-shadow: 0 0 25px rgba(0, 229, 255, 0.7); }
  button:disabled {
    background: rgba(60, 70, 80, 0.6);
    color: #4a5a60;
    cursor: not-allowed; transform: none;
  }
  button.small { padding: 6px 14px; font-size: 12px; margin-top: 0; }
  .controls-info { margin-top: 16px; font-size: 13px; color: #708090; line-height: 1.9; }
  .controls-info b { color: #00e5ff; }
  .alliance {
    margin: 6px 0; padding: 12px 18px;
    background: rgba(20, 30, 50, 0.6);
    border-radius: 10px; font-size: 14px;
    display: inline-block; max-width: 780px;
  }
  .alliance b { letter-spacing: 1px; }
  .allianceNameRow {
    display: flex; align-items: center; gap: 10px;
    justify-content: center; margin-bottom: 8px;
  }
  .allianceNameRow input {
    width: 180px; padding: 6px 12px;
    font-family: inherit; font-size: 15px; font-weight: bold;
    color: #fff; background: rgba(0, 0, 0, 0.5);
    border: 2px solid rgba(0, 200, 255, 0.5);
    border-radius: 8px; text-align: center;
    outline: none; pointer-events: all;
  }
  .allianceNameRow input:focus { border-color: #00e5ff; box-shadow: 0 0 15px rgba(0, 229, 255, 0.6); }
  .allianceNameRow input.enemy { border-color: rgba(255, 61, 113, 0.5); }
  .allianceNameRow input.enemy:focus { border-color: #ff3d71; box-shadow: 0 0 15px rgba(255, 61, 113, 0.6); }
  .allianceNameRow input.empty { border-color: rgba(255, 200, 0, 0.8); animation: pulseEmpty 1.2s infinite; }
  .nameList {
    display: flex; flex-wrap: wrap; gap: 6px;
    justify-content: center; margin-top: 8px;
  }
  .nameList input {
    width: 88px; padding: 4px 8px;
    font-family: inherit; font-size: 12px;
    color: #fff; background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(0, 200, 255, 0.4);
    border-radius: 5px; text-align: center;
    outline: none; pointer-events: all;
  }
  .nameList input:focus { border-color: #00e5ff; box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); }
  .nameList input.enemy { border-color: rgba(255, 61, 113, 0.4); }
  .nameList input.enemy:focus { border-color: #ff3d71; box-shadow: 0 0 10px rgba(255, 61, 113, 0.5); }
  .nameList input.empty,
  .allianceNameRow input.empty { animation: pulseEmpty 1.2s infinite; }
  @keyframes pulseEmpty {
    0%, 100% { box-shadow: 0 0 6px rgba(255, 200, 0, 0.5); }
    50% { box-shadow: 0 0 16px rgba(255, 200, 0, 0.9); }
  }
  .flagEditPanel {
    display: flex; gap: 16px; margin: 10px 0;
    align-items: flex-start; justify-content: center;
    flex-wrap: wrap;
  }
  .flagBox {
    display: flex; flex-direction: column;
    align-items: center; gap: 6px;
    padding: 10px;
    background: rgba(0, 0, 0, 0.4);
    border-radius: 10px;
    border: 2px solid rgba(0, 200, 255, 0.3);
    pointer-events: all;
  }
  .flagBox.enemy { border-color: rgba(255, 61, 113, 0.3); }
  #flagCanvas1, #flagCanvas2 {
    cursor: crosshair; border-radius: 6px;
    background: #fff; display: block; image-rendering: pixelated;
  }
  .flagTools { display: flex; gap: 4px; flex-wrap: wrap; justify-content: center; max-width: 240px; }
  .colorSwatch {
    width: 22px; height: 22px; border-radius: 50%;
    cursor: pointer; border: 2px solid rgba(255, 255, 255, 0.4);
    transition: transform 0.12s;
  }
  .colorSwatch:hover { transform: scale(1.15); }
  .colorSwatch.active { border-color: #ffdd00; transform: scale(1.2); }
  .flagBtnRow { display: flex; gap: 6px; margin-top: 4px; }
  .flagBtnRow button {
    padding: 5px 10px; font-size: 11px; margin-top: 0;
    background: rgba(60, 70, 90, 0.8); color: #c0c8d0;
  }
  .flagBtnRow button:hover { background: rgba(80, 90, 110, 1); box-shadow: none; }
  .tabRow { display: flex; gap: 8px; margin-bottom: 12px; }
  .tabBtn {
    padding: 6px 20px; font-size: 13px; margin-top: 0;
    background: rgba(40, 50, 70, 0.8); color: #a0b0c0;
    border-radius: 8px 8px 0 0;
  }
  .tabBtn.active { background: linear-gradient(90deg, #00e5ff, #00a0ff); color: #001018; }
  .laneInfo {
    background: rgba(0, 200, 100, 0.15);
    border: 1px solid rgba(0, 255, 160, 0.5);
    border-radius: 8px; padding: 10px 18px; margin: 8px 0;
    font-size: 13px; color: #00ffa0; max-width: 600px;
  }
  .warningBanner {
    background: rgba(255, 200, 0, 0.15);
    border: 1px solid rgba(255, 200, 0, 0.7);
    color: #ffcc00; padding: 8px 16px; border-radius: 8px;
    font-size: 13px; margin: 8px 0; max-width: 640px;
  }
</style>
</head>
<body>
<div id="gameContainer">
  <canvas id="game" width="1000" height="600"></canvas>
  <div id="ui">
    <div class="screen" id="menuScreen">
      <h1>矛盾升级:皇室战争</h1>
      <h2>三 路 战 场 · 6 v 6</h2>

      <div class="tabRow">
        <button class="tabBtn active" id="tabNames" onclick="switchTab('names')">📝 编辑名字</button>
        <button class="tabBtn" id="tabFlags" onclick="switchTab('flags')">🎨 绘制旗帜</button>
      </div>

      <div id="namesTab">
        <div class="warningBanner" id="nameWarning">
          ⚠️ 必须填完 2 个阵营名 + 12 个成员名才能开始游戏!
        </div>

        <div class="alliance" style="border:2px solid #3d9bff;">
          <div class="allianceNameRow">
            <b style="color:#3d9bff;">🔵 我方阵营名:</b>
            <input type="text" id="allyName1" maxlength="10" placeholder="请输入阵营名" class="empty">
          </div>
          <div class="nameList" id="ally1List"></div>
        </div>

        <div class="alliance" style="border:2px solid #ff3d71;">
          <div class="allianceNameRow">
            <b style="color:#ff3d71;">🔴 敌方阵营名:</b>
            <input type="text" id="allyName2" maxlength="10" placeholder="请输入阵营名" class="enemy empty">
          </div>
          <div class="nameList" id="ally2List"></div>
        </div>
      </div>

      <div id="flagsTab" style="display:none;">
        <p>点击画布绘制你的阵营旗帜(16×16 像素)</p>
        <div class="flagEditPanel">
          <div class="flagBox">
            <b style="color:#3d9bff; font-size:13px;">🔵 我方阵营旗帜</b>
            <canvas id="flagCanvas1" width="192" height="192"></canvas>
            <div class="flagTools" id="tools1"></div>
            <div class="flagBtnRow">
              <button onclick="clearFlag(1)">清空</button>
              <button onclick="resetFlag(1)">默认</button>
            </div>
          </div>
          <div class="flagBox enemy">
            <b style="color:#ff3d71; font-size:13px;">🔴 敌方阵营旗帜</b>
            <canvas id="flagCanvas2" width="192" height="192"></canvas>
            <div class="flagTools" id="tools2"></div>
            <div class="flagBtnRow">
              <button onclick="clearFlag(2)">清空</button>
              <button onclick="resetFlag(2)">默认</button>
            </div>
          </div>
        </div>
      </div>

      <div class="laneInfo">
        ⚔️ <b>三路进攻:</b>敌方会从左桥、右桥、中路同时进攻!<br>
        中路敌人可直穿河流,需重点防守。
      </div>

      <button id="startBtn" onclick="startGame()" disabled>开始对战</button>

      <div class="controls-info">
        <b>操作:</b>点击卡牌选择 → 点击战场部署(仅限己方半场)<br>
        <b>资源:</b>自动恢复「能量」(10 上限),卡牌消耗能量<br>
        <b>目标:</b>摧毁敌方国王塔,或时间结束时比分占优<br>
        <b>时间:</b>3 分钟常规 + 1 分钟加时
      </div>
    </div>
    <div class="screen hidden" id="gameOverScreen">
      <h1 id="gameOverTitle">对战结束</h1>
      <p id="gameOverMsg"></p>
      <p id="gameOverStat"></p>
      <button onclick="backToMenu()">返回主菜单</button>
    </div>
  </div>
</div>
<script>
// ===================== 基础配置 =====================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;

const FIELD_TOP = 80;
const FIELD_BOTTOM = 500;
const MID_Y = FIELD_TOP + (FIELD_BOTTOM - FIELD_TOP) / 2;
const RIVER_Y = MID_Y - 20;
const RIVER_H = 40;

const LANES = {
  LEFT:  { id:'LEFT',  x: W * 0.20, name:'左路', bridgeX: W * 0.20 },
  MID:   { id:'MID',   x: W * 0.50, name:'中路', bridgeX: null, midLane: true },
  RIGHT: { id:'RIGHT', x: W * 0.80, name:'右路', bridgeX: W * 0.80 },
};
const BRIDGE_X1 = LANES.LEFT.bridgeX;
const BRIDGE_X2 = LANES.RIGHT.bridgeX;
const BRIDGE_W = 80;
const MID_CROSS_W = 120;
const TOWER_W = 60, TOWER_H = 70;
const KING_W = 80, KING_H = 90;

const NAMES = { ally1: ['','','','','',''], ally2: ['','','','','',''] };
const ALLIANCE_NAMES = { ally1: '', ally2: '' };
const ALLY1_COLORS = ['#3d9bff', '#00e5ff', '#00ffa0', '#66d9ff', '#a0e8ff', '#7fff7f'];
const ALLY2_COLORS = ['#ff3d71', '#ffaa00', '#ff6b9d', '#cc3300', '#a02050', '#ff8800'];

// ===================== 旗帜系统 =====================
const FLAG_SIZE = 16;
let flagData1 = null, flagData2 = null;
const PALETTE = [
  '#000000','#ffffff','#ff0000','#00ff00','#0000ff','#ffff00',
  '#ff00ff','#00ffff','#ff8800','#884400','#888888','#3d9bff',
  '#ff3d71','#00ffa0','#ffaa00','#a02050',
];
let currentColor1 = '#ff0000', currentColor2 = '#ff3d71';
let currentTool1 = 'pen', currentTool2 = 'pen';

function createEmptyFlag() {
  const f = [];
  for (let y = 0; y < FLAG_SIZE; y++) f.push(new Array(FLAG_SIZE).fill(null));
  return f;
}
function createDefaultFlag1() {
  const f = createEmptyFlag();
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) f[y][x] = '#3d9bff';
  const cx = 8, cy = 8;
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
    const d = Math.hypot(x - cx + 0.5, y - cy + 0.5);
    if (d < 3) f[y][x] = '#ffdd00';
    else if (d < 4) f[y][x] = '#ffaa00';
  }
  return f;
}
function createDefaultFlag2() {
  const f = createEmptyFlag();
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) f[y][x] = '#8a1a2f';
  for (let i = 2; i < 14; i++) { f[i][i] = '#ff3d71'; f[i][15-i] = '#ff3d71'; }
  for (let i = 4; i < 12; i++) { f[8][i] = '#ffaa00'; f[i][8] = '#ffaa00'; }
  return f;
}
function renderFlag(flagCanvas, flagData) {
  const fctx = flagCanvas.getContext('2d');
  const cellSize = flagCanvas.width / FLAG_SIZE;
  fctx.imageSmoothingEnabled = false;
  fctx.fillStyle = '#f0f0f0';
  fctx.fillRect(0, 0, flagCanvas.width, flagCanvas.height);
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
    if ((x + y) % 2 === 0) {
      fctx.fillStyle = '#e0e0e0';
      fctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
    }
  }
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
    const c = flagData[y][x];
    if (c) { fctx.fillStyle = c; fctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); }
  }
  fctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
  fctx.lineWidth = 1;
  for (let i = 0; i <= FLAG_SIZE; i++) {
    fctx.beginPath(); fctx.moveTo(i * cellSize, 0); fctx.lineTo(i * cellSize, flagCanvas.height); fctx.stroke();
    fctx.beginPath(); fctx.moveTo(0, i * cellSize); fctx.lineTo(flagCanvas.width, i * cellSize); fctx.stroke();
  }
}
function initFlagEditor() {
  flagData1 = createDefaultFlag1();
  flagData2 = createDefaultFlag2();
  renderFlag(document.getElementById('flagCanvas1'), flagData1);
  renderFlag(document.getElementById('flagCanvas2'), flagData2);
  buildPalette('tools1', 1);
  buildPalette('tools2', 2);
  bindFlagEvents('flagCanvas1', 1);
  bindFlagEvents('flagCanvas2', 2);
}
function buildPalette(containerId, which) {
  const c = document.getElementById(containerId);
  c.innerHTML = '';
  PALETTE.forEach(color => {
    const sw = document.createElement('div');
    sw.className = 'colorSwatch';
    sw.style.background = color;
    sw.addEventListener('click', () => {
      if (which === 1) currentColor1 = color; else currentColor2 = color;
      c.querySelectorAll('.colorSwatch').forEach(s => s.classList.remove('active'));
      sw.classList.add('active');
      if (which === 1) currentTool1 = 'pen'; else currentTool2 = 'pen';
    });
    c.appendChild(sw);
  });
  const tools = ['pen', 'eraser', 'fill'];
  const toolNames = { pen:'✏️画', eraser:'🧽擦', fill:'🪣填' };
  tools.forEach(t => {
    const btn = document.createElement('button');
    btn.textContent = toolNames[t];
    btn.style.cssText = 'padding:3px 8px; font-size:11px; margin-top:0; background:rgba(60,70,90,0.8); color:#fff;';
    btn.addEventListener('click', () => {
      if (which === 1) currentTool1 = t; else currentTool2 = t;
      c.querySelectorAll('button').forEach(b => b.style.background = 'rgba(60,70,90,0.8)');
      btn.style.background = 'linear-gradient(90deg,#00e5ff,#00ffa0)';
      btn.style.color = '#001018';
    });
    c.appendChild(btn);
  });
}
function bindFlagEvents(canvasId, which) {
  const c = document.getElementById(canvasId);
  let isDrawing = false;
  function getPixel(e) {
    const rect = c.getBoundingClientRect();
    const x = Math.floor((e.clientX - rect.left) / rect.width * FLAG_SIZE);
    const y = Math.floor((e.clientY - rect.top) / rect.height * FLAG_SIZE);
    if (x < 0 || x >= FLAG_SIZE || y < 0 || y >= FLAG_SIZE) return null;
    return { x, y };
  }
  function paintAt(p) {
    if (!p) return;
    const flagData = which === 1 ? flagData1 : flagData2;
    const tool = which === 1 ? currentTool1 : currentTool2;
    const color = which === 1 ? currentColor1 : currentColor2;
    if (tool === 'pen') flagData[p.y][p.x] = color;
    else if (tool === 'eraser') flagData[p.y][p.x] = null;
    else if (tool === 'fill') floodFill(flagData, p.x, p.y, flagData[p.y][p.x], color);
    renderFlag(c, flagData);
  }
  c.addEventListener('mousedown', e => { isDrawing = true; paintAt(getPixel(e)); });
  c.addEventListener('mousemove', e => {
    if (!isDrawing) return;
    if (which === 1 && currentTool1 === 'fill') return;
    if (which === 2 && currentTool2 === 'fill') return;
    paintAt(getPixel(e));
  });
  c.addEventListener('mouseup', () => isDrawing = false);
  c.addEventListener('mouseleave', () => isDrawing = false);
}
function floodFill(flag, x, y, targetColor, newColor) {
  if (targetColor === newColor) return;
  const stack = [[x, y]];
  const visited = new Set();
  while (stack.length > 0) {
    const [cx, cy] = stack.pop();
    if (cx < 0 || cx >= FLAG_SIZE || cy < 0 || cy >= FLAG_SIZE) continue;
    if (visited.has(cy * FLAG_SIZE + cx)) continue;
    visited.add(cy * FLAG_SIZE + cx);
    if (flag[cy][cx] !== targetColor) continue;
    flag[cy][cx] = newColor;
    stack.push([cx+1, cy], [cx-1, cy], [cx, cy+1], [cx, cy-1]);
  }
}
function clearFlag(which) {
  const flagData = which === 1 ? flagData1 : flagData2;
  for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) flagData[y][x] = null;
  renderFlag(document.getElementById(which === 1 ? 'flagCanvas1' : 'flagCanvas2'), flagData);
}
function resetFlag(which) {
  if (which === 1) flagData1 = createDefaultFlag1();
  else flagData2 = createDefaultFlag2();
  renderFlag(document.getElementById(which === 1 ? 'flagCanvas1' : 'flagCanvas2'),
    which === 1 ? flagData1 : flagData2);
}
function drawFlagOnGame(x, y, w, h, flagData, borderColor) {
  if (!flagData) return;
  const cellW = w / FLAG_SIZE;
  const cellH = h / FLAG_SIZE;
  ctx.fillStyle = borderColor;
  ctx.fillRect(x - 3, y - 3, w + 6, h + 6);
  ctx.fillStyle = '#f0f0f0';
  ctx.fillRect(x, y, w, h);
  for (let py = 0; py < FLAG_SIZE; py++) for (let px = 0; px < FLAG_SIZE; px++) {
    const c = flagData[py][px];
    if (c) { ctx.fillStyle = c; ctx.fillRect(x + px * cellW, y + py * cellH, cellW, cellH); }
  }
}
function switchTab(tab) {
  document.getElementById('namesTab').style.display = tab === 'names' ? 'block' : 'none';
  document.getElementById('flagsTab').style.display = tab === 'flags' ? 'block' : 'none';
  document.getElementById('tabNames').classList.toggle('active', tab === 'names');
  document.getElementById('tabFlags').classList.toggle('active', tab === 'flags');
}

// ===================== 名字编辑 =====================
function buildNameInputs() {
  const list1 = document.getElementById('ally1List');
  const list2 = document.getElementById('ally2List');
  list1.innerHTML = ''; list2.innerHTML = '';
  for (let i = 0; i < 6; i++) {
    const inp1 = document.createElement('input');
    inp1.type = 'text'; inp1.value = NAMES.ally1[i]; inp1.maxLength = 8;
    inp1.placeholder = '待填写';
    inp1.className = NAMES.ally1[i] ? '' : 'empty';
    inp1.addEventListener('input', () => {
      NAMES.ally1[i] = inp1.value.trim();
      inp1.className = NAMES.ally1[i] ? '' : 'empty';
      checkAllNames();
    });
    list1.appendChild(inp1);
    const inp2 = document.createElement('input');
    inp2.type = 'text';
    inp2.className = NAMES.ally2[i] ? 'enemy' : 'enemy empty';
    inp2.value = NAMES.ally2[i]; inp2.maxLength = 8;
    inp2.placeholder = '待填写';
    inp2.addEventListener('input', () => {
      NAMES.ally2[i] = inp2.value.trim();
      inp2.className = NAMES.ally2[i] ? 'enemy' : 'enemy empty';
      checkAllNames();
    });
    list2.appendChild(inp2);
  }
  const an1 = document.getElementById('allyName1');
  an1.value = ALLIANCE_NAMES.ally1;
  an1.className = ALLIANCE_NAMES.ally1 ? '' : 'empty';
  an1.addEventListener('input', () => {
    ALLIANCE_NAMES.ally1 = an1.value.trim();
    an1.className = ALLIANCE_NAMES.ally1 ? '' : 'empty';
    checkAllNames();
  });
  const an2 = document.getElementById('allyName2');
  an2.value = ALLIANCE_NAMES.ally2;
  an2.className = ALLIANCE_NAMES.ally2 ? 'enemy' : 'enemy empty';
  an2.addEventListener('input', () => {
    ALLIANCE_NAMES.ally2 = an2.value.trim();
    an2.className = ALLIANCE_NAMES.ally2 ? 'enemy' : 'enemy empty';
    checkAllNames();
  });
  checkAllNames();
}
function checkAllNames() {
  const allyName1OK = ALLIANCE_NAMES.ally1 && ALLIANCE_NAMES.ally1.trim().length > 0;
  const allyName2OK = ALLIANCE_NAMES.ally2 && ALLIANCE_NAMES.ally2.trim().length > 0;
  const mem1OK = NAMES.ally1.every(n => n && n.trim().length > 0);
  const mem2OK = NAMES.ally2.every(n => n && n.trim().length > 0);
  const allFilled = allyName1OK && allyName2OK && mem1OK && mem2OK;
  const btn = document.getElementById('startBtn');
  const warn = document.getElementById('nameWarning');
  if (allFilled) {
    btn.disabled = false;
    warn.style.background = 'rgba(0, 255, 160, 0.15)';
    warn.style.border = '1px solid rgba(0, 255, 160, 0.7)';
    warn.style.color = '#00ffa0';
    warn.textContent = '✅ 全部填写完成,可以开始对战!';
  } else {
    btn.disabled = true;
    warn.style.background = 'rgba(255, 200, 0, 0.15)';
    warn.style.border = '1px solid rgba(255, 200, 0, 0.7)';
    warn.style.color = '#ffcc00';
    const nameCount = (allyName1OK ? 1 : 0) + (allyName2OK ? 1 : 0);
    const memCount = NAMES.ally1.filter(n => n.trim()).length + NAMES.ally2.filter(n => n.trim()).length;
    warn.textContent = `⚠️ 必须填完 2 个阵营名 + 12 个成员名(阵营名 ${nameCount}/2,成员名 ${memCount}/12)`;
  }
}
function getOwnerName(side, index) {
  const list = side === 'player' ? NAMES.ally1 : NAMES.ally2;
  return list[index] || (side === 'player' ? '我方' : '敌方');
}

// ===================== 卡牌定义 =====================
const CARDS = {
  p_leader:      { idx:0, name:'领袖', cost:3, hp:200, atk:40, range:120, speed:0, type:'troop', color:ALLY1_COLORS[0], spawn:'ground', count:1, atkType:'bolt', atkColor:'#00e5ff' },
  p_leaderWar:   { idx:0, name:'战神领袖', cost:4, hp:400, atk:60, range:30, speed:50, type:'troop', color:'#4a5a70', spawn:'ground', count:1, atkType:'slash', atkColor:'#e0e8f0' },
  p_leaderMega:  { idx:0, name:'超级领袖', cost:7, hp:1500, atk:200, range:80, speed:25, type:'troop', color:ALLY1_COLORS[0], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#00e5ff' },
  p_1:      { idx:1, name:'神射', cost:3, hp:180, atk:45, range:100, speed:35, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, atkType:'arrow', atkColor:'#00e5ff' },
  p_1Sniper:{ idx:1, name:'狙击手', cost:4, hp:250, atk:90, range:200, speed:30, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, atkType:'snipe', atkColor:'#a0e8ff' },
  p_1Mega:  { idx:1, name:'超级狙击', cost:7, hp:1300, atk:220, range:160, speed:22, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#a0e8ff' },
  p_2:       { idx:2, name:'重装', cost:3, hp:220, atk:35, range:110, speed:25, type:'troop', color:ALLY1_COLORS[2], spawn:'ground', count:1, atkType:'bolt', atkColor:'#00ffa0' },
  p_2Shield: { idx:2, name:'盾牌', cost:3, hp:800, atk:0, range:0, speed:0, type:'building', color:ALLY1_COLORS[2], spawn:'ground', count:1 },
  p_2Drones: { idx:2, name:'无人机', cost:4, hp:80, atk:100, range:40, speed:80, type:'troop', color:ALLY1_COLORS[2], spawn:'air', count:6, atkType:'rapid', atkColor:'#00ffa0' },
  p_3:      { idx:3, name:'法师', cost:3, hp:190, atk:38, range:130, speed:30, type:'troop', color:ALLY1_COLORS[3], spawn:'ground', count:1, atkType:'magic', atkColor:'#66d9ff' },
  p_3Sci:   { idx:3, name:'科学官', cost:3, hp:180, atk:30, range:140, speed:35, type:'troop', color:'#ffffff', spawn:'ground', count:1, atkType:'beam', atkColor:'#00e5ff' },
  p_4:      { idx:4, name:'冰霜', cost:3, hp:210, atk:42, range:125, speed:28, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, atkType:'frost', atkColor:'#a0e8ff' },
  p_4Ice:   { idx:4, name:'寒冰', cost:4, hp:230, atk:75, range:180, speed:32, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, atkType:'frost', atkColor:'#e0f4ff' },
  p_4Mega:  { idx:4, name:'超极冰', cost:7, hp:1350, atk:200, range:140, speed:24, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#e0f4ff' },
  p_5:       { idx:5, name:'铁卫', cost:3, hp:230, atk:35, range:100, speed:32, type:'troop', color:ALLY1_COLORS[5], spawn:'ground', count:1, atkType:'bolt', atkColor:'#7fff7f' },
  p_5Shield: { idx:5, name:'大盾', cost:3, hp:850, atk:0, range:0, speed:0, type:'building', color:ALLY1_COLORS[5], spawn:'ground', count:1 },
  p_5Drones: { idx:5, name:'铁卫机群', cost:4, hp:90, atk:110, range:45, speed:85, type:'troop', color:ALLY1_COLORS[5], spawn:'air', count:5, atkType:'rapid', atkColor:'#7fff7f' },
  spellLaser: { idx:-1, name:'联盟激光', cost:4, type:'spell', damage:250, radius:80, color:'#00e5ff', owner:'联盟' },
  spellBomb:  { idx:-1, name:'联盟轰炸', cost:5, type:'spell', damage:350, radius:110, color:'#ff3d71', owner:'联盟' },
};
const ENEMY_CARDS = {
  e_leader:     { idx:0, name:'领袖', cost:2, hp:250, atk:30, range:30, speed:60, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, atkType:'slash', atkColor:'#ff8fa0' },
  e_leaderBrute:{ idx:0, name:'壮汉', cost:5, hp:900, atk:70, range:35, speed:45, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, atkType:'slash', atkColor:'#ff6060' },
  e_leaderMega: { idx:0, name:'超级', cost:8, hp:2000, atk:150, range:100, speed:30, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ff3d71' },
  e_1:      { idx:1, name:'疾跑', cost:3, hp:200, atk:50, range:100, speed:55, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, atkType:'arrow', atkColor:'#ffaa00' },
  e_1Run:   { idx:1, name:'冲锋', cost:3, hp:400, atk:40, range:30, speed:75, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, atkType:'slash', atkColor:'#ffd060' },
  e_1Mega:  { idx:1, name:'超级跑', cost:7, hp:1400, atk:180, range:90, speed:25, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ffaa00' },
  e_2:      { idx:2, name:'飞行', cost:3, hp:170, atk:60, range:150, speed:30, type:'troop', color:ALLY2_COLORS[2], spawn:'ground', count:1, atkType:'bolt', atkColor:'#ff6b9d' },
  e_2Fly:   { idx:2, name:'飞鹰', cost:4, hp:300, atk:50, range:50, speed:90, type:'troop', color:ALLY2_COLORS[2], spawn:'air', count:2, atkType:'rapid', atkColor:'#ff9fb0' },
  e_3:      { idx:3, name:'群攻', cost:3, hp:230, atk:38, range:110, speed:30, type:'troop', color:ALLY2_COLORS[3], spawn:'ground', count:1, atkType:'magic', atkColor:'#cc3300' },
  e_3Swarm: { idx:3, name:'蜂群', cost:4, hp:150, atk:20, range:30, speed:80, type:'troop', color:ALLY2_COLORS[3], spawn:'ground', count:3, atkType:'rapid', atkColor:'#ff5522' },
  e_4:      { idx:4, name:'重甲', cost:3, hp:220, atk:45, range:105, speed:38, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, atkType:'bolt', atkColor:'#a02050' },
  e_4Brute: { idx:4, name:'铁壁', cost:5, hp:950, atk:75, range:40, speed:42, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, atkType:'slash', atkColor:'#d04070' },
  e_4Mega:  { idx:4, name:'超级甲', cost:7, hp:1500, atk:190, range:95, speed:26, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#a02050' },
  e_5:      { idx:5, name:'空袭', cost:3, hp:200, atk:55, range:130, speed:35, type:'troop', color:ALLY2_COLORS[5], spawn:'ground', count:1, atkType:'arrow', atkColor:'#ff8800' },
  e_5Fly:   { idx:5, name:'轰炸机', cost:4, hp:320, atk:55, range:55, speed:88, type:'troop', color:ALLY2_COLORS[5], spawn:'air', count:2, atkType:'rapid', atkColor:'#ffaa66' },
  e_5Mega:  { idx:5, name:'超级炸', cost:7, hp:1450, atk:195, range:110, speed:24, type:'troop', color:ALLY2_COLORS[5], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ff8800' },
};
let cardDeck = Object.keys(CARDS);
let enemyDeck = Object.keys(ENEMY_CARDS);

// ===================== 游戏状态 =====================
let game = null;
function createGame() {
  return {
    state: 'playing',
    time: 180, overtime: false, overtimeTime: 60,
    elixir: 5, enemyElixir: 5, maxElixir: 10,
    elixirTimer: 0, enemyElixirTimer: 0,
    elixirRate: 1/2.8,
    playerHand: [], playerNext: null,
    enemyHand: [], enemyNext: null,
    units: [],
    // ===== 攻击特效容器 =====
    projectiles: [],   // 弹道
    beams: [],         // 光束
    slashes: [],       // 斩击
    hitSparks: [],     // 命中火花
    playerKing:  { x: W/2, y: FIELD_BOTTOM - 40, hp: 3000, maxHp: 3000, w: KING_W, h: KING_H, side:'player', type:'king', target:null, attackTimer:0, attackFlash:0 },
    playerTowerL:{ x: LANES.LEFT.x + 60, y: FIELD_BOTTOM - 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'player', type:'princess', lane:'LEFT', target:null, attackTimer:0, attackFlash:0 },
    playerTowerR:{ x: LANES.RIGHT.x - 60, y: FIELD_BOTTOM - 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'player', type:'princess', lane:'RIGHT', target:null, attackTimer:0, attackFlash:0 },
    enemyKing:   { x: W/2, y: FIELD_TOP + 40, hp: 3000, maxHp: 3000, w: KING_W, h: KING_H, side:'enemy', type:'king', target:null, attackTimer:0, attackFlash:0 },
    enemyTowerL: { x: LANES.LEFT.x + 60, y: FIELD_TOP + 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'enemy', type:'princess', lane:'LEFT', target:null, attackTimer:0, attackFlash:0 },
    enemyTowerR: { x: LANES.RIGHT.x - 60, y: FIELD_TOP + 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'enemy', type:'princess', lane:'RIGHT', target:null, attackTimer:0, attackFlash:0 },
    effects: [], floatingTexts: [],
    selectedCard: null,
    mouseX: 0, mouseY: 0,
    enemyAITimer: 0,
    lastTime: 0, totalTime: 0,
    playerCrowns: 0, enemyCrowns: 0, result: null,
    laneWarnings: { LEFT: 0, MID: 0, RIGHT: 0 },
    execution: null,
    screenShake: 0,
  };
}

// ===================== 全局入口 =====================
window.startGame = function() {
  const allyName1OK = ALLIANCE_NAMES.ally1 && ALLIANCE_NAMES.ally1.trim().length > 0;
  const allyName2OK = ALLIANCE_NAMES.ally2 && ALLIANCE_NAMES.ally2.trim().length > 0;
  const mem1OK = NAMES.ally1.every(n => n && n.trim().length > 0);
  const mem2OK = NAMES.ally2.every(n => n && n.trim().length > 0);
  if (!(allyName1OK && allyName2OK && mem1OK && mem2OK)) { checkAllNames(); return; }
  try { initAudio(); } catch(e){}
  document.getElementById('menuScreen').classList.add('hidden');
  game = createGame();
  initHands();
  requestAnimationFrame(gameLoop);
};
window.backToMenu = function() {
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('menuScreen').classList.remove('hidden');
  game = null;
  renderMenuBackground();
};
window.switchTab = switchTab;
window.clearFlag = clearFlag;
window.resetFlag = resetFlag;

// ===================== 音效 =====================
let audioCtx = null;
function initAudio() {
  if (!audioCtx) { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){} }
}
function playSound(type) {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.connect(g); g.connect(audioCtx.destination);
  const presets = {
    deploy:{w:'sine',f1:400,f2:600,d:0.15,v:0.08},
    hit:{w:'triangle',f1:300,f2:200,d:0.08,v:0.05},
    explode:{w:'sawtooth',f1:200,f2:40,d:0.4,v:0.15},
    tower:{w:'square',f1:150,f2:80,d:0.6,v:0.15},
    spell:{w:'sawtooth',f1:800,f2:100,d:0.5,v:0.12},
    victory:{w:'sine',f1:400,f2:1200,d:1,v:0.2},
    defeat:{w:'sine',f1:400,f2:100,d:1,v:0.2},
    warn:{w:'square',f1:600,f2:900,d:0.2,v:0.1},
    slash:{w:'sawtooth',f1:2000,f2:100,d:0.3,v:0.3},
    blood:{w:'sawtooth',f1:100,f2:20,d:0.8,v:0.2},
    bolt:{w:'square',f1:1200,f2:400,d:0.1,v:0.06},
    laser:{w:'sawtooth',f1:1500,f2:800,d:0.15,v:0.05},
    magic:{w:'sine',f1:600,f2:1200,d:0.2,v:0.05},
    frost:{w:'sine',f1:1800,f2:600,d:0.2,v:0.05},
    arrow:{w:'triangle',f1:800,f2:300,d:0.1,v:0.05},
    rapid:{w:'square',f1:900,f2:600,d:0.05,v:0.03},
    snipe:{w:'sawtooth',f1:2200,f2:200,d:0.2,v:0.12},
  };
  const p = presets[type]; if (!p) return;
  o.type = p.w;
  o.frequency.setValueAtTime(p.f1, t);
  o.frequency.exponentialRampToValueAtTime(p.f2, t + p.d);
  g.gain.setValueAtTime(p.v, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + p.d);
  o.start(t); o.stop(t + p.d);
}

// ===================== 手牌 =====================
function drawCard() {
  const remaining = cardDeck.filter(k => !game.playerHand.includes(k) && k !== game.playerNext);
  if (remaining.length === 0) return cardDeck[Math.floor(Math.random() * cardDeck.length)];
  return remaining[Math.floor(Math.random() * remaining.length)];
}
function drawEnemyCard() { return enemyDeck[Math.floor(Math.random() * enemyDeck.length)]; }
function initHands() {
  game.playerHand = [];
  for (let i = 0; i < 4; i++) game.playerHand.push(drawCard());
  game.playerNext = drawCard();
  game.enemyHand = [];
  for (let i = 0; i < 4; i++) game.enemyHand.push(drawEnemyCard());
  game.enemyNext = drawEnemyCard();
}
function getCardDisplayName(key, side) {
  const def = side === 'player' ? CARDS[key] : ENEMY_CARDS[key];
  if (!def) return '';
  if (def.idx === undefined || def.idx < 0) return def.name;
  const ownerName = getOwnerName(side, def.idx);
  if (def.name === '领袖') return ownerName;
  return ownerName + '·' + def.name;
}
function getCardOwner(key, side) {
  const def = side === 'player' ? CARDS[key] : ENEMY_CARDS[key];
  if (!def) return '';
  if (def.idx === undefined || def.idx < 0) return def.owner || '联盟';
  return getOwnerName(side, def.idx);
}
function getLaneOfUnit(u) {
  if (u.lane) return u.lane;
  if (u.x < W * 0.35) return 'LEFT';
  if (u.x > W * 0.65) return 'RIGHT';
  return 'MID';
}
function canCrossAt(x, y, flying) {
  if (flying) return true;
  if (Math.abs(y - MID_Y) > RIVER_H + 20) return true;
  const inLeftBridge = Math.abs(x - BRIDGE_X1) < BRIDGE_W / 2;
  const inRightBridge = Math.abs(x - BRIDGE_X2) < BRIDGE_W / 2;
  const inMid = Math.abs(x - W / 2) < MID_CROSS_W / 2;
  return inLeftBridge || inRightBridge || inMid;
}

// ===================== 游戏循环 =====================
function gameLoop(timestamp) {
  if (!game) return;
  if (!game.lastTime) game.lastTime = timestamp;
  const dt = Math.min((timestamp - game.lastTime) / 1000, 0.05);
  game.lastTime = timestamp;
  if (game.state === 'playing') update(dt);
  else if (game.state === 'execution') updateExecution(dt);
  render();
  requestAnimationFrame(gameLoop);
}

function update(dt) {
  game.totalTime += dt;
  if (game.screenShake > 0) game.screenShake -= dt;
  for (const k in game.laneWarnings) { if (game.laneWarnings[k] > 0) game.laneWarnings[k] -= dt; }
  if (!game.overtime) {
    game.time -= dt;
    if (game.time <= 0) {
      game.time = 0;
      if (game.playerCrowns === game.enemyCrowns) game.overtime = true;
      else { endGame(); return; }
    }
  } else {
    game.overtimeTime -= dt;
    if (game.overtimeTime <= 0) { endGame(); return; }
  }
  game.elixirTimer += dt;
  if (game.elixirTimer >= game.elixirRate) { game.elixirTimer = 0; if (game.elixir < game.maxElixir) game.elixir++; }
  game.enemyElixirTimer += dt;
  if (game.enemyElixirTimer >= game.elixirRate) { game.enemyElixirTimer = 0; if (game.enemyElixir < game.maxElixir) game.enemyElixir++; }
  updateUnits(dt);
  updateTowers(dt);
  updateEnemyAI(dt);
  updateProjectiles(dt);
  updateBeams(dt);
  updateSlashes(dt);
  updateHitSparks(dt);
  updateEffects(dt);
  updateFloatingTexts(dt);
  if (game.enemyKing.hp <= 0 || game.playerKing.hp <= 0) endGame();
}

// ===================== 单位逻辑 =====================
function spawnUnit(cardKey, x, y, side, forcedLane) {
  const def = side === 'player' ? CARDS[cardKey] : ENEMY_CARDS[cardKey];
  if (!def) return null;
  const count = def.count || 1;
  for (let i = 0; i < count; i++) {
    const offsetX = count > 1 ? (i - (count-1)/2) * 25 : 0;
    const offsetY = count > 1 ? (i % 2 === 0 ? -15 : 15) : 0;
    const ux = x + offsetX, uy = y + offsetY;
    let lane = forcedLane;
    if (!lane) {
      if (ux < W * 0.35) lane = 'LEFT';
      else if (ux > W * 0.65) lane = 'RIGHT';
      else lane = 'MID';
    }
    game.units.push({
      key: cardKey, def, side, lane,
      x: ux, y: uy,
      hp: def.hp, maxHp: def.hp,
      atk: def.atk, range: def.range, speed: def.speed,
      attackTimer: 0, attackInterval: 1.0,
      target: null, dead: false, hitFlash: 0,
      flying: def.spawn === 'air',
      color: def.color, big: def.big || false,
      owner: getCardOwner(cardKey, side),
      displayName: getCardDisplayName(cardKey, side),
      atkType: def.atkType || 'bolt',
      atkColor: def.atkColor || def.color,
      facing: side === 'player' ? -1 : 1, // 1=朝下,-1=朝上
      attackAnimTimer: 0,
      // 自动计算朝向:目标在其上方为 1,下方为 -1
    });
  }
  playSound('deploy');
  return true;
}

function deployCard(handIndex, x, y) {
  const cardKey = game.playerHand[handIndex];
  if (!cardKey) return false;
  const def = CARDS[cardKey];
  if (def.type === 'spell') {
    if (game.elixir < def.cost) return false;
    if (y < FIELD_TOP || y > FIELD_BOTTOM) return false;
    game.elixir -= def.cost;
    castSpell(cardKey, x, y, 'player');
    game.playerHand[handIndex] = game.playerNext;
    game.playerNext = drawCard();
    game.selectedCard = null;
    return true;
  }
  if (y < MID_Y + 10) return false;
  if (y < FIELD_TOP || y > FIELD_BOTTOM) return false;
  if (game.elixir < def.cost) return false;
  let lane;
  if (x < W * 0.35) lane = 'LEFT';
  else if (x > W * 0.65) lane = 'RIGHT';
  else lane = 'MID';
  if (lane === 'MID' && !def.flying && y < MID_Y + RIVER_H) {
    if (y > MID_Y - RIVER_H / 2 && y < MID_Y + RIVER_H / 2) {
      if (!canCrossAt(x, y, false)) return false;
    }
  }
  if (!spawnUnit(cardKey, x, y, 'player', lane)) return false;
  game.elixir -= def.cost;
  game.playerHand[handIndex] = game.playerNext;
  game.playerNext = drawCard();
  game.selectedCard = null;
  return true;
}

function castSpell(cardKey, x, y, side) {
  const def = side === 'player' ? CARDS[cardKey] : ENEMY_CARDS[cardKey];
  playSound('spell');
  addEffect(x, y, 'spell', def.color, 0.6);
  for (const u of game.units) {
    if (u.dead) continue;
    if (side === 'player' && u.side !== 'enemy') continue;
    if (side === 'enemy' && u.side !== 'player') continue;
    if (Math.hypot(u.x - x, u.y - y) <= def.radius) damageUnit(u, def.damage);
  }
  const towers = side === 'player'
    ? [game.enemyKing, game.enemyTowerL, game.enemyTowerR]
    : [game.playerKing, game.playerTowerL, game.playerTowerR];
  for (const t of towers) {
    if (t.hp <= 0) continue;
    if (Math.hypot(t.x - x, t.y - y) <= def.radius) damageTower(t, def.damage);
  }
}

function damageUnit(u, dmg) {
  if (u.dead) return;
  u.hp -= dmg;
  u.hitFlash = 0.15;
  spawnHitSparks(u.x, u.y, u.side === 'player' ? '#66d9ff' : '#ff8fa0');
  addFloatingText(u.x, u.y - 20, '-' + Math.round(dmg), '#ff6b6b');
  if (u.hp <= 0) {
    u.dead = true;
    addEffect(u.x, u.y, 'boom', u.color, 0.4);
    playSound('hit');
  }
}

function damageTower(t, dmg) {
  if (t.hp <= 0) return;
  t.hp -= dmg;
  t.attackFlash = 0.15;
  spawnHitSparks(t.x, t.y, t.side === 'player' ? '#66d9ff' : '#ff8fa0');
  addFloatingText(t.x, t.y - 40, '-' + Math.round(dmg), '#ff6b6b');
  if (t.hp <= 0) {
    t.hp = 0;
    playSound('tower');
    addEffect(t.x, t.y, 'explosion', '#ff3d71', 0.8);
    game.screenShake = 0.5;
    if (t.side === 'player') game.enemyCrowns++;
    else game.playerCrowns++;
  }
}

// ===================== 攻击特效生成 =====================
function spawnAttackEffect(attacker, target, isTowerTarget) {
  const atkType = attacker.atkType || 'bolt';
  const color = attacker.atkColor || attacker.color;
  const sx = attacker.x, sy = attacker.y;
  const tx = target.x, ty = target.y;

  // 攻击动画
  attacker.attackAnimTimer = 0.25;

  // 根据类型生成不同特效
  switch (atkType) {
    case 'bolt':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.25,
        color, size: 5, type: 'bolt', dead: false,
      });
      playSound('bolt');
      break;
    case 'arrow':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.3,
        color, size: 4, type: 'arrow', dead: false,
        angle: Math.atan2(ty - sy, tx - sx),
      });
      playSound('arrow');
      break;
    case 'snipe':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.15,
        color, size: 6, type: 'snipe', dead: false,
      });
      playSound('snipe');
      break;
    case 'laser':
      game.beams.push({
        x1: sx, y1: sy, x2: tx, y2: ty,
        color, width: 8, life: 0.25, maxLife: 0.25, type: 'laser', dead: false,
      });
      playSound('laser');
      game.screenShake = Math.max(game.screenShake, 0.08);
      break;
    case 'beam':
      game.beams.push({
        x1: sx, y1: sy, x2: tx, y2: ty,
        color, width: 5, life: 0.2, maxLife: 0.2, type: 'beam', dead: false,
      });
      playSound('laser');
      break;
    case 'slash':
      game.slashes.push({
        x: tx, y: ty, angle: Math.atan2(ty - sy, tx - sx),
        color, life: 0.25, maxLife: 0.25, size: 40, dead: false,
      });
      playSound('slash');
      break;
    case 'magic':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.35,
        color, size: 7, type: 'magic', dead: false,
        spin: 0,
      });
      playSound('magic');
      break;
    case 'frost':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.3,
        color, size: 6, type: 'frost', dead: false,
      });
      playSound('frost');
      break;
    case 'rapid':
      game.projectiles.push({
        x: sx, y: sy, sx, sy, tx, ty,
        target, t: 0, dur: 0.12,
        color, size: 3, type: 'rapid', dead: false,
      });
      playSound('rapid');
      break;
  }
}

function updateProjectiles(dt) {
  for (const p of game.projectiles) {
    if (p.dead) continue;
    p.t += dt;
    if (p.t >= p.dur) { p.dead = true; continue; }
    const t = p.t / p.dur;
    p.x = p.sx + (p.tx - p.sx) * t;
    p.y = p.sy + (p.ty - p.sy) * t;
    if (p.type === 'magic') p.spin = (p.spin || 0) + dt * 15;
  }
  game.projectiles = game.projectiles.filter(p => !p.dead);
}

function updateBeams(dt) {
  for (const b of game.beams) {
    if (b.dead) continue;
    b.life -= dt;
    if (b.life <= 0) b.dead = true;
  }
  game.beams = game.beams.filter(b => !b.dead);
}

function updateSlashes(dt) {
  for (const s of game.slashes) {
    if (s.dead) continue;
    s.life -= dt;
    if (s.life <= 0) s.dead = true;
  }
  game.slashes = game.slashes.filter(s => !s.dead);
}

function spawnHitSparks(x, y, color) {
  const count = 6;
  for (let i = 0; i < count; i++) {
    const ang = Math.random() * Math.PI * 2;
    const spd = 60 + Math.random() * 120;
    game.hitSparks.push({
      x, y,
      vx: Math.cos(ang) * spd,
      vy: Math.sin(ang) * spd,
      size: 2 + Math.random() * 3,
      color, life: 0.4, maxLife: 0.4, dead: false,
    });
  }
}

function updateHitSparks(dt) {
  for (const s of game.hitSparks) {
    if (s.dead) continue;
    s.x += s.vx * dt;
    s.y += s.vy * dt;
    s.vx *= 0.9;
    s.vy *= 0.9;
    s.life -= dt;
    if (s.life <= 0) s.dead = true;
  }
  game.hitSparks = game.hitSparks.filter(s => !s.dead);
}

// ===================== 单位攻击循环 =====================
function updateUnits(dt) {
  for (const u of game.units) {
    if (u.dead) continue;
    if (u.hitFlash > 0) u.hitFlash -= dt;
    if (u.attackTimer > 0) u.attackTimer -= dt;
    if (u.attackAnimTimer > 0) u.attackAnimTimer -= dt;
    let target = null, minDist = Infinity;
    const towers = u.side === 'player'
      ? [game.enemyTowerL, game.enemyTowerR, game.enemyKing]
      : [game.playerTowerL, game.playerTowerR, game.playerKing];
    for (const t of towers) {
      if (t.hp <= 0) continue;
      if (t.type === 'king' && u.side === 'player' && game.enemyTowerL.hp > 0 && game.enemyTowerR.hp > 0) continue;
      if (t.type === 'king' && u.side === 'enemy' && game.playerTowerL.hp > 0 && game.playerTowerR.hp > 0) continue;
      const d = Math.hypot(t.x - u.x, t.y - u.y);
      if (d < minDist) { minDist = d; target = { type: 'tower', obj: t }; }
    }
    for (const other of game.units) {
      if (other === u || other.dead || other.side === u.side) continue;
      const d = Math.hypot(other.x - u.x, other.y - u.y);
      if (d < minDist) { minDist = d; target = { type: 'unit', obj: other }; }
    }
    u.target = target;
    if (!target) continue;

    // 朝向
    u.facing = target.obj.y > u.y ? 1 : -1;

    const tx = target.obj.x, ty = target.obj.y;
    const dist = Math.hypot(tx - u.x, ty - u.y);
    const attackRange = u.range + (target.type === 'tower' ? 30 : 15);
    if (dist <= attackRange && u.atk > 0) {
      if (u.attackTimer <= 0) {
        u.attackTimer = u.attackInterval;
        // 生成攻击特效
        spawnAttackEffect(u, target.obj, target.type === 'tower');
        // 延迟造成伤害(让特效先飞过去)
        const delay = u.atkType === 'slash' ? 0.15 : 0.2;
        const dmgTarget = target.obj;
        const dmgAmount = u.atk;
        setTimeout(() => {
          if (!game || game.state !== 'playing') return;
          if (target.type === 'tower') damageTower(dmgTarget, dmgAmount);
          else damageUnit(dmgTarget, dmgAmount);
        }, delay * 1000);
      }
    } else if (u.speed > 0) {
      const dx = tx - u.x, dy = ty - u.y;
      const len = Math.hypot(dx, dy) || 1;
      let nx = dx / len, ny = dy / len;
      if (!u.flying && !canCrossAt(u.x, u.y, false) && (u.y < MID_Y + RIVER_H / 2 && u.y > MID_Y - RIVER_H / 2)) {
        const leftDist = Math.abs(u.x - BRIDGE_X1);
        const rightDist = Math.abs(u.x - BRIDGE_X2);
        const midDist = Math.abs(u.x - W/2);
        const minD = Math.min(leftDist, rightDist, midDist);
        let targetX;
        if (minD === leftDist) targetX = BRIDGE_X1;
        else if (minD === rightDist) targetX = BRIDGE_X2;
        else targetX = W/2;
        const bdx = targetX - u.x;
        if (Math.abs(bdx) > 5) { nx = bdx > 0 ? 1 : -1; ny = 0; }
        else { ny = u.side === 'player' ? -1 : 1; nx = 0; }
      }
      u.x += nx * u.speed * dt;
      u.y += ny * u.speed * dt;
      u.x = Math.max(30, Math.min(W - 30, u.x));
      u.y = Math.max(FIELD_TOP + 20, Math.min(FIELD_BOTTOM - 20, u.y));
      if (u.x < W * 0.35) u.lane = 'LEFT';
      else if (u.x > W * 0.65) u.lane = 'RIGHT';
      else u.lane = 'MID';
    }
  }
  game.units = game.units.filter(u => !u.dead);
}

function updateTowers(dt) {
  const allTowers = [game.playerKing, game.playerTowerL, game.playerTowerR, game.enemyKing, game.enemyTowerL, game.enemyTowerR];
  for (const t of allTowers) {
    if (t.hp <= 0) continue;
    if (t.attackFlash > 0) t.attackFlash -= dt;
    t.attackTimer = (t.attackTimer || 0) - dt;
    if (t.attackTimer > 0) continue;
    const range = t.type === 'king' ? 180 : 150;
    const dmg = t.type === 'king' ? 90 : 60;
    for (const u of game.units) {
      if (u.dead) continue;
      if (t.side === 'player' && u.side !== 'enemy') continue;
      if (t.side === 'enemy' && u.side !== 'player') continue;
      if (Math.hypot(u.x - t.x, u.y - t.y) <= range) {
        // 塔的攻击特效
        const towerColor = t.side === 'player' ? '#3d9bff' : '#ff3d71';
        game.beams.push({
          x1: t.x, y1: t.y, x2: u.x, y2: u.y,
          color: towerColor, width: 4, life: 0.2, maxLife: 0.2, type: 'tower', dead: false,
        });
        spawnHitSparks(u.x, u.y, towerColor);
        damageUnit(u, dmg);
        t.attackTimer = 1.0;
        t.attackFlash = 0.2;
        addEffect(u.x, u.y, 'hit', '#fff', 0.2);
        break;
      }
    }
  }
}

// ===================== 敌方 AI =====================
function updateEnemyAI(dt) {
  game.enemyAITimer += dt;
  if (game.enemyAITimer < 1.5) return;
  game.enemyAITimer = 0;
  let bestKey = null, bestCost = Infinity;
  for (const k of game.enemyHand) {
    const def = ENEMY_CARDS[k];
    if (def && def.cost <= game.enemyElixir && def.cost < bestCost) { bestCost = def.cost; bestKey = k; }
  }
  if (game.enemyElixir >= 8) {
    for (const k of game.enemyHand) {
      const def = ENEMY_CARDS[k];
      if (def && def.cost === 8 && def.cost <= game.enemyElixir) { bestKey = k; bestCost = 8; break; }
    }
  }
  if (!bestKey) return;
  const def = ENEMY_CARDS[bestKey];
  if (!def) return;
  const laneStrength = { LEFT: 0, MID: 0, RIGHT: 0 };
  for (const u of game.units) {
    if (u.side !== 'player' || u.dead) continue;
    laneStrength[getLaneOfUnit(u)] += u.hp + u.atk * 10;
  }
  const towerHp = { LEFT: game.playerTowerL.hp, MID: game.playerKing.hp, RIGHT: game.playerTowerR.hp };
  let chosenLane;
  if (def.spawn === 'air') {
    chosenLane = ['LEFT', 'MID', 'RIGHT'][Math.floor(Math.random() * 3)];
  } else {
    const weakness = {
      LEFT: (1 - towerHp.LEFT / 1400) * 100 + (100 - Math.min(laneStrength.LEFT, 100)) + Math.random() * 30,
      MID:  (1 - towerHp.MID / 3000) * 50 + (100 - Math.min(laneStrength.MID, 100)) + Math.random() * 30,
      RIGHT:(1 - towerHp.RIGHT / 1400) * 100 + (100 - Math.min(laneStrength.RIGHT, 100)) + Math.random() * 30,
    };
    if (Math.random() < 0.7) {
      chosenLane = Object.keys(weakness).reduce((a, b) => weakness[a] > weakness[b] ? a : b);
    } else {
      chosenLane = ['LEFT', 'MID', 'RIGHT'][Math.floor(Math.random() * 3)];
    }
  }
  game.laneWarnings[chosenLane] = 1.5;
  const laneX = LANES[chosenLane].x;
  const x = laneX + (Math.random() - 0.5) * 40;
  const y = MID_Y - 40 - Math.random() * 30;
  if (def.type === 'spell') {
    let target = null;
    for (const u of game.units) {
      if (u.side !== 'player' || u.dead) continue;
      if (!target || u.y < target.y) target = u;
    }
    if (target) {
      castSpell(bestKey, target.x, target.y, 'enemy');
      game.enemyElixir -= def.cost;
      const idx = game.enemyHand.indexOf(bestKey);
      if (idx >= 0) { game.enemyHand[idx] = game.enemyNext; game.enemyNext = drawEnemyCard(); }
    }
  } else {
    spawnUnit(bestKey, x, y, 'enemy', chosenLane);
    game.enemyElixir -= def.cost;
    const idx = game.enemyHand.indexOf(bestKey);
    if (idx >= 0) { game.enemyHand[idx] = game.enemyNext; game.enemyNext = drawEnemyCard(); }
  }
}

// ===================== 特效管理 =====================
function addFloatingText(x, y, text, color) {
  game.floatingTexts.push({ x, y, text, color, life: 1, vy: -50, dead: false });
}
function updateFloatingTexts(dt) {
  for (const f of game.floatingTexts) {
    f.y += f.vy * dt; f.life -= dt * 1.5;
    if (f.life <= 0) f.dead = true;
  }
  game.floatingTexts = game.floatingTexts.filter(f => !f.dead);
}
function addEffect(x, y, type, color, life) {
  game.effects.push({ x, y, type, color, life, maxLife: life, radius: 0 });
}
function updateEffects(dt) {
  for (const e of game.effects) {
    e.life -= dt;
    if (e.type === 'spell') e.radius = (1 - e.life / e.maxLife) * 100;
    if (e.type === 'boom') e.radius = (1 - e.life / e.maxLife) * 40;
    if (e.type === 'explosion') e.radius = (1 - e.life / e.maxLife) * 80;
    if (e.life <= 0) e.dead = true;
  }
  game.effects = game.effects.filter(e => !e.dead);
}

// ===================== 处决动画 =====================
function startExecution(victimSide, winnerSide) {
  const victimKing = victimSide === 'player' ? game.playerKing : game.enemyKing;
  game.state = 'execution';
  game.execution = {
    timer: 0, victimSide, winnerSide,
    victimX: victimKing.x, victimY: victimKing.y,
    executioner: {
      x: W/2, y: winnerSide === 'player' ? FIELD_BOTTOM - 40 : FIELD_TOP + 40,
      startX: W/2, startY: winnerSide === 'player' ? FIELD_BOTTOM - 40 : FIELD_TOP + 40,
      endX: victimKing.x, endY: victimKing.y - 40,
    },
    bloodParticles: [],
    phase: 0, phaseTimer: 0,
    headFalling: null,
    showTitle: false, titleText: '', titleSub: '',
    shakeTime: 0,
  };
  playSound('slash');
}
function updateExecution(dt) {
  const ex = game.execution;
  if (!ex) return;
  ex.timer += dt; ex.phaseTimer += dt;
  if (ex.phase === 0) {
    if (ex.phaseTimer >= 1.5) { ex.phase = 1; ex.phaseTimer = 0; playSound('warn'); }
  } else if (ex.phase === 1) {
    const t = Math.min(1, ex.phaseTimer / 1.2);
    const ease = 1 - Math.pow(1 - t, 3);
    ex.executioner.x = ex.executioner.startX + (ex.executioner.endX - ex.executioner.startX) * ease;
    ex.executioner.y = ex.executioner.startY + (ex.executioner.endY - ex.executioner.startY) * ease;
    if (t >= 1) { ex.phase = 2; ex.phaseTimer = 0; playSound('slash'); }
  } else if (ex.phase === 2) {
    if (ex.phaseTimer >= 0.5) {
      ex.phase = 3; ex.phaseTimer = 0;
      playSound('blood');
      for (let i = 0; i < 40; i++) {
        const ang = Math.random() * Math.PI * 2;
        const spd = 100 + Math.random() * 250;
        ex.bloodParticles.push({
          x: ex.victimX, y: ex.victimY - 20,
          vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd - 80,
          size: 3 + Math.random() * 6,
          life: 1.5, maxLife: 1.5,
          color: Math.random() < 0.7 ? '#8a0020' : '#cc0033',
        });
      }
      ex.headFalling = {
        x: ex.victimX, y: ex.victimY - 20,
        vx: (Math.random() - 0.5) * 100, vy: -150, rot: 0,
        vrot: (Math.random() - 0.5) * 10,
      };
      ex.shakeTime = 0.8;
    }
  } else if (ex.phase === 3) {
    for (const p of ex.bloodParticles) {
      p.x += p.vx * dt; p.y += p.vy * dt;
      p.vy += 400 * dt; p.life -= dt;
    }
    ex.bloodParticles = ex.bloodParticles.filter(p => p.life > 0);
    if (ex.headFalling) {
      ex.headFalling.x += ex.headFalling.vx * dt;
      ex.headFalling.y += ex.headFalling.vy * dt;
      ex.headFalling.vy += 600 * dt;
      ex.headFalling.rot += ex.headFalling.vrot * dt;
    }
    if (ex.shakeTime > 0) ex.shakeTime -= dt;
    if (ex.phaseTimer >= 2.0) {
      ex.phase = 4; ex.phaseTimer = 0; ex.showTitle = true;
      const winnerName = ex.winnerSide === 'player' ? ALLIANCE_NAMES.ally1 : ALLIANCE_NAMES.ally2;
      const loserName = ex.victimSide === 'player' ? ALLIANCE_NAMES.ally1 : ALLIANCE_NAMES.ally2;
      ex.titleText = winnerName + ' 胜利!';
      ex.titleSub = loserName + ' 已被处决';
    }
  } else if (ex.phase === 4) {
    for (const p of ex.bloodParticles) {
      p.x += p.vx * dt; p.y += p.vy * dt;
      p.vy += 400 * dt; p.life -= dt;
    }
    ex.bloodParticles = ex.bloodParticles.filter(p => p.life > 0);
    if (ex.headFalling) {
      ex.headFalling.x += ex.headFalling.vx * dt;
      ex.headFalling.y += ex.headFalling.vy * dt;
      ex.headFalling.vy += 600 * dt;
      ex.headFalling.rot += ex.headFalling.vrot * dt;
    }
    if (ex.phaseTimer >= 2.5) finalizeEndGame(ex.winnerSide);
  }
}
function finalizeEndGame(winnerSide) {
  game.state = 'ended';
  const result = winnerSide === 'player' ? 'victory' : 'defeat';
  game.result = result;
  const titleEl = document.getElementById('gameOverTitle');
  const msg = winnerSide === 'player'
    ? ALLIANCE_NAMES.ally1 + ' 通过处决赢得了胜利!'
    : ALLIANCE_NAMES.ally2 + ' 处决了' + ALLIANCE_NAMES.ally1 + '!';
  if (result === 'victory') {
    titleEl.textContent = ALLIANCE_NAMES.ally1 + ' 胜利!';
    titleEl.style.background = 'linear-gradient(90deg,#00ffa0,#00e5ff)';
  } else {
    titleEl.textContent = ALLIANCE_NAMES.ally2 + ' 胜利!';
    titleEl.style.background = 'linear-gradient(90deg,#ff3d71,#ff6b6b)';
  }
  titleEl.style.webkitBackgroundClip = 'text';
  titleEl.style.webkitTextFillColor = 'transparent';
  document.getElementById('gameOverMsg').textContent = msg;
  document.getElementById('gameOverStat').textContent =
    `皇冠比分:${ALLIANCE_NAMES.ally1} ${game.playerCrowns} - ${game.enemyCrowns} ${ALLIANCE_NAMES.ally2}`;
  document.getElementById('gameOverScreen').classList.remove('hidden');
}

// ===================== 渲染 =====================
function render() {
  ctx.save();
  if (game && game.execution && game.execution.shakeTime > 0) {
    const k = game.execution.shakeTime * 15;
    ctx.translate((Math.random() - 0.5) * k, (Math.random() - 0.5) * k);
  }
  if (game && game.screenShake > 0) {
    const k = game.screenShake * 20;
    ctx.translate((Math.random() - 0.5) * k, (Math.random() - 0.5) * k);
  }
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, '#1a2040');
  grad.addColorStop(0.5, '#0d1525');
  grad.addColorStop(1, '#1a2040');
  ctx.fillStyle = grad;
  ctx.fillRect(-40, -40, W + 80, H + 80);
  if (!game) { ctx.restore(); return; }

  drawField();
  drawTower(game.enemyKing, '敌', game.execution && game.execution.victimSide === 'enemy');
  drawTower(game.enemyTowerL, '敌', false);
  drawTower(game.enemyTowerR, '敌', false);
  drawTower(game.playerKing, '我', game.execution && game.execution.victimSide === 'player');
  drawTower(game.playerTowerL, '我', false);
  drawTower(game.playerTowerR, '我', false);

  if (game.state !== 'execution') {
    for (const u of game.units) drawUnit(u);
  } else {
    ctx.globalAlpha = 0.35;
    for (const u of game.units) drawUnit(u);
    ctx.globalAlpha = 1;
  }

  // ===== 攻击特效渲染(单位上方) =====
  drawSlashes();
  drawProjectiles();
  drawBeams();
  drawHitSparks();

  drawEffects();
  drawFloatingTexts();
  drawLaneWarnings();
  if (game.execution) drawExecution();
  drawTopBar();
  if (game.state !== 'execution') { drawCards(); drawElixirBar(); drawDeployPreview(); }
  ctx.restore();
}

// ===== 弹道绘制 =====
function drawProjectiles() {
  for (const p of game.projectiles) {
    if (p.dead) continue;
    ctx.save();
    ctx.shadowColor = p.color;
    ctx.shadowBlur = 15;
    const t = p.t / p.dur;
    if (p.type === 'bolt') {
      // 能量球
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fill();
      // 拖尾
      ctx.globalAlpha = 0.6;
      ctx.beginPath();
      ctx.arc(p.x - (p.tx - p.sx) * 0.05, p.y - (p.ty - p.sy) * 0.05, p.size * 0.7, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 0.3;
      ctx.beginPath();
      ctx.arc(p.x - (p.tx - p.sx) * 0.1, p.y - (p.ty - p.sy) * 0.1, p.size * 0.4, 0, Math.PI * 2);
      ctx.fill();
    } else if (p.type === 'arrow') {
      // 箭矢
      ctx.translate(p.x, p.y);
      ctx.rotate(p.angle);
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.moveTo(-12, 0);
      ctx.lineTo(8, -3);
      ctx.lineTo(12, 0);
      ctx.lineTo(8, 3);
      ctx.closePath();
      ctx.fill();
      // 箭头光
      ctx.globalAlpha = 0.5;
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(12, 0, 2, 0, Math.PI * 2);
      ctx.fill();
    } else if (p.type === 'snipe') {
      // 狙击弹:长条形
      const ang = Math.atan2(p.ty - p.sy, p.tx - p.sx);
      ctx.translate(p.x, p.y);
      ctx.rotate(ang);
      ctx.fillStyle = p.color;
      ctx.fillRect(-20, -2, 30, 4);
      ctx.fillStyle = '#fff';
      ctx.fillRect(5, -3, 6, 6);
    } else if (p.type === 'magic') {
      // 魔法弹:旋转六角星
      ctx.translate(p.x, p.y);
      ctx.rotate(p.spin || 0);
      ctx.fillStyle = p.color;
      ctx.beginPath();
      for (let i = 0; i < 6; i++) {
        const ang = i * Math.PI / 3;
        const r = i % 2 === 0 ? p.size : p.size * 0.5;
        const px = Math.cos(ang) * r;
        const py = Math.sin(ang) * r;
        if (i === 0) ctx.moveTo(px, py);
        else ctx.lineTo(px, py);
      }
      ctx.closePath();
      ctx.fill();
      // 中心光
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(0, 0, p.size * 0.3, 0, Math.PI * 2);
      ctx.fill();
    } else if (p.type === 'frost') {
      // 霜冻弹:雪花
      ctx.translate(p.x, p.y);
      ctx.strokeStyle = p.color;
      ctx.lineWidth = 2;
      for (let i = 0; i < 6; i++) {
        const ang = i * Math.PI / 3;
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(Math.cos(ang) * p.size, Math.sin(ang) * p.size);
        ctx.stroke();
      }
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(0, 0, 2, 0, Math.PI * 2);
      ctx.fill();
    } else if (p.type === 'rapid') {
      // 快速弹:小光点
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }
  ctx.globalAlpha = 1;
}

// ===== 光束绘制 =====
function drawBeams() {
  for (const b of game.beams) {
    if (b.dead) continue;
    const alpha = b.life / b.maxLife;
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.shadowColor = b.color;
    ctx.shadowBlur = 20;
    // 外光晕
    ctx.strokeStyle = b.color;
    ctx.lineWidth = b.width * 2;
    ctx.globalAlpha = alpha * 0.4;
    ctx.beginPath();
    ctx.moveTo(b.x1, b.y1);
    ctx.lineTo(b.x2, b.y2);
    ctx.stroke();
    // 内芯
    ctx.globalAlpha = alpha;
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = b.width * 0.5;
    ctx.beginPath();
    ctx.moveTo(b.x1, b.y1);
    ctx.lineTo(b.x2, b.y2);
    ctx.stroke();
    ctx.restore();
  }
  ctx.globalAlpha = 1;
}

// ===== 斩击绘制 =====
function drawSlashes() {
  for (const s of game.slashes) {
    if (s.dead) continue;
    const alpha = s.life / s.maxLife;
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.translate(s.x, s.y);
    ctx.rotate(s.angle);
    ctx.shadowColor = s.color;
    ctx.shadowBlur = 20;
    // 斩击弧线
    ctx.strokeStyle = s.color;
    ctx.lineWidth = 6;
    ctx.beginPath();
    ctx.arc(0, 0, s.size, -Math.PI * 0.4, Math.PI * 0.4);
    ctx.stroke();
    // 内芯白
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.arc(0, 0, s.size, -Math.PI * 0.4, Math.PI * 0.4);
    ctx.stroke();
    ctx.restore();
  }
  ctx.globalAlpha = 1;
}

// ===== 命中火花 =====
function drawHitSparks() {
  for (const s of game.hitSparks) {
    if (s.dead) continue;
    const alpha = s.life / s.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = s.color;
    ctx.beginPath();
    ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

function drawExecution() {
  const ex = game.execution;
  if (!ex) return;
  if (ex.phase === 0) {
    ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(0.7, ex.phaseTimer / 1.5 * 0.7)})`;
    ctx.fillRect(0, 0, W, H);
    const alpha = Math.min(1, ex.phaseTimer / 0.5) * Math.min(1, (1.5 - ex.phaseTimer) / 0.3);
    ctx.fillStyle = `rgba(255, 40, 60, ${alpha})`;
    ctx.font = 'bold 72px Microsoft YaHei';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('处  决', W/2, H/2);
    ctx.font = 'bold 20px Microsoft YaHei';
    ctx.fillStyle = `rgba(255, 200, 0, ${alpha})`;
    ctx.fillText('胜者将赐予败者最后一击...', W/2, H/2 + 70);
  }
  if (ex.phase >= 1 && ex.phase <= 3) {
    ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
    ctx.fillRect(0, 0, W, H);
    const e = ex.executioner;
    const winnerColor = ex.winnerSide === 'player' ? '#3d9bff' : '#ff3d71';
    ctx.save();
    ctx.translate(e.x, e.y);
    const dx = e.endX - e.startX, dy = e.endY - e.startY;
    const ang = Math.atan2(dy, dx);
    ctx.rotate(ang);
    ctx.shadowColor = winnerColor; ctx.shadowBlur = 30;
    ctx.fillStyle = '#e0e8f0';
    ctx.beginPath();
    ctx.moveTo(30, -8); ctx.lineTo(120, -4); ctx.lineTo(140, 0); ctx.lineTo(120, 4); ctx.lineTo(30, 8);
    ctx.closePath(); ctx.fill();
    ctx.fillStyle = '#4a5a70';
    ctx.fillRect(-10, -6, 40, 12);
    ctx.fillStyle = winnerColor;
    ctx.fillRect(28, -14, 8, 28);
    ctx.restore();
  }
  if (ex.phase === 2) {
    const t = ex.phaseTimer / 0.5;
    const flashAlpha = Math.sin(t * Math.PI) * 0.9;
    ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha})`;
    ctx.fillRect(0, 0, W, H);
    ctx.save();
    ctx.strokeStyle = `rgba(255, 60, 90, ${flashAlpha})`;
    ctx.lineWidth = 6;
    ctx.shadowColor = '#ff3d71'; ctx.shadowBlur = 40;
    ctx.beginPath();
    ctx.moveTo(ex.victimX - 200, ex.victimY - 150);
    ctx.lineTo(ex.victimX + 200, ex.victimY + 150);
    ctx.stroke();
    ctx.restore();
  }
  if (ex.phase >= 3) {
    for (const p of ex.bloodParticles) {
      const a = Math.max(0, p.life / p.maxLife);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    if (ex.phaseTimer < 1) {
      const t = ex.phaseTimer;
      ctx.fillStyle = `rgba(140, 0, 30, ${0.6 * (1 - t)})`;
      ctx.beginPath();
      ctx.arc(ex.victimX, ex.victimY, 100 + t * 200, 0, Math.PI * 2);
      ctx.fill();
    }
    if (ex.headFalling) {
      ctx.save();
      ctx.translate(ex.headFalling.x, ex.headFalling.y);
      ctx.rotate(ex.headFalling.rot);
      ctx.fillStyle = '#e0c8a0';
      ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = '#000';
      ctx.beginPath(); ctx.arc(-5, -3, 2, 0, Math.PI * 2); ctx.arc(5, -3, 2, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = '#8a0020';
      ctx.beginPath();
      ctx.arc(0, 12, 3, 0, Math.PI * 2);
      ctx.arc(-8, 10, 2, 0, Math.PI * 2);
      ctx.arc(8, 10, 2, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }
  if (ex.phase === 4 && ex.showTitle) {
    const t = ex.phaseTimer;
    const alpha = Math.min(1, t / 0.5);
    ctx.fillStyle = `rgba(0, 0, 0, ${0.5 * alpha})`;
    ctx.fillRect(0, 0, W, H);
    ctx.save();
    ctx.shadowColor = ex.winnerSide === 'player' ? '#00ffa0' : '#ff3d71';
    ctx.shadowBlur = 30;
    ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
    ctx.font = 'bold 60px Microsoft YaHei';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(ex.titleText, W/2, H/2 - 40);
    ctx.restore();
    ctx.fillStyle = `rgba(255, 200, 0, ${alpha})`;
    ctx.font = 'bold 22px Microsoft YaHei';
    ctx.fillText(ex.titleSub, W/2, H/2 + 30);
    ctx.fillStyle = `rgba(160, 176, 192, ${alpha})`;
    ctx.font = '14px Microsoft YaHei';
    ctx.fillText('正在结束战斗...', W/2, H/2 + 80);
  }
}

function drawField() {
  ctx.fillStyle = 'rgba(60, 20, 50, 0.4)';
  ctx.fillRect(0, FIELD_TOP, W, MID_Y - RIVER_H/2 - FIELD_TOP);
  ctx.fillStyle = 'rgba(20, 40, 80, 0.5)';
  ctx.fillRect(0, MID_Y + RIVER_H/2, W, FIELD_BOTTOM - MID_Y - RIVER_H/2);
  ctx.setLineDash([6, 8]);
  ctx.lineWidth = 1.5;
  [W * 0.35, W * 0.65].forEach(lx => {
    ctx.strokeStyle = 'rgba(100, 120, 150, 0.3)';
    ctx.beginPath(); ctx.moveTo(lx, FIELD_TOP); ctx.lineTo(lx, FIELD_BOTTOM); ctx.stroke();
  });
  ctx.setLineDash([]);
  ctx.font = 'bold 11px Microsoft YaHei';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
  ctx.fillText('左路', LANES.LEFT.x, FIELD_TOP + 20);
  ctx.fillText('中路', LANES.MID.x, FIELD_TOP + 20);
  ctx.fillText('右路', LANES.RIGHT.x, FIELD_TOP + 20);
  ctx.fillText('左路', LANES.LEFT.x, FIELD_BOTTOM - 20);
  ctx.fillText('中路', LANES.MID.x, FIELD_BOTTOM - 20);
  ctx.fillText('右路', LANES.RIGHT.x, FIELD_BOTTOM - 20);
  drawFlagOnGame(20, FIELD_TOP + 8, 32, 32, flagData2, '#ff3d71');
  ctx.fillStyle = 'rgba(255, 61, 113, 0.9)';
  ctx.font = 'bold 12px Microsoft YaHei'; ctx.textAlign = 'left';
  ctx.fillText(ALLIANCE_NAMES.ally2, 60, FIELD_TOP + 24);
  drawFlagOnGame(20, FIELD_BOTTOM - 40, 32, 32, flagData1, '#3d9bff');
  ctx.fillStyle = 'rgba(61, 155, 255, 0.9)';
  ctx.fillText(ALLIANCE_NAMES.ally1, 60, FIELD_BOTTOM - 24);
  const riverGrad = ctx.createLinearGradient(0, RIVER_Y, 0, RIVER_Y + RIVER_H);
  riverGrad.addColorStop(0, 'rgba(0, 150, 220, 0.7)');
  riverGrad.addColorStop(0.5, 'rgba(0, 200, 255, 0.9)');
  riverGrad.addColorStop(1, 'rgba(0, 150, 220, 0.7)');
  ctx.fillStyle = riverGrad;
  ctx.fillRect(0, RIVER_Y, W, RIVER_H);
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
  ctx.lineWidth = 1;
  const t = game.totalTime;
  for (let x = 0; x < W; x += 40) {
    const offset = Math.sin(t * 2 + x * 0.05) * 5;
    ctx.beginPath();
    ctx.moveTo(x, RIVER_Y + RIVER_H/2 + offset);
    ctx.lineTo(x + 20, RIVER_Y + RIVER_H/2 - offset);
    ctx.stroke();
  }
  ctx.fillStyle = 'rgba(200, 180, 120, 0.25)';
  ctx.fillRect(W/2 - MID_CROSS_W/2, RIVER_Y - 3, MID_CROSS_W, RIVER_H + 6);
  ctx.strokeStyle = 'rgba(200, 180, 120, 0.6)';
  ctx.lineWidth = 2;
  ctx.strokeRect(W/2 - MID_CROSS_W/2, RIVER_Y - 3, MID_CROSS_W, RIVER_H + 6);
  ctx.fillStyle = 'rgba(255, 220, 150, 0.7)';
  ctx.font = 'bold 10px Microsoft YaHei'; ctx.textAlign = 'center';
  ctx.fillText('浅滩', W/2, MID_Y + 3);
  [BRIDGE_X1, BRIDGE_X2].forEach(bx => {
    ctx.fillStyle = '#5a4530';
    ctx.fillRect(bx - BRIDGE_W/2, RIVER_Y - 5, BRIDGE_W, RIVER_H + 10);
    ctx.strokeStyle = '#8a6540'; ctx.lineWidth = 3;
    ctx.strokeRect(bx - BRIDGE_W/2, RIVER_Y - 5, BRIDGE_W, RIVER_H + 10);
    for (let y = RIVER_Y; y < RIVER_Y + RIVER_H; y += 10) {
      ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
      ctx.beginPath(); ctx.moveTo(bx - BRIDGE_W/2, y); ctx.lineTo(bx + BRIDGE_W/2, y); ctx.stroke();
    }
  });
}

function drawLaneWarnings() {
  if (!game) return;
  const t = game.totalTime;
  for (const lane of ['LEFT', 'MID', 'RIGHT']) {
    const timer = game.laneWarnings[lane];
    if (timer > 0) {
      const alpha = Math.min(1, timer) * (0.5 + Math.sin(t * 12) * 0.3);
      const lx = LANES[lane].x;
      const grad = ctx.createLinearGradient(lx - 100, 0, lx + 100, 0);
      grad.addColorStop(0, 'rgba(255, 61, 113, 0)');
      grad.addColorStop(0.5, `rgba(255, 61, 113, ${alpha * 0.4})`);
      grad.addColorStop(1, 'rgba(255, 61, 113, 0)');
      ctx.fillStyle = grad;
      ctx.fillRect(lx - 100, FIELD_TOP, 200, FIELD_BOTTOM - FIELD_TOP);
      ctx.fillStyle = `rgba(255, 61, 113, ${alpha})`;
      ctx.font = 'bold 16px Microsoft YaHei';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('⚠️ ' + LANES[lane].name + '来袭', lx, FIELD_TOP - 10);
    }
  }
}

function drawTower(t, label, isBeingExecuted) {
  if (t.hp <= 0) {
    ctx.fillStyle = 'rgba(50, 50, 50, 0.5)';
    ctx.fillRect(t.x - t.w/2, t.y - t.h/2, t.w, t.h);
    ctx.fillStyle = '#333';
    ctx.font = 'bold 20px Microsoft YaHei';
    ctx.textAlign = 'center';
    ctx.fillText('✕', t.x, t.y + 7);
    return;
  }
  const isPlayer = t.side === 'player';
  const baseColor = isPlayer ? '#3d9bff' : '#ff3d71';
  const darkColor = isPlayer ? '#1a4b7f' : '#8a1a2f';
  if (isBeingExecuted) {
    ctx.save();
    ctx.translate(t.x, t.y);
    ctx.rotate(Math.sin(game.totalTime * 20) * 0.05);
    ctx.translate(-t.x, -t.y);
  }
  // 攻击闪光
  if (t.attackFlash > 0) {
    ctx.save();
    ctx.globalAlpha = t.attackFlash * 3;
    ctx.shadowColor = baseColor;
    ctx.shadowBlur = 30;
    ctx.fillStyle = baseColor;
    ctx.fillRect(t.x - t.w/2 - 5, t.y - t.h/2 - 5, t.w + 10, t.h + 10);
    ctx.restore();
  }
  ctx.fillStyle = darkColor;
  ctx.fillRect(t.x - t.w/2, t.y - t.h/2, t.w, t.h);
  ctx.fillStyle = baseColor;
  ctx.fillRect(t.x - t.w/2 + 3, t.y - t.h/2 + 3, t.w - 6, t.h - 6);
  ctx.fillStyle = darkColor;
  if (t.type === 'king') {
    ctx.beginPath();
    ctx.moveTo(t.x - t.w/2 - 5, t.y - t.h/2);
    ctx.lineTo(t.x + t.w/2 + 5, t.y - t.h/2);
    ctx.lineTo(t.x + t.w/2 - 5, t.y - t.h/2 - 15);
    ctx.lineTo(t.x, t.y - t.h/2 - 5);
    ctx.lineTo(t.x - t.w/2 + 5, t.y - t.h/2 - 15);
    ctx.closePath(); ctx.fill();
    ctx.fillStyle = '#ffdd00';
    ctx.beginPath(); ctx.arc(t.x, t.y - t.h/2 - 5, 4, 0, Math.PI * 2); ctx.fill();
  } else {
    ctx.beginPath();
    ctx.moveTo(t.x - t.w/2 - 5, t.y - t.h/2);
    ctx.lineTo(t.x + t.w/2 + 5, t.y - t.h/2);
    ctx.lineTo(t.x, t.y - t.h/2 - 20);
    ctx.closePath(); ctx.fill();
  }
  const flag = isPlayer ? flagData1 : flagData2;
  const flagW = t.type === 'king' ? 40 : 32;
  const flagH = t.type === 'king' ? 40 : 32;
  drawFlagOnGame(t.x - flagW/2, t.y - flagH/2, flagW, flagH, flag, isPlayer ? '#3d9bff' : '#ff3d71');
  const hpPct = t.hp / t.maxHp;
  const barW = t.w + 10;
  ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
  ctx.fillRect(t.x - barW/2, t.y + t.h/2 + 5, barW, 6);
  ctx.fillStyle = hpPct > 0.5 ? '#00ffa0' : hpPct > 0.25 ? '#ffaa00' : '#ff3d71';
  ctx.fillRect(t.x - barW/2, t.y + t.h/2 + 5, barW * hpPct, 6);
  if (isBeingExecuted) ctx.restore();
}

function drawUnit(u) {
  const t = game.totalTime;
  const bob = Math.sin(t * 4 + u.x * 0.1) * 2;
  const size = u.big ? 1.4 : 1;
  const laneColor = u.side === 'player' ? 'rgba(61, 155, 255, 0.15)' : 'rgba(255, 61, 113, 0.15)';
  ctx.fillStyle = laneColor;
  ctx.beginPath();
  ctx.ellipse(u.x, u.y + 18 * size, 24 * size, 10 * size, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
  ctx.beginPath();
  ctx.ellipse(u.x, u.y + 18 * size, 18 * size, 6 * size, 0, 0, Math.PI * 2);
  ctx.fill();
  const flyOffset = u.flying ? Math.sin(t * 5 + u.x * 0.05) * 5 - 20 : 0;

  // 攻击时前冲位移
  let attackOffset = 0;
  if (u.attackAnimTimer > 0) {
    attackOffset = Math.sin((1 - u.attackAnimTimer / 0.25) * Math.PI) * 6 * u.facing;
  }

  ctx.save();
  ctx.translate(u.x, u.y + bob + flyOffset + attackOffset);
  ctx.scale(size, size);
  ctx.strokeStyle = u.side === 'player' ? 'rgba(61, 155, 255, 0.6)' : 'rgba(255, 61, 113, 0.6)';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(0, 0, 22, 0, Math.PI * 2);
  ctx.stroke();
  ctx.fillStyle = u.color;
  if (u.big) {
    roundRectPath(-16, -16, 32, 32, 6);
    ctx.fill();
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
  } else {
    ctx.beginPath();
    ctx.arc(0, 0, 14, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
  }
  // 攻击发光
  if (u.attackAnimTimer > 0) {
    ctx.save();
    ctx.globalAlpha = u.attackAnimTimer / 0.25 * 0.8;
    ctx.shadowColor = u.atkColor;
    ctx.shadowBlur = 20;
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(0, 0, 18, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 14px Microsoft YaHei';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  const ownerFirst = (u.owner || '').charAt(0) || '?';
  ctx.fillText(ownerFirst, 0, 0);
  if (u.hitFlash > 0) {
    ctx.globalAlpha = u.hitFlash * 7;
    ctx.fillStyle = '#fff';
    ctx.beginPath(); ctx.arc(0, 0, 24, 0, Math.PI * 2); ctx.fill();
    ctx.globalAlpha = 1;
  }
  ctx.restore();

  ctx.fillStyle = u.side === 'player' ? '#66d9ff' : '#ff8fa0';
  ctx.font = 'bold 10px Microsoft YaHei';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  if (u.owner) ctx.fillText(u.owner, u.x, u.y - 32 * size + flyOffset);
  const hpPct = u.hp / u.maxHp;
  const barW = 36 * size;
  const barY = u.y + flyOffset - 26 * size;
  ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
  ctx.fillRect(u.x - barW/2, barY, barW, 4);
  ctx.fillStyle = u.side === 'player' ? '#3d9bff' : '#ff3d71';
  ctx.fillRect(u.x - barW/2, barY, barW * hpPct, 4);
}

function roundRectPath(x, y, w, h, r) {
  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 drawEffects() {
  for (const e of game.effects) {
    const alpha = Math.max(0, e.life / e.maxLife);
    ctx.globalAlpha = alpha;
    if (e.type === 'spell') {
      const r = e.radius || 0;
      const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, r);
      g.addColorStop(0, e.color);
      g.addColorStop(1, 'rgba(0,0,0,0)');
      ctx.fillStyle = g;
      ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.fill();
      ctx.strokeStyle = e.color; ctx.lineWidth = 3;
      ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.stroke();
    } else if (e.type === 'boom' || e.type === 'explosion') {
      const r = e.radius || (1 - alpha) * 40;
      const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, r);
      g.addColorStop(0, e.color);
      g.addColorStop(1, 'rgba(255, 60, 113, 0)');
      ctx.fillStyle = g;
      ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.fill();
    } else if (e.type === 'hit') {
      ctx.fillStyle = e.color;
      ctx.beginPath(); ctx.arc(e.x, e.y, 8 * (1 - alpha) + 3, 0, Math.PI * 2); ctx.fill();
    }
  }
  ctx.globalAlpha = 1;
}

function drawFloatingTexts() {
  for (const f of game.floatingTexts) {
    ctx.globalAlpha = Math.max(0, f.life);
    ctx.fillStyle = f.color;
    ctx.font = 'bold 14px Microsoft YaHei';
    ctx.textAlign = 'center';
    ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1;
}

function drawTopBar() {
  ctx.fillStyle = 'rgba(255, 61, 113, 0.3)';
  roundRectPath(10, 10, 300, 58, 8); ctx.fill();
  ctx.strokeStyle = '#ff3d71'; ctx.lineWidth = 2;
  roundRectPath(10, 10, 300, 58, 8); ctx.stroke();
  drawFlagOnGame(18, 20, 38, 38, flagData2, '#ff3d71');
  ctx.fillStyle = '#ff3d71';
  ctx.font = 'bold 15px Microsoft YaHei';
  ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
  ctx.fillText(ALLIANCE_NAMES.ally2 + ' 👑 ' + game.enemyCrowns, 62, 28);
  ctx.font = 'bold 10px Microsoft YaHei';
  ctx.fillStyle = '#ffaa00';
  ctx.fillText(NAMES.ally2.join('·'), 62, 52);
  ctx.fillStyle = 'rgba(61, 155, 255, 0.3)';
  roundRectPath(W - 310, 10, 300, 58, 8); ctx.fill();
  ctx.strokeStyle = '#3d9bff'; ctx.lineWidth = 2;
  roundRectPath(W - 310, 10, 300, 58, 8); ctx.stroke();
  drawFlagOnGame(W - 300, 20, 38, 38, flagData1, '#3d9bff');
  ctx.fillStyle = '#3d9bff';
  ctx.font = 'bold 15px Microsoft YaHei';
  ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
  ctx.fillText('👑 ' + game.playerCrowns + '  ' + ALLIANCE_NAMES.ally1, W - 256, 28);
  ctx.font = 'bold 10px Microsoft YaHei';
  ctx.fillStyle = '#00ffa0';
  ctx.fillText(NAMES.ally1.join('·'), W - 256, 52);
  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  ctx.fillRect(W/2 - 60, 10, 120, 40);
  ctx.strokeStyle = game.overtime ? '#ff3d71' : '#00e5ff';
  ctx.lineWidth = 2;
  ctx.strokeRect(W/2 - 60, 10, 120, 40);
  const timeLeft = game.overtime ? game.overtimeTime : game.time;
  const min = Math.floor(timeLeft / 60);
  const sec = Math.floor(timeLeft % 60);
  ctx.fillStyle = game.overtime ? '#ff3d71' : '#fff';
  ctx.font = 'bold 24px Microsoft YaHei';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(`${min}:${sec.toString().padStart(2,'0')}`, W/2, 30);
  if (game.overtime) {
    ctx.fillStyle = '#ff3d71';
    ctx.font = 'bold 12px Microsoft YaHei';
    ctx.fillText('加时赛', W/2, 58);
  }
}

function drawCards() {
  const cardW = 100, cardH = 70;
  const gap = 8;
  const totalW = 4 * cardW + 3 * gap;
  const startX = (W - totalW) / 2;
  const y = H - 90;
  ctx.fillStyle = 'rgba(20, 30, 50, 0.8)';
  roundRectPath(startX - cardW - gap - 10, y + 15, cardW * 0.7, cardH * 0.7, 5);
  ctx.fill();
  ctx.strokeStyle = '#4a5a70'; ctx.lineWidth = 1.5; ctx.stroke();
  const nextDef = CARDS[game.playerNext];
  if (nextDef) {
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 10px Microsoft YaHei';
    ctx.textAlign = 'center';
    ctx.fillText('下一张', startX - cardW/2 - gap - 10, y + 10);
    ctx.fillStyle = nextDef.color;
    ctx.beginPath();
    ctx.arc(startX - cardW * 0.65 - gap, y + 15 + cardH * 0.35, 12, 0, Math.PI * 2);
    ctx.fill();
  }
  for (let i = 0; i < 4; i++) {
    const key = game.playerHand[i];
    if (!key) continue;
    const def = CARDS[key];
    const x = startX + i * (cardW + gap);
    const canAfford = game.elixir >= def.cost;
    const selected = game.selectedCard === i;
    ctx.save();
    if (selected) { ctx.shadowColor = '#00e5ff'; ctx.shadowBlur = 20; }
    ctx.fillStyle = selected ? 'rgba(0, 229, 255, 0.3)' : 'rgba(20, 30, 50, 0.9)';
    roundRectPath(x, y, cardW, cardH, 6); ctx.fill();
    ctx.strokeStyle = selected ? '#00e5ff' : (canAfford ? '#4a9bdf' : '#4a5a70');
    ctx.lineWidth = 2; ctx.stroke();
    ctx.restore();
    ctx.save();
    ctx.translate(x + cardW/2, y + cardH/2 - 12);
    ctx.fillStyle = canAfford ? def.color : '#555';
    ctx.beginPath(); ctx.arc(0, 0, 16, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 11px Microsoft YaHei';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    const ownerName = getCardOwner(key, 'player');
    ctx.fillText(ownerName.charAt(0) || '?', 0, 0);
    ctx.restore();
    ctx.fillStyle = canAfford ? '#fff' : '#666';
    ctx.font = 'bold 9px Microsoft YaHei';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(getCardDisplayName(key, 'player'), x + cardW/2, y + cardH - 18);
    ctx.fillStyle = canAfford ? '#66d9ff' : '#555';
    ctx.font = 'bold 9px Microsoft YaHei';
    ctx.fillText('【' + ownerName + '】', x + cardW/2, y + cardH - 6);
    ctx.fillStyle = canAfford ? '#ff69b4' : '#666';
    ctx.beginPath(); ctx.arc(x + 12, y + 12, 9, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 11px Microsoft YaHei';
    ctx.fillText(def.cost, x + 12, y + 12);
  }
}

function drawElixirBar() {
  const barW = 400, barH = 20;
  const x = (W - barW) / 2;
  const y = H - 20;
  ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
  roundRectPath(x, y, barW, barH, 10); ctx.fill();
  const segW = barW / 10;
  for (let i = 0; i < 10; i++) {
    if (i < game.elixir) {
      const grad = ctx.createLinearGradient(x + i*segW, 0, x + (i+1)*segW, 0);
      grad.addColorStop(0, '#ff69b4');
      grad.addColorStop(1, '#ff1493');
      ctx.fillStyle = grad;
    } else {
      ctx.fillStyle = 'rgba(50, 50, 60, 0.6)';
    }
    roundRectPath(x + i*segW + 2, y + 2, segW - 4, barH - 4, 6); ctx.fill();
  }
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 12px Microsoft YaHei';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(game.elixir + ' / 10', W/2, y + barH/2);
}

function drawDeployPreview() {
  if (game.selectedCard === null) return;
  const cardKey = game.playerHand[game.selectedCard];
  if (!cardKey) return;
  const def = CARDS[cardKey];
  const mx = game.mouseX, my = game.mouseY;
  const isSpell = def.type === 'spell';
  const validZone = isSpell ? (my >= FIELD_TOP && my <= FIELD_BOTTOM) : (my >= MID_Y + 10 && my <= FIELD_BOTTOM);
  const canAfford = game.elixir >= def.cost;
  const valid = validZone && canAfford;
  ctx.fillStyle = valid ? 'rgba(0, 255, 160, 0.08)' : 'rgba(255, 61, 113, 0.08)';
  if (isSpell) ctx.fillRect(0, FIELD_TOP, W, FIELD_BOTTOM - FIELD_TOP);
  else ctx.fillRect(0, MID_Y + 10, W, FIELD_BOTTOM - MID_Y - 10);
  ctx.save();
  ctx.globalAlpha = 0.6;
  ctx.fillStyle = valid ? '#00ffa0' : '#ff3d71';
  ctx.beginPath(); ctx.arc(mx, my, isSpell ? def.radius : 25, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = valid ? '#00ffa0' : '#ff3d71';
  ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(mx, my, isSpell ? def.radius : 25, 0, Math.PI * 2); ctx.stroke();
  ctx.restore();
}

function endGame() {
  if (game.state === 'ended' || game.state === 'execution') return;
  let winner;
  if (game.playerKing.hp <= 0) winner = 'enemy';
  else if (game.enemyKing.hp <= 0) winner = 'player';
  else if (game.playerCrowns > game.enemyCrowns) winner = 'player';
  else if (game.playerCrowns < game.enemyCrowns) winner = 'enemy';
  else {
    const pHp = game.playerKing.hp + game.playerTowerL.hp + game.playerTowerR.hp;
    const eHp = game.enemyKing.hp + game.enemyTowerL.hp + game.enemyTowerR.hp;
    if (pHp > eHp) winner = 'player';
    else if (pHp < eHp) winner = 'enemy';
    else winner = 'player';
  }
  const victim = winner === 'player' ? 'enemy' : 'player';
  startExecution(victim, winner);
}

// ===================== 输入 =====================
canvas.addEventListener('mousemove', e => {
  if (!game) return;
  const rect = canvas.getBoundingClientRect();
  const sx = canvas.width / rect.width;
  const sy = canvas.height / rect.height;
  game.mouseX = (e.clientX - rect.left) * sx;
  game.mouseY = (e.clientY - rect.top) * sy;
});
canvas.addEventListener('click', e => {
  if (!game || game.state !== 'playing') return;
  initAudio();
  const rect = canvas.getBoundingClientRect();
  const sx = canvas.width / rect.width;
  const sy = canvas.height / rect.height;
  const mx = (e.clientX - rect.left) * sx;
  const my = (e.clientY - rect.top) * sy;
  const cardW = 100, cardH = 70, gap = 8;
  const totalW = 4 * cardW + 3 * gap;
  const startX = (W - totalW) / 2;
  const y = H - 90;
  for (let i = 0; i < 4; i++) {
    const x = startX + i * (cardW + gap);
    if (mx >= x && mx <= x + cardW && my >= y && my <= y + cardH) {
      const key = game.playerHand[i];
      if (!key) return;
      const def = CARDS[key];
      if (game.elixir < def.cost) return;
      game.selectedCard = game.selectedCard === i ? null : i;
      return;
    }
  }
  if (game.selectedCard !== null) deployCard(game.selectedCard, mx, my);
});
canvas.addEventListener('contextmenu', e => {
  e.preventDefault();
  if (game) game.selectedCard = null;
});
window.addEventListener('keydown', e => {
  if (e.code === 'Escape' && game) game.selectedCard = null;
});

function renderMenuBackground() {
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, '#0a1220');
  grad.addColorStop(1, '#0d1a2d');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);
}

buildNameInputs();
initFlagEditor();
renderMenuBackground();
</script>
</body>
</html>

Game Source: 矛盾升级:皇室战争

Creator: HyperShark77

Libraries: none

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