🎮ArcadeLab

模块化空间站 · 编队 AI 对战

by ApexFalcon66
1472 lines45.2 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>模块化空间站 · 编队 AI 对战</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; }
  body {
    background: radial-gradient(circle at 50% 45%, #0c1228 0%, #03050c 100%);
    font-family: system-ui, -apple-system, sans-serif;
    -webkit-user-select:none; user-select:none;
  }

  /* ===== 视角 ===== */
  .viewport {
    position: fixed; inset: 0;
    overflow: hidden;
    cursor: grab;
    touch-action: none;
  }
  .viewport.grabbing { cursor: grabbing; }

  .scene {
    position: absolute;
    left: 50%; top: 50%;
    width: 1200px; height: 1200px;
    margin-left: -600px; margin-top: -600px;
    transform-origin: 50% 50%;
    will-change: transform;
  }

  /* ===== 星空 ===== */
  .star-layer { position:absolute; inset:0; z-index:0; }
  .star { position:absolute; background:#fff; border-radius:50%; animation: twinkle 3s infinite ease-in-out; }
  @keyframes twinkle { 0%,100% { opacity:.15; } 50% { opacity:.95; } }

  /* ===== 空间站 ===== */
  .station {
    position: absolute;
    left: 0; top: 0;
    width: 440px; height: 440px;
    will-change: transform;
    z-index: 10;
  }

  .station.theme-blue {
    --hull1:#ffffff; --hull2:#dfe8f2; --hull3:#b8c6d6;
    --panelA:#1d4d86; --panelB:#2b6cb5; --accent:#4a90d9;
    --win:#2d4d6e; --glow:rgba(120,200,255,.55);
  }
  .station.theme-red {
    --hull1:#fff4ef; --hull2:#f0dcd4; --hull3:#c9b3a8;
    --panelA:#8a2a1d; --panelB:#b84530; --accent:#e05a3a;
    --win:#6e2a1d; --glow:rgba(255,150,100,.55);
  }

  /* ===== 模块 ===== */
  .mod {
    position: absolute;
    border-radius: 3px;
    box-shadow: 0 3px 14px rgba(0,0,0,.45), inset 0 -2px 5px rgba(150,170,200,.25);
    z-index: 1;
  }
  .mod.kind-body {
    background: linear-gradient(180deg, var(--hull1) 0%, var(--hull2) 50%, var(--hull3) 100%);
    border: 1px solid rgba(140,160,180,.6);
  }
  .mod.kind-panel {
    background: repeating-linear-gradient(0deg,
      var(--panelA) 0px, var(--panelA) 8px,
      var(--panelB) 8px, var(--panelB) 10px);
    border: 2px solid var(--accent);
    box-shadow: 0 0 16px var(--glow);
  }

  .win {
    position: absolute;
    width: 8px; height: 4px;
    background: var(--win);
    border-radius: 1px;
    box-shadow: 0 0 4px var(--glow);
  }

  /* ===== 炮塔 ===== */
  .turret { position:absolute; width:0; height:0; z-index: 3; }

  .turret-rail .turret-base {
    position:absolute; width:30px; height:30px; left:-15px; top:-15px;
    border-radius:5px;
    background: linear-gradient(180deg, var(--hull1), var(--hull3));
    border:2px solid #8a9bb0;
    box-shadow: 0 0 14px var(--glow);
  }
  .turret-rail .turret-barrel {
    position:absolute; width:54px; height:10px; left:0; top:-5px;
    transform-origin:0 50%;
    background: linear-gradient(180deg,#f4f9ff 0%,#b8c6d6 55%,#7f8ea3 100%);
    border:1px solid #6a7e92;
    border-radius:0 3px 3px 0;
    box-shadow: 0 0 8px var(--glow);
  }

  .turret-laser .turret-base {
    position:absolute; width:24px; height:24px; left:-12px; top:-12px;
    border-radius:50%;
    background: radial-gradient(circle at 35% 30%, #ffffff, #9fc4e8 70%, #6a8aaa);
    border:2px solid #5f7fa0;
    box-shadow: 0 0 14px var(--glow);
  }
  .turret-laser .turret-barrel {
    position:absolute; width:30px; height:6px; left:0; top:-3px;
    transform-origin:0 50%;
    background: linear-gradient(90deg,#e8f8ff 0%,#6fc8ff 60%,#3aa6e8 100%);
    border:1px solid #2f9ae0;
    border-radius:0 3px 3px 0;
    box-shadow: 0 0 10px var(--glow);
  }

  /* ===== 导弹巢 ===== */
  .missile-pod {
    position: absolute;
    width: 18px; height: 28px;
    border-radius: 4px;
    background:
      radial-gradient(circle at 30% 22%, var(--win) 2px, transparent 2.4px),
      radial-gradient(circle at 70% 22%, var(--win) 2px, transparent 2.4px),
      radial-gradient(circle at 30% 50%, var(--win) 2px, transparent 2.4px),
      radial-gradient(circle at 70% 50%, var(--win) 2px, transparent 2.4px),
      radial-gradient(circle at 30% 78%, var(--win) 2px, transparent 2.4px),
      radial-gradient(circle at 70% 78%, var(--win) 2px, transparent 2.4px),
      linear-gradient(180deg, var(--hull1), var(--hull3));
    border: 2px solid #8a9bb0;
    box-shadow: 0 0 10px rgba(255,190,120,.35);
    z-index: 3;
  }

  /* ===== 推进器喷管 + 动态火焰 ===== */
  .thruster-bell {
    position: absolute;
    left: 206px; top: 324px;
    width: 28px; height: 14px;
    background: linear-gradient(180deg,#eef3f9 0%,#b7c4d3 55%,#8a9bb0 100%);
    clip-path: polygon(0 0, 100% 0, 82% 100%, 18% 100%);
    z-index: 2;
  }

  .thruster-flame {
    position: absolute;
    left: 0; top: 0;
    width: 0; height: 0;
    transform-origin: 0 0;
    pointer-events: none;
    z-index: 0;
    opacity: 0;
    will-change: transform, opacity;
  }
  .tf-glow {
    position: absolute;
    left: -22px; top: -30px;
    width: 100px; height: 60px;
    background: radial-gradient(ellipse at 22% 50%, rgba(255,160,50,.5) 0%, rgba(255,120,0,0) 68%);
    border-radius: 50%;
  }
  .tf-outer {
    position: absolute;
    left: 2px; top: -12px;
    width: 68px; height: 24px;
    transform-origin: 0 50%;
    background: radial-gradient(ellipse at 0% 50%, #ffd166 0%, #ff9a1f 30%, #ff5f00 62%, rgba(255,90,0,0) 100%);
    clip-path: polygon(0 50%, 100% 10%, 100% 90%);
    filter: blur(.8px);
    animation: flameFlicker .16s infinite alternate ease-in-out;
  }
  .tf-core {
    position: absolute;
    left: 2px; top: -6px;
    width: 38px; height: 12px;
    transform-origin: 0 50%;
    background: radial-gradient(ellipse at 0% 50%, #ffffff 0%, #fff4b8 45%, rgba(255,200,70,0) 100%);
    clip-path: polygon(0 50%, 100% 12%, 100% 88%);
    animation: flameFlicker .13s infinite alternate-reverse ease-in-out;
  }
  @keyframes flameFlicker {
    from { transform: scaleX(.86); opacity:.8; }
    to   { transform: scaleX(1.12); opacity:1; }
  }

  /* ===== 血条 ===== */
  .hp-bar {
    position: absolute;
    height: 4px;
    background: rgba(0,0,0,.55);
    border: 1px solid rgba(160,200,255,.35);
    border-radius: 2px;
    opacity: 0;
    transition: opacity .4s;
    pointer-events: none;
    z-index: 6;
    box-shadow: 0 0 6px rgba(0,0,0,.6);
    overflow: hidden;
  }
  .hp-fill {
    height: 100%; width: 100%;
    background: #4ade80;
    transition: width .18s linear, background .3s, box-shadow .3s;
    box-shadow: 0 0 6px rgba(74,222,128,.7);
  }

  /* ===== 弹丸 ===== */
  .projectile { position:absolute; left:0; top:0; pointer-events:none; z-index:20; }

  .rail-slug {
    width:34px; height:6px;
    margin:-3px 0 0 -17px;
    border-radius:3px;
    background: linear-gradient(90deg, rgba(120,220,255,0) 0%, #7fdcff 45%, #ffffff 80%, #ffffff 100%);
    box-shadow: 0 0 12px #7fdcff, 0 0 26px rgba(127,220,255,.6);
  }

  .missile {
    width:16px; height:6px;
    margin:-3px 0 0 -8px;
    border-radius:0 3px 3px 0;
    background: linear-gradient(90deg,#ffb347 0%,#e8eef5 55%,#ffffff 100%);
    box-shadow: 0 0 6px rgba(255,150,50,.75);
  }
  .missile::before {
    content:"";
    position:absolute;
    right:100%; top:50%;
    width:24px; height:6px;
    margin-top:-3px;
    background: linear-gradient(90deg, rgba(255,120,0,0) 0%, #ff8c1a 55%, #ffd166 100%);
    border-radius:3px 0 0 3px;
    transform-origin:100% 50%;
    filter: blur(1px);
    animation: missileFlame .1s infinite alternate;
  }
  @keyframes missileFlame {
    from { transform: scaleX(.75); opacity:.8; }
    to   { transform: scaleX(1.15); opacity:1; }
  }

  .laser-beam {
    position:absolute; left:0; top:0;
    height:4px; margin-top:-2px;
    transform-origin:0 50%;
    border-radius:2px;
    background: linear-gradient(90deg, #ffffff 0%, #d8f4ff 18%, #6fc8ff 55%, rgba(79,184,255,0) 100%);
    box-shadow: 0 0 14px #4fb8ff, 0 0 34px rgba(79,184,255,.75);
    animation: laserFade .38s ease-out forwards;
    pointer-events:none;
    z-index:22;
  }
  .laser-beam::before {
    content:"";
    position:absolute;
    left:0; top:50%;
    width:100%; height:14px;
    margin-top:-7px;
    border-radius:7px;
    background: linear-gradient(90deg, rgba(180,240,255,.55) 0%, rgba(120,210,255,.35) 30%, rgba(79,184,255,0) 100%);
    filter: blur(2px);
  }
  @keyframes laserFade {
    0% { opacity:1; }
    65% { opacity:1; }
    100% { opacity:0; }
  }

  /* ===== 碎片 / 闪光 ===== */
  .fragment {
    position: absolute;
    border-radius: 1px;
    pointer-events: none;
    z-index: 25;
    animation: fragFly .9s ease-out forwards;
  }
  @keyframes fragFly {
    0%   { transform: translate(0,0) rotate(0deg); opacity: 1; }
    100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
  }

  .muzzle-flash {
    position:absolute; left:0; top:0;
    width:48px; height:48px;
    margin:-24px 0 0 -24px;
    border-radius:50%;
    background: radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(140,225,255,.65) 38%, rgba(60,180,255,0) 70%);
    animation: flashPop .22s ease-out forwards;
    pointer-events:none;
    z-index:30;
  }
  @keyframes flashPop {
    from { opacity:1; transform: scale(.6); }
    to   { opacity:0; transform: scale(1.3); }
  }

  /* ===== HUD ===== */
  .hud {
    position: fixed;
    top: 14px; left: 14px; right: 14px;
    display: flex;
    justify-content: space-between;
    gap: 12px;
    pointer-events: none;
    z-index: 40;
  }
  .hud-item { flex: 1; max-width: 44%; }
  .hud-tag {
    font-size: 11px;
    letter-spacing: 2px;
    margin-bottom: 4px;
    color: #cfe6ff;
    text-shadow: 0 0 8px rgba(120,200,255,.5);
  }
  .hud-tag.red { color: #ffc0a8; text-shadow: 0 0 8px rgba(255,150,100,.5); }
  .hud-bar {
    height: 8px;
    background: rgba(0,0,0,.5);
    border: 1px solid rgba(160,200,255,.3);
    border-radius: 4px;
    overflow: hidden;
  }
  .hud-fill {
    height: 100%; width: 0%;
    transition: width .25s linear;
  }
  .hud-fill.blue { background: linear-gradient(90deg, #3aa6e8, #7fdcff); }
  .hud-fill.red  { background: linear-gradient(90deg, #e05a3a, #ffa060); }

  /* ===== 开局面板 ===== */
  .overlay {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(4, 8, 18, .78);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    transition: opacity .4s;
  }
  .overlay.hide { opacity: 0; pointer-events: none; }

  .panel {
    width: min(88vw, 420px);
    padding: 24px 20px 20px;
    background: linear-gradient(180deg, rgba(24,40,70,.92), rgba(12,20,38,.94));
    border: 1px solid rgba(100,170,255,.35);
    border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0,0,0,.6), 0 0 40px rgba(80,160,255,.15);
    text-align: center;
  }
  .panel h1 {
    font-size: 16px;
    letter-spacing: 3px;
    color: #cfe6ff;
    margin-bottom: 22px;
    font-weight: 600;
  }
  .row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 24px;
  }
  .side { flex: 1; }
  .side .label {
    font-size: 12px;
    letter-spacing: 2px;
    margin-bottom: 10px;
  }
  .blue-side .label { color: #7fdcff; }
  .red-side .label { color: #ffa060; }
  .count {
    font-size: 34px;
    font-weight: 700;
    line-height: 1;
    margin-bottom: 12px;
    font-variant-numeric: tabular-nums;
  }
  .blue-side .count { color: #7fdcff; text-shadow: 0 0 18px rgba(127,220,255,.6); }
  .red-side .count  { color: #ffa060; text-shadow: 0 0 18px rgba(255,160,96,.6); }
  .btns { display: flex; justify-content: center; gap: 10px; }
  .btns button {
    width: 38px; height: 38px;
    font-size: 20px;
    color: #cfe6ff;
    background: rgba(60,110,180,.35);
    border: 1px solid rgba(100,170,255,.45);
    border-radius: 10px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .btns button:active { background: rgba(80,140,220,.55); }
  .vs {
    font-size: 13px;
    letter-spacing: 2px;
    color: rgba(180,210,240,.55);
    align-self: center;
  }
  .start-btn {
    width: 100%;
    padding: 14px;
    font-size: 14px;
    letter-spacing: 4px;
    color: #061020;
    font-weight: 700;
    background: linear-gradient(90deg, #7fdcff, #4fb8ff);
    border: none;
    border-radius: 12px;
    cursor: pointer;
    box-shadow: 0 6px 24px rgba(80,180,255,.4);
    -webkit-tap-highlight-color: transparent;
  }
  .start-btn:active { transform: translateY(1px); }

  /* ===== 结果 ===== */
  .result {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 30px;
    font-weight: 600;
    color: #fff;
    letter-spacing: 6px;
    background: rgba(0,0,0,.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity .5s;
    z-index: 60;
    text-shadow: 0 0 24px rgba(120,200,255,.9), 0 0 48px rgba(120,200,255,.5);
  }
  .result.show { opacity: 1; }

  .restart-btn {
    position: fixed;
    right: 16px; bottom: 16px;
    z-index: 50;
    padding: 9px 16px;
    font-size: 12px;
    letter-spacing: 1px;
    color: #cfe6ff;
    background: rgba(20,40,70,.7);
    border: 1px solid rgba(100,170,255,.45);
    border-radius: 8px;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .restart-btn:active { background: rgba(40,80,130,.85); }

  .hint {
    position: fixed;
    left: 50%; bottom: 20px;
    transform: translateX(-50%);
    color: rgba(180,210,240,.5);
    font-size: 11px;
    letter-spacing: 2px;
    white-space: nowrap;
    pointer-events: none;
    z-index: 20;
  }
</style>
</head>
<body>

<div class="viewport" id="viewport">
  <div class="scene" id="scene">
    <div class="star-layer" id="starLayer"></div>
  </div>
</div>

<div class="hud">
  <div class="hud-item">
    <div class="hud-tag">蓝舰 · 编队完整度</div>
    <div class="hud-bar"><div class="hud-fill blue" id="hudA"></div></div>
  </div>
  <div class="hud-item">
    <div class="hud-tag red">赤舰 · 编队完整度</div>
    <div class="hud-bar"><div class="hud-fill red" id="hudB"></div></div>
  </div>
</div>

<div class="result" id="result"></div>
<button class="restart-btn" id="restartBtn">重新开战</button>
<div class="hint">单指拖动 · 双指缩放 · AI 自动对战</div>

<!-- 开局面板 -->
<div class="overlay" id="overlay">
  <div class="panel">
    <h1>编 队 对 战 · 部署</h1>
    <div class="row">
      <div class="side blue-side">
        <div class="label">蓝 舰 数 量</div>
        <div class="count" id="cntA">2</div>
        <div class="btns">
          <button data-side="A" data-d="-1">−</button>
          <button data-side="A" data-d="1">+</button>
        </div>
      </div>
      <div class="vs">VS</div>
      <div class="side red-side">
        <div class="label">赤 舰 数 量</div>
        <div class="count" id="cntB">2</div>
        <div class="btns">
          <button data-side="B" data-d="-1">−</button>
          <button data-side="B" data-d="1">+</button>
        </div>
      </div>
    </div>
    <button class="start-btn" id="startBtn">开 始 战 斗</button>
  </div>
</div>

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

  const viewport   = document.getElementById('viewport');
  const scene      = document.getElementById('scene');
  const starLayer  = document.getElementById('starLayer');
  const hudA       = document.getElementById('hudA');
  const hudB       = document.getElementById('hudB');
  const resultEl   = document.getElementById('result');
  const overlay    = document.getElementById('overlay');
  const startBtn   = document.getElementById('startBtn');
  const restartBtn = document.getElementById('restartBtn');
  const cntAEl     = document.getElementById('cntA');
  const cntBEl     = document.getElementById('cntB');

  const SCENE  = 1200;
  const CENTER = 220;
  const MIN_COUNT = 1;
  const MAX_COUNT = 4;

  /* ============================================================
     模块定义
     ============================================================ */
  const MODULE_DEFS = [
    { id:'panel-lt',  x:79,  y:136, w:26,  h:72, hp:70,  kind:'panel' },
    { id:'panel-lb',  x:79,  y:232, w:26,  h:72, hp:70,  kind:'panel' },
    { id:'panel-rt',  x:335, y:136, w:26,  h:72, hp:70,  kind:'panel' },
    { id:'panel-rb',  x:335, y:232, w:26,  h:72, hp:70,  kind:'panel' },
    { id:'arm-up',    x:208, y:128, w:24,  h:80, hp:130, kind:'body', weapon:'rail' },
    { id:'arm-down',  x:208, y:232, w:24,  h:80, hp:130, kind:'body' },
    { id:'arm-left',  x:92,  y:208, w:80,  h:24, hp:130, kind:'body', weapon:'missileL' },
    { id:'arm-right', x:268, y:208, w:80,  h:24, hp:130, kind:'body', weapon:'missileR' },
    { id:'core',      x:172, y:208, w:96,  h:24, hp:300, kind:'body', weapon:'laser' },
    { id:'port',      x:215, y:116, w:10,  h:12, hp:50,  kind:'body' },
    { id:'ship',      x:50,  y:213, w:30,  h:14, hp:70,  kind:'body' },
    { id:'thruster',  x:206, y:324, w:28,  h:14, hp:100, kind:'body' }
  ];

  const WEAPONS = {
    rail:    { cooldown: 1.8, damage: 40, speed: 2200 },
    laser:   { cooldown: 2.6, damage: 30 },
    missile: { cooldown: 3.2, damage: 55, speed: 680 }
  };

  /* ============================================================
     全局状态
     ============================================================ */
  const state = {
    stations: [],
    projectiles: [],
    over: false,
    started: false
  };

  let countA = 2;
  let countB = 2;

  /* ============================================================
     工具函数
     ============================================================ */
  function circleRectHit(cx, cy, r, rx, ry, rw, rh) {
    const nx = Math.max(rx, Math.min(cx, rx + rw));
    const ny = Math.max(ry, Math.min(cy, ry + rh));
    const dx = cx - nx;
    const dy = cy - ny;
    return dx * dx + dy * dy <= r * r;
  }

  function rayRectIntersect(ox, oy, dx, dy, rx, ry, rw, rh) {
    let tmin = -Infinity, tmax = Infinity;
    if (Math.abs(dx) < 1e-9) {
      if (ox < rx || ox > rx + rw) return null;
    } else {
      let t1 = (rx - ox) / dx, t2 = (rx + rw - ox) / dx;
      if (t1 > t2) { const tmp = t1; t1 = t2; t2 = tmp; }
      tmin = Math.max(tmin, t1); tmax = Math.min(tmax, t2);
      if (tmin > tmax) return null;
    }
    if (Math.abs(dy) < 1e-9) {
      if (oy < ry || oy > ry + rh) return null;
    } else {
      let t1 = (ry - oy) / dy, t2 = (ry + rh - oy) / dy;
      if (t1 > t2) { const tmp = t1; t1 = t2; t2 = tmp; }
      tmin = Math.max(tmin, t1); tmax = Math.min(tmax, t2);
      if (tmin > tmax) return null;
    }
    if (tmax < 0) return null;
    return tmin < 0 ? 0 : tmin;
  }

  /* ============================================================
     星空
     ============================================================ */
  (function buildStars() {
    const frag = document.createDocumentFragment();
    for (let i = 0; i < 240; i++) {
      const s = document.createElement('div');
      s.className = 'star';
      const size = (Math.random() * 2 + 1).toFixed(1);
      s.style.width = size + 'px';
      s.style.height = size + 'px';
      s.style.left = (Math.random() * 100).toFixed(2) + '%';
      s.style.top  = (Math.random() * 100).toFixed(2) + '%';
      s.style.animationDelay    = (Math.random() * 3).toFixed(2) + 's';
      s.style.animationDuration = (2 + Math.random() * 3).toFixed(2) + 's';
      frag.appendChild(s);
    }
    starLayer.appendChild(frag);
  })();

  /* ============================================================
     空间站
     ============================================================ */
  function Station(team, x, y, idx) {
    this.team = team;
    this.uid = team + idx;
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.alive = true;
    this.modules = [];
    this.enemy = null;
    this.targetTimer = Math.random() * 0.9;

    /* ---- 随机个性:让每艘舰的动作都不同 ---- */
    this.prefBase    = 300 + Math.random() * 220;       // 理想交战距离
    this.prefDist    = this.prefBase;
    this.aggr        = 0.55 + Math.random() * 0.95;     // 追击积极性
    this.orbitDir    = Math.random() < 0.5 ? -1 : 1;    // 绕行方向
    this.strafeMag   = 0.3 + Math.random() * 0.8;       // 侧向机动强度
    this.strafeFreq  = 0.35 + Math.random() * 0.85;     // 侧向机动频率
    this.strafePhase = Math.random() * Math.PI * 2;     // 侧向机动相位
    this.wanderTimer = 1 + Math.random() * 2.4;         // 随机游走计时
    this.jitter      = (Math.random() - 0.5) * 0.35;    // 瞄准抖动
    this.accel       = 60 + Math.random() * 60;         // 加速度
    this.maxSpeed    = 85 + Math.random() * 60;         // 最大速度

    this.cooldowns = {
      rail:    0.8 + Math.random() * 2.2,
      laser:   1.0 + Math.random() * 2.6,
      missile: 1.4 + Math.random() * 3.2
    };

    this.el = document.createElement('div');
    this.el.className = 'station theme-' + (team === 'A' ? 'blue' : 'red');
    this.applyPos();
    scene.appendChild(this.el);

    this.build();
  }

  Station.prototype.applyPos = function () {
    this.el.style.transform =
      'translate3d(' + (this.x - CENTER).toFixed(1) + 'px, ' +
      (this.y - CENTER).toFixed(1) + 'px, 0)';
  };

  Station.prototype.build = function () {
    const self = this;

    /* 推进器火焰(放在最底层) */
    this.flameEl = document.createElement('div');
    this.flameEl.className = 'thruster-flame';
    this.flameEl.innerHTML =
      '<div class="tf-glow"></div><div class="tf-outer"></div><div class="tf-core"></div>';
    this.el.appendChild(this.flameEl);

    /* 喷管 */
    const bell = document.createElement('div');
    bell.className = 'thruster-bell';
    this.el.appendChild(bell);

    /* 模块 */
    MODULE_DEFS.forEach(function (def) {
      const el = document.createElement('div');
      el.className = 'mod kind-' + def.kind;
      el.style.left   = def.x + 'px';
      el.style.top    = def.y + 'px';
      el.style.width  = def.w + 'px';
      el.style.height = def.h + 'px';

      let winList = [];
      if (def.id === 'arm-up' || def.id === 'arm-down') winList = [[8,26],[8,48]];
      else if (def.id === 'arm-left' || def.id === 'arm-right') winList = [[28,10],[44,10]];
      else if (def.id === 'core') winList = [[14,10],[78,10]];
      winList.forEach(function (p) {
        const w = document.createElement('div');
        w.className = 'win';
        w.style.left = p[0] + 'px';
        w.style.top  = p[1] + 'px';
        el.appendChild(w);
      });
      self.el.appendChild(el);

      const barW = Math.max(def.w, 36);
      const bar = document.createElement('div');
      bar.className = 'hp-bar';
      bar.style.width = barW + 'px';
      bar.style.left  = (def.x + def.w / 2 - barW / 2) + 'px';
      bar.style.top   = (def.y - 8) + 'px';
      const fill = document.createElement('div');
      fill.className = 'hp-fill';
      bar.appendChild(fill);
      self.el.appendChild(bar);

      self.modules.push({
        def: def, el: el, bar: bar, fill: fill,
        hp: def.hp, maxHp: def.hp, alive: true, hideTimer: null
      });
    });

    this.railTurret  = this.buildTurret('rail',  220, 122);
    this.laserTurret = this.buildTurret('laser', 220, 220);
    this.missileL    = this.buildPod(81, 206);
    this.missileR    = this.buildPod(341, 206);
  };

  Station.prototype.buildTurret = function (type, x, y) {
    const wrap = document.createElement('div');
    wrap.className = 'turret turret-' + type;
    wrap.style.left = x + 'px';
    wrap.style.top  = y + 'px';

    const base = document.createElement('div');
    base.className = 'turret-base';
    wrap.appendChild(base);

    const barrel = document.createElement('div');
    barrel.className = 'turret-barrel';
    wrap.appendChild(barrel);

    this.el.appendChild(wrap);
    return { wrap: wrap, barrel: barrel, x: x, y: y };
  };

  Station.prototype.buildPod = function (x, y) {
    const el = document.createElement('div');
    el.className = 'missile-pod';
    el.style.left = x + 'px';
    el.style.top  = y + 'px';
    this.el.appendChild(el);
    return { wrap: el, x: x, y: y, w: 18, h: 28 };
  };

  Station.prototype.getModule = function (id) {
    return this.modules.find(function (m) { return m.def.id === id; });
  };

  Station.prototype.isModuleAlive = function (id) {
    const m = this.getModule(id);
    return !!(m && m.alive);
  };

  Station.prototype.moduleWorld = function (m) {
    return {
      x: this.x - CENTER + m.def.x,
      y: this.y - CENTER + m.def.y,
      w: m.def.w,
      h: m.def.h
    };
  };

  Station.prototype.getAimPoint = function () {
    const core = this.getModule('core');
    if (core && core.alive) {
      const r = this.moduleWorld(core);
      return { x: r.x + r.w / 2, y: r.y + r.h / 2 };
    }
    return { x: this.x, y: this.y };
  };

  Station.prototype.updateHpBar = function (m) {
    const ratio = Math.max(0, m.hp / m.maxHp);
    m.fill.style.width = (ratio * 100) + '%';
    let c;
    if (ratio > 0.6) c = '#4ade80';
    else if (ratio > 0.3) c = '#facc15';
    else c = '#f87171';
    m.fill.style.background = c;
    m.fill.style.boxShadow = '0 0 6px ' + c;
  };

  Station.prototype.showHpBar = function (m) {
    if (!m.alive) return;
    m.bar.style.opacity = 1;
    clearTimeout(m.hideTimer);
    m.hideTimer = setTimeout(function () {
      if (m.alive) m.bar.style.opacity = 0;
    }, 2600);
  };

  Station.prototype.damageModule = function (m, dmg) {
    if (!m.alive || !this.alive) return;
    m.hp -= dmg;
    if (m.hp <= 0) {
      m.hp = 0;
      this.shatterModule(m);
    } else {
      this.updateHpBar(m);
      this.showHpBar(m);
    }
  };

  Station.prototype.shatterModule = function (m) {
    m.alive = false;
    m.el.style.display = 'none';

    const w = m.def.weapon;
    if (w === 'rail')     this.railTurret.wrap.style.display = 'none';
    if (w === 'laser')    this.laserTurret.wrap.style.display = 'none';
    if (w === 'missileL') this.missileL.wrap.style.display = 'none';
    if (w === 'missileR') this.missileR.wrap.style.display = 'none';

    m.bar.style.opacity = 0;
    setTimeout(function () { if (m.bar.parentNode) m.bar.remove(); }, 500);

    const r = this.moduleWorld(m);
    const color = this.team === 'A' ? '#7fdcff' : '#ffa060';
    spawnFragments(r, color);
    spawnFlash(r.x + r.w / 2, r.y + r.h / 2, 'boom');

    if (m.def.id === 'core') {
      this.alive = false;
      this.flameEl.style.opacity = '0';
      checkGameOver();
    }
  };

  /* ============================================================
     AI
     ============================================================ */
  Station.prototype.pickTarget = function () {
    let best = null, bestD = Infinity;
    for (let i = 0; i < state.stations.length; i++) {
      const s = state.stations[i];
      if (s === this || s.team === this.team || !s.alive) continue;
      const dx = s.x - this.x, dy = s.y - this.y;
      const d = dx * dx + dy * dy;
      if (d < bestD) { bestD = d; best = s; }
    }
    this.enemy = best;
  };

  Station.prototype.updateFlame = function (ax, ay) {
    const mag = Math.hypot(ax, ay);
    if (!this.alive || mag < 0.08) {
      this.flameEl.style.opacity = '0';
      return;
    }
    // 火焰指向推力的反方向
    const ang = Math.atan2(-ay, -ax);
    const len = 0.55 + Math.min(mag, 1) * 1.15;
    this.flameEl.style.transform =
      'translate(220px, 338px) rotate(' + ang.toFixed(3) + 'rad) scaleX(' + len.toFixed(2) + ')';
    this.flameEl.style.opacity = (0.5 + Math.min(mag, 1) * 0.5).toFixed(2);
  };

  Station.prototype.updateAI = function (dt) {
    if (!this.alive) {
      const damp = Math.pow(0.3, dt);
      this.vx *= damp; this.vy *= damp;
      this.x += this.vx * dt; this.y += this.vy * dt;
      this.applyPos();
      this.flameEl.style.opacity = '0';
      return;
    }

    /* 定时重新选目标 */
    this.targetTimer -= dt;
    if (this.targetTimer <= 0 || !this.enemy || !this.enemy.alive) {
      this.pickTarget();
      this.targetTimer = 0.8 + Math.random() * 1.2;
    }

    const enemy = this.enemy;
    if (!enemy) {
      this.flameEl.style.opacity = '0';
      this.applyPos();
      return;
    }

    const dx = enemy.x - this.x;
    const dy = enemy.y - this.y;
    const dist = Math.hypot(dx, dy) || 1;
    const ang = Math.atan2(dy, dx);

    /* 随机游走:周期性改变绕行方向、理想距离、机动强度 */
    this.wanderTimer -= dt;
    if (this.wanderTimer <= 0) {
      this.wanderTimer = 1.4 + Math.random() * 2.8;
      this.orbitDir = Math.random() < 0.5 ? -1 : 1;
      this.prefDist = this.prefBase + (Math.random() - 0.5) * 200;
      this.strafeMag = 0.3 + Math.random() * 0.8;
      this.jitter = (Math.random() - 0.5) * 0.35;
    }

    let thrustMag = 0;
    let thrustAng = ang;

    if (dist > this.prefDist + 40) {
      thrustMag = Math.min(1, (dist - this.prefDist) / 240) * this.aggr;
      thrustAng = ang + this.jitter;
    } else if (dist < this.prefDist - 40) {
      thrustMag = Math.min(1, (this.prefDist - dist) / 240);
      thrustAng = ang + Math.PI;
    }

    const t = performance.now() / 1000;
    const side = Math.sin(t * this.strafeFreq + this.strafePhase) * this.strafeMag * this.orbitDir;

    let ax = Math.cos(thrustAng) * thrustMag + Math.cos(ang + Math.PI / 2) * side;
    let ay = Math.sin(thrustAng) * thrustMag + Math.sin(ang + Math.PI / 2) * side;

    /* 边界排斥 */
    const m = 300;
    if (this.x < m) ax += (m - this.x) / m;
    if (this.x > SCENE - m) ax -= (this.x - (SCENE - m)) / m;
    if (this.y < m) ay += (m - this.y) / m;
    if (this.y > SCENE - m) ay -= (this.y - (SCENE - m)) / m;

    /* 推力限幅 */
    const amag = Math.hypot(ax, ay);
    if (amag > 1) { ax /= amag; ay /= amag; }

    this.vx += ax * this.accel * dt;
    this.vy += ay * this.accel * dt;

    const damp = Math.pow(0.55, dt);
    this.vx *= damp;
    this.vy *= damp;

    const sp = Math.hypot(this.vx, this.vy);
    if (sp > this.maxSpeed) {
      this.vx = this.vx / sp * this.maxSpeed;
      this.vy = this.vy / sp * this.maxSpeed;
    }

    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.applyPos();

    /* 主动移动时推进器喷火 */
    this.updateFlame(ax, ay);

    this.updateWeapons(dt);
  };

  Station.prototype.updateWeapons = function (dt) {
    const enemy = this.enemy;
    if (!enemy || !enemy.alive) return;

    const target = enemy.getAimPoint();

    if (this.isModuleAlive('arm-up')) {
      const a = Math.atan2(target.y - (this.y - 98), target.x - this.x);
      this.railTurret.barrel.style.transform = 'rotate(' + a + 'rad)';
    }
    if (this.isModuleAlive('core')) {
      const a = Math.atan2(target.y - this.y, target.x - this.x);
      this.laserTurret.barrel.style.transform = 'rotate(' + a + 'rad)';
    }

    this.cooldowns.rail    -= dt;
    this.cooldowns.laser   -= dt;
    this.cooldowns.missile -= dt;

    if (this.cooldowns.rail <= 0 && this.isModuleAlive('arm-up')) {
      this.fireRail(target);
      this.cooldowns.rail = WEAPONS.rail.cooldown * (0.8 + Math.random() * 0.5);
    }
    if (this.cooldowns.laser <= 0 && this.isModuleAlive('core')) {
      this.fireLaser(target);
      this.cooldowns.laser = WEAPONS.laser.cooldown * (0.8 + Math.random() * 0.5);
    }
    if (this.cooldowns.missile <= 0) {
      let fired = false;
      if (this.isModuleAlive('arm-left'))  { this.fireMissile('L', target); fired = true; }
      if (this.isModuleAlive('arm-right')) { this.fireMissile('R', target); fired = true; }
      if (fired) this.cooldowns.missile = WEAPONS.missile.cooldown * (0.8 + Math.random() * 0.5);
    }
  };

  Station.prototype.fireRail = function (target) {
    const wx = this.x;
    const wy = this.y - 98;
    const ang = Math.atan2(target.y - wy, target.x - wx);
    const bx = wx + Math.cos(ang) * 56;
    const by = wy + Math.sin(ang) * 56;

    spawnProjectile({
      type: 'rail', owner: this,
      x: bx, y: by, ang: ang,
      vx: Math.cos(ang) * WEAPONS.rail.speed,
      vy: Math.sin(ang) * WEAPONS.rail.speed,
      damage: WEAPONS.rail.damage,
      radius: 9, maxLife: 1.5
    });
    spawnFlash(bx, by, 'rail');
  };

  Station.prototype.fireLaser = function (target) {
    const wx = this.x;
    const wy = this.y;
    const ang = Math.atan2(target.y - wy, target.x - wx);
    const bx = wx + Math.cos(ang) * 30;
    const by = wy + Math.sin(ang) * 30;

    const cosA = Math.cos(ang);
    const sinA = Math.sin(ang);

    /* 找最近的命中点(所有敌舰) */
    let hitDist = 1600;
    let hitMod = null, hitStation = null;

    for (let i = 0; i < state.stations.length; i++) {
      const s = state.stations[i];
      if (s.team === this.team || !s.alive) continue;
      for (let k = 0; k < s.modules.length; k++) {
        const m = s.modules[k];
        if (!m.alive) continue;
        const r = s.moduleWorld(m);
        const tt = rayRectIntersect(bx, by, cosA, sinA, r.x, r.y, r.w, r.h);
        if (tt !== null && tt < hitDist) {
          hitDist = tt;
          hitMod = m;
          hitStation = s;
        }
      }
    }

    spawnBeam(bx, by, ang, hitDist);

    if (hitMod && hitStation) {
      hitStation.damageModule(hitMod, WEAPONS.laser.damage);
      const hr = hitStation.moduleWorld(hitMod);
      spawnFlash(hr.x + hr.w / 2, hr.y + hr.h / 2, 'laser');
    }
  };

  Station.prototype.fireMissile = function (side, target) {
    const pod = side === 'L' ? this.missileL : this.missileR;
    const wx = this.x - CENTER + pod.x + pod.w / 2;
    const wy = this.y - CENTER + pod.y + pod.h / 2;
    const ang = Math.atan2(target.y - wy, target.x - wx);

    spawnProjectile({
      type: 'missile', owner: this,
      x: wx, y: wy, ang: ang,
      vx: Math.cos(ang) * WEAPONS.missile.speed,
      vy: Math.sin(ang) * WEAPONS.missile.speed,
      damage: WEAPONS.missile.damage,
      radius: 8, maxLife: 2.5
    });
    spawnFlash(wx, wy, 'missile');
  };

  /* ============================================================
     弹丸 / 特效
     ============================================================ */
  function spawnProjectile(p) {
    const el = document.createElement('div');
    el.className = 'projectile ' + (p.type === 'rail' ? 'rail-slug' : 'missile');
    el.style.transform =
      'translate3d(' + p.x.toFixed(1) + 'px, ' + p.y.toFixed(1) + 'px, 0) rotate(' + p.ang + 'rad)';
    scene.appendChild(el);
    p.el = el;
    p.life = 0;
    state.projectiles.push(p);
  }

  function spawnFlash(x, y, type) {
    const el = document.createElement('div');
    el.className = 'muzzle-flash';
    el.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0)';

    if (type === 'missile') {
      el.style.background = 'radial-gradient(circle, rgba(255,240,200,.95) 0%, rgba(255,160,60,.7) 35%, rgba(255,120,0,0) 70%)';
    } else if (type === 'boom') {
      el.style.width = '100px';
      el.style.height = '100px';
      el.style.margin = '-50px 0 0 -50px';
      el.style.background = 'radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(255,180,80,.75) 35%, rgba(255,120,0,0) 70%)';
    }
    scene.appendChild(el);
    setTimeout(function () { el.remove(); }, 320);
  }

  function spawnBeam(x, y, ang, len) {
    const el = document.createElement('div');
    el.className = 'laser-beam';
    el.style.width = len + 'px';
    el.style.transform =
      'translate3d(' + x + 'px, ' + y + 'px, 0) rotate(' + ang + 'rad)';
    scene.appendChild(el);
    setTimeout(function () { el.remove(); }, 440);
  }

  function spawnFragments(rect, color) {
    const cols = Math.max(2, Math.round(rect.w / 22));
    const rows = Math.max(2, Math.round(rect.h / 18));
    const cw = rect.w / cols;
    const ch = rect.h / rows;
    const total = Math.min(cols * rows, 24);

    for (let i = 0; i < total; i++) {
      const c = i % cols;
      const r = Math.floor(i / cols);
      const fx = rect.x + c * cw;
      const fy = rect.y + r * ch;

      const el = document.createElement('div');
      el.className = 'fragment';
      el.style.left   = fx + 'px';
      el.style.top    = fy + 'px';
      el.style.width  = (cw * 0.82) + 'px';
      el.style.height = (ch * 0.82) + 'px';
      el.style.background = color;
      el.style.boxShadow = '0 0 8px ' + color;

      const a = Math.random() * Math.PI * 2;
      const d = 30 + Math.random() * 80;
      el.style.setProperty('--dx', (Math.cos(a) * d).toFixed(1) + 'px');
      el.style.setProperty('--dy', (Math.sin(a) * d).toFixed(1) + 'px');
      el.style.setProperty('--rot', (Math.random() * 720 - 360).toFixed(0) + 'deg');
      el.style.animationDelay = (Math.random() * 0.08).toFixed(2) + 's';

      scene.appendChild(el);
      setTimeout(function () { el.remove(); }, 1150);
    }
  }

  function spawnParticles(x, y, count, color, size) {
    for (let i = 0; i < count; i++) {
      const el = document.createElement('div');
      el.className = 'fragment';
      const s = size * (0.5 + Math.random());
      el.style.width  = s + 'px';
      el.style.height = s + 'px';
      el.style.borderRadius = '50%';
      el.style.background = color;
      el.style.left = (x - s / 2) + 'px';
      el.style.top  = (y - s / 2) + 'px';
      el.style.boxShadow = '0 0 8px ' + color;

      const a = Math.random() * Math.PI * 2;
      const d = 20 + Math.random() * 60;
      el.style.setProperty('--dx', (Math.cos(a) * d).toFixed(1) + 'px');
      el.style.setProperty('--dy', (Math.sin(a) * d).toFixed(1) + 'px');
      el.style.setProperty('--rot', '0deg');

      scene.appendChild(el);
      setTimeout(function () { el.remove(); }, 1000);
    }
  }

  /* ============================================================
     主循环
     ============================================================ */
  let lastT = performance.now();

  function loop(now) {
    const dt = Math.min((now - lastT) / 1000, 0.05);
    lastT = now;

    if (state.stations.length) {
      for (let i = 0; i < state.stations.length; i++) {
        state.stations[i].updateAI(dt);
      }
      updateProjectiles(dt);
      updateHUD();
    }

    requestAnimationFrame(loop);
  }

  function updateProjectiles(dt) {
    for (let i = state.projectiles.length - 1; i >= 0; i--) {
      const p = state.projectiles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.life += dt;

      let removed = false;

      /* 命中检测:所有敌舰 */
      for (let si = 0; si < state.stations.length; si++) {
        const s = state.stations[si];
        if (s.team === p.owner.team || !s.alive) continue;
        for (let k = 0; k < s.modules.length; k++) {
          const m = s.modules[k];
          if (!m.alive) continue;
          const r = s.moduleWorld(m);
          if (circleRectHit(p.x, p.y, p.radius, r.x, r.y, r.w, r.h)) {
            s.damageModule(m, p.damage);
            spawnParticles(p.x, p.y, 8, p.type === 'missile' ? '#ffb060' : '#7fdcff', 4);
            p.el.remove();
            state.projectiles.splice(i, 1);
            removed = true;
            break;
          }
        }
        if (removed) break;
      }
      if (removed) continue;

      if (p.life > p.maxLife ||
          p.x < -300 || p.x > SCENE + 300 ||
          p.y < -300 || p.y > SCENE + 300) {
        p.el.remove();
        state.projectiles.splice(i, 1);
        continue;
      }

      p.el.style.transform =
        'translate3d(' + p.x.toFixed(1) + 'px, ' + p.y.toFixed(1) + 'px, 0) rotate(' + p.ang + 'rad)';
    }
  }

  /* ============================================================
     HUD / 胜负
     ============================================================ */
  let lastHudA = -1, lastHudB = -1;

  function teamHealth(team) {
    let total = 0, max = 0;
    for (let i = 0; i < state.stations.length; i++) {
      const s = state.stations[i];
      if (s.team !== team) continue;
      for (let k = 0; k < s.modules.length; k++) {
        const m = s.modules[k];
        max += m.maxHp;
        if (m.alive) total += m.hp;
      }
    }
    return max > 0 ? total / max : 0;
  }

  function updateHUD() {
    const rA = teamHealth('A');
    const rB = teamHealth('B');

    if (Math.abs(rA - lastHudA) > 0.001) {
      hudA.style.width = (rA * 100).toFixed(1) + '%';
      lastHudA = rA;
    }
    if (Math.abs(rB - lastHudB) > 0.001) {
      hudB.style.width = (rB * 100).toFixed(1) + '%';
      lastHudB = rB;
    }
  }

  function checkGameOver() {
    if (state.over || !state.started) return;

    let aliveA = false, aliveB = false;
    for (let i = 0; i < state.stations.length; i++) {
      const s = state.stations[i];
      if (!s.alive) continue;
      if (s.team === 'A') aliveA = true;
      else aliveB = true;
    }
    if (aliveA && aliveB) return;

    state.over = true;

    let text;
    if (!aliveA && !aliveB) text = '同 归 于 尽';
    else if (!aliveB) text = '蓝 舰 胜 利';
    else text = '赤 舰 胜 利';

    setTimeout(function () {
      resultEl.textContent = text;
      resultEl.classList.add('show');
    }, 700);
  }

  /* ============================================================
     开局 / 部署
     ============================================================ */
  function startBattle(nA, nB) {
    /* 清场 */
    state.stations.forEach(function (s) { s.el.remove(); });
    state.projectiles.forEach(function (p) { p.el.remove(); });
    state.stations = [];
    state.projectiles = [];
    state.over = false;
    resultEl.classList.remove('show');

    /* 蓝方 */
    const gapA = nA > 1 ? Math.min(250, 720 / (nA - 1)) : 0;
    const y0A = 600 - gapA * (nA - 1) / 2;
    for (let i = 0; i < nA; i++) {
      const y = y0A + i * gapA + (Math.random() - 0.5) * 50;
      const x = 250 + Math.random() * 110;
      state.stations.push(new Station('A', x, y, i));
    }

    /* 赤方 */
    const gapB = nB > 1 ? Math.min(250, 720 / (nB - 1)) : 0;
    const y0B = 600 - gapB * (nB - 1) / 2;
    for (let i = 0; i < nB; i++) {
      const y = y0B + i * gapB + (Math.random() - 0.5) * 50;
      const x = 840 + Math.random() * 110;
      state.stations.push(new Station('B', x, y, i));
    }

    /* 初始选目标 */
    state.stations.forEach(function (s) { s.pickTarget(); });

    lastHudA = -1;
    lastHudB = -1;

    state.started = true;
    fitCamera();
  }

  /* ============================================================
     视角控制
     ============================================================ */
  let scale = 1, tx = 0, ty = 0;

  function applyTransform() {
    scene.style.transform = 'translate(' + tx + 'px, ' + ty + 'px) scale(' + scale + ')';
  }

  function fitCamera() {
    const r = viewport.getBoundingClientRect();
    const fit = Math.min(r.width / 1150, r.height / 1150);
    scale = Math.max(0.22, Math.min(1.2, fit));
    tx = 0;
    ty = 0;
    applyTransform();
  }

  fitCamera();

  const pointers = new Map();
  let pinchDist = 0, pinchCX = 0, pinchCY = 0;

  viewport.addEventListener('pointerdown', function (e) {
    e.preventDefault();
    try { viewport.setPointerCapture(e.pointerId); } catch (err) {}
    pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
    viewport.classList.add('grabbing');

    if (pointers.size === 2) {
      const pts = [...pointers.values()];
      pinchDist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
      pinchCX = (pts[0].x + pts[1].x) / 2;
      pinchCY = (pts[0].y + pts[1].y) / 2;
    }
  }, { passive: false });

  viewport.addEventListener('pointermove', function (e) {
    const p = pointers.get(e.pointerId);
    if (!p) return;

    const dx = e.clientX - p.x;
    const dy = e.clientY - p.y;
    p.x = e.clientX;
    p.y = e.clientY;

    if (pointers.size === 1) {
      tx += dx;
      ty += dy;
      applyTransform();
      return;
    }

    const pts = [...pointers.values()];
    const a = pts[0], b = pts[1];
    const dist = Math.hypot(b.x - a.x, b.y - a.y);
    const c2x = (a.x + b.x) / 2;
    const c2y = (a.y + b.y) / 2;

    if (pinchDist > 0 && dist > 0) {
      tx += c2x - pinchCX;
      ty += c2y - pinchCY;

      const rect = viewport.getBoundingClientRect();
      const cX = rect.width / 2;
      const cY = rect.height / 2;
      const mx = c2x - rect.left;
      const my = c2y - rect.top;

      let ns = scale * (dist / pinchDist);
      ns = Math.max(0.22, Math.min(3, ns));

      if (ns !== scale) {
        const k = ns / scale;
        const ux = mx - cX;
        const uy = my - cY;
        tx = ux * (1 - k) + k * tx;
        ty = uy * (1 - k) + k * ty;
        scale = ns;
      }
      applyTransform();
    }

    pinchDist = dist;
    pinchCX = c2x;
    pinchCY = c2y;
  }, { passive: false });

  function endPointer(e) {
    if (!pointers.has(e.pointerId)) return;
    pointers.delete(e.pointerId);
    if (pointers.size === 0) viewport.classList.remove('grabbing');
    if (pointers.size === 1) pinchDist = 0;
  }

  viewport.addEventListener('pointerup', endPointer);
  viewport.addEventListener('pointercancel', endPointer);

  /* ============================================================
     开局面板交互
     ============================================================ */
  document.querySelectorAll('.btns button').forEach(function (btn) {
    btn.addEventListener('click', function () {
      const side = btn.dataset.side;
      const d = parseInt(btn.dataset.d, 10);
      if (side === 'A') {
        countA = Math.max(MIN_COUNT, Math.min(MAX_COUNT, countA + d));
        cntAEl.textContent = countA;
      } else {
        countB = Math.max(MIN_COUNT, Math.min(MAX_COUNT, countB + d));
        cntBEl.textContent = countB;
      }
    });
  });

  startBtn.addEventListener('click', function () {
    overlay.classList.add('hide');
    startBattle(countA, countB);
  });

  restartBtn.addEventListener('click', function () {
    window.location.reload();
  });

  requestAnimationFrame(loop);

})();
</script>

</body>
</html>

Game Source: 模块化空间站 · 编队 AI 对战

Creator: ApexFalcon66

Libraries: none

Complexity: complex (1472 lines, 45.2 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: ai-apexfalcon66" to link back to the original. Then publish at arcadelab.ai/publish.