🎮ArcadeLab

2036 舰队交战 - 完全体

by ApexFalcon66
697 lines33.5 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>2036 舰队交战 - 完全体</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background: #04060f; font-family: system-ui, 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: 4000px; height: 4000px; margin-left: -2000px; margin-top: -2000px; transform-origin: 50% 50%; will-change: transform; background: radial-gradient(circle at 50% 50%, #0a1225 0%, #03050c 100%); }
  
  /* 星空背景容器 */
  #starLayer { position: absolute; width: 4000px; height: 4000px; top: 0; left: 0; pointer-events: none; }
  .star { position:absolute; background:#fff; border-radius:50%; animation: twinkle 3s infinite ease-in-out; }
  @keyframes twinkle { 0%,100% { opacity:.15; } 50% { opacity:.95; } }

  /* ================= 舰船与模块 ================= */
  .ship { position: absolute; left: 0; top: 0; width: 0; height: 0; will-change: transform; z-index: 10; transform-origin: 0 0; }
  
  /* 舱段基础样式 */
  .mod { position: absolute; border-radius: 2px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); }
  .theme-blue { --c1:#fff; --c2:#dce4ee; --c3:#b8c6d6; --bd:#7a8ea2; --p1:#1d4d86; --p2:#2b6cb5; --accent:#4a90d9; }
  .theme-red { --c1:#fff4ef; --c2:#f0dcd4; --c3:#c9b3a8; --bd:#8a5a4a; --p1:#8a2a1d; --p2:#b84530; --accent:#e05a3a; }

  /* 各类型模块 */
  .mod-body { background: linear-gradient(180deg, var(--c1) 0%, var(--c2) 50%, var(--c3) 100%); border: 1px solid var(--bd); z-index: 10; }
  .mod-panel { background: repeating-linear-gradient(90deg, var(--p1) 0px, var(--p1) 8px, var(--p2) 8px, var(--p2) 10px); border: 1px solid var(--accent); box-shadow: 0 0 10px var(--accent); z-index: 5; }
  .mod-heat { background: repeating-linear-gradient(0deg, #8a2a1d 0px, #8a2a1d 4px, #e05a3a 4px, #e05a3a 6px); border: 1px solid #e05a3a; z-index: 5; }
  .mod-radar { background: linear-gradient(180deg, #3a4b5c, #1c2631); border: 1px solid #6a8aaa; box-shadow: inset 0 0 10px rgba(80,180,255,0.5); z-index: 11; }
  .mod-missile { background: repeating-linear-gradient(90deg, #2d3748 0px, #2d3748 6px, #4a5568 6px, #4a5568 8px); border: 1px solid #718096; z-index: 12; }
  .mod-port { background: #1a2a3a; border: 2px solid #4ade80; box-shadow: 0 0 10px rgba(74,222,128,0.5); z-index: 12; }

  /* 桁架 */
  .truss { position: absolute; background: #475569; border: 1px solid #1e293b; z-index: 2; }

  /* ================= 炮塔与武器 ================= */
  .turret { position: absolute; width: 0; height: 0; z-index: 20; }
  .turret-base { position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fff, #9aaec2 70%, #5f7fa0); border: 1px solid #4a5a6a; box-shadow: 0 2px 4px rgba(0,0,0,0.5); }
  .turret-barrel { position: absolute; left: 0; top: 50%; margin-top: -3px; height: 6px; border-radius: 0 2px 2px 0; transform-origin: 0 50%; }
  
  .barrel-rail { width: 35px; background: linear-gradient(180deg, #fff, #7fdcff, #2b6cb5); box-shadow: 0 0 8px #7fdcff; border: 1px solid #aee; }
  .barrel-ciws { width: 16px; background: linear-gradient(180deg, #fff, #a0aec0, #4a5568); border: 1px solid #ccc; }
  .barrel-laser { width: 45px; background: linear-gradient(180deg, #ffaaaa, #ff2222, #880000); box-shadow: 0 0 12px #ff2222; border: 1px solid #f88; }

  /* ================= 血条与特效 ================= */
  .hp-bar { position:absolute; height:4px; background:rgba(0,0,0,0.7); border:1px solid #555; border-radius:2px; opacity:0; z-index: 30; transition: opacity 0.3s; pointer-events: none; }
  .hp-fill { height:100%; background:#4ade80; width:100%; transition: width 0.1s, background 0.3s; }
  .hp-bar.show { opacity: 1; }

  .projectile { position:absolute; left:0; top:0; pointer-events:none; z-index: 15; will-change: transform; }
  .p-rail { width:24px; height:4px; margin:-2px 0 0 -12px; background:#7fdcff; box-shadow:0 0 10px #7fdcff; border-radius:2px; }
  .p-missile { width:12px; height:6px; margin:-3px 0 0 -6px; background:#e8eef5; border-radius:0 3px 3px 0; box-shadow:0 0 5px #f90; }
  .p-missile::before { content:""; position:absolute; right:100%; width:15px; height:6px; background:linear-gradient(90deg, transparent, #f90); animation: flame 0.1s infinite alternate; }
  .p-ciws { width:4px; height:4px; margin:-2px 0 0 -2px; background:#ff0; border-radius:50%; box-shadow:0 0 6px #ff0; }
  @keyframes flame { from { opacity: 0.6; } to { opacity: 1; } }

  .laser-beam { position:absolute; height:4px; transform-origin:0 50%; background:linear-gradient(90deg, #fff, #f00); box-shadow:0 0 15px #f00; z-index:15; border-radius:2px; }
  .supply-beam { position:absolute; height:4px; transform-origin:0 50%; background:linear-gradient(90deg, #4ade80, transparent); box-shadow:0 0 10px #4ade80; z-index:15; border-radius:2px; animation:pulse 0.3s infinite alternate; }
  @keyframes pulse { from { opacity: 0.5; } to { opacity: 1; } }

  .muzzle { position:absolute; width:30px; height:30px; margin:-15px 0 0 -15px; border-radius:50%; background:radial-gradient(circle, #fff, transparent); animation:pop 0.15s forwards; pointer-events:none; z-index:16; }
  @keyframes pop { from { transform:scale(0.5); opacity:1; } to { transform:scale(1.5); opacity:0; } }

  .frag { position:absolute; border-radius:1px; animation:fly 0.8s ease-out forwards; z-index:18; }
  @keyframes fly { 0% { transform:translate(0,0) rotate(0); opacity:1; } 100% { transform:translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity:0; } }

  /* ================= 推进器喷火 ================= */
  .thruster-flame { position:absolute; width:0; height:0; transform-origin:0 0; pointer-events:none; z-index: 1; opacity:0; will-change: transform, opacity; }
  .tf-glow { position:absolute; left:-20px; top:-25px; width:80px; height:50px; background:radial-gradient(ellipse at 20% 50%, rgba(255,150,50,0.6) 0%, transparent 70%); border-radius:50%; }
  .tf-core { position:absolute; left:0px; top:-6px; width:40px; height:12px; background:radial-gradient(ellipse at 0% 50%, #fff, #ffd166, transparent); clip-path: polygon(0 50%, 100% 10%, 100% 90%); animation: flame 0.1s infinite alternate; }

  /* ================= UI 面板 ================= */
  .overlay { position:fixed; inset:0; z-index:100; display:flex; align-items:center; justify-content:center; background:rgba(4,8,18,0.85); backdrop-filter:blur(5px); transition:opacity 0.4s; }
  .overlay.hide { opacity:0; pointer-events:none; }
  .panel { width:min(95vw, 500px); padding:20px; background:linear-gradient(180deg, #182846, #0c1426); border:1px solid #4af; border-radius:12px; box-shadow:0 10px 40px rgba(0,100,255,0.2); text-align:center; color:#cfe6ff; }
  .panel h1 { font-size:18px; letter-spacing:2px; margin-bottom:20px; color:#fff; text-shadow:0 0 10px #4af; }
  
  .fleet-setup { display:flex; gap:10px; margin-bottom:20px; }
  .fleet-col { flex:1; background:rgba(0,0,0,0.3); padding:10px; border-radius:8px; border:1px solid #444; }
  .fleet-col.blue { border-color:#4af; }
  .fleet-col.red { border-color:#f64; }
  .ship-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; font-size:13px; }
  .btns button { width:24px; height:24px; background:#4af; color:#000; border:none; border-radius:4px; font-weight:bold; cursor:pointer; }
  .btns button:active { transform:scale(0.9); }
  
  .start-btn { width:100%; padding:12px; font-size:16px; font-weight:bold; letter-spacing:2px; background:#4af; color:#000; border:none; border-radius:8px; cursor:pointer; box-shadow:0 4px 15px rgba(0,100,255,0.4); }
  
  .hud { position:fixed; top:10px; left:10px; right:10px; display:flex; justify-content:space-between; pointer-events:none; z-index:50; color:#fff; font-size:12px; text-shadow:1px 1px 2px #000; }
  .hud-bar { width:120px; height:6px; background:#333; border-radius:3px; overflow:hidden; margin-top:4px; border: 1px solid #555; }
  .hud-fill { height:100%; transition:width 0.2s; }
  .hud-fill.blue { background:#4af; }
  .hud-fill.red { background:#f64; }
</style>
</head>
<body>

<div class="viewport" id="viewport">
  <div class="scene" id="scene">
    <div id="starLayer"></div>
  </div>
</div>

<div class="hud">
  <div>
    <div>蓝方编队</div>
    <div class="hud-bar"><div class="hud-fill blue" id="hudA" style="width:100%"></div></div>
  </div>
  <div style="text-align:right">
    <div>红方编队</div>
    <div class="hud-bar" style="margin-left:auto"><div class="hud-fill red" id="hudB" style="width:100%"></div></div>
  </div>
</div>

<div class="overlay" id="overlay">
  <div class="panel">
    <h1>2036 · 舰队部署</h1>
    <div class="fleet-setup">
      <div class="fleet-col blue">
        <div style="color:#4af; font-weight:bold; margin-bottom:10px;">蓝方</div>
        <div class="ship-row"><span>突击舰</span> <div class="btns"><button data-side="A" data-type="assault" data-d="-1">-</button> <span id="cntA_assault">1</span> <button data-side="A" data-type="assault" data-d="1">+</button></div></div>
        <div class="ship-row"><span>激光舰</span> <div class="btns"><button data-side="A" data-type="laser" data-d="-1">-</button> <span id="cntA_laser">1</span> <button data-side="A" data-type="laser" data-d="1">+</button></div></div>
        <div class="ship-row"><span>补给舰</span> <div class="btns"><button data-side="A" data-type="supply" data-d="-1">-</button> <span id="cntA_supply">1</span> <button data-side="A" data-type="supply" data-d="1">+</button></div></div>
      </div>
      <div class="fleet-col red">
        <div style="color:#f64; font-weight:bold; margin-bottom:10px;">红方</div>
        <div class="ship-row"><span>突击舰</span> <div class="btns"><button data-side="B" data-type="assault" data-d="-1">-</button> <span id="cntB_assault">1</span> <button data-side="B" data-type="assault" data-d="1">+</button></div></div>
        <div class="ship-row"><span>激光舰</span> <div class="btns"><button data-side="B" data-type="laser" data-d="-1">-</button> <span id="cntB_laser">1</span> <button data-side="B" data-type="laser" data-d="1">+</button></div></div>
        <div class="ship-row"><span>补给舰</span> <div class="btns"><button data-side="B" data-type="supply" data-d="-1">-</button> <span id="cntB_supply">1</span> <button data-side="B" data-type="supply" data-d="1">+</button></div></div>
      </div>
    </div>
    <button class="start-btn" id="startBtn">开始交战</button>
  </div>
</div>

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

  const state = { ships: [], projectiles: [], started: false, over: false };
  // 初始视角变量
  let scale = 0.15, tx = 0, ty = 0;
  const viewport = document.getElementById('viewport');
  const scene = document.getElementById('scene');

  function getDist(ax, ay, bx, by) { return Math.hypot(bx - ax, by - ay); }
  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  
  function pointInRotatedRect(px, py, cx, cy, w, h, angle) {
    const cos = Math.cos(-angle);
    const sin = Math.sin(-angle);
    const dx = px - cx;
    const dy = py - cy;
    const lx = dx * cos - dy * sin;
    const ly = dx * sin + dy * cos;
    return Math.abs(lx) <= w/2 && Math.abs(ly) <= h/2;
  }

  // 星空
  (function initStars() {
    const layer = document.getElementById('starLayer');
    let html = '';
    for (let i=0; i<500; i++) {
      let s = Math.random()*2 + 1;
      html += `<div class="star" style="width:${s}px;height:${s}px;left:${Math.random()*100}%;top:${Math.random()*100}%;animation-delay:${Math.random()*3}s"></div>`;
    }
    layer.innerHTML = html;
  })();

  /* ================= 战舰数据定义 ================= */
  const SHIP_DEFS = {
    assault: {
      name: '突击舰', maxSpeed: 400, turnSpeed: 3, radar: 4000,
      modules: [
        { id:'core', x:-25, y:-25, w:50, h:50, type:'body', hp:300 },
        { id:'weapon', x:25, y:-20, w:40, h:40, type:'body', hp:150 },
        { id:'life', x:-75, y:-20, w:25, h:40, type:'body', hp:100 },
        { id:'radar', x:-50, y:-15, w:25, h:30, type:'radar', hp:100 },
        { id:'engine', x:-105, y:-12, w:30, h:24, type:'body', hp:120 },
        { id:'missile1', x:30, y:-35, w:30, h:15, type:'missile', hp:80 },
        { id:'missile2', x:30, y:20, w:30, h:15, type:'missile', hp:80 },
        { id:'supply', x:-20, y:25, w:40, h:15, type:'body', hp:80 }
      ],
      trusses: [{x:-40, y:-25, w:6, h:20}, {x:30, y:-25, w:6, h:20}, {x:-40, y:5, w:6, h:20}, {x:30, y:5, w:6, h:20}],
      panels: [{x:-70, y:-45, w:60, h:20}, {x:0, y:-45, w:60, h:20}, {x:-70, y:25, w:60, h:20}, {x:0, y:25, w:60, h:20}],
      heats: [{x:-45, y:-20, w:20, h:40}, {x:25, y:-20, w:20, h:40}],
      turrets: [{ type:'rail', x:45, y:-10, w:35, h:6 }, { type:'rail', x:45, y:10, w:35, h:6 }, { type:'ciws', x:20, y:0, w:16, h:6 }],
      weaponData: { rail: { max: 24, reload: 2.0 }, missile: { max: 2, reload: 5.0 }, ciws: { max: 15000 } }
    },
    laser: {
      name: '激光舰', maxSpeed: 200, turnSpeed: 2, radar: 2000,
      modules: [
        { id:'weapon', x:15, y:-25, w:45, h:50, type:'body', hp:150 },
        { id:'core', x:-25, y:-30, w:40, h:60, type:'body', hp:300 },
        { id:'life', x:-55, y:-20, w:30, h:40, type:'body', hp:100 },
        { id:'engine', x:-85, y:-15, w:30, h:30, type:'body', hp:120 },
        { id:'supply', x:-20, y:30, w:30, h:15, type:'body', hp:80 }
      ],
      trusses: [{x:-20, y:-25, w:6, h:20}, {x:10, y:-25, w:6, h:20}, {x:-20, y:5, w:6, h:20}, {x:10, y:5, w:6, h:20}],
      panels: [{x:-50, y:-45, w:60, h:20}, {x:-20, y:25, w:60, h:20}],
      heats: [{x:-30, y:-20, w:20, h:15}, {x:0, y:-20, w:20, h:15}],
      turrets: [{ type:'laser', x:60, y:0, w:50, h:10 }, { type:'ciws', x:30, y:-25, w:16, h:6 }, { type:'ciws', x:30, y:25, w:16, h:6 }, { type:'ciws', x:50, y:-20, w:16, h:6 }, { type:'ciws', x:50, y:20, w:16, h:6 }],
      weaponData: { laser: { maxHeat: 15, coolTime: 8 }, ciws: { max: 15000 } }
    },
    supply: {
      name: '补给舰', maxSpeed: 400, turnSpeed: 4, radar: 2000,
      modules: [
        { id:'core', x:-30, y:-30, w:70, h:60, type:'body', hp:500 },
        { id:'engine', x:-60, y:-15, w:30, h:30, type:'body', hp:150 },
        { id:'life', x:-75, y:-10, w:15, h:20, type:'body', hp:200 },
        { id:'port1', x:40, y:-15, w:5, h:30, type:'port', hp:50 },
        { id:'port2', x:-5, y:-35, w:30, h:5, type:'port', hp:50 },
        { id:'port3', x:-5, y:30, w:30, h:5, type:'port', hp:50 }
      ],
      trusses: [{x:-30, y:-35, w:6, h:20}, {x:20, y:-35, w:6, h:20}, {x:-30, y:15, w:6, h:20}, {x:20, y:15, w:6, h:20}],
      panels: [{x:-60, y:-55, w:60, h:20}, {x:-10, y:-55, w:60, h:20}, {x:-60, y:35, w:60, h:20}, {x:-10, y:35, w:60, h:20}],
      heats: [],
      turrets: [ { type:'ciws', x:0, y:0, w:16, h:6 } ],
      weaponData: { ciws: { max: 15000 } }
    }
  };

  /* ================= 舰船类 ================= */
  class Ship {
    constructor(team, type, x, y) {
      this.team = team; this.type = type; this.def = SHIP_DEFS[type];
      this.x = x; this.y = y; this.angle = team === 'A' ? 0 : Math.PI;
      this.vx = 0; this.vy = 0; this.alive = true;
      this.radius = 60; 
      this.radarRange = this.def.radar;
      
      this.ammo = { rail: 0, missile: 0, ciws: 0, laserHeat: 0, laserCool: 0 };
      if (type === 'assault') { this.ammo.rail = 24; this.ammo.missile = 2; this.ammo.ciws = 15000; }
      if (type === 'laser') { this.ammo.ciws = 15000; }
      if (type === 'supply') { this.ammo.ciws = 15000; }

      this.cooldowns = { rail: 0, missile: 0, ciws: 0 };
      this.enemy = null;
      this.supplyTarget = null;
      
      this.prefDist = 300 + Math.random() * 200;
      this.orbitDir = Math.random() < 0.5 ? -1 : 1;
      this.strafeFreq = 0.5 + Math.random() * 1.0;
      this.phase = Math.random() * Math.PI * 2;
      this.wanderTimer = 1 + Math.random() * 2;

      this.el = document.createElement('div');
      this.el.className = `ship theme-${team === 'A' ? 'blue' : 'red'}`;
      this.buildDOM();
      scene.appendChild(this.el);
      this.updateDOM();
    }

    buildDOM() {
      this.modules = [];
      this.def.modules.forEach(m => {
        const div = document.createElement('div');
        div.className = `mod mod-${m.type}`;
        div.style.left = m.x + 'px'; div.style.top = m.y + 'px';
        div.style.width = m.w + 'px'; div.style.height = m.h + 'px';
        
        const bar = document.createElement('div');
        bar.className = 'hp-bar';
        bar.style.width = m.w + 'px'; bar.style.left = '0px'; bar.style.top = '-6px';
        bar.innerHTML = '<div class="hp-fill"></div>';
        div.appendChild(bar);
        
        this.el.appendChild(div);
        this.modules.push({ def: m, el: div, bar: bar, fill: bar.firstChild, hp: m.hp, maxHp: m.hp, alive: true, hideTimer: null });
      });

      if (this.def.trusses) this.def.trusses.forEach(t => {
        const div = document.createElement('div');
        div.className = 'truss';
        div.style.left = t.x + 'px'; div.style.top = t.y + 'px';
        div.style.width = t.w + 'px'; div.style.height = t.h + 'px';
        this.el.appendChild(div);
      });

      if (this.def.panels) this.def.panels.forEach(p => {
        const div = document.createElement('div');
        div.className = 'mod mod-panel';
        div.style.left = p.x + 'px'; div.style.top = p.y + 'px';
        div.style.width = p.w + 'px'; div.style.height = p.h + 'px';
        this.el.appendChild(div);
      });

      if (this.def.heats) this.def.heats.forEach(h => {
        const div = document.createElement('div');
        div.className = 'mod mod-heat';
        div.style.left = h.x + 'px'; div.style.top = h.y + 'px';
        div.style.width = h.w + 'px'; div.style.height = h.h + 'px';
        this.el.appendChild(div);
      });

      const engineMod = this.def.modules.find(m => m.id === 'engine');
      if (engineMod) {
        this.flameEl = document.createElement('div');
        this.flameEl.className = 'thruster-flame';
        this.flameEl.innerHTML = '<div class="tf-glow"></div><div class="tf-core"></div>';
        this.flameEl.style.left = engineMod.x + 'px';
        this.flameEl.style.top = (engineMod.y + engineMod.h/2) + 'px';
        this.el.appendChild(this.flameEl);
      }

      this.turrets = [];
      this.def.turrets.forEach(t => {
        const wrap = document.createElement('div');
        wrap.className = 'turret';
        wrap.style.left = t.x + 'px'; wrap.style.top = t.y + 'px';
        
        const base = document.createElement('div');
        base.className = 'turret-base';
        if (t.type === 'laser') { base.style.width = '24px'; base.style.height = '24px'; base.style.margin = '-12px 0 0 -12px'; }
        if (t.type === 'ciws' && this.type === 'laser') { base.style.background = 'radial-gradient(circle, #f88, #a22)'; }
        wrap.appendChild(base);

        const barrel = document.createElement('div');
        barrel.className = `turret-barrel barrel-${t.type === 'laser' ? 'laser' : (t.type === 'rail' ? 'rail' : 'ciws')}`;
        barrel.style.width = t.w + 'px'; barrel.style.height = t.h + 'px';
        barrel.style.marginTop = (-t.h/2) + 'px';
        wrap.appendChild(barrel);

        this.el.appendChild(wrap);
        this.turrets.push({ type: t.type, el: wrap, barrel: barrel });
      });
    }

    updateDOM() {
      this.el.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.angle}rad)`;
    }

    takeDamage(mod, dmg, px, py) {
      if (!mod.alive || !this.alive) return;
      mod.hp -= dmg;
      mod.bar.classList.add('show');
      mod.fill.style.width = Math.max(0, (mod.hp / mod.maxHp) * 100) + '%';
      mod.fill.style.background = mod.hp > mod.maxHp*0.5 ? '#4ade80' : (mod.hp > mod.maxHp*0.2 ? '#facc15' : '#f87171');
      
      clearTimeout(mod.hideTimer);
      mod.hideTimer = setTimeout(() => { if (mod.alive) mod.bar.classList.remove('show'); }, 2000);

      if (mod.hp <= 0) {
        mod.alive = false;
        mod.el.style.display = 'none';
        for(let i=0; i<10; i++) spawnFrag(px||this.x, py||this.y, this.team);
        if (mod.def.id === 'core') this.die();
      }
    }

    die() {
      this.alive = false;
      this.el.style.display = 'none';
      for(let i=0; i<30; i++) spawnFrag(this.x, this.y, this.team);
      checkGameOver();
    }

    updateRadar() {
      if (this.type === 'supply') {
        let bestF = null, minD = Infinity;
        state.ships.forEach(s => {
          if (s !== this && s.team === this.team && s.alive && s.type === 'assault') {
            if (s.ammo.rail < 5 || s.ammo.missile < 1) {
              let d = getDist(this.x, this.y, s.x, s.y);
              if (d < this.radarRange && d < minD) { minD = d; bestF = s; }
            }
          }
        });
        if (bestF) { this.supplyTarget = bestF; return; } else this.supplyTarget = null;
      }

      if (!this.enemy || !this.enemy.alive) {
        let bestE = null, minD = Infinity;
        state.ships.forEach(s => {
          if (s.team !== this.team && s.alive) {
            let d = getDist(this.x, this.y, s.x, s.y);
            if (d < this.radarRange && d < minD) { minD = d; bestE = s; }
          }
        });
        this.enemy = bestE;
      }
    }

    update(dt) {
      if (!this.alive) return;

      this.cooldowns.rail = Math.max(0, this.cooldowns.rail - dt);
      this.cooldowns.missile = Math.max(0, this.cooldowns.missile - dt);
      this.cooldowns.ciws = Math.max(0, this.cooldowns.ciws - dt);
      if (this.type === 'laser' && this.ammo.laserCool > 0) {
        this.ammo.laserCool -= dt; if (this.ammo.laserCool <= 0) this.ammo.laserHeat = 0;
      }

      this.updateRadar();

      let moveX = this.x, moveY = this.y;
      let thrustMag = 0;

      if (this.type === 'supply' && this.supplyTarget) {
        const target = this.supplyTarget;
        const dist = getDist(this.x, this.y, target.x, target.y);
        if (dist > 60) { moveX = target.x; moveY = target.y; thrustMag = 0.8; } 
        else {
          this.vx *= 0.8; this.vy *= 0.8;
          this.angle = Math.atan2(target.y - this.y, target.x - this.x);
          if (target.ammo.rail < 24) target.ammo.rail = Math.min(24, target.ammo.rail + 2*dt);
          if (target.ammo.missile < 2) target.ammo.missile = Math.min(2, target.ammo.missile + 0.5*dt);
          if (Math.random() < 0.3) spawnSupplyBeam(this.x, this.y, target.x, target.y);
          thrustMag = 0;
        }
      } else if (this.enemy && this.enemy.alive) {
        const dist = getDist(this.x, this.y, this.enemy.x, this.enemy.y);
        const angToE = Math.atan2(this.enemy.y - this.y, this.enemy.x - this.x);
        
        this.wanderTimer -= dt;
        if (this.wanderTimer <= 0) {
          this.wanderTimer = 1.5 + Math.random() * 2;
          this.prefDist = 300 + Math.random() * 200;
          this.orbitDir = Math.random() < 0.5 ? -1 : 1;
        }

        let wantD = this.type === 'laser' ? 600 : this.prefDist;
        let targetAngle = angToE;

        if (dist > wantD + 50) { targetAngle = angToE; thrustMag = 0.8; }
        else if (dist < wantD - 50) { targetAngle = angToE + Math.PI; thrustMag = 0.8; }
        else { targetAngle = angToE + Math.PI/2 * this.orbitDir; thrustMag = 0.5; }

        let diff = targetAngle - this.angle;
        while(diff > Math.PI) diff -= Math.PI*2;
        while(diff < -Math.PI) diff += Math.PI*2;
        this.angle += clamp(diff, -this.def.turnSpeed * dt, this.def.turnSpeed * dt);

        this.vx = Math.cos(this.angle) * this.def.maxSpeed * thrustMag;
        this.vy = Math.sin(this.angle) * this.def.maxSpeed * thrustMag;
        this.x += this.vx * dt; this.y += this.vy * dt;
      } else {
        this.vx *= 0.9; this.vy *= 0.9;
        this.x += this.vx * dt; this.y += this.vy * dt;
      }

      this.x = clamp(this.x, 500, 3500);
      this.y = clamp(this.y, 500, 3500);
      this.updateDOM();

      if (this.flameEl) {
        if (thrustMag > 0.1) {
          this.flameEl.style.opacity = '1';
          this.flameEl.style.transform = `rotate(${Math.PI}rad) scaleX(${0.6 + thrustMag})`;
        } else {
          this.flameEl.style.opacity = '0';
        }
      }

      this.handleWeapons(dt);
    }

    handleWeapons(dt) {
      const enemy = this.enemy;
      if (!enemy || !enemy.alive) return;
      
      const targetAng = Math.atan2(enemy.y - this.y, enemy.x - this.x);
      const localAng = targetAng - this.angle;
      const dist = getDist(this.x, this.y, enemy.x, enemy.y);

      this.turrets.forEach(t => {
        if (t.type === 'laser' || t.type === 'rail' || t.type === 'ciws') t.barrel.style.transform = `rotate(${localAng}rad)`;
      });

      if (this.ammo.ciws > 0 && this.cooldowns.ciws <= 0) {
        for (let p of state.projectiles) {
          if (p.team !== this.team && p.type !== 'ciws' && getDist(this.x, this.y, p.x, p.y) < 500) {
            this.ammo.ciws--; this.cooldowns.ciws = 0.05;
            const a = Math.atan2(p.y - this.y, p.x - this.x) + (Math.random()-0.5)*0.2;
            spawnProjectile('ciws', this.x, this.y, a, 2500, 20, this.team);
            break;
          }
        }
      }

      if (this.type === 'assault') {
        if (dist < 1200 && Math.abs(localAng) < 0.4) {
          if (this.ammo.rail > 0 && this.cooldowns.rail <= 0) {
            this.ammo.rail--; this.cooldowns.rail = this.def.weaponData.rail.reload;
            spawnProjectile('rail', this.x, this.y, targetAng, 2200, 40, this.team);
            spawnMuzzle(this.x, this.y);
          }
          if (this.ammo.missile > 0 && this.cooldowns.missile <= 0) {
            this.ammo.missile--; this.cooldowns.missile = this.def.weaponData.missile.reload;
            spawnProjectile('missile', this.x, this.y, targetAng, 600, 80, this.team);
          }
        }
      } else if (this.type === 'laser') {
        if (dist < 900 && Math.abs(localAng) < 0.1 && this.ammo.laserHeat < this.def.weaponData.laser.maxHeat && this.ammo.laserCool <= 0) {
          this.ammo.laserHeat += dt;
          enemy.modules.forEach(m => { if(m.alive) enemy.takeDamage(m, 20*dt, m.x+enemy.x, m.y+enemy.y); });
          if (Math.random() < 0.4) spawnLaser(this.x, this.y, targetAng, dist);
          if (this.ammo.laserHeat >= this.def.weaponData.laser.maxHeat) this.ammo.laserCool = this.def.weaponData.laser.coolTime;
        }
      }
    }
  }

  /* ================= 投影物与特效 ================= */
  function spawnProjectile(type, x, y, angle, speed, dmg, team) {
    const el = document.createElement('div');
    el.className = `projectile p-${type}`;
    scene.appendChild(el);
    state.projectiles.push({ el, type, team, x, y, dmg, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 0, maxLife: 2.0, angle });
  }

  function spawnLaser(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(() => el.remove(), 60);
  }

  function spawnSupplyBeam(x1, y1, x2, y2) {
    const ang = Math.atan2(y2-y1, x2-x1);
    const len = getDist(x1,y1,x2,y2);
    const el = document.createElement('div');
    el.className = 'supply-beam';
    el.style.width = len + 'px';
    el.style.transform = `translate3d(${x1}px, ${y1}px, 0) rotate(${ang}rad)`;
    scene.appendChild(el);
    setTimeout(() => el.remove(), 300);
  }

  function spawnMuzzle(x, y) {
    const el = document.createElement('div');
    el.className = 'muzzle';
    el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
    scene.appendChild(el);
    setTimeout(() => el.remove(), 200);
  }

  function spawnFrag(x, y, team) {
    const el = document.createElement('div');
    el.className = 'frag';
    const s = 3 + Math.random()*5;
    el.style.width = s+'px'; el.style.height = s+'px';
    el.style.background = team === 'A' ? '#7fdcff' : '#ffa060';
    el.style.left = x+'px'; el.style.top = y+'px';
    const a = Math.random()*Math.PI*2;
    const d = 20 + Math.random()*60;
    el.style.setProperty('--dx', Math.cos(a)*d+'px');
    el.style.setProperty('--dy', Math.sin(a)*d+'px');
    el.style.setProperty('--rot', (Math.random()*360)+'deg');
    scene.appendChild(el);
    setTimeout(()=>el.remove(), 800);
  }

  /* ================= 主循环 ================= */
  let lastT = performance.now();
  function loop(now) {
    const dt = Math.min((now - lastT) / 1000, 0.05);
    lastT = now;

    if (state.started && !state.over) {
      state.ships.forEach(s => s.update(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;
        p.el.style.transform = `translate3d(${p.x}px, ${p.y}px, 0) rotate(${p.angle}rad)`;

        let hit = false;
        if (p.life < p.maxLife) {
          for (let s of state.ships) {
            if (!s.alive || s.team === p.team) continue;
            for (let m of s.modules) {
              if (!m.alive) continue;
              const cos = Math.cos(s.angle);
              const sin = Math.sin(s.angle);
              const mx = s.x + (m.def.x + m.def.w/2)*cos - (m.def.y + m.def.h/2)*sin;
              const my = s.y + (m.def.x + m.def.w/2)*sin + (m.def.y + m.def.h/2)*cos;
              
              if (pointInRotatedRect(p.x, p.y, mx, my, m.def.w, m.def.h, s.angle)) {
                s.takeDamage(m, p.dmg, p.x, p.y);
                hit = true; break;
              }
            }
            if (hit) break;

            if (p.type !== 'ciws') {
              for (let q of state.projectiles) {
                if (q.type === 'ciws' && q.team !== p.team) {
                  if (getDist(p.x, p.y, q.x, q.y) < 15) { hit = true; q.life = 99; break; }
                }
              }
            }
          }
        } else hit = true;

        if (hit) { p.el.remove(); state.projectiles.splice(i, 1); }
      }
      updateHUD();
    }
    requestAnimationFrame(loop);
  }

  function updateHUD() {
    let hpA = 0, hpB = 0;
    state.ships.forEach(s => {
      s.modules.forEach(m => {
        if (m.alive) { if(s.team==='A') hpA += m.hp; else hpB += m.hp; }
      });
    });
    document.getElementById('hudA').style.width = Math.min(100, (hpA / 4000 * 100)) + '%';
    document.getElementById('hudB').style.width = Math.min(100, (hpB / 4000 * 100)) + '%';
  }

  function checkGameOver() {
    let aAlive = false, bAlive = false;
    state.ships.forEach(s => { if (s.alive) { if (s.team === 'A') aAlive = true; else bAlive = true; } });
    if (!aAlive || !bAlive) {
      state.over = true;
      setTimeout(() => { alert(aAlive ? "蓝方胜利" : (bAlive ? "红方胜利" : "同归于尽")); location.reload(); }, 800);
    }
  }

  // 缩放拖拽
  let pointers = new Map(), pinchD = 0;
  function applyT() { scene.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; }
  
  viewport.addEventListener('pointerdown', e => {
    viewport.setPointerCapture(e.pointerId);
    pointers.set(e.pointerId, {x: e.clientX, y: e.clientY});
    if (pointers.size === 2) {
      let pts = [...pointers.values()];
      pinchD = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
    }
  });
  
  viewport.addEventListener('pointermove', e => {
    if (!pointers.has(e.pointerId)) return;
    let p = pointers.get(e.pointerId);
    let dx = e.clientX - p.x, dy = e.clientY - p.y;
    p.x = e.clientX; p.y = e.clientY;

    if (pointers.size === 1) {
      tx += dx; ty += dy; applyT();
    } else if (pointers.size === 2) {
      let pts = [...pointers.values()];
      let d = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
      if (pinchD > 0) { scale *= (d / pinchD); scale = clamp(scale, 0.08, 1.5); applyT(); }
      pinchD = d;
    }
  });
  
  viewport.addEventListener('pointerup', e => { pointers.delete(e.pointerId); pinchD = 0; });
  viewport.addEventListener('pointercancel', e => { pointers.delete(e.pointerId); pinchD = 0; });

  // 开局配置
  const counts = { A:{assault:1,laser:1,supply:1}, B:{assault:1,laser:1,supply:1} };
  document.querySelectorAll('.btns button').forEach(btn => {
    btn.onclick = () => {
      let side = btn.dataset.side, type = btn.dataset.type, d = parseInt(btn.dataset.d);
      counts[side][type] = clamp(counts[side][type] + d, 0, 4);
      document.getElementById(`cnt${side}_${type}`).innerText = counts[side][type];
    };
  });

  document.getElementById('startBtn').onclick = () => {
    document.getElementById('overlay').classList.add('hide');
    
    // 生成舰队 (X轴 1000 到 3000,Y轴居中在 2000)
    let curY = 2000 - (counts.A.assault + counts.A.laser + counts.A.supply)*150 / 2;
    for (let i=0; i<counts.A.assault; i++) { state.ships.push(new Ship('A','assault', 1000, curY)); curY+=150; }
    for (let i=0; i<counts.A.laser; i++) { state.ships.push(new Ship('A','laser', 1000, curY)); curY+=150; }
    for (let i=0; i<counts.A.supply; i++) { state.ships.push(new Ship('A','supply', 1000, curY)); curY+=150; }

    curY = 2000 - (counts.B.assault + counts.B.laser + counts.B.supply)*150 / 2;
    for (let i=0; i<counts.B.assault; i++) { state.ships.push(new Ship('B','assault', 3000, curY)); curY+=150; }
    for (let i=0; i<counts.B.laser; i++) { state.ships.push(new Ship('B','laser', 3000, curY)); curY+=150; }
    for (let i=0; i<counts.B.supply; i++) { state.ships.push(new Ship('B','supply', 3000, curY)); curY+=150; }

    state.started = true;
    
    // === 修复核心:视角完美居中在 2000, 2000 并缩放到刚好装下全场 ===
    tx = 0; 
    ty = 0;
    // 因为整个场景宽度是4000,我们要根据屏幕宽度算出合适的缩放
    scale = Math.min(window.innerWidth / 2400, window.innerHeight / 2400); 
    applyT();
  };

  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: 2036 舰队交战 - 完全体

Creator: ApexFalcon66

Libraries: none

Complexity: complex (697 lines, 33.5 KB)

The full source code is displayed above on this page.

Remix Instructions

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