🎮ArcadeLab

模块化空间站 · 完整度系统

by ApexFalcon66
1104 lines33.8 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>模块化空间站 · 完整度系统</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; }
  .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: 380px; top: 380px;
    width: 440px; height: 440px;
    animation: float 7s ease-in-out infinite;
  }
  @keyframes float {
    0%,100% { transform: translateY(0); }
    50%     { transform: translateY(-10px); }
  }

  .mod {
    position:absolute;
    background:#fff;
    border-radius:3px;
    box-shadow: 0 3px 14px rgba(0,0,0,.45), inset 0 -2px 5px rgba(150,170,200,.25);
  }
  .core      { width:96px; height:24px; left:172px; top:208px; }
  .arm-left  { width:80px; height:24px; left:92px;  top:208px; }
  .arm-right { width:80px; height:24px; left:268px; top:208px; }
  .arm-up    { width:24px; height:80px; left:208px; top:128px; }
  .arm-down  { width:24px; height:80px; left:208px; top:232px; }

  .panel {
    position:absolute;
    width:26px; height:72px;
    background: repeating-linear-gradient(0deg, #1d4d86 0px, #1d4d86 8px, #2b6cb5 8px, #2b6cb5 10px);
    border:2px solid #4a90d9;
    border-radius:2px;
    box-shadow: 0 0 16px rgba(74,144,217,.35);
  }
  .panel-lt { left:79px;  top:136px; }
  .panel-lb { left:79px;  top:232px; }
  .panel-rt { left:335px; top:136px; }
  .panel-rb { left:335px; top:232px; }

  .win {
    position:absolute;
    width:8px; height:4px;
    background:#2d4d6e;
    border-radius:1px;
    box-shadow: 0 0 4px rgba(120,190,255,.6);
  }

  .port { position:absolute; background:#b9c6d4; border:1px solid #8496aa; border-radius:2px; }
  .port-v { width:10px; height:12px; }

  .ship {
    position:absolute;
    width:30px; height:14px;
    background:#e8eef5;
    border:2px solid #98aabd;
    border-radius:3px;
  }

  /* ===== 底部推进器 ===== */
  .thruster { position:absolute; left:206px; top:324px; width:28px; height:14px; }
  .thruster .bell {
    width:100%; height:100%;
    background: linear-gradient(180deg,#eef3f9 0%,#b7c4d3 55%,#8a9bb0 100%);
    clip-path: polygon(0 0, 100% 0, 82% 100%, 18% 100%);
  }
  .flame { position:absolute; left:50%; top:13px; width:60px; height:80px; transform:translateX(-50%); pointer-events:none; }
  .flame-outer {
    position:absolute; left:50%; top:0;
    width:30px; height:62px;
    transform:translateX(-50%); transform-origin:50% 0;
    background: radial-gradient(ellipse at 50% 12%, #ffd166 0%, #ff9a1f 35%, #ff5f00 68%, rgba(255,90,0,0) 100%);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
    filter: blur(.6px);
    animation: flameFlicker .16s infinite alternate ease-in-out;
  }
  .flame-core {
    position:absolute; left:50%; top:0;
    width:13px; height:34px;
    transform:translateX(-50%); transform-origin:50% 0;
    background: radial-gradient(ellipse at 50% 15%, #ffffff 0%, #fff4b8 45%, #ffc247 100%);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
    animation: flameFlicker .13s infinite alternate-reverse ease-in-out;
  }
  .flame-glow {
    position:absolute; left:50%; top:-6px;
    width:80px; height:80px;
    transform:translateX(-50%);
    background: radial-gradient(circle, rgba(255,150,40,.42) 0%, rgba(255,120,0,0) 68%);
    border-radius:50%;
    animation: glowPulse .5s infinite alternate ease-in-out;
  }
  @keyframes flameFlicker {
    from { transform: translateX(-50%) scale(.88,.78); opacity:.82; }
    to   { transform: translateX(-50%) scale(1.06,1.15); opacity:1; }
  }
  @keyframes glowPulse {
    from { opacity:.55; transform: translateX(-50%) scale(.9); }
    to   { opacity:.95; transform: translateX(-50%) scale(1.1); }
  }

  /* ===== 上方电磁炮 ===== */
  .rail-turret { position:absolute; left:220px; top:122px; width:0; height:0; z-index:12; }
  .rail-base {
    position:absolute; width:30px; height:30px;
    left:-15px; top:-15px;
    border-radius:5px;
    background: linear-gradient(180deg,#ffffff,#b9c6d4);
    border:2px solid #8a9bb0;
    box-shadow: 0 0 14px rgba(120,200,255,.6);
  }
  .rail-barrel {
    position:absolute; left:0; top:-5px;
    width:54px; height:10px;
    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 rgba(120,200,255,.4);
    transition: transform .09s linear;
  }

  /* ===== 核心舱激光炮 ===== */
  .laser-turret { position:absolute; left:220px; top:220px; width:0; height:0; z-index:13; }
  .laser-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 rgba(90,190,255,.7);
  }
  .laser-barrel {
    position:absolute; left:0; top:-3px;
    width:30px; height:6px;
    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 rgba(80,190,255,.85);
    transition: transform .09s linear;
  }

  /* ===== 左右导弹巢 ===== */
  .missile-pod {
    position:absolute;
    width:18px; height:28px;
    top:206px;
    border-radius:4px;
    background:
      radial-gradient(circle at 30% 22%, #2d4d6e 2px, transparent 2.4px),
      radial-gradient(circle at 70% 22%, #2d4d6e 2px, transparent 2.4px),
      radial-gradient(circle at 30% 50%, #2d4d6e 2px, transparent 2.4px),
      radial-gradient(circle at 70% 50%, #2d4d6e 2px, transparent 2.4px),
      radial-gradient(circle at 30% 78%, #2d4d6e 2px, transparent 2.4px),
      radial-gradient(circle at 70% 78%, #2d4d6e 2px, transparent 2.4px),
      linear-gradient(180deg,#ffffff,#b9c6d4);
    border:2px solid #8a9bb0;
    box-shadow: 0 0 10px rgba(255,190,120,.35);
    z-index:12;
  }
  .missile-pod.left  { left:81px; }
  .missile-pod.right { left:341px; }

  /* ===== 模块完整度血条 ===== */
  .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: 30;
    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);
  }

  /* ===== 碎片 ===== */
  .fragment {
    position: absolute;
    border-radius: 1px;
    pointer-events: none;
    z-index: 26;
    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; }
  }

  /* ===== 陨石 ===== */
  .meteor {
    position: absolute;
    left: 0; top: 0;
    border-radius: 50%;
    background: radial-gradient(circle at 34% 30%, #cfc0b2 0%, #8a7a6d 45%, #4a3f38 78%, #2b241f 100%);
    box-shadow: 0 0 14px rgba(255,160,80,.32), inset -3px -3px 8px rgba(0,0,0,.65);
    pointer-events: none;
    z-index: 8;
    will-change: transform;
  }
  .meteor::after {
    content:"";
    position:absolute;
    inset:-7px;
    border-radius:50%;
    background: radial-gradient(circle, rgba(255,170,90,.3) 0%, rgba(255,140,60,0) 70%);
    pointer-events:none;
  }

  /* ===== 弹体 ===== */
  .projectile { position:absolute; left:0; top:0; pointer-events:none; }

  .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 .34s ease-out forwards;
    pointer-events:none;
  }
  .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; }
  }

  .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 .2s ease-out forwards;
    pointer-events:none;
  }
  @keyframes flashPop {
    from { opacity:1; transform: scale(.6); }
    to   { opacity:0; transform: scale(1.3); }
  }

  /* ===== UI ===== */
  .reset-btn {
    position:fixed; top:16px; right:16px; z-index:50;
    padding:8px 14px;
    font-size:12px; letter-spacing:1px;
    color:#cfe6ff;
    background: rgba(20,40,70,.65);
    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;
  }
  .reset-btn:active { background: rgba(40,80,130,.85); }

  .hint {
    position:fixed; left:50%; bottom:18px;
    transform:translateX(-50%);
    color: rgba(180,210,240,.55);
    font-size:12px; letter-spacing:1.5px;
    white-space:nowrap;
    pointer-events:none;
    z-index:20;
    transition: color .3s;
  }
  .hint.danger { color: #ff8080; }
</style>
</head>
<body>

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

    <div class="station" id="station">
      <!-- 太阳能帆板 -->
      <div class="panel panel-lt" id="modPanelLt"></div>
      <div class="panel panel-lb" id="modPanelLb"></div>
      <div class="panel panel-rt" id="modPanelRt"></div>
      <div class="panel panel-rb" id="modPanelRb"></div>

      <!-- 纵向臂 -->
      <div class="mod arm-up" id="modArmUp">
        <div class="win" style="left:8px; top:26px;"></div>
        <div class="win" style="left:8px; top:48px;"></div>
      </div>
      <div class="mod arm-down" id="modArmDown">
        <div class="win" style="left:8px; top:26px;"></div>
        <div class="win" style="left:8px; top:48px;"></div>
      </div>

      <!-- 横向臂 -->
      <div class="mod arm-left" id="modArmLeft">
        <div class="win" style="left:28px; top:10px;"></div>
        <div class="win" style="left:44px; top:10px;"></div>
      </div>
      <div class="mod arm-right" id="modArmRight">
        <div class="win" style="left:28px; top:10px;"></div>
        <div class="win" style="left:44px; top:10px;"></div>
      </div>

      <!-- 核心舱 -->
      <div class="mod core" id="modCore">
        <div class="win" style="left:14px; top:10px;"></div>
        <div class="win" style="left:78px; top:10px;"></div>
      </div>

      <!-- 顶部对接口 -->
      <div class="port port-v" id="modPort" style="left:215px; top:116px;"></div>

      <!-- 停靠飞船 -->
      <div class="ship" id="modShip" style="left:50px; top:213px;"></div>

      <!-- 底部推进器 -->
      <div class="thruster" id="modThruster">
        <div class="bell"></div>
        <div class="flame">
          <div class="flame-glow"></div>
          <div class="flame-outer"></div>
          <div class="flame-core"></div>
        </div>
      </div>

      <!-- 上方电磁炮 -->
      <div class="rail-turret">
        <div class="rail-base"></div>
        <div class="rail-barrel" id="railBarrel"></div>
      </div>

      <!-- 核心舱激光炮 -->
      <div class="laser-turret">
        <div class="laser-base"></div>
        <div class="laser-barrel" id="laserBarrel"></div>
      </div>

      <!-- 左右导弹巢 -->
      <div class="missile-pod left" id="podLeft"></div>
      <div class="missile-pod right" id="podRight"></div>
    </div>
  </div>
</div>

<button class="reset-btn" id="resetBtn">重置视角</button>
<div class="hint" id="hint">单指拖动 · 双指缩放 · 点击开火</div>

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

  const viewport    = document.getElementById('viewport');
  const scene       = document.getElementById('scene');
  const station     = document.getElementById('station');
  const starLayer   = document.getElementById('starLayer');
  const resetBtn    = document.getElementById('resetBtn');
  const hintEl      = document.getElementById('hint');
  const railBarrel  = document.getElementById('railBarrel');
  const laserBarrel = document.getElementById('laserBarrel');

  /* ---------- 星空 ---------- */
  const frag = document.createDocumentFragment();
  for (let i = 0; i < 200; 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);

  /* ---------- 视角状态 ---------- */
  const MIN_SCALE = 0.35;
  const MAX_SCALE = 3;
  let scale = 1, tx = 0, ty = 0;

  function apply() {
    scene.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
  }

  (function initScale() {
    const r = viewport.getBoundingClientRect();
    const fit = Math.min(r.width / 560, r.height / 560);
    scale = Math.max(MIN_SCALE, Math.min(1, fit));
    apply();
  })();

  /* ---------- 指针手势 ---------- */
  const pointers = new Map();
  let gestureMulti = false;
  let startX = 0, startY = 0, startT = 0;
  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 === 1) {
      gestureMulti = false;
      startX = e.clientX;
      startY = e.clientY;
      startT = performance.now();
    } else if (pointers.size === 2) {
      gestureMulti = true;
      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;
      apply();
      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(MIN_SCALE, Math.min(MAX_SCALE, 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;
      }
      apply();
    }

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

  function endPointer(e) {
    if (!pointers.has(e.pointerId)) return;

    const wasSingle = (pointers.size === 1);
    pointers.delete(e.pointerId);

    if (pointers.size === 0) viewport.classList.remove('grabbing');

    if (wasSingle && !gestureMulti) {
      const dt = performance.now() - startT;
      const dist = Math.hypot(e.clientX - startX, e.clientY - startY);
      if (dist < 12 && dt < 400) {
        fireAll(e.clientX, e.clientY);
      }
    }

    if (pointers.size === 0) {
      gestureMulti = false;
      pinchDist = 0;
    } else if (pointers.size === 1) {
      pinchDist = 0;
    }
  }

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

  /* ============================================================
     模块完整度系统
     ============================================================ */
  const STATION_ORIGIN = { x: 380, y: 380 };
  let off = { x: 0, y: 0 };   // 空间站浮动偏移

  const MODULE_DEFS = [
    { id:'panel-lt',  sel:'#modPanelLt',  x:79,  y:136, w:26,  h:72, hp:40,  fragColor:'#3a7bc8' },
    { id:'panel-lb',  sel:'#modPanelLb',  x:79,  y:232, w:26,  h:72, hp:40,  fragColor:'#3a7bc8' },
    { id:'panel-rt',  sel:'#modPanelRt',  x:335, y:136, w:26,  h:72, hp:40,  fragColor:'#3a7bc8' },
    { id:'panel-rb',  sel:'#modPanelRb',  x:335, y:232, w:26,  h:72, hp:40,  fragColor:'#3a7bc8' },
    { id:'arm-up',    sel:'#modArmUp',    x:208, y:128, w:24,  h:80, hp:90,  fragColor:'#e8eef5', weaponSel:'.rail-turret' },
    { id:'arm-down',  sel:'#modArmDown',  x:208, y:232, w:24,  h:80, hp:90,  fragColor:'#e8eef5' },
    { id:'arm-left',  sel:'#modArmLeft',  x:92,  y:208, w:80,  h:24, hp:90,  fragColor:'#e8eef5', weaponSel:'.missile-pod.left' },
    { id:'arm-right', sel:'#modArmRight', x:268, y:208, w:80,  h:24, hp:90,  fragColor:'#e8eef5', weaponSel:'.missile-pod.right' },
    { id:'core',      sel:'#modCore',     x:172, y:208, w:96,  h:24, hp:160, fragColor:'#e8eef5', weaponSel:'.laser-turret' },
    { id:'port',      sel:'#modPort',     x:215, y:116, w:10,  h:12, hp:30,  fragColor:'#b9c6d4' },
    { id:'ship',      sel:'#modShip',     x:50,  y:213, w:30,  h:14, hp:50,  fragColor:'#e8eef5' },
    { id:'thruster',  sel:'#modThruster', x:206, y:324, w:28,  h:14, hp:60,  fragColor:'#b7c4d3' }
  ];

  const modules   = [];
  const moduleMap = new Map();

  function initModules() {
    MODULE_DEFS.forEach(function (def) {
      const el = document.querySelector(def.sel);
      if (!el) return;

      // 创建血条
      const bar = document.createElement('div');
      bar.className = 'hp-bar';
      const bw = Math.max(def.w, 30);
      bar.style.width = bw + 'px';
      bar.style.left  = (def.x + def.w / 2 - bw / 2) + 'px';
      bar.style.top   = (def.y - 9) + 'px';

      const fill = document.createElement('div');
      fill.className = 'hp-fill';
      bar.appendChild(fill);
      station.appendChild(bar);

      const m = {
        id: def.id,
        el: el,
        bar: bar,
        fill: fill,
        weaponSel: def.weaponSel || null,
        x: def.x, y: def.y, w: def.w, h: def.h,
        fragColor: def.fragColor || '#e8eef5',
        maxHp: def.hp,
        hp: def.hp,
        alive: true,
        hideTimer: null
      };
      modules.push(m);
      moduleMap.set(m.id, m);
    });
  }
  initModules();

  function isAlive(id) {
    const m = moduleMap.get(id);
    return !!(m && m.alive);
  }

  function moduleWorldRect(m) {
    return {
      x: STATION_ORIGIN.x + off.x + m.x,
      y: STATION_ORIGIN.y + off.y + m.y,
      w: m.w,
      h: m.h
    };
  }

  function updateHpBar(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;
  }

  function showHpBar(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);
  }

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

  /* ---------- 碎裂 ---------- */
  function shatterModule(m) {
    m.alive = false;
    clearTimeout(m.hideTimer);

    m.el.style.display = 'none';
    if (m.weaponSel) {
      const w = station.querySelector(m.weaponSel);
      if (w) w.style.display = 'none';
    }
    m.bar.style.opacity = 0;
    setTimeout(function () { if (m.bar.parentNode) m.bar.remove(); }, 500);

    const rect = moduleWorldRect(m);
    spawnFragments(rect, m.fragColor);

    // 中心闪光
    spawnFlash(rect.x + rect.w / 2, rect.y + rect.h / 2, 'boom');

    if (m.id === 'core') {
      hintEl.textContent = '⚠ 核心舱损毁 · 空间站失去控制';
      hintEl.classList.add('danger');
    }
  }

  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 rgba(200,230,255,.7)';

      const ang = Math.random() * Math.PI * 2;
      const dist = 30 + Math.random() * 80;
      el.style.setProperty('--dx', (Math.cos(ang) * dist).toFixed(1) + 'px');
      el.style.setProperty('--dy', (Math.sin(ang) * dist).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, baseSize) {
    for (let i = 0; i < count; i++) {
      const el = document.createElement('div');
      el.className = 'fragment';
      const s = baseSize * (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 ang = Math.random() * Math.PI * 2;
      const dist = 20 + Math.random() * 60;
      el.style.setProperty('--dx', (Math.cos(ang) * dist).toFixed(1) + 'px');
      el.style.setProperty('--dy', (Math.sin(ang) * dist).toFixed(1) + 'px');
      el.style.setProperty('--rot', '0deg');

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

  /* ============================================================
     陨石系统
     ============================================================ */
  const meteors = [];
  let meteorTimer = 4;       // 开局 4 秒安全期
  let elapsed = 0;

  function spawnMeteor() {
    const ang  = Math.random() * Math.PI * 2;
    const dist = 620 + Math.random() * 260;
    const x = 600 + Math.cos(ang) * dist;
    const y = 600 + Math.sin(ang) * dist;

    // 瞄准空间站中心附近
    const tx = STATION_ORIGIN.x + off.x + 220 + (Math.random() - 0.5) * 130;
    const ty = STATION_ORIGIN.y + off.y + 220 + (Math.random() - 0.5) * 130;

    const a = Math.atan2(ty - y, tx - x);
    const speed = 80 + Math.random() * 80;
    const r = 7 + Math.random() * 8;

    const el = document.createElement('div');
    el.className = 'meteor';
    el.style.width  = (r * 2) + 'px';
    el.style.height = (r * 2) + 'px';
    scene.appendChild(el);

    meteors.push({
      el: el,
      x: x, y: y, r: r,
      vx: Math.cos(a) * speed,
      vy: Math.sin(a) * speed,
      rot: Math.random() * 360,
      spin: (Math.random() - 0.5) * 2.5
    });
  }

  function destroyMeteor(index) {
    const mt = meteors[index];
    if (!mt) return;
    spawnParticles(mt.x, mt.y, 7, '#ffb060', 4);
    spawnParticles(mt.x, mt.y, 4, '#ffffff', 3);
    mt.el.remove();
    meteors.splice(index, 1);
  }

  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 updateMeteors(dt) {
    elapsed += dt;

    // 难度随时间提升
    meteorTimer -= dt;
    if (meteorTimer <= 0) {
      const interval = Math.max(0.9, 2.4 - elapsed * 0.018);
      meteorTimer = interval * (0.7 + Math.random() * 0.6);
      if (meteors.length < 24) spawnMeteor();
    }

    for (let i = meteors.length - 1; i >= 0; i--) {
      const mt = meteors[i];
      mt.x += mt.vx * dt;
      mt.y += mt.vy * dt;
      mt.rot += mt.spin * dt * 60;

      mt.el.style.transform =
        `translate(${(mt.x - mt.r).toFixed(1)}px, ${(mt.y - mt.r).toFixed(1)}px) rotate(${mt.rot.toFixed(0)}deg)`;

      // 与模块碰撞
      let hit = false;
      for (let k = 0; k < modules.length; k++) {
        const m = modules[k];
        if (!m.alive) continue;
        const rc = moduleWorldRect(m);
        if (circleRectHit(mt.x, mt.y, mt.r, rc.x, rc.y, rc.w, rc.h)) {
          damageModule(m, 12 + Math.random() * 10);
          hit = true;
          break;
        }
      }
      if (hit) { destroyMeteor(i); continue; }

      // 飞出边界
      if (mt.x < -500 || mt.x > 1700 || mt.y < -500 || mt.y > 1700) {
        mt.el.remove();
        meteors.splice(i, 1);
      }
    }
  }

  /* ============================================================
     武器系统
     ============================================================ */
  const projectiles = [];

  function getStationOffset() {
    const t = getComputedStyle(station).transform;
    if (t && t !== 'none') {
      const m = t.match(/matrix\(([^)]+)\)/);
      if (m) {
        const v = m[1].split(',').map(Number);
        return { x: v[4], y: v[5] };
      }
    }
    return { x: 0, y: 0 };
  }

  function screenToScene(clientX, clientY) {
    const rect = viewport.getBoundingClientRect();
    const cX = rect.width / 2;
    const cY = rect.height / 2;
    const mx = clientX - rect.left;
    const my = clientY - rect.top;
    return {
      x: (mx - cX - tx) / scale,
      y: (my - cY - ty) / scale
    };
  }

  function fireAll(clientX, clientY) {
    const target = screenToScene(clientX, clientY);
    const O = {
      x: STATION_ORIGIN.x + off.x,
      y: STATION_ORIGIN.y + off.y
    };

    // 电磁炮 → 需要上纵臂存活
    if (isAlive('arm-up')) {
      fireRailgun(O.x + 220, O.y + 122, target.x, target.y);
    }
    // 激光 → 需要核心舱存活
    if (isAlive('core')) {
      fireLaser(O.x + 220, O.y + 220, target.x, target.y);
    }
    // 导弹 → 左右横臂各自存活
    if (isAlive('arm-left')) {
      fireMissile(O.x + 90, O.y + 220, target.x, target.y);
    }
    if (isAlive('arm-right')) {
      fireMissile(O.x + 350, O.y + 220, target.x, target.y);
    }
  }

  function fireRailgun(bx, by, tx, ty) {
    const angle = Math.atan2(ty - by, tx - bx);
    railBarrel.style.transform = `rotate(${angle}rad)`;

    const len = 56;
    const mx = bx + Math.cos(angle) * len;
    const my = by + Math.sin(angle) * len;

    const el = document.createElement('div');
    el.className = 'projectile rail-slug';
    scene.appendChild(el);

    projectiles.push({
      el: el, x: mx, y: my, angle: angle,
      vx: Math.cos(angle) * 2200,
      vy: Math.sin(angle) * 2200,
      life: 0, maxLife: 1.4,
      radius: 9
    });

    spawnFlash(mx, my, 'rail');
  }

  function fireLaser(bx, by, tx, ty) {
    const angle = Math.atan2(ty - by, tx - bx);
    laserBarrel.style.transform = `rotate(${angle}rad)`;

    const len0 = 30;
    const mx = bx + Math.cos(angle) * len0;
    const my = by + Math.sin(angle) * len0;

    const dist = Math.hypot(tx - mx, ty - my) + 320;

    const beam = document.createElement('div');
    beam.className = 'laser-beam';
    beam.style.width = dist + 'px';
    beam.style.transform = `translate(${mx}px, ${my}px) rotate(${angle}rad)`;
    scene.appendChild(beam);

    setTimeout(function () { beam.remove(); }, 400);
    spawnFlash(mx, my, 'laser');

    // 激光沿途击毁陨石
    laserRayHit(mx, my, angle, Math.min(dist, 1400));
  }

  function laserRayHit(x0, y0, angle, maxDist) {
    const cosA = Math.cos(angle);
    const sinA = Math.sin(angle);
    const hits = [];

    for (let j = 0; j < meteors.length; j++) {
      const mt = meteors[j];
      const dx = mt.x - x0;
      const dy = mt.y - y0;
      const proj = dx * cosA + dy * sinA;
      if (proj <= 0 || proj > maxDist) continue;
      const perp = Math.abs(-dx * sinA + dy * cosA);
      if (perp < mt.r + 10) hits.push(j);
    }

    hits.sort(function (a, b) { return b - a; });
    hits.forEach(function (j) { destroyMeteor(j); });
  }

  function fireMissile(bx, by, tx, ty) {
    const angle = Math.atan2(ty - by, tx - bx);

    const el = document.createElement('div');
    el.className = 'projectile missile';
    el.style.transform = `translate(${bx}px, ${by}px) rotate(${angle}rad)`;
    scene.appendChild(el);

    projectiles.push({
      el: el, x: bx, y: by, angle: angle,
      vx: Math.cos(angle) * 680,
      vy: Math.sin(angle) * 680,
      life: 0, maxLife: 2.4,
      radius: 8
    });

    spawnFlash(bx, by, 'missile');
  }

  function spawnFlash(x, y, type) {
    const el = document.createElement('div');
    el.className = 'muzzle-flash';
    el.style.transform = `translate(${x}px, ${y}px)`;
    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 === 'rail') {
      el.style.background = 'radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(140,230,255,.7) 35%, rgba(80,190,255,0) 70%)';
    } else if (type === 'boom') {
      el.style.width = '90px';
      el.style.height = '90px';
      el.style.margin = '-45px 0 0 -45px';
      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(); }, 260);
  }

  /* ---------- 弹丸更新 + 碰撞 ---------- */
  function updateProjectiles(dt) {
    for (let i = projectiles.length - 1; i >= 0; i--) {
      const p = projectiles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.life += dt;

      let removed = false;

      // 与陨石碰撞
      for (let j = meteors.length - 1; j >= 0; j--) {
        const mt = meteors[j];
        const dx = p.x - mt.x;
        const dy = p.y - mt.y;
        if (dx * dx + dy * dy < (mt.r + p.radius) * (mt.r + p.radius)) {
          destroyMeteor(j);
          p.el.remove();
          projectiles.splice(i, 1);
          removed = true;
          break;
        }
      }
      if (removed) continue;

      // 生命周期 / 边界
      if (p.life > p.maxLife ||
          p.x < -800 || p.x > 2000 ||
          p.y < -800 || p.y > 2000) {
        p.el.remove();
        projectiles.splice(i, 1);
        continue;
      }

      p.el.style.transform =
        `translate(${p.x.toFixed(1)}px, ${p.y.toFixed(1)}px) rotate(${p.angle}rad)`;
    }
  }

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

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

    off = getStationOffset();

    updateMeteors(dt);
    updateProjectiles(dt);

    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);

  /* ---------- 重置视角 ---------- */
  resetBtn.addEventListener('click', function () {
    const r = viewport.getBoundingClientRect();
    const fit = Math.min(r.width / 560, r.height / 560);
    const targetScale = Math.max(MIN_SCALE, Math.min(1, fit));

    scene.style.transition = 'transform .3s ease-out';
    scale = targetScale;
    tx = 0;
    ty = 0;
    apply();

    setTimeout(function () { scene.style.transition = ''; }, 320);
  });

})();
</script>

</body>
</html>

Game Source: 模块化空间站 · 完整度系统

Creator: ApexFalcon66

Libraries: none

Complexity: complex (1104 lines, 33.8 KB)

The full source code is displayed above on this page.

Remix Instructions

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