🎮ArcadeLab

钢铁前线 - KARDS规则版

by HyperShark77
1389 lines63.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, user-scalable=no">
<title>钢铁前线 - KARDS规则版</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  body { background: #0a0a0f; font-family: 'Microsoft YaHei', sans-serif; color: #e0e8f0; min-height: 100vh; overflow-x: hidden; user-select: none; }
  .screen { display: none; }
  .screen.active { display: block; }

  /* ============ 主菜单 ============ */
  #menuScreen { padding: 20px 16px; min-height: 100vh; }
  .title { text-align: center; font-size: 40px; letter-spacing: 8px; background: linear-gradient(90deg, #ffb000, #ff3d3d, #ffb000); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 4px; filter: drop-shadow(0 0 30px rgba(255, 100, 0, 0.4)); font-weight: 900; }
  .subtitle { text-align: center; color: #708090; font-size: 13px; letter-spacing: 5px; margin-bottom: 20px; }
  .faction-config { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: 1000px; margin: 0 auto; }
  @media (max-width: 760px) { .faction-config { grid-template-columns: 1fr; } .title { font-size: 26px; } }
  .side-panel { background: rgba(20, 25, 35, 0.7); border-radius: 14px; padding: 16px; border: 2px solid rgba(74, 155, 223, 0.3); }
  .side-panel.enemy { border-color: rgba(255, 61, 113, 0.3); }
  .side-title { font-size: 17px; font-weight: bold; letter-spacing: 2px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid rgba(74, 155, 223, 0.2); }
  .side-title.ally { color: #4a9bdf; }
  .side-title.enemy { color: #ff3d71; border-bottom-color: rgba(255, 61, 113, 0.2); }
  .faction-slot { background: rgba(30, 40, 55, 0.6); border-radius: 10px; padding: 10px; margin-bottom: 8px; border: 1px solid rgba(74, 155, 223, 0.2); display: flex; gap: 10px; align-items: center; }
  .faction-slot.filled { border-color: rgba(0, 255, 160, 0.5); background: rgba(0, 255, 160, 0.05); }
  .faction-index { width: 26px; height: 26px; border-radius: 50%; background: rgba(74, 155, 223, 0.3); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 13px; color: #4a9bdf; flex-shrink: 0; }
  .side-panel.enemy .faction-index { background: rgba(255, 61, 113, 0.3); color: #ff3d71; }
  .faction-inputs { flex: 1; display: flex; flex-direction: column; gap: 5px; }
  .faction-inputs input[type="text"] { background: rgba(0, 0, 0, 0.4); border: 1.5px solid rgba(74, 155, 223, 0.4); color: #e0e8f0; padding: 5px 10px; border-radius: 6px; font-family: inherit; font-size: 13px; outline: none; width: 100%; }
  .faction-inputs input[type="text"]:focus { border-color: #4a9bdf; box-shadow: 0 0 12px rgba(74, 155, 223, 0.5); }
  .side-panel.enemy .faction-inputs input[type="text"]:focus { border-color: #ff3d71; box-shadow: 0 0 12px rgba(255, 61, 113, 0.5); }
  .faction-inputs input.empty { border-color: rgba(255, 200, 0, 0.6); animation: pulseInput 1.5s infinite; }
  @keyframes pulseInput { 0%, 100% { box-shadow: 0 0 6px rgba(255, 200, 0, 0.3); } 50% { box-shadow: 0 0 16px rgba(255, 200, 0, 0.7); } }
  .file-btn { position: relative; overflow: hidden; display: inline-block; padding: 4px 12px; background: rgba(74, 155, 223, 0.15); border: 1px dashed rgba(74, 155, 223, 0.4); border-radius: 6px; font-size: 11px; color: #7a95b0; cursor: pointer; text-align: center; }
  .side-panel.enemy .file-btn { background: rgba(255, 61, 113, 0.15); border-color: rgba(255, 61, 113, 0.4); color: #b07a8a; }
  .file-btn.has-img { border-style: solid; color: #00ffa0; border-color: rgba(0, 255, 160, 0.5); }
  .file-btn input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
  .faction-preview { width: 40px; height: 40px; border-radius: 6px; background: #1a1e2a; border: 2px solid rgba(74, 155, 223, 0.3); background-size: cover; background-position: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #3a4555; font-size: 18px; }
  .faction-preview.ally { background: linear-gradient(135deg, #1a3a5f, #2a5a9f); color: #66b8ff; }
  .faction-preview.enemy { background: linear-gradient(135deg, #5f1a3a, #9f2a5a); color: #ff8fb0; }
  .faction-preview.filled { border-color: #00ffa0; }
  .hint { text-align: center; margin: 14px 0; color: #ffcc00; font-size: 13px; }
  .hint.ready { color: #00ffa0; }
  .start-btn { display: block; margin: 14px auto 0; padding: 14px 50px; font-size: 17px; font-weight: bold; letter-spacing: 4px; color: #001018; background: linear-gradient(90deg, #00e5ff, #00ffa0); border: none; border-radius: 10px; cursor: pointer; font-family: inherit; }
  .start-btn:hover:not(:disabled) { transform: scale(1.05); box-shadow: 0 0 40px rgba(0, 229, 255, 0.7); }
  .start-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; }

  /* ============ 战场 ============ */
  #gameScreen { min-height: 100vh; display: flex; flex-direction: column; padding: 4px; background: radial-gradient(ellipse at center, #1a1f2f 0%, #080a10 100%); position: relative; }

  .hq-bar { display: flex; justify-content: space-between; align-items: center; padding: 4px 12px; border-radius: 8px; background: rgba(20, 25, 35, 0.85); border: 2px solid rgba(255, 61, 113, 0.3); margin-bottom: 3px; }
  .hq-bar.player { border-color: rgba(74, 155, 223, 0.3); }
  .hq-name { font-size: 12px; font-weight: bold; letter-spacing: 1px; }
  .hq-hp { display: flex; align-items: center; gap: 6px; cursor: pointer; }
  .hq-hp:hover .hp-value { transform: scale(1.15); }
  .hp-icon { font-size: 16px; }
  .hp-value { font-size: 18px; font-weight: bold; color: #ff3d71; transition: transform 0.15s; }
  .hq-bar.player .hp-value { color: #4a9bdf; }
  .kredit-info { display: flex; align-items: center; gap: 5px; font-size: 12px; }
  .kredit-icon { color: #ffb000; font-size: 15px; }

  .battlefield { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 3px; min-height: 0; }

  .row-wrap { position: relative; }
  .row-label { position: absolute; top: 50%; left: 3px; transform: translateY(-50%); font-size: 9px; color: #4a5a70; letter-spacing: 2px; writing-mode: vertical-rl; z-index: 5; }

  .row { border-radius: 8px; padding: 4px; display: flex; gap: 5px; align-items: center; justify-content: center; min-height: 88px; flex-wrap: nowrap; overflow-x: auto; transition: all 0.2s; }
  .row.back { background: rgba(15, 20, 30, 0.5); border: 1.5px dashed rgba(74, 155, 223, 0.15); min-height: 78px; }
  .row.front {
    background: linear-gradient(180deg, rgba(255, 61, 113, 0.04), rgba(255, 176, 0, 0.08), rgba(74, 155, 223, 0.04));
    border: 2.5px solid rgba(255, 176, 0, 0.5);
    box-shadow: inset 0 0 30px rgba(255, 176, 0, 0.1);
    position: relative;
    min-height: 96px;
  }
  .row.front::before {
    content: '⚔ 公 用 前 线 ⚔';
    position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
    background: #0a0a0f; color: #ffb000; font-size: 10px; padding: 0 10px;
    letter-spacing: 3px; font-weight: bold;
  }
  .row.front.controlled-player { border-color: #4a9bdf; box-shadow: inset 0 0 30px rgba(74, 155, 223, 0.2); }
  .row.front.controlled-player::before { content: '⚔ 我方控制前线 ⚔'; color: #4a9bdf; }
  .row.front.controlled-enemy { border-color: #ff3d71; box-shadow: inset 0 0 30px rgba(255, 61, 113, 0.2); }
  .row.front.controlled-enemy::before { content: '⚔ 敌方控制前线 ⚔'; color: #ff3d71; }
  .row.drop-hover { background: rgba(0, 255, 160, 0.15); border-color: #00ffa0; }
  .row.drop-invalid { background: rgba(255, 61, 113, 0.15); border-color: #ff3d71; }

  /* 单位卡 */
  .unit {
    width: 68px; height: 92px; border-radius: 7px;
    position: relative;
    background-size: cover; background-position: center;
    background-color: #1a1e2a;
    border: 2px solid rgba(74, 155, 223, 0.7);
    cursor: pointer;
    transition: transform 0.15s, box-shadow 0.15s;
    overflow: hidden;
    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
    flex-shrink: 0;
  }
  .unit::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 40%, rgba(0, 0, 0, 0.85) 100%); }
  .unit.enemy-unit { border-color: rgba(255, 61, 113, 0.8); }
  .unit.enemy-unit::before { content: ''; position: absolute; inset: -2px; border-radius: 7px; pointer-events: none; box-shadow: inset 0 0 12px rgba(255, 61, 113, 0.5); }
  .unit:hover { transform: translateY(-3px); }
  .unit.selected { border-color: #00ffa0; box-shadow: 0 0 25px rgba(0, 255, 160, 0.9), 0 0 40px rgba(0, 255, 160, 0.5); transform: translateY(-6px) scale(1.05); z-index: 10; }
  .unit.can-attack::after { content: ''; position: absolute; top: -4px; right: -4px; width: 12px; height: 12px; border-radius: 50%; background: #ffb000; box-shadow: 0 0 12px #ffb000; animation: pulseDot 1s infinite; z-index: 2; }
  .unit.can-move::before { content: '↗'; position: absolute; top: -4px; right: -4px; width: 14px; height: 14px; border-radius: 50%; background: #00ffa0; box-shadow: 0 0 12px #00ffa0; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #000; font-weight: bold; z-index: 3; animation: pulseDot 1s infinite; }
  @keyframes pulseDot { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.3); } }
  .unit.attacking { animation: attackShake 0.3s; }
  @keyframes attackShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
  .unit.hit { animation: hitShake 0.3s; }
  @keyframes hitShake { 0%, 100% { transform: translateX(0); filter: brightness(1); } 50% { transform: translateX(3px); filter: brightness(2.5); } }
  .unit-name { position: absolute; bottom: 22px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; padding: 0 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .unit-faction { position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .unit-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
  .unit-atk { color: #ffb000; text-shadow: 0 0 6px #000; }
  .unit-def { color: #4a9bdf; text-shadow: 0 0 6px #000; }
  .unit-cost { position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%; background: rgba(0, 0, 0, 0.85); border: 1.5px solid #ffb000; color: #ffb000; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; z-index: 2; }
  .unit-owner-tag { position: absolute; top: 3px; right: 3px; font-size: 8px; padding: 1px 4px; border-radius: 3px; z-index: 2; font-weight: bold; }
  .unit-owner-tag.ally { background: rgba(74, 155, 223, 0.9); color: #fff; }
  .unit-owner-tag.enemy { background: rgba(255, 61, 113, 0.9); color: #fff; }

  .empty-slot { width: 68px; height: 92px; border-radius: 7px; border: 1.5px dashed rgba(74, 155, 223, 0.15); display: flex; align-items: center; justify-content: center; color: rgba(74, 155, 223, 0.25); font-size: 18px; flex-shrink: 0; }

  /* 手牌区 */
  .hand-area { display: flex; flex-direction: column; gap: 3px; }
  .hand { display: flex; gap: 6px; justify-content: center; flex-wrap: nowrap; overflow-x: auto; min-height: 100px; align-items: flex-end; padding: 5px; background: rgba(15, 20, 30, 0.6); border-radius: 10px; border: 1.5px solid rgba(74, 155, 223, 0.2); }
  .card {
    width: 68px; height: 100px; border-radius: 7px;
    background-size: cover; background-position: center;
    background-color: #1a1e2a;
    border: 2px solid rgba(74, 155, 223, 0.6);
    cursor: pointer;
    transition: transform 0.15s, box-shadow 0.15s;
    position: relative; overflow: hidden;
    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
    flex-shrink: 0;
  }
  .card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0, 0, 0, 0.85) 100%); }
  .card.skill-card { border-color: rgba(155, 89, 255, 0.8); background: linear-gradient(135deg, rgba(155, 89, 255, 0.3), rgba(0, 200, 255, 0.2)); }
  .card.skill-card::after { background: linear-gradient(180deg, transparent 30%, rgba(30, 0, 60, 0.9) 100%); }
  .card.skill-card::before { content: '✦ 技能'; position: absolute; top: 3px; right: 3px; font-size: 8px; color: #d4b3ff; background: rgba(155, 89, 255, 0.4); padding: 1px 5px; border-radius: 3px; z-index: 3; font-weight: bold; }
  .card:hover:not(.unaffordable):not(.dragging) { transform: translateY(-10px) scale(1.05); box-shadow: 0 12px 25px rgba(0, 229, 255, 0.5); border-color: #00e5ff; }
  .card.skill-card:hover:not(.unaffordable) { box-shadow: 0 12px 25px rgba(155, 89, 255, 0.7); border-color: #d4b3ff; }
  .card.unaffordable { opacity: 0.4; cursor: not-allowed; filter: grayscale(60%); }
  .card.dragging { opacity: 0.3; }
  .card-name { position: absolute; bottom: 24px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; padding: 0 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .card-faction { position: absolute; bottom: 11px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .card-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
  .card-desc { position: absolute; bottom: 8px; left: 0; right: 0; text-align: center; font-size: 8px; color: #d4b3ff; z-index: 1; padding: 0 3px; line-height: 1.2; }
  .card-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -70%); font-size: 26px; z-index: 1; filter: drop-shadow(0 0 8px rgba(155, 89, 255, 0.8)); }
  .move-cost-badge { position: absolute; bottom: 52px; left: 50%; transform: translateX(-50%); font-size: 8px; color: #00ffa0; background: rgba(0, 0, 0, 0.7); padding: 1px 5px; border-radius: 3px; z-index: 2; border: 1px solid rgba(0, 255, 160, 0.5); }
  .unit .move-cost-badge { bottom: 42px; }

  /* 拖拽幽灵 */
  .ghost-card {
    position: fixed; width: 68px; height: 100px; border-radius: 7px;
    background-size: cover; background-position: center;
    background-color: #1a1e2a;
    border: 2px solid #00e5ff;
    box-shadow: 0 0 30px rgba(0, 229, 255, 0.8), 0 12px 40px rgba(0, 0, 0, 0.7);
    pointer-events: none; z-index: 1000;
    transform: translate(-50%, -50%);
    opacity: 0.92; overflow: hidden;
  }
  .ghost-card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0, 0, 0, 0.85) 100%); }
  .ghost-card.invalid { border-color: #ff3d71; box-shadow: 0 0 30px rgba(255, 61, 113, 0.8), 0 12px 40px rgba(0, 0, 0, 0.7); }
  .ghost-card .card-name { position: absolute; bottom: 24px; left: 0; right: 0; text-align: center; font-size: 10px; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 1; }
  .ghost-card .card-faction { position: absolute; bottom: 11px; left: 0; right: 0; text-align: center; font-size: 8px; color: #a0c8ff; z-index: 1; }
  .ghost-card .card-stats { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 1px 5px; font-size: 12px; font-weight: bold; z-index: 1; }
  .ghost-card .unit-cost { position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%; background: rgba(0, 0, 0, 0.85); border: 1.5px solid #ffb000; color: #ffb000; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; z-index: 2; }

  /* 攻击箭头 */
  #arrowLayer { position: fixed; inset: 0; pointer-events: none; z-index: 500; }

  /* 回合信息 */
  .turn-bar { display: flex; justify-content: space-between; align-items: center; padding: 5px 12px; background: rgba(20, 25, 35, 0.85); border-radius: 8px; border: 2px solid rgba(74, 155, 223, 0.3); }
  .turn-indicator { font-size: 13px; font-weight: bold; letter-spacing: 2px; }
  .turn-indicator.player { color: #4a9bdf; }
  .turn-indicator.enemy { color: #ff3d71; }
  .round-info { font-size: 12px; color: #a0b0c0; }
  .end-turn-btn { padding: 7px 20px; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #001018; background: linear-gradient(90deg, #ffb000, #ff8000); border: none; border-radius: 8px; cursor: pointer; font-family: inherit; box-shadow: 0 0 20px rgba(255, 176, 0, 0.4); }
  .end-turn-btn:hover:not(:disabled) { transform: scale(1.05); }
  .end-turn-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; box-shadow: none; }

  /* 提示 */
  .toast { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); padding: 9px 22px; background: rgba(0, 0, 0, 0.9); border: 2px solid #ffb000; border-radius: 8px; color: #ffb000; font-size: 13px; font-weight: bold; z-index: 2000; animation: toastIn 0.3s, toastOut 0.3s 1.2s forwards; pointer-events: none; }
  @keyframes toastIn { from { opacity: 0; transform: translate(-50%, -20px); } to { opacity: 1; transform: translate(-50%, 0); } }
  @keyframes toastOut { to { opacity: 0; transform: translate(-50%, -20px); } }

  .float-text { position: fixed; font-size: 20px; font-weight: bold; pointer-events: none; z-index: 800; text-shadow: 0 0 8px currentColor; animation: floatUp 1s ease-out forwards; }
  @keyframes floatUp { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-70px); opacity: 0; } }

  .spark { position: fixed; pointer-events: none; z-index: 700; border-radius: 50%; }

  .screen-shake { animation: shakeScreen 0.35s; }
  @keyframes shakeScreen { 0%, 100% { transform: translate(0, 0); } 20% { transform: translate(-4px, 3px); } 40% { transform: translate(4px, -3px); } 60% { transform: translate(-3px, -2px); } 80% { transform: translate(3px, 2px); } }

  .overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1500; animation: fadeIn 0.5s; }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  .overlay-title { font-size: 56px; font-weight: 900; letter-spacing: 8px; margin-bottom: 20px; }
  .overlay-title.win { background: linear-gradient(90deg, #00ffa0, #00e5ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; filter: drop-shadow(0 0 30px rgba(0, 255, 160, 0.6)); }
  .overlay-title.lose { background: linear-gradient(90deg, #ff3d71, #ff6b6b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; filter: drop-shadow(0 0 30px rgba(255, 61, 113, 0.6)); }
  .overlay-sub { color: #a0b0c0; font-size: 15px; margin-bottom: 30px; letter-spacing: 2px; }
  .overlay-btn { padding: 13px 40px; font-size: 15px; font-weight: bold; letter-spacing: 3px; color: #001018; background: linear-gradient(90deg, #00e5ff, #00ffa0); border: none; border-radius: 10px; cursor: pointer; font-family: inherit; }
  .overlay-btn:hover { transform: scale(1.05); }

  .rule-badge { position: fixed; bottom: 4px; left: 4px; background: rgba(0, 0, 0, 0.75); border: 1px solid rgba(255, 176, 0, 0.4); border-radius: 6px; padding: 5px 9px; font-size: 9px; color: #ffb000; z-index: 100; line-height: 1.5; max-width: 280px; }
  .rule-badge b { color: #00e5ff; }

  .skill-hint { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 16px 32px; background: rgba(155, 89, 255, 0.95); border: 3px solid #d4b3ff; border-radius: 12px; color: #fff; font-size: 16px; font-weight: bold; z-index: 2000; box-shadow: 0 0 40px rgba(155, 89, 255, 0.8); pointer-events: none; animation: skillPop 0.8s forwards; }
  @keyframes skillPop { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 30% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.3); } }

  /* 底部操作栏 */
  .action-bar { position: fixed; bottom: 130px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; z-index: 600; pointer-events: none; }
  .action-bar > * { pointer-events: auto; }
  .action-btn { padding: 11px 24px; border-radius: 25px; font-family: inherit; font-size: 13px; font-weight: bold; letter-spacing: 1.5px; cursor: pointer; border: none; }
  .move-btn { background: linear-gradient(90deg, #00ffa0, #00e5ff); color: #001018; box-shadow: 0 0 30px rgba(0, 255, 160, 0.6); animation: pulseBtn 1.5s infinite; }
  .attack-btn { background: linear-gradient(90deg, #ffb000, #ff8000); color: #001018; box-shadow: 0 0 30px rgba(255, 176, 0, 0.6); animation: pulseBtn 1.5s infinite; }
  @keyframes pulseBtn { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
  .cancel-btn { background: rgba(60, 70, 80, 0.9); color: #c0c8d0; }
  .move-btn:disabled, .attack-btn:disabled { background: rgba(60, 70, 80, 0.6); color: #4a5a60; cursor: not-allowed; box-shadow: none; animation: none; }

  /* 选中单位信息 */
  .unit-info { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); opacity: 0; padding: 12px 24px; background: rgba(0, 0, 0, 0.9); border: 2px solid #00ffa0; border-radius: 10px; z-index: 900; pointer-events: none; transition: all 0.2s; text-align: center; }
  .unit-info.show { transform: translate(-50%, -50%) scale(1); opacity: 1; }
</style>
</head>
<body>

<!-- ==================== 主菜单 ==================== -->
<div class="screen active" id="menuScreen">
  <h1 class="title">钢铁前线</h1>
  <div class="subtitle">STEEL FRONTLINE · KARDS 规则</div>

  <div class="faction-config">
    <div class="side-panel ally">
      <div class="side-title ally">🔵 我方阵营(3 个)</div>
      <div id="allyFactions"></div>
    </div>
    <div class="side-panel enemy">
      <div class="side-title enemy">🔴 敌方阵营(3 个)</div>
      <div id="enemyFactions"></div>
    </div>
  </div>

  <div class="hint" id="hint">⚠️ 请填完 6 个阵营名称(图片可选)</div>
  <button class="start-btn" id="startBtn" disabled onclick="startGame()">开 始 对 战</button>
</div>

<!-- ==================== 战场 ==================== -->
<div class="screen" id="gameScreen">
  <div class="hq-bar">
    <div class="hq-name" style="color:#ff3d71;" id="enemyName">敌军指挥部</div>
    <div class="hq-hp" onclick="attackHQ('enemy')">
      <span class="hp-icon">❤️</span>
      <span class="hp-value" id="enemyHQ">30</span>
    </div>
    <div class="kredit-info">
      <span class="kredit-icon">◆</span>
      <span id="enemyKredit">0</span>
      <span style="color:#708090; font-size:11px;">手牌 <span id="enemyHandCount">0</span></span>
    </div>
  </div>

  <div class="battlefield">
    <div class="row-wrap">
      <div class="row-label">敌后</div>
      <div class="row back enemy-row" id="enemyBackRow"></div>
    </div>

    <div class="row-wrap">
      <div class="row front" id="frontLineRow"></div>
    </div>

    <div class="turn-bar">
      <div class="turn-indicator player" id="turnIndicator">🔵 我方回合</div>
      <div class="round-info" id="roundInfo">第 1 回合</div>
      <button class="end-turn-btn" id="endTurnBtn" onclick="endTurn()">结 束 回 合</button>
    </div>

    <div class="row-wrap">
      <div class="row-label">我后</div>
      <div class="row back" id="playerBackRow"></div>
    </div>
  </div>

  <div class="hq-bar player">
    <div class="hq-name" style="color:#4a9bdf;" id="playerName">我军指挥部</div>
    <div class="hq-hp" onclick="attackHQ('player')">
      <span class="hp-icon">❤️</span>
      <span class="hp-value" id="playerHQ">30</span>
    </div>
    <div class="kredit-info">
      <span class="kredit-icon">◆</span>
      <span id="playerKredit">0</span>
    </div>
  </div>

  <div class="hand-area">
    <div class="hand" id="playerHand"></div>
  </div>
</div>

<svg id="arrowLayer"></svg>

<div class="rule-badge">
  <b>KARDS 规则:</b><br>
  <b>①</b> 卡牌只能先部署到我方后方<br>
  <b>②</b> 花行动费才能从后方 → 前线<br>
  <b>③</b> 前线只能被一方控制<br>
  <b>④</b> 后方单位可攻击敌方前线<br>
  <b>⑤</b> 只有前线单位能攻击敌方 HQ
</div>

<script>
/* ============================================================
   钢铁前线 - KARDS 规则严格版(修复版)
   ============================================================ */

const allyFactions = [{ name:'', image:null }, { name:'', image:null }, { name:'', image:null }];
const enemyFactions = [{ name:'', image:null }, { name:'', image:null }, { name:'', image:null }];

const CARD_TEMPLATES = [
  { name: '步兵班',   cost: 1, attack: 1, defense: 2, moveCost: 1 },
  { name: '精锐步兵', cost: 2, attack: 2, defense: 3, moveCost: 1 },
  { name: '装甲车',   cost: 3, attack: 3, defense: 3, moveCost: 2 },
  { name: '中型坦克', cost: 4, attack: 4, defense: 5, moveCost: 2 },
  { name: '重型坦克', cost: 5, attack: 6, defense: 6, moveCost: 3 },
];

const SKILL_CARDS = [
  { name: '空袭', cost: 3, type: 'skill', skill: 'airstrike', icon: '✈️', desc: '前线敌方单位 -3 血' },
  { name: '增援', cost: 2, type: 'skill', skill: 'reinforce', icon: '📦', desc: '立即抽 2 张牌' },
  { name: '战地医院', cost: 3, type: 'skill', skill: 'heal', icon: '🏥', desc: '恢复 HQ 5 血' },
  { name: '冲锋号', cost: 2, type: 'skill', skill: 'charge', icon: '📯', desc: '我方单位立刻可攻击' },
  { name: '铁血', cost: 4, type: 'skill', skill: 'buff', icon: '⚔️', desc: '我方单位 +1攻 +1防' },
  { name: '烟幕', cost: 3, type: 'skill', skill: 'smoke', icon: '💨', desc: '敌方单位本回合无法攻击' },
];

const MAX_FRONT = 6;
const MAX_BACK = 5;

let game = null;
let dragging = null;
let movingUnit = null;

function createGame() {
  return {
    turn: 'player',
    round: 1,
    playerHQ: 30,
    enemyHQ: 30,
    playerKredit: 1,
    enemyKredit: 1,
    maxKredit: 10,
    playerHand: [],
    enemyHand: [],
    frontLine: [],
    frontOwner: null,
    playerBack: [],
    enemyBack: [],
    playerDeck: buildDeck('player'),
    enemyDeck: buildDeck('enemy'),
    selectedUnit: null,
    busy: false,
  };
}

function buildDeck(side) {
  const factions = side === 'player' ? allyFactions : enemyFactions;
  const deck = [];
  for (let round = 0; round < 2; round++) {
    for (let f = 0; f < 3; f++) {
      CARD_TEMPLATES.forEach(tpl => {
        deck.push({
          factionIndex: f,
          factionName: factions[f].name,
          factionImage: factions[f].image,
          name: tpl.name,
          cost: tpl.cost,
          attack: tpl.attack,
          defense: tpl.defense,
          moveCost: tpl.moveCost,
          type: 'unit',
        });
      });
    }
  }
  SKILL_CARDS.forEach(sk => {
    for (let i = 0; i < 2; i++) {
      deck.push({
        factionIndex: -1,
        factionName: '战术',
        factionImage: null,
        name: sk.name,
        cost: sk.cost,
        type: 'skill',
        skill: sk.skill,
        icon: sk.icon,
        desc: sk.desc,
      });
    }
  });
  for (let i = deck.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  return deck;
}

/* ============ 主菜单 ============ */
function buildFactionConfig() {
  const a = document.getElementById('allyFactions');
  const e = document.getElementById('enemyFactions');
  a.innerHTML = ''; e.innerHTML = '';
  [1, 2, 3].forEach((num, i) => {
    a.appendChild(createFactionSlot('ally', i, num));
    e.appendChild(createFactionSlot('enemy', i, num));
  });
}

function createFactionSlot(side, index, num) {
  const faction = side === 'ally' ? allyFactions[index] : enemyFactions[index];
  const div = document.createElement('div');
  div.className = 'faction-slot';
  div.id = `slot-${side}-${index}`;
  div.innerHTML = `
    <div class="faction-index">${num}</div>
    <div class="faction-inputs">
      <input type="text" placeholder="阵营名 ${num}" maxlength="8" class="empty">
      <label class="file-btn">📷 上传图片(可选)<input type="file" accept="image/*"></label>
    </div>
    <div class="faction-preview ${side}">?</div>
  `;
  const input = div.querySelector('input[type="text"]');
  const fileBtn = div.querySelector('.file-btn');
  const file = div.querySelector('input[type="file"]');
  const preview = div.querySelector('.faction-preview');

  input.addEventListener('input', () => {
    faction.name = input.value.trim();
    input.classList.toggle('empty', !faction.name);
    updateSlotState(side, index);
    checkReady();
  });
  file.addEventListener('change', evt => {
    const f = evt.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = ev => {
      faction.image = ev.target.result;
      preview.style.backgroundImage = `url(${faction.image})`;
      preview.textContent = '';
      preview.classList.add('filled');
      fileBtn.classList.add('has-img');
      fileBtn.childNodes[0].textContent = '✓ 已上传';
      updateSlotState(side, index);
      checkReady();
    };
    reader.readAsDataURL(f);
  });
  return div;
}

function updateSlotState(side, index) {
  const faction = side === 'ally' ? allyFactions[index] : enemyFactions[index];
  const slot = document.getElementById(`slot-${side}-${index}`);
  if (faction.name) slot.classList.add('filled');
  else slot.classList.remove('filled');
}

function checkReady() {
  const ok = [...allyFactions, ...enemyFactions].every(f => f.name);
  const hint = document.getElementById('hint');
  const btn = document.getElementById('startBtn');
  if (ok) { hint.textContent = '✅ 阵营名已就绪,可以开始(未上传图片的用默认色)'; hint.classList.add('ready'); btn.disabled = false; }
  else {
    const c = [...allyFactions, ...enemyFactions].filter(f => f.name).length;
    hint.textContent = `⚠️ 请填完 6 个阵营名称(${c}/6),图片可选`;
    hint.classList.remove('ready');
    btn.disabled = true;
  }
}

/* ============ 开始游戏 ============ */
function startGame() {
  document.getElementById('menuScreen').classList.remove('active');
  document.getElementById('gameScreen').classList.add('active');
  game = createGame();
  for (let i = 0; i < 3; i++) drawCard('player');
  for (let i = 0; i < 3; i++) drawCard('enemy');
  render();
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('touchmove', onTouchMove, { passive: false });
  document.addEventListener('mouseup', onMouseUp);
  document.addEventListener('touchend', onTouchEnd);
  document.addEventListener('keydown', onKeyDown);
  document.addEventListener('click', onClickOutside);
}

function drawCard(side) {
  const g = game;
  if (side === 'player') {
    if (g.playerDeck.length > 0 && g.playerHand.length < 7) g.playerHand.push(g.playerDeck.pop());
  } else {
    if (g.enemyDeck.length > 0 && g.enemyHand.length < 7) g.enemyHand.push(g.enemyDeck.pop());
  }
}

/* ============ 渲染 ============ */
function render() {
  if (!game) return;
  const g = game;
  document.getElementById('playerHQ').textContent = g.playerHQ;
  document.getElementById('enemyHQ').textContent = g.enemyHQ;
  document.getElementById('playerKredit').textContent = g.playerKredit;
  document.getElementById('enemyKredit').textContent = g.enemyKredit;
  document.getElementById('enemyHandCount').textContent = g.enemyHand.length;
  document.getElementById('roundInfo').textContent = `第 ${g.round} 回合`;
  const ind = document.getElementById('turnIndicator');
  if (g.turn === 'player') { ind.textContent = '🔵 我方回合'; ind.className = 'turn-indicator player'; }
  else { ind.textContent = '🔴 敌方回合'; ind.className = 'turn-indicator enemy'; }
  document.getElementById('endTurnBtn').disabled = g.turn !== 'player' || g.busy;

  renderRow('enemyBackRow', g.enemyBack, 'back');
  renderFrontLine();
  renderRow('playerBackRow', g.playerBack, 'back');
  renderHand();

  document.getElementById('playerName').textContent = allyFactions[0].name + ' 指挥部';
  document.getElementById('enemyName').textContent = enemyFactions[0].name + ' 指挥部';

  updateActionBar();

  if (g.selectedUnit) drawAttackArrows(); else clearArrows();
}

/* 底部操作栏 */
function updateActionBar() {
  const existing = document.getElementById('actionBar');
  if (existing) existing.remove();
  if (!game || game.turn !== 'player' || game.busy) return;
  const sel = game.selectedUnit;
  if (!sel || sel.owner !== 'player') return;

  const bar = document.createElement('div');
  bar.className = 'action-bar';
  bar.id = 'actionBar';

  // 移动按钮(仅后方单位)
  if (sel.lane === 'back') {
    const canMove = game.playerKredit >= sel.moveCost
      && game.frontOwner !== 'enemy'
      && !game.frontLine.some(u => u.owner === 'enemy')
      && game.frontLine.length < MAX_FRONT;
    const moveBtn = document.createElement('button');
    moveBtn.className = 'move-btn action-btn';
    moveBtn.textContent = canMove ? `↗ 移动到前线(消耗 ${sel.moveCost})` : `↗ 移动到前线(条件不足)`;
    moveBtn.disabled = !canMove;
    moveBtn.onclick = (e) => { e.stopPropagation(); executeMove(); };
    bar.appendChild(moveBtn);
  }

  const cancelBtn = document.createElement('button');
  cancelBtn.className = 'cancel-btn action-btn';
  cancelBtn.textContent = '取消';
  cancelBtn.onclick = (e) => { e.stopPropagation(); game.selectedUnit = null; movingUnit = null; clearArrows(); render(); };
  bar.appendChild(cancelBtn);

  document.body.appendChild(bar);
}

function renderRow(rowId, units, lane) {
  const row = document.getElementById(rowId);
  row.innerHTML = '';
  units.forEach(u => row.appendChild(buildUnitEl(u, lane)));
  for (let i = units.length; i < MAX_BACK; i++) {
    const el = document.createElement('div');
    el.className = 'empty-slot';
    el.textContent = '·';
    row.appendChild(el);
  }
}

function renderFrontLine() {
  const row = document.getElementById('frontLineRow');
  row.innerHTML = '';
  row.classList.remove('controlled-player', 'controlled-enemy');
  if (game.frontOwner === 'player') row.classList.add('controlled-player');
  if (game.frontOwner === 'enemy') row.classList.add('controlled-enemy');

  game.frontLine.forEach(u => row.appendChild(buildUnitEl(u, 'front')));
  for (let i = game.frontLine.length; i < MAX_FRONT; i++) {
    const el = document.createElement('div');
    el.className = 'empty-slot';
    el.textContent = '·';
    row.appendChild(el);
  }
}

function buildUnitEl(u, lane) {
  const el = document.createElement('div');
  const side = u.owner;
  el.className = 'unit' + (side === 'enemy' ? ' enemy-unit' : '');
  if (side === 'player' && game.selectedUnit === u) el.classList.add('selected');
  if (side === 'player' && u.canAttack && game.turn === 'player') el.classList.add('can-attack');
  if (side === 'player' && u.canMove && lane === 'back' && game.turn === 'player') el.classList.add('can-move');
  if (u.image) el.style.backgroundImage = `url(${u.image})`;

  let moveCostHtml = '';
  if (side === 'player' && lane === 'back' && u.type === 'unit') {
    moveCostHtml = `<div class="move-cost-badge">↗ ${u.moveCost}</div>`;
  }

  el.innerHTML = `
    <div class="unit-cost">${u.cost}</div>
    <div class="unit-owner-tag ${side === 'player' ? 'ally' : 'enemy'}">${side === 'player' ? '我' : '敌'}</div>
    ${moveCostHtml}
    <div class="unit-name">${u.name}</div>
    <div class="unit-faction">${u.factionName}</div>
    <div class="unit-stats">
      <span class="unit-atk">⚔${u.attack}</span>
      <span class="unit-def">🛡${u.currentDefense}</span>
    </div>
  `;
  el.addEventListener('click', e => {
    e.stopPropagation();
    onUnitClick(side, u, lane);
  });
  return el;
}

function renderHand() {
  const hand = document.getElementById('playerHand');
  hand.innerHTML = '';
  game.playerHand.forEach((card, idx) => {
    const canAfford = game.playerKredit >= card.cost;
    const isSkill = card.type === 'skill';
    const el = document.createElement('div');
    el.className = 'card' + (isSkill ? ' skill-card' : '') + (canAfford ? '' : ' unaffordable');
    el.dataset.handIndex = idx;
    if (!isSkill && card.factionImage) el.style.backgroundImage = `url(${card.factionImage})`;

    if (isSkill) {
      el.innerHTML = `
        <div class="unit-cost">${card.cost}</div>
        <div class="card-icon">${card.icon}</div>
        <div class="card-name">${card.name}</div>
        <div class="card-desc">${card.desc}</div>
      `;
      el.addEventListener('click', () => useSkill(idx));
    } else {
      el.innerHTML = `
        <div class="unit-cost">${card.cost}</div>
        <div class="move-cost-badge">↗ ${card.moveCost}</div>
        <div class="card-name">${card.name}</div>
        <div class="card-faction">${card.factionName}</div>
        <div class="card-stats">
          <span class="unit-atk">⚔${card.attack}</span>
          <span class="unit-def">🛡${card.defense}</span>
        </div>
      `;
      el.addEventListener('mousedown', e => startDrag(e, idx));
      el.addEventListener('touchstart', e => { e.preventDefault(); startDrag(e, idx); }, { passive: false });
    }
    hand.appendChild(el);
  });
}

/* ============ 拖拽 ============ */
function startDrag(e, handIndex) {
  if (!game || game.turn !== 'player' || game.busy) return;
  const card = game.playerHand[handIndex];
  if (!card || card.type === 'skill') return;
  if (game.playerKredit < card.cost) { toast('圣水不足!'); return; }
  if (game.playerBack.length >= MAX_BACK) { toast('我方后方已满!'); return; }

  dragging = { card, handIndex, ghostEl: null };
  const ghost = document.createElement('div');
  ghost.className = 'ghost-card';
  if (card.factionImage) ghost.style.backgroundImage = `url(${card.factionImage})`;
  ghost.innerHTML = `
    <div class="unit-cost">${card.cost}</div>
    <div class="card-name">${card.name}</div>
    <div class="card-faction">${card.factionName}</div>
    <div class="card-stats">
      <span class="unit-atk">⚔${card.attack}</span>
      <span class="unit-def">🛡${card.defense}</span>
    </div>
  `;
  document.body.appendChild(ghost);
  dragging.ghostEl = ghost;
  moveGhost(e);
  renderHand();
  const el = document.querySelector(`.card[data-hand-index="${handIndex}"]`);
  if (el) el.classList.add('dragging');
}

function moveGhost(e) {
  if (!dragging || !dragging.ghostEl) return;
  const x = e.clientX !== undefined ? e.clientX : e.touches[0].clientX;
  const y = e.clientY !== undefined ? e.clientY : e.touches[0].clientY;
  dragging.ghostEl.style.left = x + 'px';
  dragging.ghostEl.style.top = y + 'px';

  const target = detectDropTarget(x, y);
  dragging.ghostEl.classList.toggle('invalid', !target);
  document.querySelectorAll('.row').forEach(r => r.classList.remove('drop-hover'));
  if (target) target.row.classList.add('drop-hover');
}

function detectDropTarget(x, y) {
  const els = document.elementsFromPoint(x, y);
  for (const el of els) {
    if (el.id === 'playerBackRow') {
      if (game.playerBack.length < MAX_BACK) return { row: el, lane: 'back' };
      return null;
    }
    if (el.id === 'frontLineRow' || el.id === 'enemyBackRow') return null;
  }
  return null;
}

function onMouseMove(e) { if (dragging) moveGhost(e); }
function onTouchMove(e) { if (dragging) { e.preventDefault(); moveGhost(e); } }
function onMouseUp(e) { if (dragging) finishDrag(e.clientX, e.clientY); }
function onTouchEnd(e) { if (dragging) { const t = e.changedTouches[0]; finishDrag(t.clientX, t.clientY); } }

function finishDrag(x, y) {
  if (!dragging) return;
  const d = dragging;
  const target = detectDropTarget(x, y);

  if (target) {
    const card = d.card;
    if (game.playerKredit >= card.cost) {
      game.playerKredit -= card.cost;
      game.playerHand.splice(d.handIndex, 1);
      const newUnit = {
        ...card,
        owner: 'player',
        currentDefense: card.defense,
        canAttack: false,
        canMove: true,
        image: card.factionImage,
        lane: 'back',
      };
      game.playerBack.push(newUnit);
      showFloatText('+ ' + card.name, '#00e5ff', target.row);
    }
  }
  if (d.ghostEl) d.ghostEl.remove();
  document.querySelectorAll('.row').forEach(r => r.classList.remove('drop-hover'));
  dragging = null;
  render();
}

function onKeyDown(e) {
  if (e.code === 'Escape') {
    if (dragging) { if (dragging.ghostEl) dragging.ghostEl.remove(); dragging = null; render(); }
    if (movingUnit) movingUnit = null;
    if (game && game.selectedUnit) { game.selectedUnit = null; clearArrows(); render(); }
  }
}

function onClickOutside(e) {
  if (!game) return;
  if (e.target.closest('.unit')) return;
  if (e.target.closest('.row')) return;
  if (e.target.closest('.hq-bar')) return;
  if (e.target.closest('.hand')) return;
  if (e.target.closest('.action-bar')) return;
  if (game.selectedUnit) { game.selectedUnit = null; clearArrows(); }
  if (movingUnit) movingUnit = null;
  render();
}

/* ============ 移动 ============ */
function executeMove() {
  if (!movingUnit) return;
  const u = movingUnit;

  if (game.playerKredit < u.moveCost) { toast(`需要 ${u.moveCost} 圣水才能移动到前线`); return; }
  if (game.frontOwner === 'enemy') { toast('敌方控制前线,无法进入!'); return; }
  if (game.frontLine.some(x => x.owner === 'enemy')) { toast('前线有敌军,无法进入!'); return; }
  if (game.frontLine.length >= MAX_FRONT) { toast('前线已满!'); return; }

  game.playerKredit -= u.moveCost;
  const idx = game.playerBack.indexOf(u);
  if (idx >= 0) game.playerBack.splice(idx, 1);
  u.lane = 'front';
  u.canMove = false;
  u.canAttack = false;
  game.frontLine.push(u);
  game.frontOwner = 'player';

  movingUnit = null;
  game.selectedUnit = null;
  render();
}

/* ============ 单位点击(核心修复) ============ */
function onUnitClick(side, unit, lane) {
  if (!game || game.turn !== 'player' || game.busy) return;

  // 我方单位
  if (side === 'player') {
    if (game.selectedUnit === unit) {
      // 再次点击 = 取消选中
      game.selectedUnit = null;
      movingUnit = null;
      clearArrows();
      render();
      return;
    }

    // 选中该单位(无论在前线还是后方)
    game.selectedUnit = unit;

    // 后方单位同时进入移动模式
    if (lane === 'back') {
      movingUnit = unit;
    } else {
      movingUnit = null;
    }

    // 显示攻击箭头
    drawAttackArrows();
    render();
    return;
  }

  // 敌方单位
  if (side === 'enemy') {
    if (!game.selectedUnit) {
      toast('请先选中我方单位');
      return;
    }
    const attacker = game.selectedUnit;
    if (!attacker.canAttack) {
      toast('该单位本回合已行动');
      return;
    }
    if (!canAttackTarget(attacker, unit)) {
      if (attacker.lane === 'back') toast('后方单位只能攻击敌方前线单位!');
      else toast('无法攻击该目标');
      return;
    }
    performAttack(attacker, unit);
  }
}

function canAttackTarget(attacker, target) {
  if (attacker.owner !== 'player') return false;
  if (target.owner !== 'enemy') return false;
  if (attacker.lane === 'front') return true;
  // 后方:只能打敌方前线
  return target.lane === 'front';
}

function canAttackHQ(attacker) {
  if (!attacker) return false;
  if (attacker.owner !== 'player') return false;
  if (attacker.lane !== 'front') return false;
  return !game.frontLine.some(u => u.owner === 'enemy');
}

/* ============ 攻击箭头 ============ */
function drawAttackArrows() {
  clearArrows();
  const svg = document.getElementById('arrowLayer');
  const attacker = game.selectedUnit;
  if (!attacker) return;
  const attackerEl = findUnitEl(attacker);
  if (!attackerEl) return;
  const aRect = attackerEl.getBoundingClientRect();
  const ax = aRect.left + aRect.width / 2;
  const ay = aRect.top + aRect.height / 2;

  const targets = [];
  game.frontLine.forEach(u => {
    if (u.owner === 'enemy' && canAttackTarget(attacker, u)) {
      const el = findUnitEl(u);
      if (el) { const r = el.getBoundingClientRect(); targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'unit' }); }
    }
  });
  game.enemyBack.forEach(u => {
    if (canAttackTarget(attacker, u)) {
      const el = findUnitEl(u);
      if (el) { const r = el.getBoundingClientRect(); targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'unit' }); }
    }
  });
  if (canAttackHQ(attacker)) {
    const hqEl = document.getElementById('enemyHQ');
    const r = hqEl.getBoundingClientRect();
    targets.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, type: 'hq' });
  }
  targets.forEach((t, i) => drawArrowSVG(svg, ax, ay, t.x, t.y, t.type === 'hq' ? '#ff3d71' : '#ffb000', i));
}

function drawArrowSVG(svg, x1, y1, x2, y2, color, delay) {
  const dx = x2 - x1, dy = y2 - y1;
  const len = Math.sqrt(dx * dx + dy * dy);
  const angle = Math.atan2(dy, dx);
  const shorten = 28;
  const sx = x1 + Math.cos(angle) * shorten;
  const sy = y1 + Math.sin(angle) * shorten;
  const ex = x2 - Math.cos(angle) * shorten;
  const ey = y2 - Math.sin(angle) * shorten;

  const midX = (sx + ex) / 2, midY = (sy + ey) / 2;
  const perpX = -Math.sin(angle), perpY = Math.cos(angle);
  const curve = Math.min(60, len * 0.2);
  const ctrlX = midX + perpX * curve;
  const ctrlY = midY + perpY * curve;

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', `M ${sx} ${sy} Q ${ctrlX} ${ctrlY} ${ex} ${ey}`);
  path.setAttribute('stroke', color);
  path.setAttribute('stroke-width', '4');
  path.setAttribute('fill', 'none');
  path.setAttribute('stroke-linecap', 'round');
  path.style.filter = `drop-shadow(0 0 8px ${color})`;
  path.style.animation = `dashIn 0.4s ${delay * 0.05}s both`;
  path.setAttribute('stroke-dasharray', '12 6');
  svg.appendChild(path);

  const headLen = 14, headAngle = 0.5;
  const hx1 = ex - Math.cos(angle - headAngle) * headLen;
  const hy1 = ey - Math.sin(angle - headAngle) * headLen;
  const hx2 = ex - Math.cos(angle + headAngle) * headLen;
  const hy2 = ey - Math.sin(angle + headAngle) * headLen;
  const head = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
  head.setAttribute('points', `${ex},${ey} ${hx1},${hy1} ${hx2},${hy2}`);
  head.setAttribute('fill', color);
  head.style.filter = `drop-shadow(0 0 8px ${color})`;
  svg.appendChild(head);

  const ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  ring.setAttribute('cx', x2);
  ring.setAttribute('cy', y2);
  ring.setAttribute('r', '16');
  ring.setAttribute('stroke', color);
  ring.setAttribute('stroke-width', '3');
  ring.setAttribute('fill', 'none');
  ring.style.animation = 'ringPulse 1s infinite';
  ring.style.filter = `drop-shadow(0 0 10px ${color})`;
  svg.appendChild(ring);
}

function clearArrows() { document.getElementById('arrowLayer').innerHTML = ''; }

function findUnitEl(unit) {
  const rows = [
    { id: 'frontLineRow', arr: game.frontLine },
    { id: 'playerBackRow', arr: game.playerBack },
    { id: 'enemyBackRow', arr: game.enemyBack },
  ];
  for (const r of rows) {
    const idx = r.arr.indexOf(unit);
    if (idx >= 0) {
      const row = document.getElementById(r.id);
      const children = row.querySelectorAll('.unit');
      if (children[idx]) return children[idx];
    }
  }
  return null;
}

/* ============ 攻击 HQ ============ */
function attackHQ(targetSide) {
  if (!game || game.turn !== 'player' || game.busy) return;
  if (targetSide !== 'enemy') return;
  const attacker = game.selectedUnit;
  if (!attacker) { toast('请先选中一个前线单位'); return; }
  if (!attacker.canAttack) { toast('该单位本回合已行动'); return; }
  if (!canAttackHQ(attacker)) {
    if (attacker.lane !== 'front') toast('后方单位无法攻击 HQ!');
    else toast('前线还有敌军,请先击败它们!');
    return;
  }
  const dmg = attacker.attack;
  game.enemyHQ -= dmg;
  attacker.canAttack = false;
  showFloatText(`-${dmg}`, '#ff3d71', document.getElementById('enemyHQ'));
  spawnSparks(window.innerWidth / 2, 60, '#ff3d71', 20);
  shakeScreen();
  game.selectedUnit = null;
  movingUnit = null;
  clearArrows();
  render();
  checkWin();
}

/* ============ 攻击执行 ============ */
function performAttack(attacker, defender) {
  const g = game;
  g.busy = true;
  const atkEl = findUnitEl(attacker);
  const defEl = findUnitEl(defender);
  if (atkEl) atkEl.classList.add('attacking');
  if (defEl) defEl.classList.add('hit');
  if (atkEl && defEl) {
    const ar = atkEl.getBoundingClientRect(), dr = defEl.getBoundingClientRect();
    drawStrikeLine(ar.left + ar.width / 2, ar.top + ar.height / 2, dr.left + dr.width / 2, dr.top + dr.height / 2);
  }
  defender.currentDefense -= attacker.attack;
  attacker.currentDefense -= defender.attack;
  if (defEl) { showFloatText(`-${attacker.attack}`, '#ff3d71', defEl); const r = defEl.getBoundingClientRect(); spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ffb000', 14); }
  if (atkEl) showFloatText(`-${defender.attack}`, '#ffb000', atkEl);
  shakeScreen();
  attacker.canAttack = false;
  g.selectedUnit = null;
  movingUnit = null;
  clearArrows();
  setTimeout(() => {
    game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
    game.playerBack = game.playerBack.filter(u => u.currentDefense > 0);
    game.enemyBack = game.enemyBack.filter(u => u.currentDefense > 0);
    updateFrontOwner();
    g.busy = false;
    render();
    checkWin();
  }, 400);
}

function updateFrontOwner() {
  const allyInFront = game.frontLine.filter(u => u.owner === 'player');
  const enemyInFront = game.frontLine.filter(u => u.owner === 'enemy');
  if (enemyInFront.length > 0) game.frontOwner = 'enemy';
  else if (allyInFront.length > 0) game.frontOwner = 'player';
  else game.frontOwner = null;
}

function drawStrikeLine(x1, y1, x2, y2) {
  const svg = document.getElementById('arrowLayer');
  const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  line.setAttribute('x1', x1); line.setAttribute('y1', y1);
  line.setAttribute('x2', x2); line.setAttribute('y2', y2);
  line.setAttribute('stroke', '#fff');
  line.setAttribute('stroke-width', '6');
  line.setAttribute('stroke-linecap', 'round');
  line.style.filter = 'drop-shadow(0 0 12px #fff) drop-shadow(0 0 20px #ffb000)';
  svg.appendChild(line);
  setTimeout(() => line.remove(), 300);
}

function spawnSparks(x, y, color, count) {
  for (let i = 0; i < count; i++) {
    const spark = document.createElement('div');
    spark.className = 'spark';
    const size = 3 + Math.random() * 5;
    spark.style.width = size + 'px';
    spark.style.height = size + 'px';
    spark.style.background = color;
    spark.style.left = x + 'px';
    spark.style.top = y + 'px';
    spark.style.boxShadow = `0 0 12px ${color}`;
    document.body.appendChild(spark);
    const angle = Math.random() * Math.PI * 2;
    const dist = 40 + Math.random() * 80;
    const tx = Math.cos(angle) * dist, ty = Math.sin(angle) * dist;
    spark.animate([
      { transform: 'translate(-50%, -50%) scale(1)', opacity: 1 },
      { transform: `translate(calc(-50% + ${tx}px), calc(-50% + ${ty}px)) scale(0)`, opacity: 0 },
    ], { duration: 600, easing: 'cubic-bezier(0.15, 0.7, 0.4, 1)' });
    setTimeout(() => spark.remove(), 600);
  }
}

function shakeScreen() {
  const el = document.getElementById('gameScreen');
  el.classList.add('screen-shake');
  setTimeout(() => el.classList.remove('screen-shake'), 350);
}

/* ============ 技能卡 ============ */
function useSkill(handIndex) {
  if (!game || game.turn !== 'player' || game.busy) return;
  const card = game.playerHand[handIndex];
  if (!card || card.type !== 'skill') return;
  if (game.playerKredit < card.cost) { toast('圣水不足!'); return; }

  game.playerKredit -= card.cost;
  game.playerHand.splice(handIndex, 1);

  const hint = document.createElement('div');
  hint.className = 'skill-hint';
  hint.textContent = `${card.icon} ${card.name}!`;
  document.body.appendChild(hint);
  setTimeout(() => hint.remove(), 800);

  switch (card.skill) {
    case 'airstrike': {
      game.frontLine.forEach(u => {
        if (u.owner === 'enemy') {
          u.currentDefense -= 3;
          const el = findUnitEl(u);
          if (el) {
            el.classList.add('hit');
            const r = el.getBoundingClientRect();
            spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 12);
            showFloatText('-3', '#ff3d71', el);
          }
        }
      });
      shakeScreen();
      setTimeout(() => {
        game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
        updateFrontOwner();
        render();
      }, 500);
      break;
    }
    case 'reinforce': {
      for (let i = 0; i < 2; i++) drawCard('player');
      toast('抽了 2 张牌');
      render();
      break;
    }
    case 'heal': {
      game.playerHQ = Math.min(30, game.playerHQ + 5);
      showFloatText('+5', '#00ffa0', document.getElementById('playerHQ'));
      render();
      break;
    }
    case 'charge': {
      game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = true; });
      toast('冲锋号!前线单位可立即攻击');
      render();
      break;
    }
    case 'buff': {
      game.frontLine.forEach(u => { if (u.owner === 'player') { u.attack += 1; u.currentDefense += 1; u.defense += 1; } });
      game.playerBack.forEach(u => { u.attack += 1; u.currentDefense += 1; u.defense += 1; });
      toast('铁血!全员 +1/+1');
      render();
      break;
    }
    case 'smoke': {
      game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = false; });
      game.enemyBack.forEach(u => u.canAttack = false);
      toast('烟幕!敌军无法攻击');
      render();
      break;
    }
  }
}

/* ============ 结束回合 ============ */
function endTurn() {
  if (!game || game.turn !== 'player' || game.busy) return;
  if (dragging) return;
  game.selectedUnit = null;
  movingUnit = null;
  clearArrows();
  game.turn = 'enemy';
  game.round++;
  render();
  setTimeout(() => enemyTurnStart(), 500);
}

function enemyTurnStart() {
  if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
  game.enemyKredit = Math.min(game.maxKredit, game.enemyKredit + 1);
  drawCard('enemy');
  game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = true; });
  game.enemyBack.forEach(u => { u.canAttack = true; u.canMove = true; });
  render();
  setTimeout(() => enemyPlayCards(), 500);
}

function enemyPlayCards() {
  if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
  const playable = game.enemyHand
    .map((c, i) => ({ c, i }))
    .filter(x => x.c.cost <= game.enemyKredit)
    .sort((a, b) => b.c.cost - a.c.cost);

  if (playable.length > 0 && Math.random() < 0.9) {
    const pick = playable[0];
    const card = pick.c;
    if (card.type === 'skill') {
      game.enemyKredit -= card.cost;
      game.enemyHand.splice(pick.i, 1);
      if (card.skill === 'airstrike') {
        game.frontLine.forEach(u => { if (u.owner === 'player') u.currentDefense -= 3; });
        setTimeout(() => { game.frontLine = game.frontLine.filter(u => u.currentDefense > 0); updateFrontOwner(); render(); }, 500);
      } else if (card.skill === 'heal') {
        game.enemyHQ = Math.min(30, game.enemyHQ + 5);
      } else if (card.skill === 'buff') {
        game.frontLine.forEach(u => { if (u.owner === 'enemy') { u.attack += 1; u.currentDefense += 1; } });
        game.enemyBack.forEach(u => { u.attack += 1; u.currentDefense += 1; });
      } else if (card.skill === 'charge') {
        game.frontLine.forEach(u => { if (u.owner === 'enemy') u.canAttack = true; });
        game.enemyBack.forEach(u => u.canAttack = true);
      } else if (card.skill === 'smoke') {
        game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = false; });
        game.playerBack.forEach(u => u.canAttack = false);
      } else if (card.skill === 'reinforce') {
        for (let i = 0; i < 2; i++) drawCard('enemy');
      }
      render();
      setTimeout(() => enemyPlayCards(), 500);
      return;
    }
    if (game.enemyBack.length >= MAX_BACK) {
      setTimeout(() => enemyMovePhase(), 300);
      return;
    }
    game.enemyKredit -= card.cost;
    game.enemyHand.splice(pick.i, 1);
    const newUnit = { ...card, owner: 'enemy', currentDefense: card.defense, canAttack: false, canMove: true, image: card.factionImage, lane: 'back' };
    game.enemyBack.push(newUnit);
    showFloatText('敌军部署:' + card.name, '#ff3d71', document.getElementById('enemyBackRow'));
    render();
    setTimeout(() => enemyPlayCards(), 400);
    return;
  }
  setTimeout(() => enemyMovePhase(), 300);
}

function enemyMovePhase() {
  if (game.playerHQ <= 0 || game.enemyHQ <= 0) { setTimeout(() => enemyAttackPhase(), 300); return; }

  if (game.frontOwner === 'player') { setTimeout(() => enemyAttackPhase(), 300); return; }
  if (game.frontLine.some(u => u.owner === 'player')) { setTimeout(() => enemyAttackPhase(), 300); return; }
  if (game.frontLine.length >= MAX_FRONT) { setTimeout(() => enemyAttackPhase(), 300); return; }

  const movable = game.enemyBack.filter(u => u.canMove && u.moveCost <= game.enemyKredit);
  if (movable.length === 0) { setTimeout(() => enemyAttackPhase(), 300); return; }

  movable.sort((a, b) => b.attack - a.attack);
  const u = movable[0];

  game.enemyKredit -= u.moveCost;
  const idx = game.enemyBack.indexOf(u);
  if (idx >= 0) game.enemyBack.splice(idx, 1);
  u.lane = 'front';
  u.canMove = false;
  u.canAttack = false;
  game.frontLine.push(u);
  game.frontOwner = 'enemy';

  const el = findUnitEl(u);
  if (el) {
    const r = el.getBoundingClientRect();
    spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 10);
  }
  render();
  setTimeout(() => enemyMovePhase(), 600);
}

function enemyAttackPhase() {
  if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
  const allAttackers = [
    ...game.frontLine.filter(u => u.owner === 'enemy' && u.canAttack).map(u => ({ u, lane: 'front' })),
    ...game.enemyBack.filter(u => u.canAttack).map(u => ({ u, lane: 'back' })),
  ];
  if (allAttackers.length === 0) { setTimeout(endEnemyTurn, 300); return; }
  const pick = allAttackers[0];
  const attacker = pick.u;
  attacker.canAttack = false;

  let targetInfo = null;
  if (pick.lane === 'front') {
    const myFront = game.frontLine.filter(u => u.owner === 'player');
    if (myFront.length > 0) {
      let best = myFront.find(u => u.currentDefense <= attacker.attack) || myFront[0];
      targetInfo = { unit: best };
    } else if (game.playerBack.length > 0 && Math.random() < 0.6) {
      targetInfo = { unit: game.playerBack[0] };
    } else {
      targetInfo = { hq: true };
    }
  } else {
    const myFront = game.frontLine.filter(u => u.owner === 'player');
    if (myFront.length > 0) {
      let best = myFront.find(u => u.currentDefense <= attacker.attack) || myFront[0];
      targetInfo = { unit: best };
    }
  }

  if (targetInfo && targetInfo.hq) {
    game.playerHQ -= attacker.attack;
    showFloatText(`-${attacker.attack}`, '#ffb000', document.getElementById('playerHQ'));
    spawnSparks(window.innerWidth / 2, window.innerHeight - 80, '#ffb000', 15);
    shakeScreen();
    render();
    checkWin();
    setTimeout(() => enemyAttackPhase(), 400);
    return;
  }
  if (targetInfo && targetInfo.unit) {
    const def = targetInfo.unit;
    const atkEl = findUnitEl(attacker);
    const defEl = findUnitEl(def);
    if (atkEl) atkEl.classList.add('attacking');
    if (defEl) defEl.classList.add('hit');
    if (atkEl && defEl) {
      const ar = atkEl.getBoundingClientRect(), dr = defEl.getBoundingClientRect();
      drawStrikeLine(ar.left + ar.width / 2, ar.top + ar.height / 2, dr.left + dr.width / 2, dr.top + dr.height / 2);
    }
    def.currentDefense -= attacker.attack;
    attacker.currentDefense -= def.attack;
    if (defEl) { showFloatText(`-${attacker.attack}`, '#ffb000', defEl); const r = defEl.getBoundingClientRect(); spawnSparks(r.left + r.width / 2, r.top + r.height / 2, '#ff3d71', 12); }
    if (atkEl) showFloatText(`-${def.attack}`, '#ff3d71', atkEl);
    shakeScreen();
    render();
    setTimeout(() => {
      game.frontLine = game.frontLine.filter(u => u.currentDefense > 0);
      game.playerBack = game.playerBack.filter(u => u.currentDefense > 0);
      game.enemyBack = game.enemyBack.filter(u => u.currentDefense > 0);
      updateFrontOwner();
      render();
      checkWin();
      setTimeout(() => enemyAttackPhase(), 400);
    }, 400);
    return;
  }
  setTimeout(() => enemyAttackPhase(), 200);
}

function endEnemyTurn() {
  if (game.playerHQ <= 0 || game.enemyHQ <= 0) return;
  game.turn = 'player';
  game.playerKredit = Math.min(game.maxKredit, game.playerKredit + 1);
  drawCard('player');
  game.frontLine.forEach(u => { if (u.owner === 'player') u.canAttack = true; });
  game.playerBack.forEach(u => { u.canAttack = true; u.canMove = true; });
  render();
}

/* ============ 胜负 ============ */
function checkWin() {
  if (game.playerHQ <= 0) { showOverlay('lose'); return true; }
  if (game.enemyHQ <= 0) { showOverlay('win'); return true; }
  return false;
}

function showOverlay(type) {
  game.busy = true;
  const overlay = document.createElement('div');
  overlay.className = 'overlay';
  const win = type === 'win';
  overlay.innerHTML = `
    <div class="overlay-title ${type}">${win ? '胜 利' : '失 败'}</div>
    <div class="overlay-sub">${win ? allyFactions[0].name + ' 军团突破了敌军防线' : enemyFactions[0].name + ' 军团攻陷了我军指挥部'}</div>
    <button class="overlay-btn" onclick="location.reload()">再 战 一 局</button>
  `;
  document.body.appendChild(overlay);
}

/* ============ 提示 ============ */
function toast(msg) {
  const t = document.createElement('div');
  t.className = 'toast';
  t.textContent = msg;
  document.body.appendChild(t);
  setTimeout(() => t.remove(), 1600);
}

function showFloatText(text, color, targetEl) {
  if (!targetEl) return;
  const r = targetEl.getBoundingClientRect();
  const span = document.createElement('div');
  span.className = 'float-text';
  span.textContent = text;
  span.style.color = color;
  span.style.left = (r.left + r.width / 2 + (Math.random() - 0.5) * 30) + 'px';
  span.style.top = (r.top + r.height / 2 - 20) + 'px';
  document.body.appendChild(span);
  setTimeout(() => span.remove(), 1000);
}

/* ============ 动画样式 ============ */
const style = document.createElement('style');
style.textContent = `
  @keyframes dashIn { from { opacity: 0; stroke-dashoffset: 40; } to { opacity: 1; stroke-dashoffset: 0; } }
  @keyframes ringPulse { 0%, 100% { r: 16; opacity: 0.8; } 50% { r: 22; opacity: 0.4; } }
`;
document.head.appendChild(style);

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

Game Source: 钢铁前线 - KARDS规则版

Creator: HyperShark77

Libraries: none

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