🎮ArcadeLab

指挥官 · Commander

by SwiftGalaxy49
2408 lines77.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, maximum-scale=1.0, user-scalable=no">
<title>指挥官 · Commander</title>
<style>
  :root{
    --cw: min(66px, 12vw, 7.2vh);
    --ch: calc(var(--cw) * 1.38);
    --panel: #1c2130;
    --line: #2a3350;
    --accent: #4a7cff;
    --gold: #ffd75e;
    --text: #e6ebf5;
    --dim: #7c86a6;
    --red: #ff6b6b;
  }
  *{ box-sizing: border-box; }
  html, body{ height: 100%; margin: 0; }
  body{
    background: radial-gradient(ellipse at 50% -10%, #1e2942 0%, #0e1220 62%);
    color: var(--text);
    font-family: system-ui, "Microsoft YaHei", sans-serif;
    overflow: hidden;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  #app{ height: 100vh; height: 100dvh; display: flex; flex-direction: column; }

  /* 顶栏 */
  .topbar{
    flex: 0 0 auto; height: 42px;
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 12px;
    border-bottom: 1px solid var(--line);
    background: rgba(20,24,36,.72);
  }
  .brand{ font-weight: 700; letter-spacing: 3px; font-size: 15px; }
  .brand small{ color: var(--dim); font-weight: 400; font-size: 9px; letter-spacing: 2px; margin-left: 6px; }
  .top-actions{ display: flex; align-items: center; gap: 8px; }

  /* 棋盘 */
  .board{
    flex: 1 1 auto; min-height: 0;
    width: 100%; max-width: 720px; margin: 0 auto;
    display: flex; flex-direction: column; gap: 4px;
    padding: 6px 8px 8px;
  }
  .hand-row{
    flex: 0 0 auto;
    display: flex; justify-content: center; align-items: center;
    height: calc(var(--ch) + 4px);
  }
  .hand-row .card + .card{ margin-left: calc(var(--cw) * -0.22); }

  .battlefield{
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column; gap: 6px;
  }
  .lane{
    flex: 1 1 0; min-height: 0;
    display: flex; align-items: center; justify-content: center;
    gap: 4px; border-radius: 10px; padding: 4px;
  }
  .lane-enemy{ background: rgba(255,107,107,.05); }
  .lane-contested{ background: rgba(255,215,94,.05); }
  .lane-player{ background: rgba(74,124,255,.07); }

  .slot{
    flex: 0 0 auto;
    width: var(--cw); height: var(--ch);
    border-radius: 9px;
    border: 1px dashed rgba(74,124,255,.18);
    display: grid; place-items: center;
    transition: border-color .15s, background .15s, box-shadow .15s;
  }
  .slot.empty.valid-target{
    border-color: var(--accent);
    background: rgba(74,124,255,.12);
    border-style: solid; cursor: pointer;
    box-shadow: 0 0 10px rgba(74,124,255,.28) inset;
  }
  .slot.empty.valid-target:hover{ background: rgba(74,124,255,.22); }

  .hq-slot{
    flex: 0 0 auto;
    width: var(--cw); height: var(--ch);
    border-radius: 9px;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 2px;
    background: linear-gradient(180deg, #232b42, #1a2032);
    border: 1px solid var(--line);
    color: #c9d2e8;
    transition: border-color .15s, box-shadow .15s, background .15s;
  }
  .hq-slot .hq-label{ font-size: calc(var(--cw) * 0.14); letter-spacing: 1px; color: var(--dim); }
  .hq-slot .hq-hp{
    font-size: calc(var(--cw) * 0.26); font-weight: 800;
    font-variant-numeric: tabular-nums; color: var(--gold); line-height: 1;
  }
  .hq-slot.enemy .hq-hp{ color: var(--red); }
  .hq-slot.valid-target{
    cursor: pointer;
    border-color: var(--gold);
    box-shadow: 0 0 0 1px var(--gold), 0 0 12px rgba(255,215,94,.4);
  }

  /* 卡牌 */
  .card{
    position: relative;
    width: var(--cw); height: var(--ch);
    border-radius: 9px; overflow: hidden;
    display: flex; flex-direction: column;
    border: 2px solid #f0f2f8;
    box-shadow: 0 3px 9px rgba(0,0,0,.5);
    transition: transform .12s ease, box-shadow .12s ease, opacity .2s, border-color .15s;
  }
  .card.r-common{ background: linear-gradient(180deg, #232838 0%, #12151f 100%); }
  .card.r-rare{ background: linear-gradient(180deg, #1c3a5c 0%, #0e1d30 100%); }
  .card.r-epic{ background: linear-gradient(180deg, #38205c 0%, #1c0e30 100%); }
  .card.r-legendary{ background: linear-gradient(180deg, #5c4a1c 0%, #30260e 100%); }
  .card.r-legendary .name{ color: #ffe6a3; }

  .card .cost{
    position: absolute; top: 3px; left: 3px;
    width: calc(var(--cw) * 0.28); height: calc(var(--cw) * 0.28);
    border-radius: 50%;
    display: grid; place-items: center;
    background: var(--accent); color: #fff;
    font-size: calc(var(--cw) * 0.16); font-weight: 700;
    box-shadow: 0 1px 4px rgba(0,0,0,.4);
    z-index: 1;
  }
  .card .name{
    flex: 1 1 auto;
    display: grid; place-items: center;
    text-align: center;
    padding: 0 4px;
    font-size: calc(var(--cw) * 0.185);
    font-weight: 700;
    color: #e8eefc;
    line-height: 1.25; word-break: break-all;
  }
  .card .stats{
    display: flex; justify-content: space-between;
    padding: 3px 6px 4px;
    font-size: calc(var(--cw) * 0.2); font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  .card .atk{ color: var(--gold); }
  .card .hp{ color: #ff8a8a; }

  .card.command{ background: linear-gradient(180deg, #1c3350 0%, #0f1a28 100%) !important; }
  .card.command .cost{ background: #3f8fd8; }

  .card.back{
    display: grid; place-items: center;
    background: repeating-linear-gradient(45deg, #171d2c 0 6px, #1e2740 6px 12px) !important;
    border-color: #333f63;
    color: #4d5980; font-size: calc(var(--cw) * 0.16);
    letter-spacing: 2px; font-weight: 700;
  }

  .card.hand{ cursor: pointer; }
  .card.hand:hover{ transform: translateY(-12px); box-shadow: 0 10px 18px rgba(0,0,0,.6); }
  .card.hand.unaffordable{ opacity: .42; }
  .card.hand.unaffordable:hover{ transform: translateY(-4px); }

  .card.unit.exhausted{ opacity: .5; filter: grayscale(.45); }
  .card.unit.can-act{ cursor: pointer; }
  .card.unit.can-act:hover{ transform: translateY(-4px); }
  .card.owned-enemy::before{
    content: '';
    position: absolute; inset: 0;
    border-radius: 7px;
    border: 1px solid rgba(255,107,107,.45);
    pointer-events: none;
  }

  .card.selected{
    border-color: var(--gold) !important;
    box-shadow: 0 0 0 2px var(--gold), 0 8px 16px rgba(255,215,94,.35) !important;
    transform: translateY(-6px);
  }
  .card.targetable{
    cursor: pointer;
    box-shadow: 0 0 0 2px var(--red), 0 0 12px rgba(255,107,107,.5) !important;
  }

  /* 按钮 */
  .btn{
    border: 0; border-radius: 9px; cursor: pointer;
    padding: 8px 16px; font-size: 13px; font-weight: 600;
    color: #fff; background: var(--accent);
    font-family: inherit;
    transition: filter .15s, transform .08s;
  }
  .btn:hover{ filter: brightness(1.1); }
  .btn:active{ transform: scale(.97); }
  .btn:disabled{ opacity: .35; cursor: not-allowed; filter: none; transform: none; }
  .btn.ghost{ background: transparent; border: 1px solid var(--line); color: #9aa4bf; font-weight: 400; }
  .topbar .btn{ padding: 5px 11px; font-size: 12px; }

  .text-btn{
    padding: 5px 12px; border-radius: 9px;
    background: rgba(28,33,48,.9); border: 1px solid var(--line);
    color: var(--text); font-size: 12px; font-family: inherit;
    cursor: pointer;
    transition: border-color .16s, background .16s, transform .1s;
  }
  .text-btn:hover{ border-color: #3a5aa8; background: #222b42; }
  .text-btn:active{ transform: scale(.97); }

  /* 信息条 */
  .info-bar{
    flex: 0 0 auto;
    display: flex; align-items: center; gap: 10px;
    padding: 6px 10px;
    background: rgba(20,24,36,.6);
    border-top: 1px solid var(--line);
    font-size: 12px;
  }
  .info-side{ display: flex; align-items: center; gap: 8px; flex: 1 1 0; }
  .info-side.right{ justify-content: flex-end; }
  .info-hp{ color: #c9d2e8; font-variant-numeric: tabular-nums; }
  .info-kredits{
    font-weight: 700; color: #7fb0ff;
    background: rgba(74,124,255,.13); border: 1px solid #2f4890;
    padding: 2px 8px; border-radius: 99px; font-variant-numeric: tabular-nums;
  }
  .turn-info{
    flex: 0 0 auto; font-size: 12px; color: var(--dim);
    letter-spacing: .5px; text-align: center; min-width: 130px;
  }
  .diff-tag{
    flex: 0 0 auto; font-size: 10px; font-weight: 700;
    letter-spacing: 1px; padding: 2px 8px; border-radius: 99px;
    border: 1px solid var(--line); color: var(--dim);
  }
  .diff-tag.easy{ color: #6ee7a8; border-color: #2d6b4c; background: rgba(110,231,168,.08); }
  .diff-tag.normal{ color: #7fb0ff; border-color: #2f4890; background: rgba(74,124,255,.08); }
  .diff-tag.hard{ color: #ffb45e; border-color: #8a5a2e; background: rgba(255,180,94,.08); }

  .coin-badge{
    display: inline-flex; align-items: center; gap: 5px;
    padding: 4px 10px; border-radius: 99px;
    background: linear-gradient(180deg, rgba(255,215,94,.16), rgba(255,183,74,.06));
    border: 1px solid rgba(255,215,94,.38);
    color: var(--gold); font-size: 12px; font-weight: 700;
    font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .coin-badge.small{ padding: 3px 9px; font-size: 11px; }

  .toast{
    position: fixed; left: 50%; bottom: 60px; transform: translate(-50%, 12px);
    background: rgba(28,33,48,.96); border: 1px solid var(--line);
    color: #cfe0ff; font-size: 12px; padding: 8px 16px; border-radius: 99px;
    opacity: 0; pointer-events: none; transition: all .22s ease; z-index: 90;
    white-space: nowrap;
  }
  .toast.show{ opacity: 1; transform: translate(-50%, 0); }

  /* 弹窗通用 */
  .modal{
    position: fixed; inset: 0; z-index: 50;
    display: none; place-items: center; padding: 16px;
    background: rgba(6,9,16,.78);
    -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  }
  .modal.show{ display: grid; }
  .modal-card{
    width: min(400px, 100%);
    max-height: calc(100vh - 32px); overflow: auto;
    background: var(--panel); border: 1px solid var(--line);
    border-radius: 16px; padding: 18px;
    box-shadow: 0 20px 50px rgba(0,0,0,.6);
  }
  .modal-head{ display: flex; align-items: center; justify-content: space-between; }
  .modal-head h2{ margin: 0; font-size: 15px; letter-spacing: 1px; }
  .btn-x{
    background: transparent; border: 0; color: var(--dim);
    font-size: 16px; cursor: pointer; padding: 2px 6px;
  }
  .btn-x:hover{ color: var(--text); }
  .hint{ font-size: 11px; line-height: 1.7; color: var(--dim); margin: 10px 0 12px; }
  .modal-card textarea{
    width: 100%; height: 74px; box-sizing: border-box;
    padding: 9px; border-radius: 9px; resize: none;
    border: 1px solid var(--line); background: #10131d; color: #7c86a6;
    font-size: 11px; font-family: ui-monospace, monospace; outline: none;
  }
  .modal-card textarea:focus, .modal-card input:focus{ border-color: #3a5aa8; }
  .modal-card input{
    width: 100%; box-sizing: border-box; margin-top: 8px;
    padding: 9px 11px; border-radius: 9px;
    border: 1px solid var(--line); background: #10131d; color: var(--text);
    font-size: 13px; font-family: inherit; outline: none;
  }
  .modal-card .btn{ width: 100%; margin-top: 8px; }
  .save-status{ min-height: 16px; margin-top: 10px; font-size: 12px; text-align: center; color: #6ee7a8; }

  /* 卡牌详情 */
  .cd-layout{ display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 4px 0; }
  .cd-preview .card{ --cw: 120px; pointer-events: none; }
  .cd-info{ width: 100%; display: flex; flex-direction: column; gap: 8px; }
  .cd-name{ text-align: center; font-size: 18px; font-weight: 700; letter-spacing: 2px; margin: 0; }
  .cd-badges{ display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
  .cd-rarity{
    font-size: 11px; font-weight: 700; letter-spacing: 1px;
    padding: 3px 10px; border-radius: 99px;
    background: #2a3350; color: #9aa4bf; border: 1px solid #38446a;
  }
  .cd-rarity.r-rare{ background: rgba(74,158,255,.16); color: #90c0ff; border-color: #3a72b8; }
  .cd-rarity.r-epic{ background: rgba(160,107,255,.16); color: #c6a1ff; border-color: #6b4fb8; }
  .cd-rarity.r-legendary{ background: rgba(255,183,74,.16); color: #ffd75e; border-color: #a8823a; }
  .cd-cost-badge{
    font-size: 11px; font-weight: 700; letter-spacing: 1px;
    padding: 3px 10px; border-radius: 99px;
    background: rgba(74,124,255,.15); color: #7fb0ff; border: 1px solid #2f4890;
  }
  .cd-stat-row{ display: flex; gap: 10px; margin-top: 4px; }
  .cd-stat{
    flex: 1 1 0; display: flex; flex-direction: column;
    align-items: center; gap: 3px; padding: 10px 6px;
    border-radius: 11px; background: rgba(12,16,32,.6); border: 1px solid var(--line);
  }
  .cd-stat small{ font-size: 10px; color: var(--dim); letter-spacing: 1px; }
  .cd-stat b{ font-size: 20px; font-variant-numeric: tabular-nums; }
  .cd-stat.atk b{ color: var(--gold); }
  .cd-stat.hp b{ color: #ff8a8a; }
  .cd-desc{
    margin: 4px 0 0; padding: 10px 12px; border-radius: 10px;
    background: rgba(12,16,32,.6); border: 1px solid var(--line);
    font-size: 12.5px; line-height: 1.7; color: #b9c3da; min-height: 42px;
  }

  /* 结算 */
  .overlay{
    position: fixed; inset: 0; z-index: 55;
    display: none; place-items: center; background: rgba(6,9,16,.8);
  }
  .overlay.show{ display: grid; }
  .overlay-card{
    text-align: center; padding: 30px 40px;
    background: var(--panel); border: 1px solid var(--line);
    border-radius: 18px; min-width: 240px;
  }
  .overlay-title{ font-size: 26px; font-weight: 700; letter-spacing: 6px; margin-bottom: 10px; }
  .overlay-title.win{ color: var(--gold); }
  .overlay-title.lose{ color: #ff7b7b; }
  .overlay-reward{
    font-size: 13px; font-weight: 700; color: var(--gold);
    margin-bottom: 18px; letter-spacing: 1px;
  }

  /* 大厅 */
  .lobby{
    position: fixed; inset: 0; z-index: 40;
    display: grid; place-items: center; padding: 20px;
    background: radial-gradient(ellipse at 50% 0%, #1e2942 0%, #0b0e18 65%);
    transition: opacity .28s ease, visibility .28s;
  }
  .lobby.hidden{ opacity: 0; visibility: hidden; pointer-events: none; }
  .lobby-top{
    position: absolute; top: 14px; right: 14px;
    display: flex; align-items: center; gap: 8px;
  }
  .lobby-inner{ width: min(300px, 100%); display: flex; flex-direction: column; gap: 18px; }
  .lobby-logo{ text-align: center; }
  .lobby-title{
    font-size: 34px; font-weight: 800; letter-spacing: 8px; text-indent: 8px;
    background: linear-gradient(180deg, #ffffff 0%, #8fb2ff 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    line-height: 1.1;
  }
  .lobby-sub{ margin-top: 6px; font-size: 9px; letter-spacing: 3px; color: var(--dim); }
  .lobby-menu{ display: flex; flex-direction: column; gap: 8px; }
  .menu-btn{
    display: flex; align-items: center;
    width: 100%; text-align: left; padding: 10px 13px;
    border-radius: 11px; cursor: pointer; font-family: inherit;
    background: rgba(28,33,48,.9); border: 1px solid var(--line);
    color: var(--text);
    transition: background .16s, border-color .16s, transform .1s;
  }
  .menu-btn:hover{ border-color: #3a5aa8; background: #222b42; transform: translateY(-1px); }
  .menu-btn:active{ transform: scale(.985); }
  .menu-btn:disabled{ opacity: .38; cursor: not-allowed; transform: none;
    border-color: var(--line); background: rgba(28,33,48,.9); }
  .menu-btn.primary{
    background: linear-gradient(180deg, #3f6fe8, #3559c4);
    border-color: #4a7cff;
  }
  .menu-btn.primary:hover{ background: linear-gradient(180deg, #4b7cf5, #3a62d6); border-color: #6b93ff; }
  .mb-text{ display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .mb-text b{ font-size: 13px; font-weight: 600; letter-spacing: .5px; }
  .mb-text small{ font-size: 10px; color: #8d98b8; }
  .menu-btn.primary .mb-text small{ color: #c6d6ff; }
  .lobby-foot{ text-align: center; font-size: 10px; color: #4d5980; letter-spacing: 1px; }

  /* 商店 */
  .shop-balance{
    display: flex; align-items: baseline; justify-content: center; gap: 8px;
    margin: 12px 0 14px; padding: 11px; border-radius: 12px;
    background: linear-gradient(180deg, rgba(255,215,94,.10), rgba(255,183,74,.04));
    border: 1px solid rgba(255,215,94,.28);
    color: var(--gold); font-size: 21px; font-weight: 800;
    font-variant-numeric: tabular-nums;
  }
  .shop-balance small{ font-size: 11px; font-weight: 400; color: #b39a52; letter-spacing: 1px; }
  .shop-list{ display: flex; flex-direction: column; gap: 11px; }
  .shop-pack{
    display: flex; align-items: center; gap: 12px;
    padding: 13px 14px; border-radius: 13px;
    background: linear-gradient(180deg, #232b42 0%, #1a2032 100%);
    border: 1px solid var(--line);
    transition: border-color .16s, transform .1s;
  }
  .shop-pack:hover{ border-color: #3a5aa8; transform: translateY(-1px); }
  .shop-pack-body{ flex: 1 1 auto; min-width: 0; }
  .shop-pack-name{ font-size: 14px; font-weight: 700; letter-spacing: .5px; }
  .shop-pack.officer .shop-pack-name{ color: var(--gold); }
  .shop-pack-desc{ font-size: 11px; color: var(--dim); margin-top: 4px; line-height: 1.5; }
  .shop-buy{
    flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px;
    padding: 9px 14px; border-radius: 10px;
    font-size: 13px; font-weight: 700;
    font-family: inherit; cursor: pointer;
    color: #fff; border: 0;
    background: linear-gradient(180deg, #3f6fe8, #3559c4);
    font-variant-numeric: tabular-nums; white-space: nowrap;
    transition: filter .15s, transform .08s;
  }
  .shop-buy:hover{ filter: brightness(1.12); }
  .shop-buy:active{ transform: scale(.96); }
  .shop-buy:disabled{ opacity: .35; cursor: not-allowed; filter: none; transform: none; }

  /* 战役弹窗 */
  .battle-label{
    font-size: 11px; color: var(--dim); letter-spacing: 1.5px;
    margin: 14px 0 8px;
  }
  .battle-label:first-of-type{ margin-top: 8px; }

  .diff-list{ display: flex; flex-direction: column; gap: 8px; }
  .diff-btn{
    display: flex; align-items: center; gap: 12px;
    padding: 11px 14px; border-radius: 12px;
    background: rgba(28,33,48,.9);
    border: 1px solid var(--line);
    color: var(--text); font-family: inherit;
    cursor: pointer; text-align: left;
    transition: border-color .15s, background .15s, transform .1s;
  }
  .diff-btn:hover{ border-color: #3a5aa8; }
  .diff-btn.selected{
    border-color: var(--accent);
    background: rgba(74,124,255,.13);
    box-shadow: 0 0 0 1px var(--accent);
  }
  .diff-btn .d-name{ font-size: 14px; font-weight: 700; letter-spacing: .5px; }
  .diff-btn .d-desc{ font-size: 11px; color: var(--dim); margin-top: 2px; line-height: 1.5; }
  .diff-btn .d-reward{
    flex: 0 0 auto; font-size: 13px; font-weight: 800;
    color: var(--gold); font-variant-numeric: tabular-nums;
    padding: 4px 10px; border-radius: 99px;
    background: rgba(255,215,94,.1); border: 1px solid rgba(255,215,94,.3);
  }
  .diff-btn.selected .d-reward{
    background: rgba(255,215,94,.18);
  }

  .deck-pick-list{ display: flex; flex-direction: column; gap: 8px; }
  .deck-pick{
    display: flex; align-items: center; gap: 10px;
    padding: 10px 14px; border-radius: 11px;
    background: rgba(28,33,48,.9);
    border: 1px solid var(--line);
    color: var(--text); font-family: inherit;
    cursor: pointer; text-align: left;
    transition: border-color .15s, background .15s;
  }
  .deck-pick:hover{ border-color: #3a5aa8; }
  .deck-pick.selected{
    border-color: var(--accent);
    background: rgba(74,124,255,.13);
    box-shadow: 0 0 0 1px var(--accent);
  }
  .deck-pick:disabled{ opacity: .35; cursor: not-allowed; }
  .deck-pick .dp-name{ flex: 1 1 auto; font-size: 13px; font-weight: 600; }
  .deck-pick .dp-count{ font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }
  .deck-pick.selected .dp-count{ color: #90c0ff; }

  /* 开包 */
  .pack-overlay{
    position: fixed; inset: 0; z-index: 70;
    display: none; place-items: center; padding: 20px;
    background: radial-gradient(ellipse at 50% 40%, rgba(30,41,66,.97), rgba(6,9,16,.98));
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  }
  .pack-overlay.show{ display: grid; }
  .pack-inner{ display: flex; flex-direction: column; align-items: center; gap: 22px; max-width: 100%; }
  .pack-heading{
    font-size: 15px; font-weight: 700; letter-spacing: 5px;
    color: var(--gold); text-align: center;
  }
  .pack-heading small{
    display: block; margin-top: 6px;
    font-size: 10px; font-weight: 400; letter-spacing: 2px; color: var(--dim);
  }
  .pack-cards{
    display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
    max-width: min(620px, 96vw);
  }
  .pack-cards .card{
    --cw: clamp(52px, 14vw, 76px);
    animation: cardIn .48s cubic-bezier(.18,.9,.32,1.35) backwards;
    cursor: pointer;
  }
  @keyframes cardIn{
    from{ opacity: 0; transform: translateY(34px) scale(.55) rotate(-9deg); }
    to  { opacity: 1; transform: translateY(0)    scale(1)   rotate(0deg); }
  }

  /* 图鉴 + 组卡 */
  .codex-card{ width: min(580px, 100%); }

  .codex-tabs-row{
    display: flex; align-items: center; gap: 8px;
    margin: 12px 0 10px;
  }
  .deck-tabs{ display: flex; gap: 6px; flex: 1 1 auto; }
  .deck-tab{
    padding: 5px 10px;
    border-radius: 7px;
    background: transparent;
    border: 1px solid var(--line);
    color: var(--dim);
    font-family: inherit; font-size: 11px;
    cursor: pointer; white-space: nowrap;
    transition: all .15s;
  }
  .deck-tab:hover{ color: var(--text); border-color: #3a5aa8; }
  .deck-tab.active{
    background: rgba(74,124,255,.13);
    border-color: var(--accent);
    color: var(--text);
  }
  .deck-tab small{ opacity: .7; margin-left: 3px; font-variant-numeric: tabular-nums; }

  .codex-toolbar{
    display: flex; align-items: center; gap: 10px;
    margin: 0 0 12px;
    padding: 9px 12px;
    border-radius: 10px;
    background: rgba(12,16,32,.55);
    border: 1px solid var(--line);
    font-size: 12px;
    color: var(--dim);
  }
  .codex-toolbar .deck-num{
    font-size: 15px; font-weight: 800; color: var(--gold);
    font-variant-numeric: tabular-nums;
  }
  .codex-toolbar .spacer{ flex: 1 1 auto; }

  .codex-name-row{
    display: flex; gap: 8px; align-items: center;
    margin: 0 0 12px;
  }
  .codex-name-row input{
    flex: 1 1 auto;
    margin: 0;
    padding: 8px 11px;
    font-size: 12.5px;
    border-radius: 9px;
    border: 1px solid var(--line);
    background: #10131d;
    color: var(--text);
    font-family: inherit;
    outline: none;
    box-sizing: border-box;
  }
  .codex-name-row input:focus{ border-color: #3a5aa8; }
  .codex-name-row .btn{
    width: auto; margin: 0;
    padding: 8px 14px; font-size: 12px;
  }

  .codex-legend{
    display: flex; flex-wrap: wrap; gap: 12px;
    margin-bottom: 12px;
    font-size: 10.5px; color: var(--dim);
  }
  .codex-legend span{ display: inline-flex; align-items: center; gap: 5px; }
  .codex-legend i{
    width: 11px; height: 11px; border-radius: 3px;
    border: 1.5px solid #f0f2f8;
    display: inline-block;
  }
  .codex-legend i.c-common{ background: #232838; }
  .codex-legend i.c-rare{ background: #1c3a5c; }
  .codex-legend i.c-epic{ background: #38205c; }
  .codex-legend i.c-legendary{ background: #5c4a1c; }

  .codex-grid{
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
    gap: 10px;
    justify-items: center;
    max-height: calc(100vh - 380px);
    overflow-y: auto;
    padding-right: 4px;
  }
  .codex-grid::-webkit-scrollbar{ width: 6px; }
  .codex-grid::-webkit-scrollbar-thumb{ background: #2a3350; border-radius: 99px; }

  .codex-grid .card{
    --cw: 78px;
    cursor: pointer;
    position: relative;
    margin: 0 auto;
  }
  .codex-grid .card.not-owned{
    filter: grayscale(1);
    opacity: .32;
  }
  .codex-grid .card:hover{
    transform: translateY(-4px);
    box-shadow: 0 8px 16px rgba(0,0,0,.6);
  }

  .owned-badge,
  .deck-badge{
    position: absolute;
    font-size: 9px; font-weight: 800;
    padding: 1px 5px; border-radius: 6px;
    z-index: 2;
    font-variant-numeric: tabular-nums;
    letter-spacing: .3px;
    pointer-events: none;
  }
  .owned-badge{
    top: 3px; right: 3px;
    background: rgba(74,124,255,.85); color: #fff;
  }
  .deck-badge{
    bottom: 3px; right: 3px;
    background: rgba(255,215,94,.92); color: #2a1f00;
  }

  .settings-empty{
    padding: 24px 10px; text-align: center;
    color: var(--dim); font-size: 12.5px; line-height: 2;
  }
</style>
</head>
<body>

<!-- ============================================================
     大厅
     ============================================================ -->
<div class="lobby" id="lobby">
  <div class="lobby-top">
    <div class="coin-badge">金币 <span id="lobbyCoins">1000</span></div>
    <button class="text-btn" id="settingsBtn">设置</button>
  </div>

  <div class="lobby-inner">
    <div class="lobby-logo">
      <div class="lobby-title">指挥官</div>
      <div class="lobby-sub">C O M M A N D E R</div>
    </div>

    <div class="lobby-menu">
      <button class="menu-btn primary" id="startBtn">
        <span class="mb-text">
          <b>开始战役</b>
          <small>选择难度与卡组</small>
        </span>
      </button>
      <button class="menu-btn" id="resumeBtn" disabled>
        <span class="mb-text">
          <b>继续当前对局</b>
          <small>回到上一局战场</small>
        </span>
      </button>
      <button class="menu-btn" id="loadBtn">
        <span class="mb-text">
          <b>读取存档</b>
          <small>粘贴存档码还原进度</small>
        </span>
      </button>
      <button class="menu-btn" id="shopBtn">
        <span class="mb-text">
          <b>卡包商店</b>
          <small>消耗金币抽取单位卡牌</small>
        </span>
      </button>
      <button class="menu-btn" id="codexBtn">
        <span class="mb-text">
          <b>卡牌图鉴</b>
          <small>查看全部卡牌 · 编辑卡组</small>
        </span>
      </button>
    </div>

    <div class="lobby-foot">v0.6 · 支持密码存档</div>
  </div>
</div>

<!-- ============================================================
     游戏
     ============================================================ -->
<div id="app">

  <header class="topbar">
    <div class="brand">指挥官 <small>COMMANDER</small></div>
    <div class="top-actions">
      <div class="coin-badge small">金币 <span id="gameCoins">1000</span></div>
      <button class="btn ghost" id="homeBtn">大厅</button>
      <button class="btn ghost" id="saveBtn">存档</button>
    </div>
  </header>

  <main class="board">

    <div class="hand-row" id="enemyHand"></div>

    <div class="battlefield">

      <div class="lane lane-enemy">
        <div class="slot" data-lane="enemyHome" data-idx="0"></div>
        <div class="slot" data-lane="enemyHome" data-idx="1"></div>
        <div class="hq-slot enemy" data-hq="enemy">
          <span class="hq-label">敌方总部</span>
          <span class="hq-hp" id="enemyHqHp">20</span>
        </div>
        <div class="slot" data-lane="enemyHome" data-idx="3"></div>
        <div class="slot" data-lane="enemyHome" data-idx="4"></div>
      </div>

      <div class="lane lane-contested">
        <div class="slot" data-lane="contested" data-idx="0"></div>
        <div class="slot" data-lane="contested" data-idx="1"></div>
        <div class="slot" data-lane="contested" data-idx="2"></div>
        <div class="slot" data-lane="contested" data-idx="3"></div>
        <div class="slot" data-lane="contested" data-idx="4"></div>
      </div>

      <div class="lane lane-player">
        <div class="slot" data-lane="playerHome" data-idx="0"></div>
        <div class="slot" data-lane="playerHome" data-idx="1"></div>
        <div class="hq-slot" data-hq="player">
          <span class="hq-label">我方总部</span>
          <span class="hq-hp" id="playerHqHp">20</span>
        </div>
        <div class="slot" data-lane="playerHome" data-idx="3"></div>
        <div class="slot" data-lane="playerHome" data-idx="4"></div>
      </div>

    </div>

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

  </main>

  <div class="info-bar">
    <div class="info-side">
      <span class="diff-tag" id="diffTag">普通</span>
      <span class="info-hp" id="playerInfoHp">20 / 20</span>
      <span class="info-kredits" id="playerKredits">0 / 0</span>
      <span style="color:var(--dim);font-size:11px" id="playerMeta">手牌 0 · 牌库 0</span>
    </div>
    <div class="turn-info" id="turnInfo">第 1 回合 · 你的回合</div>
    <div class="info-side right">
      <span style="color:var(--dim);font-size:11px" id="enemyMeta">手牌 0 · 牌库 0</span>
      <button class="btn" id="endTurnBtn">结束回合</button>
    </div>
  </div>

  <div class="toast" id="toast"></div>

  <!-- ========== 战役弹窗 ========== -->
  <div class="modal" id="battleModal">
    <div class="modal-card">
      <div class="modal-head">
        <h2>开始战役</h2>
        <button class="btn-x" id="closeBattle">X</button>
      </div>

      <div class="battle-label">选择难度</div>
      <div class="diff-list" id="diffList"></div>

      <div class="battle-label">选择卡组</div>
      <div class="deck-pick-list" id="deckPickList"></div>

      <button class="btn" id="battleStartBtn" style="margin-top:18px" disabled>开 始 战 役</button>
    </div>
  </div>

  <!-- ========== 存档 ========== -->
  <div class="modal" id="modal">
    <div class="modal-card">
      <div class="modal-head">
        <h2>存档</h2>
        <button class="btn-x" id="closeModal">X</button>
      </div>
      <p class="hint">
        导出:设一个密码 → 生成存档码 → 复制保存(含金币与卡组)。<br>
        读入:把存档码粘贴到下面,输入同样的密码即可还原。
      </p>
      <textarea id="saveCode" placeholder="导出后这里是存档码;读档时粘贴到这里"></textarea>
      <input id="pwdInput" type="text" placeholder="密码(可留空)">
      <button class="btn" id="exportBtn">导出存档</button>
      <button class="btn" id="importBtn">读入存档</button>
      <button class="btn ghost" id="newGameBtn">重开新游戏</button>
      <div class="save-status" id="saveStatus"></div>
    </div>
  </div>

  <!-- ========== 卡包商店 ========== -->
  <div class="modal" id="shopModal">
    <div class="modal-card">
      <div class="modal-head">
        <h2>卡包商店</h2>
        <button class="btn-x" id="closeShop">X</button>
      </div>
      <div class="shop-balance">
        <span id="shopCoins">1000</span>
        <small>金币</small>
      </div>
      <div class="shop-list" id="shopList"></div>
    </div>
  </div>

  <!-- ========== 卡牌图鉴 ========== -->
  <div class="modal" id="codexModal">
    <div class="modal-card codex-card">
      <div class="modal-head">
        <h2>卡牌图鉴 · 卡组编辑</h2>
        <button class="btn-x" id="closeCodex">X</button>
      </div>

      <div class="codex-tabs-row">
        <div class="deck-tabs" id="deckTabs"></div>
      </div>

      <div class="codex-toolbar">
        <span>当前卡牌数</span>
        <span class="deck-num"><span id="deckCount">0</span> / 40</span>
        <span class="spacer"></span>
        <span style="font-size:10.5px">点击卡牌添加 · 双击看详情</span>
      </div>

      <div class="codex-name-row">
        <input id="deckNameInput" type="text" maxlength="12" placeholder="输入卡组名称">
        <button class="btn ghost" id="clearDeckBtn">清空</button>
        <button class="btn" id="saveDeckBtn">保存卡组</button>
      </div>

      <div class="codex-legend">
        <span><i class="c-common"></i>普通</span>
        <span><i class="c-rare"></i>稀有</span>
        <span><i class="c-epic"></i>特殊</span>
        <span><i class="c-legendary"></i>精锐</span>
        <span style="margin-left:auto">未拥有卡牌显示为灰色</span>
      </div>

      <div class="codex-grid" id="codexGrid"></div>
    </div>
  </div>

  <!-- ========== 卡牌详情 ========== -->
  <div class="modal" id="cardDetailModal">
    <div class="modal-card">
      <div class="modal-head">
        <h2>卡牌详情</h2>
        <button class="btn-x" id="closeCardDetail">X</button>
      </div>
      <div class="cd-layout">
        <div class="cd-preview" id="cdPreview"></div>
        <div class="cd-info">
          <h3 class="cd-name" id="cdName">卡牌</h3>
          <div class="cd-badges">
            <span class="cd-rarity" id="cdRarity">普通</span>
            <span class="cd-cost-badge" id="cdCostBadge">费用 1</span>
          </div>
          <div class="cd-stat-row">
            <div class="cd-stat atk"><small>攻击</small><b id="cdAtk">1</b></div>
            <div class="cd-stat hp"><small>生命</small><b id="cdHp">1</b></div>
          </div>
          <p class="cd-desc" id="cdDesc">描述</p>
        </div>
      </div>
    </div>
  </div>

  <!-- ========== 设置 ========== -->
  <div class="modal" id="settingsModal">
    <div class="modal-card">
      <div class="modal-head">
        <h2>设置</h2>
        <button class="btn-x" id="closeSettings">X</button>
      </div>
      <div class="settings-empty">
        设置项还在开发中……<br>
        之后会加入音效、画质、语言等选项。
      </div>
    </div>
  </div>

  <!-- ========== 结算 ========== -->
  <div class="overlay" id="overlay">
    <div class="overlay-card">
      <div class="overlay-title" id="overlayTitle">胜利</div>
      <div class="overlay-reward" id="overlayReward">+50 金币</div>
      <button class="btn" id="overlayBtn">再来一局</button>
    </div>
  </div>

  <!-- ========== 开包结果 ========== -->
  <div class="pack-overlay" id="packOverlay">
    <div class="pack-inner">
      <div class="pack-heading" id="packTitle">
        普通包
        <small>双击卡牌可查看详情</small>
      </div>
      <div class="pack-cards" id="packCards"></div>
      <button class="btn" id="packOk">确 定</button>
    </div>
  </div>

</div>

<script>
/* =========================================================
   存档模块
   ========================================================= */
const Save = (() => {
  const PREFIX = 'SAVE1.';
  const MAGIC  = 'GAME-SAVE-OK';

  function hashPassword(pwd) {
    let h = 2166136261 >>> 0;
    const s = 'x' + pwd + 'x';
    for (let i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = Math.imul(h, 16777619) >>> 0;
    }
    return h >>> 0;
  }
  function makeRng(seed) {
    let s = (seed || 1) >>> 0;
    return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s; };
  }
  const enc = new TextEncoder();
  const dec = new TextDecoder();

  function xorBytes(bytes, pwd) {
    const rng = makeRng(hashPassword(pwd));
    const out = new Uint8Array(bytes.length);
    for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] ^ (rng() & 0xff);
    return out;
  }
  function toBase64(bytes) {
    let s = '';
    for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
    return btoa(s);
  }
  function fromBase64(b64) {
    const s = atob(b64);
    const bytes = new Uint8Array(s.length);
    for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
    return bytes;
  }
  function exportCode(state, password) {
    const json = MAGIC + JSON.stringify(state);
    return PREFIX + toBase64(xorBytes(enc.encode(json), password || ''));
  }
  function importCode(code, password) {
    code = (code || '').trim();
    if (!code.startsWith(PREFIX)) return { error: '存档格式不正确' };
    try {
      const bytes = fromBase64(code.slice(PREFIX.length));
      const json = dec.decode(xorBytes(bytes, password || ''));
      if (!json.startsWith(MAGIC)) return { error: '密码错误' };
      return { data: JSON.parse(json.slice(MAGIC.length)) };
    } catch (e) {
      return { error: '存档损坏或密码错误' };
    }
  }
  return { exportCode, importCode };
})();


/* =========================================================
   稀有度
   ========================================================= */
const RARITY = {
  common:    { name: '普通', cls: 'r-common'    },
  rare:      { name: '稀有', cls: 'r-rare'      },
  epic:      { name: '特殊', cls: 'r-epic'      },
  legendary: { name: '精锐', cls: 'r-legendary' }
};
const RARITY_ORDER = ['common', 'rare', 'epic', 'legendary'];


/* =========================================================
   卡牌数据
   ========================================================= */
const CARD_DB = {
  /* 普通 */
  scout:     { name: '侦察兵',   cost: 1, atk: 1, hp: 1, type: 'unit', rarity: 'common',
               desc: '廉价的先锋,适合开局试探。' },
  conscript: { name: '新兵',     cost: 1, atk: 1, hp: 2, type: 'unit', rarity: 'common',
               desc: '刚入伍的年轻人,便宜好用。' },
  infantry:  { name: '步兵连',   cost: 2, atk: 2, hp: 3, type: 'unit', rarity: 'common',
               desc: '可靠的主力部队,攻守均衡。' },
  medic:     { name: '战地医师', cost: 2, atk: 1, hp: 4, type: 'unit', rarity: 'common',
               desc: '输出不高,但很耐打。' },
  watchman:  { name: '哨兵',     cost: 3, atk: 2, hp: 5, type: 'unit', rarity: 'common',
               desc: '坚守阵地的守卫。' },

  /* 稀有 */
  mortar:    { name: '迫击炮组', cost: 3, atk: 3, hp: 2, type: 'unit', rarity: 'rare',
               desc: '远程火力支援,攻高血薄。' },
  sniper:    { name: '狙击手',   cost: 3, atk: 4, hp: 1, type: 'unit', rarity: 'rare',
               desc: '一击毙命的猎手,极易被解。' },
  engineer:  { name: '工兵队',   cost: 3, atk: 2, hp: 5, type: 'unit', rarity: 'rare',
               desc: '能修能打的万金油。' },
  repair:    { name: '战地维修', cost: 1, atk: 0, hp: 0, type: 'command', rarity: 'rare',
               target: 'player_hq', effect: { type: 'heal_hq', value: 5 },
               desc: '点击我方总部:恢复 5 点生命。' },

  /* 特殊 */
  tank:      { name: '装甲坦克', cost: 4, atk: 4, hp: 5, type: 'unit', rarity: 'epic',
               desc: '坚固的钢铁巨兽,战场核心。' },
  artillery: { name: '重炮阵地', cost: 4, atk: 5, hp: 3, type: 'unit', rarity: 'epic',
               desc: '覆盖式轰炸,输出极高。' },
  airstrike: { name: '空中打击', cost: 3, atk: 0, hp: 0, type: 'command', rarity: 'epic',
               target: 'enemy_unit', effect: { type: 'damage', value: 5 },
               desc: '点击敌方单位:对其造成 5 点伤害。' },
  supply:    { name: '军备补给', cost: 3, atk: 0, hp: 0, type: 'command', rarity: 'epic',
               target: 'player_hq', effect: { type: 'buff_all', atk: 0, hp: 2 },
               desc: '点击我方总部:我方全部单位 +0/+2。' },

  /* 精锐 */
  bomber:    { name: '轰炸机',   cost: 5, atk: 5, hp: 4, type: 'unit', rarity: 'legendary',
               desc: '空中打击力量,攻守兼备。' },
  fortress:  { name: '要塞',     cost: 6, atk: 4, hp: 8, type: 'unit', rarity: 'legendary',
               desc: '不可撼动的堡垒,站场之王。' },
  rally:     { name: '集结号',   cost: 2, atk: 0, hp: 0, type: 'command', rarity: 'legendary',
               target: 'player_hq', effect: { type: 'buff_all', atk: 1, hp: 1 },
               desc: '点击我方总部:我方全部单位 +1/+1。' }
};
const CARD_POOL = Object.keys(CARD_DB);

const CARDS_BY_RARITY = {};
Object.entries(CARD_DB).forEach(([id, def]) => {
  (CARDS_BY_RARITY[def.rarity] ||= []).push(id);
});

const START_HP    = 20;
const HAND_LIMIT  = 6;
const MAX_KREDITS = 12;
const DECK_SIZE   = 18;
const DECK_LIMIT  = 40;
const DECK_SLOTS  = 3;

const $ = id => document.getElementById(id);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const randId = () => Math.random().toString(36).slice(2, 9);


/* =========================================================
   战役难度
   ========================================================= */
const DIFFICULTIES = {
  easy: {
    id: 'easy',
    name: '简单',
    reward: 50,
    enemyHp: 15,
    aggression: 0.45,
    desc: '敌方总部 15 点生命 · 攻击欲望较弱'
  },
  normal: {
    id: 'normal',
    name: '普通',
    reward: 100,
    enemyHp: 20,
    aggression: 0.7,
    desc: '敌方总部 20 点生命 · 常规强度'
  },
  hard: {
    id: 'hard',
    name: '困难',
    reward: 200,
    enemyHp: 25,
    aggression: 0.88,
    desc: '敌方总部 25 点生命 · 攻击欲望强烈'
  }
};
const DIFF_ORDER = ['easy', 'normal', 'hard'];


/* =========================================================
   卡包配置
   ========================================================= */
const PACKS = {
  normal: {
    name: '普通包', price: 100, count: 5, cls: '',
    desc: '5 张随机卡牌 · 保底 1 张稀有',
    odds: [
      { common: 0.70, rare: 0.25, epic: 0.04, legendary: 0.01 },
      { common: 0.70, rare: 0.25, epic: 0.04, legendary: 0.01 },
      { common: 0.70, rare: 0.25, epic: 0.04, legendary: 0.01 },
      { common: 0.70, rare: 0.25, epic: 0.04, legendary: 0.01 },
      { common: 0.00, rare: 0.85, epic: 0.13, legendary: 0.02 }
    ]
  },
  officer: {
    name: '军官包', price: 300, count: 7, cls: 'officer',
    desc: '7 张高品质卡牌 · 保底 1 张特殊或精锐',
    odds: [
      { common: 0.45, rare: 0.40, epic: 0.12, legendary: 0.03 },
      { common: 0.45, rare: 0.40, epic: 0.12, legendary: 0.03 },
      { common: 0.45, rare: 0.40, epic: 0.12, legendary: 0.03 },
      { common: 0.45, rare: 0.40, epic: 0.12, legendary: 0.03 },
      { common: 0.45, rare: 0.40, epic: 0.12, legendary: 0.03 },
      { common: 0.20, rare: 0.55, epic: 0.20, legendary: 0.05 },
      { common: 0.00, rare: 0.00, epic: 0.78, legendary: 0.22 }
    ]
  }
};

function rollRarity(odds) {
  const r = Math.random();
  let acc = 0;
  for (const key of RARITY_ORDER) {
    acc += odds[key] || 0;
    if (r < acc) return key;
  }
  return 'common';
}
function pickCardByRarity(rarity) {
  const pool = CARDS_BY_RARITY[rarity] && CARDS_BY_RARITY[rarity].length
    ? CARDS_BY_RARITY[rarity] : CARDS_BY_RARITY.common;
  return pool[Math.floor(Math.random() * pool.length)];
}


/* =========================================================
   玩家元数据(含 3 个卡组槽)
   ========================================================= */
function makeEmptyDeck(i) {
  return { name: `卡组 ${i + 1}`, cards: [] };
}

let meta = {
  coins: 1000,
  openedPacks: 0,
  ownedCards: {
    scout: 2, conscript: 2, infantry: 2, medic: 2, watchman: 2,
    mortar: 1, sniper: 1, engineer: 1, repair: 2,
    airstrike: 1
  },
  decks: [makeEmptyDeck(0), makeEmptyDeck(1), makeEmptyDeck(2)],
  activeDeckIdx: 0
};

/* 兼容旧存档:把 meta.deck 迁移到 meta.decks[0] */
function migrateMeta(m) {
  const out = Object.assign({
    coins: 0, openedPacks: 0, ownedCards: {},
    decks: [makeEmptyDeck(0), makeEmptyDeck(1), makeEmptyDeck(2)],
    activeDeckIdx: 0
  }, m || {});

  if (!out.ownedCards) out.ownedCards = {};

  // 旧版:meta.deck 是数组
  if (Array.isArray(m && m.deck)) {
    if (!Array.isArray(out.decks) || out.decks.length === 0) {
      out.decks = [makeEmptyDeck(0), makeEmptyDeck(1), makeEmptyDeck(2)];
    }
    out.decks[0] = { name: '默认卡组', cards: m.deck.slice() };
    delete out.deck;
  }

  // 保证 decks 是长度 3 的数组
  if (!Array.isArray(out.decks) || out.decks.length !== DECK_SLOTS) {
    const old = Array.isArray(out.decks) ? out.decks : [];
    out.decks = [];
    for (let i = 0; i < DECK_SLOTS; i++) {
      const d = old[i];
      if (d && typeof d === 'object') {
        out.decks.push({
          name: String(d.name || `卡组 ${i + 1}`).slice(0, 12),
          cards: Array.isArray(d.cards) ? d.cards.slice() : []
        });
      } else {
        out.decks.push(makeEmptyDeck(i));
      }
    }
  }

  if (typeof out.activeDeckIdx !== 'number' ||
      out.activeDeckIdx < 0 || out.activeDeckIdx >= DECK_SLOTS) {
    out.activeDeckIdx = 0;
  }

  return out;
}

function updateCoinDisplays() {
  ['lobbyCoins', 'gameCoins', 'shopCoins'].forEach(id => {
    const el = document.getElementById(id);
    if (el) el.textContent = meta.coins;
  });
}


/* =========================================================
   对局状态
   ========================================================= */
function buildPlayerDeck() {
  const d = meta.decks[meta.activeDeckIdx];
  if (d && Array.isArray(d.cards) && d.cards.length > 0) {
    const copy = d.cards.slice();
    for (let i = copy.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [copy[i], copy[j]] = [copy[j], copy[i]];
    }
    return copy;
  }
  // 随机牌库
  const rnd = [];
  for (let i = 0; i < DECK_SIZE; i++) {
    rnd.push(CARD_POOL[Math.floor(Math.random() * CARD_POOL.length)]);
  }
  return rnd;
}

function buildEnemyDeck(difficulty) {
  // 高难度给更高质量的牌库
  const weights = {
    easy:   { common: 0.70, rare: 0.22, epic: 0.07, legendary: 0.01 },
    normal: { common: 0.50, rare: 0.32, epic: 0.14, legendary: 0.04 },
    hard:   { common: 0.30, rare: 0.38, epic: 0.22, legendary: 0.10 }
  }[difficulty] || { common: 0.50, rare: 0.32, epic: 0.14, legendary: 0.04 };

  const d = [];
  for (let i = 0; i < DECK_SIZE; i++) {
    const rarity = rollRarity(weights);
    d.push(pickCardByRarity(rarity));
  }
  return d;
}

function makeSide(owner, hp, deck, kredits) {
  return {
    hp: hp, maxHp: hp,
    kredits: kredits ? 1 : 0,
    maxKredits: kredits ? 1 : 0,
    deck: deck,
    hand: []
  };
}

function newGame(difficulty) {
  const diff = DIFFICULTIES[difficulty] || DIFFICULTIES.normal;
  const st = {
    turn: 1,
    active: 'player',
    over: null,
    difficulty: diff.id,
    player: makeSide('player', START_HP, buildPlayerDeck(), false),
    enemy:  makeSide('enemy',  diff.enemyHp, buildEnemyDeck(diff.id), false),
    lanes: {
      playerHome: [null, null, null, null, null],
      contested:  [null, null, null, null, null],
      enemyHome:  [null, null, null, null, null]
    }
  };
  for (let i = 0; i < 4; i++) { drawCard(st, 'player'); drawCard(st, 'enemy'); }
  beginTurn(st, 'player');
  return st;
}

function drawCard(st, side) {
  const s = st[side];
  if (!s.deck.length || s.hand.length >= HAND_LIMIT) return;
  s.hand.push(s.deck.pop());
}

function beginTurn(st, side) {
  const s = st[side];
  s.maxKredits = Math.min(s.maxKredits + 1, MAX_KREDITS);
  s.kredits = s.maxKredits;
  ['playerHome', 'contested', 'enemyHome'].forEach(laneName => {
    st.lanes[laneName].forEach(u => {
      if (u && u.owner === side) u.ready = true;
    });
  });
  drawCard(st, side);
}

let gameState = null;   // 延迟到"开始战役"时创建
let selectedCard = null;
let selectedUnit = null;


/* =========================================================
   卡牌详情
   ========================================================= */
function showCardDetail(cardId) {
  const def = CARD_DB[cardId];
  if (!def) return;
  const r = RARITY[def.rarity] || RARITY.common;

  const preview = $('cdPreview');
  preview.innerHTML = '';
  const card = document.createElement('div');
  card.className = 'card ' +
    (def.type === 'command' ? 'command ' : '') + rarityCls(def);
  card.innerHTML = cardInner(def);
  preview.appendChild(card);

  $('cdName').textContent = def.name;

  const rarityEl = $('cdRarity');
  rarityEl.textContent = r.name;
  rarityEl.className = 'cd-rarity ' + r.cls;

  $('cdCostBadge').textContent =
    `费用 ${def.cost} · ${def.type === 'command' ? '指令' : '单位'}`;

  if (def.type === 'unit') {
    $('cdAtk').textContent = def.atk;
    $('cdHp').textContent = def.hp;
  } else {
    $('cdAtk').textContent = '—';
    $('cdHp').textContent = '—';
  }
  $('cdDesc').textContent = def.desc || '(暂无描述)';

  $('cardDetailModal').classList.add('show');
}

$('closeCardDetail').onclick = () => $('cardDetailModal').classList.remove('show');
$('cardDetailModal').onclick = e => {
  if (e.target === $('cardDetailModal')) $('cardDetailModal').classList.remove('show');
};


/* =========================================================
   点击 / 双击
   ========================================================= */
function bindCardInteractions(el, { single, dbl }) {
  let timer = null;
  el.addEventListener('click', () => {
    if (timer) { clearTimeout(timer); timer = null; return; }
    if (!single) return;
    timer = setTimeout(() => { timer = null; single(); }, 150);
  });
  el.addEventListener('dblclick', () => {
    if (timer) { clearTimeout(timer); timer = null; }
    if (dbl) dbl();
  });
}


/* =========================================================
   工具
   ========================================================= */
function findUnit(uid) {
  for (const laneName of ['playerHome', 'contested', 'enemyHome']) {
    const lane = gameState.lanes[laneName];
    for (const u of lane) if (u && u.uid === uid) return u;
  }
  return null;
}
function findUnitSlot(uid) {
  for (const laneName of ['playerHome', 'contested', 'enemyHome']) {
    const lane = gameState.lanes[laneName];
    for (let i = 0; i < lane.length; i++) {
      if (lane[i] && lane[i].uid === uid) return { laneName, idx: i };
    }
  }
  return null;
}
function removeUnit(uid) {
  const loc = findUnitSlot(uid);
  if (loc) gameState.lanes[loc.laneName][loc.idx] = null;
}

function cardInner(def) {
  const isCmd = def.type === 'command';
  return `
    <span class="cost">${def.cost}</span>
    <div class="name">${def.name}</div>
    <div class="stats">
      <span class="atk">${isCmd ? '—' : def.atk}</span>
      <span class="hp">${isCmd ? '—' : def.hp}</span>
    </div>`;
}
function cardInnerUnit(unit) {
  const def = CARD_DB[unit.id];
  return `
    <span class="cost">${def.cost}</span>
    <div class="name">${def.name}</div>
    <div class="stats">
      <span class="atk">${unit.atk}</span>
      <span class="hp">${unit.hp}</span>
    </div>`;
}
function rarityCls(def) {
  return RARITY[def.rarity] ? RARITY[def.rarity].cls : 'r-common';
}

let toastTimer;
function toast(msg) {
  const el = $('toast');
  el.textContent = msg;
  el.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => el.classList.remove('show'), 1400);
}


/* =========================================================
   渲染
   ========================================================= */
function render() {
  if (!gameState) return;
  renderHand('enemy');
  renderHand('player');
  renderLane('enemyHome');
  renderLane('contested');
  renderLane('playerHome');
  renderHQ();
  renderInfo();
  updateSelectionHighlights();
}

function renderHand(side) {
  const handEl = $(side + 'Hand');
  handEl.innerHTML = '';
  gameState[side].hand.forEach((cardId, i) => {
    handEl.appendChild(side === 'player' ? makeHandCard(cardId, i) : makeCardBack());
  });
}

function renderLane(laneName) {
  const lane = gameState.lanes[laneName];
  const slots = document.querySelectorAll(`.slot[data-lane="${laneName}"]`);
  slots.forEach(slotEl => {
    const idx = parseInt(slotEl.dataset.idx);
    const unit = lane[idx];
    slotEl.className = 'slot' + (unit ? '' : ' empty');
    slotEl.innerHTML = '';
    if (unit) slotEl.appendChild(makeUnitCard(unit));
  });
}

function renderHQ() {
  $('playerHqHp').textContent = gameState.player.hp;
  $('enemyHqHp').textContent = gameState.enemy.hp;

  const pEl = document.querySelector('.hq-slot[data-hq="player"]');
  const eEl = document.querySelector('.hq-slot[data-hq="enemy"]');
  pEl.classList.remove('valid-target');
  eEl.classList.remove('valid-target');

  if (selectedCard) {
    const def = CARD_DB[selectedCard.cardId];
    if (def.type === 'command' && def.target === 'player_hq') {
      pEl.classList.add('valid-target');
    }
  } else if (selectedUnit) {
    eEl.classList.add('valid-target');
  }
}

function renderInfo() {
  const p = gameState.player;
  const e = gameState.enemy;

  $('playerInfoHp').textContent = `${p.hp} / ${p.maxHp}`;
  $('playerKredits').textContent = `${p.kredits} / ${p.maxKredits}`;
  $('playerMeta').textContent = `手牌 ${p.hand.length} · 牌库 ${p.deck.length}`;
  $('enemyMeta').textContent = `手牌 ${e.hand.length} · 牌库 ${e.deck.length}`;

  const diff = DIFFICULTIES[gameState.difficulty] || DIFFICULTIES.normal;
  const dt = $('diffTag');
  dt.textContent = diff.name;
  dt.className = 'diff-tag ' + diff.id;

  const t = $('turnInfo');
  if (gameState.over === 'win')       t.textContent = '胜  利';
  else if (gameState.over === 'lose') t.textContent = '失  败';
  else t.textContent = `第 ${gameState.turn} 回合 · ${gameState.active === 'player' ? '你的回合' : '对方回合'}`;

  $('endTurnBtn').disabled = gameState.active !== 'player' || !!gameState.over;
  $('homeBtn').disabled    = gameState.active !== 'player';
}

/* 玩家只能放在自己前线 */
function isValidSlotTarget(laneName, idx) {
  if (!selectedCard) return false;
  const def = CARD_DB[selectedCard.cardId];
  if (def.type !== 'unit') return false;
  if (laneName !== 'playerHome') return false;
  if (idx === 2) return false;
  if (gameState.lanes[laneName][idx]) return false;
  return true;
}

function isValidUnitTargetForCommand(unit) {
  if (!selectedCard) return false;
  const def = CARD_DB[selectedCard.cardId];
  if (def.type !== 'command') return false;
  if (def.target === 'enemy_unit') return unit.owner === 'enemy';
  if (def.target === 'player_unit') return unit.owner === 'player';
  if (def.target === 'any_unit') return true;
  return false;
}

function updateSelectionHighlights() {
  const handCards = $('playerHand').querySelectorAll('.card');
  handCards.forEach((el, i) => {
    el.classList.toggle('selected', selectedCard !== null && selectedCard.handIndex === i);
  });

  document.querySelectorAll('.slot').forEach(el => {
    const laneName = el.dataset.lane;
    const idx = parseInt(el.dataset.idx);
    el.classList.toggle('valid-target', isValidSlotTarget(laneName, idx));
  });

  document.querySelectorAll('.slot .card').forEach(cardEl => {
    const uid = cardEl.dataset.uid;
    if (!uid) return;
    const unit = findUnit(uid);
    if (!unit) return;
    cardEl.classList.toggle('targetable', isValidUnitTargetForCommand(unit));
    cardEl.classList.toggle('selected', uid === selectedUnit);
  });
}


/* =========================================================
   卡牌构建
   ========================================================= */
function makeHandCard(cardId, index) {
  const def = CARD_DB[cardId];
  const afford = gameState.player.kredits >= def.cost;

  const el = document.createElement('div');
  el.className = ['card', 'hand',
                  def.type === 'command' ? 'command' : '',
                  rarityCls(def),
                  afford ? '' : 'unaffordable']
    .filter(Boolean).join(' ');
  el.innerHTML = cardInner(def);

  bindCardInteractions(el, {
    single: () => handleHandCardClick(index, cardId),
    dbl:    () => showCardDetail(cardId)
  });
  return el;
}

function makeUnitCard(unit) {
  const def = CARD_DB[unit.id];
  const el = document.createElement('div');
  el.dataset.uid = unit.uid;

  const ready = unit.ready;
  const isMine = unit.owner === 'player';
  const canAct = isMine && ready && gameState.active === 'player' && !gameState.over;

  el.className = ['card', 'unit',
                  rarityCls(def),
                  ready ? '' : 'exhausted',
                  isMine ? '' : 'owned-enemy',
                  canAct ? 'can-act' : '']
    .filter(Boolean).join(' ');
  el.innerHTML = cardInnerUnit(unit);

  bindCardInteractions(el, {
    single: () => handleUnitClick(unit),
    dbl:    () => showCardDetail(unit.id)
  });
  return el;
}

function makeCardBack() {
  const el = document.createElement('div');
  el.className = 'card back';
  el.textContent = '指挥官';
  return el;
}


/* =========================================================
   手牌点击
   ========================================================= */
function handleHandCardClick(index, cardId) {
  if (!gameState || gameState.over || gameState.active !== 'player') return;

  if (selectedCard && selectedCard.handIndex === index) {
    selectedCard = null;
  } else {
    const def = CARD_DB[cardId];
    if (gameState.player.kredits < def.cost) { toast('费用不足'); return; }
    selectedCard = { cardId, handIndex: index };
    selectedUnit = null;
  }

  updateSelectionHighlights();
  renderHQ();
}


/* =========================================================
   单位点击
   ========================================================= */
function handleUnitClick(unit) {
  if (!gameState || gameState.over) return;

  if (selectedCard) {
    const def = CARD_DB[selectedCard.cardId];
    if (def.type === 'command' && isValidUnitTargetForCommand(unit)) {
      applyCommandToUnit(def, unit);
      return;
    }
  }

  if (unit.owner === 'player') {
    if (gameState.active !== 'player' || !unit.ready) return;
    if (selectedUnit === unit.uid) {
      selectedUnit = null;
    } else {
      selectedUnit = unit.uid;
      selectedCard = null;
    }
    updateSelectionHighlights();
    renderHQ();
    return;
  }

  if (unit.owner === 'enemy' && selectedUnit) {
    const attacker = findUnit(selectedUnit);
    if (!attacker || !attacker.ready) { selectedUnit = null; render(); return; }

    attacker.ready = false;
    unit.hp -= attacker.atk;
    attacker.hp -= unit.atk;

    if (unit.hp <= 0) removeUnit(unit.uid);
    if (attacker.hp <= 0) removeUnit(attacker.uid);

    selectedUnit = null;
    render();
  }
}


/* =========================================================
   槽位点击
   ========================================================= */
document.querySelectorAll('.slot').forEach(slotEl => {
  slotEl.addEventListener('click', () => {
    if (!gameState || gameState.over || gameState.active !== 'player') return;

    const laneName = slotEl.dataset.lane;
    const idx = parseInt(slotEl.dataset.idx);
    const unit = gameState.lanes[laneName][idx];

    if (unit) { handleUnitClick(unit); return; }

    if (selectedCard) {
      const def = CARD_DB[selectedCard.cardId];
      if (def.type === 'unit' && isValidSlotTarget(laneName, idx)) {
        placeUnit(selectedCard.cardId, selectedCard.handIndex, laneName, idx);
        return;
      }
    }

    selectedCard = null;
    selectedUnit = null;
    updateSelectionHighlights();
    renderHQ();
  });
});


/* =========================================================
   总部点击
   ========================================================= */
document.querySelectorAll('.hq-slot').forEach(hqEl => {
  hqEl.addEventListener('click', () => {
    if (!gameState || gameState.over || gameState.active !== 'player') return;
    const side = hqEl.dataset.hq;

    if (selectedCard) {
      const def = CARD_DB[selectedCard.cardId];
      if (def.type === 'command' && def.target === 'player_hq' && side === 'player') {
        applyCommandToHQ(def);
        return;
      }
    }

    if (selectedUnit && side === 'enemy') {
      const attacker = findUnit(selectedUnit);
      if (!attacker || !attacker.ready) { selectedUnit = null; render(); return; }

      attacker.ready = false;
      gameState.enemy.hp = Math.max(0, gameState.enemy.hp - attacker.atk);
      selectedUnit = null;
      render();

      if (gameState.enemy.hp <= 0) finish('win');
      return;
    }

    selectedCard = null;
    selectedUnit = null;
    updateSelectionHighlights();
    renderHQ();
  });
});


/* =========================================================
   放置单位
   ========================================================= */
function placeUnit(cardId, handIndex, laneName, idx) {
  const def = CARD_DB[cardId];
  const p = gameState.player;

  if (p.kredits < def.cost) { toast('费用不足'); return; }
  if (gameState.lanes[laneName][idx]) return;
  if (laneName !== 'playerHome') return;

  p.kredits -= def.cost;
  p.hand.splice(handIndex, 1);

  gameState.lanes[laneName][idx] = {
    uid: randId(),
    id: cardId,
    owner: 'player',
    atk: def.atk,
    hp: def.hp,
    maxHp: def.hp,
    ready: false
  };

  selectedCard = null;
  render();
}


/* =========================================================
   指令
   ========================================================= */
function applyCommandToHQ(def) {
  const p = gameState.player;
  if (p.kredits < def.cost) { toast('费用不足'); return; }

  p.kredits -= def.cost;
  p.hand.splice(selectedCard.handIndex, 1);

  const eff = def.effect;

  if (eff.type === 'heal_hq') {
    p.hp = Math.min(p.maxHp, p.hp + eff.value);
    toast(`${def.name}:总部恢复 ${eff.value} 点生命`);
  } else if (eff.type === 'buff_all') {
    let count = 0;
    ['playerHome', 'contested'].forEach(laneName => {
      gameState.lanes[laneName].forEach(u => {
        if (u && u.owner === 'player') {
          u.atk += eff.atk || 0;
          u.hp += eff.hp || 0;
          u.maxHp += eff.hp || 0;
          count++;
        }
      });
    });
    toast(count === 0 ? '场上没有我方单位'
                      : `${def.name}:${count} 个单位获得 +${eff.atk || 0}/+${eff.hp || 0}`);
  }

  selectedCard = null;
  render();
}

function applyCommandToUnit(def, target) {
  const p = gameState.player;
  if (p.kredits < def.cost) { toast('费用不足'); return; }

  p.kredits -= def.cost;
  p.hand.splice(selectedCard.handIndex, 1);

  const eff = def.effect;

  if (eff.type === 'damage') {
    target.hp -= eff.value;
    if (target.hp <= 0) removeUnit(target.uid);
    toast(`${def.name}:造成 ${eff.value} 点伤害`);
  } else if (eff.type === 'buff') {
    target.atk += eff.atk || 0;
    target.hp += eff.hp || 0;
    target.maxHp += eff.hp || 0;
    toast(`${def.name}:目标获得 +${eff.atk || 0}/+${eff.hp || 0}`);
  }

  selectedCard = null;
  render();
}


/* =========================================================
   回合流程
   ========================================================= */
$('endTurnBtn').onclick = endTurn;

function endTurn() {
  if (!gameState || gameState.over || gameState.active !== 'player') return;
  selectedCard = null;
  selectedUnit = null;
  gameState.active = 'enemy';
  beginTurn(gameState, 'enemy');
  render();
  enemyTurn();
}

async function enemyTurn() {
  await sleep(450);
  if (!gameState || gameState.over) return;

  const e = gameState.enemy;
  const diff = DIFFICULTIES[gameState.difficulty] || DIFFICULTIES.normal;

  // 出牌
  while (true) {
    let best = -1, bestCost = -1;
    e.hand.forEach((id, i) => {
      const def = CARD_DB[id];
      if (def.type !== 'unit') return;
      if (def.cost <= e.kredits && def.cost > bestCost) { best = i; bestCost = def.cost; }
    });
    if (best === -1) break;

    const target = findEnemySlot();
    if (!target) break;

    const cardId = e.hand[best];
    const def = CARD_DB[cardId];
    e.kredits -= def.cost;
    e.hand.splice(best, 1);

    gameState.lanes[target.laneName][target.idx] = {
      uid: randId(), id: cardId, owner: 'enemy',
      atk: def.atk, hp: def.hp, maxHp: def.hp, ready: false
    };

    render();
    await sleep(380);
  }

  // 攻击
  for (const laneName of ['contested', 'enemyHome']) {
    const lane = gameState.lanes[laneName];
    for (const u of lane) {
      if (!u || u.owner !== 'enemy' || !u.ready) continue;
      if (gameState.over) return;

      u.ready = false;

      const myUnits = [];
      ['playerHome', 'contested'].forEach(ln => {
        gameState.lanes[ln].forEach(x => { if (x && x.owner === 'player') myUnits.push(x); });
      });

      if (myUnits.length && Math.random() < diff.aggression) {
        const tgt = myUnits[Math.floor(Math.random() * myUnits.length)];
        tgt.hp -= u.atk;
        u.hp -= tgt.atk;
        if (tgt.hp <= 0) removeUnit(tgt.uid);
        if (u.hp <= 0) removeUnit(u.uid);
      } else {
        gameState.player.hp = Math.max(0, gameState.player.hp - u.atk);
      }

      render();
      await sleep(320);
      if (gameState.player.hp <= 0) { finish('lose'); return; }
    }
  }

  gameState.active = 'player';
  gameState.turn++;
  beginTurn(gameState, 'player');
  render();
}

/* 敌方只能放在敌方前线 */
function findEnemySlot() {
  const homeOrder = [1, 3, 0, 4];
  for (const i of homeOrder) {
    if (!gameState.lanes.enemyHome[i]) {
      return { laneName: 'enemyHome', idx: i };
    }
  }
  return null;
}


/* =========================================================
   结算
   ========================================================= */
function finish(result) {
  gameState.over = result;
  hasMatch = false;
  selectedCard = null;
  selectedUnit = null;

  const diff = DIFFICULTIES[gameState.difficulty] || DIFFICULTIES.normal;
  const reward = result === 'win' ? diff.reward : Math.floor(diff.reward * 0.3);
  meta.coins += reward;
  updateCoinDisplays();

  render();
  $('overlayTitle').textContent = result === 'win' ? '胜  利' : '失  败';
  $('overlayTitle').className = 'overlay-title ' + (result === 'win' ? 'win' : 'lose');
  $('overlayReward').textContent = `+${reward} 金币`;
  $('overlay').classList.add('show');
}


/* =========================================================
   大厅 / 场景切换
   ========================================================= */
const lobby = $('lobby');
let hasMatch = false;

function showLobby() {
  ['overlay', 'modal', 'shopModal', 'settingsModal', 'cardDetailModal',
   'packOverlay', 'codexModal', 'battleModal']
    .forEach(id => $(id).classList.remove('show'));
  $('resumeBtn').disabled = !(hasMatch && gameState && !gameState.over);
  lobby.classList.remove('hidden');
  updateCoinDisplays();
}

function enterGame() {
  lobby.classList.add('hidden');
  $('overlay').classList.remove('show');
}

$('homeBtn').onclick = showLobby;

$('resumeBtn').onclick = () => {
  if (!hasMatch || !gameState || gameState.over) return;
  selectedCard = null;
  selectedUnit = null;
  enterGame();
  render();
};

$('loadBtn').onclick = () => {
  $('saveCode').value = '';
  $('modal').classList.add('show');
};

/* 设置 */
$('settingsBtn').onclick   = () => $('settingsModal').classList.add('show');
$('closeSettings').onclick = () => $('settingsModal').classList.remove('show');
$('settingsModal').onclick = e => {
  if (e.target === $('settingsModal')) $('settingsModal').classList.remove('show');
};


/* =========================================================
   战役弹窗
   ========================================================= */
let pickedDifficulty = 'normal';
let pickedDeckIdx = 0;

function openBattleModal() {
  pickedDifficulty = 'normal';
  pickedDeckIdx = meta.activeDeckIdx || 0;
  renderBattleModal();
  $('battleModal').classList.add('show');
}

$('startBtn').onclick = openBattleModal;
$('closeBattle').onclick = () => $('battleModal').classList.remove('show');
$('battleModal').onclick = e => {
  if (e.target === $('battleModal')) $('battleModal').classList.remove('show');
};

function renderBattleModal() {
  // 难度
  const diffList = $('diffList');
  diffList.innerHTML = '';
  DIFF_ORDER.forEach(id => {
    const d = DIFFICULTIES[id];
    const btn = document.createElement('button');
    btn.className = 'diff-btn' + (pickedDifficulty === id ? ' selected' : '');
    btn.innerHTML = `
      <div style="flex:1 1 auto">
        <div class="d-name">${d.name}</div>
        <div class="d-desc">${d.desc}</div>
      </div>
      <div class="d-reward">+${d.reward}</div>
    `;
    btn.onclick = () => { pickedDifficulty = id; renderBattleModal(); };
    diffList.appendChild(btn);
  });

  // 卡组
  const deckList = $('deckPickList');
  deckList.innerHTML = '';

  let anyNonEmpty = false;
  meta.decks.forEach((deck, i) => {
    const cards = Array.isArray(deck.cards) ? deck.cards : [];
    const empty = cards.length === 0;
    if (!empty) anyNonEmpty = true;

    const btn = document.createElement('button');
    btn.className = 'deck-pick' + (pickedDeckIdx === i ? ' selected' : '');
    btn.disabled = empty;
    btn.innerHTML = `
      <div class="dp-name">${deck.name || `卡组 ${i + 1}`}</div>
      <div class="dp-count">${empty ? '空' : cards.length + ' 张'}</div>
    `;
    if (!empty) {
      btn.onclick = () => { pickedDeckIdx = i; renderBattleModal(); };
    }
    deckList.appendChild(btn);
  });

  // 如果没有非空卡组,显示提示并允许开始(随机牌库)
  if (!anyNonEmpty) {
    const note = document.createElement('div');
    note.style.cssText = 'font-size:11px;color:var(--dim);text-align:center;padding:6px';
    note.textContent = '尚未组建卡组,将使用随机牌库开局';
    deckList.appendChild(note);
  }

  // 校验选中卡组是否可用
  const selDeck = meta.decks[pickedDeckIdx];
  const selEmpty = !selDeck || !Array.isArray(selDeck.cards) || selDeck.cards.length === 0;

  $('battleStartBtn').disabled = false;
  $('battleStartBtn').textContent = anyNonEmpty && !selEmpty
    ? '开 始 战 役'
    : '开 始 战 役(随机牌库)';
}

$('battleStartBtn').onclick = () => {
  meta.activeDeckIdx = pickedDeckIdx;

  gameState = newGame(pickedDifficulty);
  hasMatch = true;
  selectedCard = null;
  selectedUnit = null;

  $('battleModal').classList.remove('show');
  enterGame();
  render();
};


/* =========================================================
   卡包商店
   ========================================================= */
$('shopBtn').onclick = () => {
  renderShop();
  updateCoinDisplays();
  $('shopModal').classList.add('show');
};
$('closeShop').onclick = () => $('shopModal').classList.remove('show');
$('shopModal').onclick = e => {
  if (e.target === $('shopModal')) $('shopModal').classList.remove('show');
};

function renderShop() {
  const list = $('shopList');
  list.innerHTML = '';
  Object.entries(PACKS).forEach(([id, pack]) => {
    const afford = meta.coins >= pack.price;
    const el = document.createElement('div');
    el.className = 'shop-pack ' + (pack.cls || '');
    el.innerHTML = `
      <div class="shop-pack-body">
        <div class="shop-pack-name">${pack.name}</div>
        <div class="shop-pack-desc">${pack.desc}</div>
      </div>
      <button class="shop-buy" data-pack="${id}" ${afford ? '' : 'disabled'}>
        ${pack.price} 金币
      </button>
    `;
    list.appendChild(el);
  });
  list.querySelectorAll('.shop-buy').forEach(btn => {
    btn.onclick = () => buyPack(btn.dataset.pack);
  });
}

function buyPack(packId) {
  const pack = PACKS[packId];
  if (!pack) return;
  if (meta.coins < pack.price) { toast('金币不足'); return; }

  meta.coins -= pack.price;
  meta.openedPacks++;
  updateCoinDisplays();

  const cardIds = [];
  for (let i = 0; i < pack.count; i++) {
    const odds = pack.odds[i] || pack.odds[pack.odds.length - 1];
    const rarity = rollRarity(odds);
    const cardId = pickCardByRarity(rarity);
    cardIds.push(cardId);
    meta.ownedCards[cardId] = (meta.ownedCards[cardId] || 0) + 1;
  }

  $('shopModal').classList.remove('show');
  showPackResult(pack, cardIds);
}

function showPackResult(pack, cardIds) {
  $('packTitle').innerHTML = `${pack.name}<small>双击卡牌可查看详情</small>`;
  const cont = $('packCards');
  cont.innerHTML = '';

  cardIds.forEach((cardId, i) => {
    const def = CARD_DB[cardId];
    const el = document.createElement('div');
    el.className = 'card ' +
      (def.type === 'command' ? 'command ' : '') + rarityCls(def);
    el.innerHTML = cardInner(def);
    el.style.animationDelay = (i * 0.12) + 's';

    bindCardInteractions(el, {
      single: null,
      dbl: () => showCardDetail(cardId)
    });

    cont.appendChild(el);
  });

  $('packOverlay').classList.add('show');
}

$('packOk').onclick = () => $('packOverlay').classList.remove('show');


/* =========================================================
   卡牌图鉴 + 3 卡组槽编辑
   ========================================================= */
let editDeckIdx = 0;    // 当前编辑的卡组槽
let draftDeck = [];     // 编辑中的卡牌数组

function openCodex() {
  editDeckIdx = meta.activeDeckIdx || 0;
  loadDeckIntoDraft(editDeckIdx);
  renderCodex();
  $('codexModal').classList.add('show');
}

function loadDeckIntoDraft(i) {
  const deck = meta.decks[i];
  draftDeck = deck && Array.isArray(deck.cards) ? deck.cards.slice() : [];
  $('deckNameInput').value = (deck && deck.name) || `卡组 ${i + 1}`;
}

$('codexBtn').onclick = openCodex;
$('closeCodex').onclick = () => {
  $('codexModal').classList.remove('show');
};
$('codexModal').onclick = e => {
  if (e.target === $('codexModal')) $('codexModal').classList.remove('show');
};

function renderCodex() {
  // 卡组 tabs
  const tabs = $('deckTabs');
  tabs.innerHTML = '';
  meta.decks.forEach((deck, i) => {
    const cards = Array.isArray(deck.cards) ? deck.cards : [];
    const btn = document.createElement('button');
    btn.className = 'deck-tab' + (i === editDeckIdx ? ' active' : '');
    btn.innerHTML = `${(deck.name || `卡组 ${i + 1}`)} <small>${cards.length}</small>`;
    btn.onclick = () => switchEditDeck(i);
    tabs.appendChild(btn);
  });

  $('deckCount').textContent = draftDeck.length;

  const grid = $('codexGrid');
  grid.innerHTML = '';

  const deckCounts = {};
  draftDeck.forEach(id => deckCounts[id] = (deckCounts[id] || 0) + 1);

  Object.keys(CARD_DB).forEach(cardId => {
    const def = CARD_DB[cardId];
    const owned = meta.ownedCards[cardId] || 0;
    const inDeck = deckCounts[cardId] || 0;

    const el = document.createElement('div');
    el.className = 'card ' +
      (def.type === 'command' ? 'command ' : '') + rarityCls(def) +
      (owned > 0 ? '' : ' not-owned');
    el.innerHTML = cardInner(def);

    if (owned > 0) {
      const b = document.createElement('span');
      b.className = 'owned-badge';
      b.textContent = 'x' + owned;
      el.appendChild(b);
    }
    if (inDeck > 0) {
      const b = document.createElement('span');
      b.className = 'deck-badge';
      b.textContent = '组' + inDeck;
      el.appendChild(b);
    }

    bindCardInteractions(el, {
      single: () => addToDraft(cardId),
      dbl:    () => showCardDetail(cardId)
    });

    grid.appendChild(el);
  });
}

function switchEditDeck(i) {
  // 先自动保存当前编辑的卡组到内存(不弹提示)
  commitDraftToMeta(false);
  editDeckIdx = i;
  loadDeckIntoDraft(i);
  renderCodex();
}

function commitDraftToMeta(showToast) {
  const name = ($('deckNameInput').value || '').trim() || `卡组 ${editDeckIdx + 1}`;
  meta.decks[editDeckIdx] = {
    name: name.slice(0, 12),
    cards: draftDeck.slice()
  };
  if (showToast) toast(`卡组已保存(${draftDeck.length} 张)`);
}

function addToDraft(cardId) {
  const owned = meta.ownedCards[cardId] || 0;
  if (owned <= 0) { toast('未拥有这张卡'); return; }
  if (draftDeck.length >= DECK_LIMIT) { toast('卡组已满 40 张'); return; }

  const inDeck = draftDeck.filter(id => id === cardId).length;
  if (inDeck >= owned) { toast('没有更多这张卡了'); return; }

  draftDeck.push(cardId);
  renderCodex();
}

$('clearDeckBtn').onclick = () => {
  draftDeck = [];
  renderCodex();
  toast('已清空当前卡组');
};

$('saveDeckBtn').onclick = () => {
  commitDraftToMeta(true);
  renderCodex();
};

/* 自动保存:关闭弹窗时,把草稿提交到 meta */
$('closeCodex').addEventListener('click', () => {
  commitDraftToMeta(false);
  meta.activeDeckIdx = editDeckIdx;
});


/* =========================================================
   存档 UI
   ========================================================= */
function saveStatus(msg) {
  const el = $('saveStatus');
  el.textContent = msg;
  clearTimeout(saveStatus.t);
  saveStatus.t = setTimeout(() => { el.textContent = ''; }, 2200);
}

$('saveBtn').onclick    = () => $('modal').classList.add('show');
$('closeModal').onclick = () => $('modal').classList.remove('show');
$('modal').onclick = e => { if (e.target === $('modal')) $('modal').classList.remove('show'); };

$('exportBtn').onclick = () => {
  const pwd = $('pwdInput').value;
  const payload = { v: 2, game: gameState, meta: meta };
  const code = Save.exportCode(payload, pwd);
  $('saveCode').value = code;
  $('saveCode').select();
  saveStatus('存档已生成,请复制保存');
};

$('importBtn').onclick = () => {
  const code = $('saveCode').value.trim();
  if (!code) { saveStatus('请先粘贴存档码'); return; }

  const pwd = $('pwdInput').value;
  const res = Save.importCode(code, pwd);
  if (res.error) { saveStatus(res.error); return; }

  const data = res.data;
  if (data && data.v === 2 && data.game && data.meta) {
    if (!isValidState(data.game)) { saveStatus('存档内容无效'); return; }
    if (typeof data.meta.coins !== 'number') { saveStatus('存档内容无效'); return; }
    gameState = data.game;
    meta = migrateMeta(data.meta);
  } else if (isValidState(data)) {
    gameState = data;
  } else {
    saveStatus('存档内容无效');
    return;
  }

  gameState.active = 'player';
  gameState.over = null;
  gameState.turn = gameState.turn || 1;
  gameState.difficulty = gameState.difficulty || 'normal';
  hasMatch = true;
  selectedCard = null;
  selectedUnit = null;

  $('modal').classList.remove('show');
  enterGame();
  render();
  updateCoinDisplays();
  toast('读取成功');
};

$('newGameBtn').onclick = () => {
  gameState = newGame('normal');
  hasMatch = true;
  selectedCard = null;
  selectedUnit = null;
  $('modal').classList.remove('show');
  enterGame();
  render();
  toast('新游戏已开始');
};

$('overlayBtn').onclick = () => {
  const diff = (gameState && gameState.difficulty) || 'normal';
  gameState = newGame(diff);
  hasMatch = true;
  selectedCard = null;
  selectedUnit = null;
  $('overlay').classList.remove('show');
  render();
};

function isValidState(s) {
  if (!s || typeof s !== 'object') return false;
  if (typeof s.turn !== 'number') return false;
  if (!s.lanes || typeof s.lanes !== 'object') return false;

  for (const side of ['player', 'enemy']) {
    const x = s[side];
    if (!x || typeof x !== 'object') return false;
    if (typeof x.hp !== 'number' || typeof x.maxHp !== 'number') return false;
    if (!Array.isArray(x.hand) || !Array.isArray(x.deck)) return false;
    if (x.hand.concat(x.deck).some(id => !CARD_DB[id])) return false;
  }

  for (const laneName of ['playerHome', 'contested', 'enemyHome']) {
    const lane = s.lanes[laneName];
    if (!Array.isArray(lane)) return false;
    for (const u of lane) {
      if (u && !CARD_DB[u.id]) return false;
    }
  }
  return true;
}


/* =========================================================
   启动
   ========================================================= */
meta = migrateMeta(meta);
updateCoinDisplays();
showLobby();
</script>
</body>
</html>

Game Source: 指挥官 · Commander

Creator: SwiftGalaxy49

Libraries: none

Complexity: complex (2408 lines, 77.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: commander-swiftgalaxy49" to link back to the original. Then publish at arcadelab.ai/publish.