🎮ArcadeLab

深空舰队 - 极简战术版

by CosmicRider48
676 lines34.4 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>深空舰队 - 极简战术版</title>
<style>
  body { margin: 0; overflow: hidden; background: #03050a; font-family: monospace; touch-action: none; }
  canvas { display: block; width: 100vw; height: 100vh; }
  
  /* 配置界面 */
  #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(3, 5, 10, 0.9); z-index: 10; }
  .panel { background: rgba(15, 25, 45, 0.95); border: 1px solid #3a5a8a; padding: 15px; border-radius: 8px; color: #fff; width: 90%; max-width: 600px; }
  h1 { text-align: center; font-size: 18px; color: #66ccff; margin: 0 0 15px 0; }
  .fleet-config { display: flex; justify-content: space-between; margin-bottom: 15px; }
  .side { width: 48%; }
  .side h3 { font-size: 13px; margin: 0 0 8px 0; color: #a0b0d0; border-bottom: 1px solid #3a5a8a; padding-bottom: 4px; text-align: center;}
  .row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 11px; }
  .btn-group button { width: 20px; height: 20px; background: #2a4a7a; border: none; color: #fff; border-radius: 3px; cursor: pointer; }
  .val { width: 16px; text-align: center; font-weight: bold; color: #66ccff;}
  #start-btn { width: 100%; padding: 10px; background: #0055ff; color: #fff; border: none; border-radius: 4px; font-size: 15px; font-weight: bold; cursor: pointer; }
  
  /* 极简战术信息面板 */
  #tactical-ui { 
    position: absolute; top: 10px; left: 10px; 
    color: #fff; display: none; 
    background: rgba(5, 10, 20, 0.65); /* 更透明 */
    border-left: 3px solid #66ccff; 
    padding: 8px 12px; border-radius: 2px; 
    font-size: 11px; pointer-events: none; 
    line-height: 1.4; box-shadow: 0 0 10px rgba(0,0,0,0.3);
    letter-spacing: 0.5px;
  }
  .tac-title { font-size: 13px; font-weight: bold; color: #66ccff; margin-bottom: 4px; }
  .tac-captain { color: #ffaa00; }
  .tac-data { color: #a0b0d0; }
  .tac-modules { margin-top: 5px; color: #00ff88; display: flex; flex-wrap: wrap; gap: 6px; max-width: 250px;}
  .mod-danger { color: #ff0044; }

  .log { position: absolute; bottom: 10px; left: 10px; font-size: 11px; color: #aaa; max-width: 300px; text-shadow: 1px 1px 2px #000; pointer-events: none;}
</style>
</head>
<body>

<div id="ui-layer">
  <div class="panel">
    <h1>深空舰队配置</h1>
    <div class="fleet-config">
      <div class="side">
        <h3 style="color:#66ccff">蓝方</h3>
        <div class="row"><span>激光舰</span> <div class="btn-group"><button onclick="updateCount('bLaser',-1)">-</button><span class="val" id="val-bLaser">2</span><button onclick="updateCount('bLaser',1)">+</button></div></div>
        <div class="row"><span>突击舰</span> <div class="btn-group"><button onclick="updateCount('bAssault',-1)">-</button><span class="val" id="val-bAssault">1</span><button onclick="updateCount('bAssault',1)">+</button></div></div>
        <div class="row"><span>导弹舰</span> <div class="btn-group"><button onclick="updateCount('bMissile',-1)">-</button><span class="val" id="val-bMissile">0</span><button onclick="updateCount('bMissile',1)">+</button></div></div>
        <div class="row"><span>补给舰</span> <div class="btn-group"><button onclick="updateCount('bSupply',-1)">-</button><span class="val" id="val-bSupply">0</span><button onclick="updateCount('bSupply',1)">+</button></div></div>
      </div>
      <div class="side">
        <h3 style="color:#ff3366">红方</h3>
        <div class="row"><span>激光舰</span> <div class="btn-group"><button onclick="updateCount('rLaser',-1)">-</button><span class="val" id="val-rLaser">0</span><button onclick="updateCount('rLaser',1)">+</button></div></div>
        <div class="row"><span>突击舰</span> <div class="btn-group"><button onclick="updateCount('rAssault',-1)">-</button><span class="val" id="val-rAssault">0</span><button onclick="updateCount('rAssault',1)">+</button></div></div>
        <div class="row"><span>导弹舰</span> <div class="btn-group"><button onclick="updateCount('rMissile',-1)">-</button><span class="val" id="val-rMissile">2</span><button onclick="updateCount('rMissile',1)">+</button></div></div>
        <div class="row"><span>补给舰</span> <div class="btn-group"><button onclick="updateCount('rSupply',-1)">-</button><span class="val" id="val-rSupply">1</span><button onclick="updateCount('rSupply',1)">+</button></div></div>
      </div>
    </div>
    <button id="start-btn" onclick="startBattle()">开 战</button>
  </div>
</div>

<div id="tactical-ui"></div>
<div class="log" id="log"></div>
<canvas id="gameCanvas"></canvas>

<script>
// ================= 基础设定 =================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W = window.innerWidth, H = window.innerHeight;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
window.addEventListener('resize', resize); resize();

const WORLD_W = 8000; const WORLD_H = 6000;
let cam = { x: WORLD_W/2, y: WORLD_H/2, zoom: 0.15 };
let selectedShip = null;

function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
function angleTo(a, b) { return Math.atan2(b.y - a.y, b.x - a.x); }
function normalizeAngle(a) { while (a > Math.PI) a -= Math.PI*2; while (a < -Math.PI) a += Math.PI*2; return a; }
function logMsg(msg) { document.getElementById('log').innerHTML = `> ${msg}<br>` + document.getElementById('log').innerHTML.slice(0, 100); }

const config = { bLaser: 2, bAssault: 1, bMissile: 0, bSupply: 0, rLaser: 0, rAssault: 0, rMissile: 2, rSupply: 1 };
function updateCount(type, delta) { config[type] = Math.max(0, config[type] + delta); document.getElementById('val-' + type).innerText = config[type]; }

let entities = [], bullets = [], particles = [];
let gameState = 'config';

// 随机名字库
const namePrefixes = ['星辰', '烈焰', '风暴', '虚空', '苍穹', '极光', '深渊', '破晓', '神盾', '利刃'];
const nameSuffixes = ['号', '之翼', '守望者', '先锋', '之盾', '幽影', '脉冲', '远征'];
const captainNames = ['张伟', '李强', '王磊', '刘洋', '陈杰', '赵明', '林奇', '周鑫', '孙宇', '高飞'];
function getRandomName() { return namePrefixes[Math.floor(Math.random()*namePrefixes.length)] + nameSuffixes[Math.floor(Math.random()*nameSuffixes.length)]; }
function getRandomCaptain() { return captainNames[Math.floor(Math.random()*captainNames.length)] + ' 舰长'; }

// ================= 触屏交互 =================
let isDragging = false, lastTouchDist = 0;
let touchStart = { x: 0, y: 0 }, camStart = { x: 0, y: 0 };
let pointerDownTime = 0, movedDist = 0;

canvas.addEventListener('pointerdown', (e) => {
  if (gameState !== 'playing') return;
  isDragging = true; pointerDownTime = Date.now(); movedDist = 0;
  touchStart = { x: e.clientX, y: e.clientY };
  camStart = { x: cam.x, y: cam.y };
});

canvas.addEventListener('pointermove', (e) => {
  if (!isDragging || gameState !== 'playing') return;
  const dx = e.clientX - touchStart.x, dy = e.clientY - touchStart.y;
  movedDist = Math.hypot(dx, dy);
  if (!selectedShip) { // 如果锁定了目标,就不能拖拽视角
    cam.x = camStart.x - dx / cam.zoom;
    cam.y = camStart.y - dy / cam.zoom;
  }
});

canvas.addEventListener('pointerup', (e) => {
  if (gameState !== 'playing') return;
  isDragging = false;
  if (movedDist < 10 && Date.now() - pointerDownTime < 300) {
    handleClick(e.clientX, e.clientY);
  }
});

canvas.addEventListener('touchmove', (e) => {
  if (gameState !== 'playing') return;
  if (e.touches.length === 2) {
    e.preventDefault();
    const currentDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
    if (lastTouchDist === 0) lastTouchDist = currentDist;
    cam.zoom = Math.max(0.05, Math.min(cam.zoom * (currentDist / lastTouchDist), 1.5));
    lastTouchDist = currentDist;
  }
}, { passive: false });
canvas.addEventListener('touchend', (e) => { if (e.touches.length < 2) lastTouchDist = 0; });

function handleClick(screenX, screenY) {
  const worldX = (screenX - W / 2) / cam.zoom + cam.x;
  const worldY = (screenY - H / 2) / cam.zoom + cam.y;
  let clickedShip = null;
  for (let i = entities.length - 1; i >= 0; i--) {
    const e = entities[i];
    if (!e.dead && Math.hypot(worldX - e.x, worldY - e.y) < e.radius + 50) {
      clickedShip = e; break;
    }
  }
  // 如果点击了空白区域,取消选中
  selectedShip = clickedShip;
  updateTacticalUI();
}

// ================= 极简战术 UI 更新 =================
const uiEl = document.getElementById('tactical-ui');

function updateTacticalUI() {
  if (!selectedShip || selectedShip.dead) {
    uiEl.style.display = 'none';
    if (selectedShip && selectedShip.dead) selectedShip = null; 
    return;
  }

  uiEl.style.display = 'block';
  const s = selectedShip;
  
  // 舰长状态计算
  const coreMod = s.modules.find(m => m.type === 'core');
  let captainStatus = '<span style="color:#00ff88">正常</span>';
  if (coreMod) {
    if (coreMod.hp <= 0) captainStatus = '<span style="color:#ff0044">阵亡</span>';
    else if (coreMod.hp <= 20) captainStatus = '<span style="color:#ff0044">重伤</span>';
    else if (coreMod.hp <= 50) captainStatus = '<span style="color:#ffaa00">轻伤</span>';
  }

  // 基础信息
  let html = `<div class="tac-title">${s.name} <span style="float:right; color:#aaa; font-size:10px;">${s.isEnemy ? '红方' : '蓝方'}</span></div>`;
  html += `<div class="tac-captain">${s.captain} [${captainStatus}]</div>`;
  
  // 弹药/乘员 (紧凑排布)
  html += `<div class="tac-data" style="margin-top:4px;">`;
  if (s.ammo !== undefined) html += `导弹:${Math.floor(s.ammo)} `;
  if (s.railgunAmmo !== undefined) html += `电磁:${s.railgunAmmo} `;
  html += `| 乘员:${s.crew.filter(c => c.pos !== -1).length}/${s.crew.length}</div>`;

  // 模块状态 (只显示活着的,用纯数字百分比)
  html += `<div class="tac-modules">`;
  s.modules.forEach((m, i) => {
    if (m.hp > 0) {
      const pct = Math.floor(m.hp);
      // 清理名字,只留前两个字,方便排版
      let typeName = m.type.replace('weapon_', '').substring(0, 2); 
      let colorClass = pct <= 20 ? 'mod-danger' : '';
      html += `<span class="${colorClass}">${typeName}[${pct}%]</span>`;
    }
  });
  html += `</div>`;

  uiEl.innerHTML = html;
}

// ================= 渲染模块 =================
const MOD_W = 45, MOD_H = 26;

function drawTurret(ctx, x, y, angle, type) {
  ctx.save(); ctx.translate(x, y); ctx.rotate(angle);
  if (type === 'railgun') {
    ctx.fillStyle = '#2a3b4a'; ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI*2); ctx.fill();
    ctx.strokeStyle = '#5c738a'; ctx.lineWidth = 1.5; ctx.stroke();
    ctx.fillStyle = '#111'; ctx.fillRect(4, -5, 22, 4); ctx.fillRect(4, 1, 22, 4);
    ctx.fillStyle = '#00ffcc'; ctx.fillRect(20, -5, 6, 4); ctx.fillRect(20, 1, 6, 4);
  } else if (type === 'pd') {
    ctx.fillStyle = '#3b3b3b'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#888'; ctx.fillRect(3, -3, 10, 6);
  } else if (type === 'laser_intercept') {
    ctx.fillStyle = '#1e2b38'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill();
    ctx.strokeStyle = '#66ccff'; ctx.lineWidth = 1; ctx.stroke();
    ctx.fillStyle = '#111'; ctx.fillRect(3, -1.5, 10, 3);
    ctx.fillStyle = '#66ccff'; ctx.beginPath(); ctx.arc(13, 0, 2, 0, Math.PI*2); ctx.fill();
  }
  ctx.restore();
}

function drawFixedMainCannon(ctx, x, y, isOverheated, broken) {
  ctx.save(); ctx.translate(x, y);
  ctx.fillStyle = broken ? '#111' : (isOverheated ? '#552222' : '#1e2b38');
  ctx.beginPath(); ctx.moveTo(-10, -12); ctx.lineTo(15, -8); ctx.lineTo(15, 8); ctx.lineTo(-10, 12); ctx.closePath(); ctx.fill();
  if(!broken) {
    ctx.strokeStyle = '#4a6b8c'; ctx.lineWidth = 1.5; ctx.stroke();
    ctx.fillStyle = '#0a0a0a'; ctx.fillRect(15, -6, 35, 4); ctx.fillRect(15, 2, 35, 4);
    if (!isOverheated) {
      ctx.fillStyle = '#66ccff'; ctx.fillRect(15, -3, 35, 2);
      ctx.beginPath(); ctx.arc(50, -4, 3, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(50, 4, 3, 0, Math.PI*2); ctx.fill();
    }
  }
  ctx.restore();
}

function drawFire(ctx, x, y) {
  const t = Date.now() * 0.01;
  for(let i=0; i<3; i++) {
    const size = Math.random() * 8 + 4;
    ctx.fillStyle = Math.random() > 0.5 ? 'rgba(255, 100, 0, 0.8)' : 'rgba(255, 200, 0, 0.8)';
    ctx.beginPath(); ctx.arc(x + (Math.random()-0.5)*20, y + (Math.random()-0.5)*10 - Math.sin(t+i)*5, size, 0, Math.PI*2); ctx.fill();
  }
}

function drawModule(ctx, module, x, y, isLast) {
  ctx.save(); ctx.translate(x, y);
  const isBroken = module.hp <= 0;
  const isCrit = module.hp > 0 && module.hp <= 20;
  
  if (module.type === 'solar' || module.type === 'radiator') {
    if (isBroken || isCrit) {
      ctx.fillStyle = '#222'; ctx.fillRect(6, -28, MOD_W-12, 28); ctx.fillRect(6, MOD_H, MOD_W-12, 28);
      if (!isBroken) { ctx.fillStyle = 'rgba(255,50,50,0.3)'; ctx.fillRect(6, -28, MOD_W-12, 28); ctx.fillRect(6, MOD_H, MOD_W-12, 28); }
    } else if (module.type === 'solar') {
      ctx.fillStyle = '#1a2b4c'; ctx.strokeStyle = '#4a7aab'; ctx.lineWidth = 1;
      ctx.fillRect(6, -28, MOD_W-12, 28); ctx.strokeRect(6, -28, MOD_W-12, 28);
      ctx.fillRect(6, MOD_H, MOD_W-12, 28); ctx.strokeRect(6, MOD_H, MOD_W-12, 28);
      ctx.strokeStyle = 'rgba(102, 204, 255, 0.3)';
      for(let i=6; i<MOD_W-6; i+=8) { ctx.beginPath(); ctx.moveTo(i, -28); ctx.lineTo(i, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(i, MOD_H); ctx.lineTo(i, MOD_H+28); ctx.stroke(); }
    } else {
      ctx.fillStyle = '#331111'; ctx.fillRect(8, -22, MOD_W-16, 22); ctx.fillRect(8, MOD_H, MOD_W-16, 22);
      if(isCrit) { drawFire(ctx, MOD_W/2, -10); drawFire(ctx, MOD_W/2, MOD_H+10); }
    }
    ctx.fillStyle = '#555'; ctx.fillRect(MOD_W/2-2, -5, 4, MOD_H+10);
    ctx.restore(); return;
  }

  let baseColor = (isBroken || isCrit) ? '#111' : '#2c3e50', highlight = (isBroken || isCrit) ? '#222' : '#4a6b8c', shadow = (isBroken || isCrit) ? '#000' : '#1a252f';
  if (module.type === 'engine' && !isBroken && !isCrit) { baseColor = '#3d2b2b'; highlight = '#6b4c4c'; }
  if (module.type.includes('weapon') && !isBroken && !isCrit) { baseColor = '#1a2b3b'; highlight = '#2e4a6b'; } 

  const chamfer = 4;
  ctx.beginPath(); ctx.moveTo(chamfer, 0); ctx.lineTo(MOD_W - chamfer, 0); ctx.lineTo(MOD_W, chamfer);
  ctx.lineTo(MOD_W, MOD_H - chamfer); ctx.lineTo(MOD_W - chamfer, MOD_H); ctx.lineTo(chamfer, MOD_H);
  ctx.lineTo(0, MOD_H - chamfer); ctx.lineTo(0, chamfer); ctx.closePath();
  const grad = ctx.createLinearGradient(0, 0, 0, MOD_H);
  grad.addColorStop(0, highlight); grad.addColorStop(0.5, baseColor); grad.addColorStop(1, shadow);
  ctx.fillStyle = grad; ctx.fill();
  
  ctx.strokeStyle = (isBroken || isCrit) ? '#000' : '#050505'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, MOD_H); ctx.stroke();
  if (isLast) { ctx.beginPath(); ctx.moveTo(MOD_W, 0); ctx.lineTo(MOD_W, MOD_H); ctx.stroke(); }

  if (!isBroken && !isCrit) {
    if (module.type === 'engine') {
      const flameLen = 15 + Math.random() * 10;
      const g = ctx.createLinearGradient(0, MOD_H/2, -flameLen, MOD_H/2);
      g.addColorStop(0, 'rgba(102, 204, 255, 0.9)'); g.addColorStop(1, 'rgba(0, 100, 255, 0)');
      ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(0, 6); ctx.lineTo(-flameLen, MOD_H/2); ctx.lineTo(0, MOD_H-6); ctx.fill();
    }
    else if (module.type === 'weapon_laser') { drawFixedMainCannon(ctx, MOD_W - 5, MOD_H/2, module.isOverheated, false); }
    else if (module.type === 'supply') {
      ctx.fillStyle = '#888'; ctx.fillRect(MOD_W/2 - 6, -10, 12, 10); ctx.fillRect(MOD_W/2 - 6, MOD_H, 12, 10); ctx.fillRect(MOD_W, MOD_H/2 - 6, 10, 12);
      ctx.fillStyle = '#0f0'; ctx.fillRect(MOD_W/2 - 4, -10, 8, 2); ctx.fillRect(MOD_W/2 - 4, MOD_H+8, 8, 2); ctx.fillRect(MOD_W+8, MOD_H/2 - 4, 2, 8);
    }
    else if (module.type === 'core') { ctx.fillStyle = 'rgba(102, 204, 255, 0.4)'; ctx.fillRect(10, 8, 25, 10); }
  }

  if (isCrit) { drawFire(ctx, MOD_W/2, MOD_H/2); }

  // 去掉了舱室上的微型血条,画面更干净
  ctx.restore();
}

// ================= 实体与战斗逻辑 =================
class Ship {
  constructor(x, y, isEnemy, color, modulesData, extrasData) {
    this.x = x; this.y = y; this.isEnemy = isEnemy; this.color = color;
    this.name = getRandomName();
    this.captain = getRandomCaptain();
    this.vx = 0; this.vy = 0; this.angle = isEnemy ? Math.PI : 0;
    this.maxSpeed = 3.0; this.turnSpeed = 0.04; this.radius = 30; this.dead = false;
    this.target = null; this.turretAngle = 0;
    this.interceptCd = 0; 
    this.orbitDir = Math.random() > 0.5 ? 1 : -1; 
    
    this.modules = modulesData.map(type => ({ type, hp: 100, maxHp: 100, isOverheated: false }));
    this.totalWidth = this.modules.length * MOD_W;
    this.extras = extrasData || [];
    
    this.crew = [{ id: 0, role: 'captain', pos: 'core' }];
    for(let i=1; i<8; i++) this.crew.push({ id: i, role: 'engineer', targetIndex: null, progress: 0, pos: Math.floor(Math.random() * this.modules.length) });
  }

  isModuleFunctional(type) {
    const mods = this.modules.filter(m => m.type === type);
    if (mods.length === 0) return false;
    return mods.some(m => m.hp > 20);
  }

  takeDamage(hitX, hitY, damage) {
    if (this.dead) return;
    const cos = Math.cos(-this.angle), sin = Math.sin(-this.angle);
    const localX = (hitX - this.x) * cos - (hitY - this.y) * sin; 
    const localY = (hitX - this.x) * sin + (hitY - this.y) * cos;
    if (Math.abs(localY) > MOD_H / 2 + 5) return; 

    const index = Math.floor((localX - (-this.totalWidth / 2)) / MOD_W);
    if (index >= 0 && index < this.modules.length) {
      const mod = this.modules[index];
      if (mod.hp > 0) {
        mod.hp -= damage;
        if (mod.hp <= 0) {
          mod.hp = 0;
          this.crew.forEach(c => { if(c.pos === index && c.role !== 'captain') c.pos = -1; });
          for(let i=0; i<15; i++) particles.push({x: hitX, y: hitY, vx: (Math.random()-0.5)*8, vy: (Math.random()-0.5)*8, life: 30, maxLife: 30, color: '#ffaa00', size: 3});
          
          if (mod.type.includes('weapon')) {
             if(index > 0) this.modules[index-1].hp -= 30;
             if(index < this.modules.length-1) this.modules[index+1].hp -= 30;
             for(let i=0; i<20; i++) particles.push({x: hitX, y: hitY, vx: (Math.random()-0.5)*15, vy: (Math.random()-0.5)*15, life: 40, maxLife: 40, color: '#ff5500', size: 4});
          }

          if (mod.type === 'core') {
            this.dead = true;
            for(let i=0; i<80; i++) particles.push({x: this.x, y: this.y, vx: (Math.random()-0.5)*25, vy: (Math.random()-0.5)*25, life: 80, maxLife: 80, color: Math.random()>0.5?'#fff':'#ff3300', size: Math.random()*6+2});
            logMsg(`${this.name} 核心舱被摧毁!${this.captain} 阵亡。`);
            if (selectedShip === this) updateTacticalUI();
          }
        }
      }
    }
  }

  tryIntercept() {
    if (this.interceptCd > 0) { this.interceptCd--; return; }
    const hasLaserPD = this.isModuleFunctional('weapon_laser');
    const hasRailgunPD = this.isModuleFunctional('railgun');
    const hasPd = this.isModuleFunctional('supply') || this.isModuleFunctional('missile'); 
    if (!hasLaserPD && !hasRailgunPD && !hasPd) return;

    let threat = null, minDist = 500;
    for (let b of bullets) {
      if (b.isEnemy !== this.isEnemy && b.type === 'missile') {
        let d = dist(this, b); if (d < minDist) { minDist = d; threat = b; }
      }
    }
    if (threat) {
      this.interceptCd = 15;
      const angle = angleTo(this, threat);
      if (hasLaserPD) bullets.push(new Bullet(this.x, this.y, angle, this.isEnemy, 'laser_pd'));
      else bullets.push(new Bullet(this.x, this.y, angle, this.isEnemy, 'pd_shell'));
    }
  }

  updateCrew() {
    this.crew.forEach(c => {
      if (c.role === 'captain' || c.pos === -1) return;
      if (c.targetIndex !== null) {
        const mod = this.modules[c.targetIndex];
        if (mod.hp >= 100 || mod.hp <= 0) { c.targetIndex = null; c.progress = 0; }
      }
      if (c.targetIndex === null) {
        for(let i=0; i<this.modules.length; i++) {
          if(this.modules[i].hp > 0 && this.modules[i].hp < 100) {
             if(!this.crew.some(other => other.targetIndex === i)) { c.targetIndex = i; c.progress = 0; break; }
          }
        }
      }
      if (c.targetIndex !== null) {
        if (c.pos !== c.targetIndex) {
           c.progress += 20 * (1/60); 
           if (c.progress >= 20) { c.pos = c.targetIndex; c.progress = 0; }
        } else {
           this.modules[c.targetIndex].hp += 0.5 * (1/60);
           if(this.modules[c.targetIndex].hp > 100) this.modules[c.targetIndex].hp = 100;
        }
      }
    });
  }

  update() {
    if (this.dead) return;
    this.updateCrew();
    this.tryIntercept(); 

    if (this.x < 200 || this.x > WORLD_W - 200) this.vx *= -0.8;
    if (this.y < 200 || this.y > WORLD_H - 200) this.vy *= -0.8;
    this.vx *= 0.98; this.vy *= 0.98; 
    this.x += this.vx; this.y += this.vy;
    
    const engineFunctional = this.isModuleFunctional('engine');
    if (!engineFunctional) { this.vx *= 0.9; this.vy *= 0.9; return; }
    this.logic();
  }

  aimTurret(target) {
    if (!target) { this.turretAngle *= 0.95; return; }
    const ta = angleTo(this, target);
    const relativeAngle = normalizeAngle(ta - this.angle);
    const diff = normalizeAngle(relativeAngle - this.turretAngle);
    this.turretAngle += Math.sign(diff) * Math.min(Math.abs(diff), 0.08);
  }

  moveToTarget(targetAngle) {
    const diff = normalizeAngle(targetAngle - this.angle);
    this.angle += Math.sign(diff) * Math.min(Math.abs(diff), this.turnSpeed);
    if (Math.abs(diff) < 0.3) {
      this.vx += Math.cos(this.angle) * 0.06; this.vy += Math.sin(this.angle) * 0.06;
      const speed = Math.hypot(this.vx, this.vy);
      if (speed > this.maxSpeed) { this.vx = (this.vx / speed) * this.maxSpeed; this.vy = (this.vy / speed) * this.maxSpeed; }
    }
  }

  dogfight(target, idealDist) {
    const d = dist(this, target);
    const ta = angleTo(this, target);
    if (d > idealDist + 150) this.moveToTarget(ta);
    else if (d < idealDist - 150) this.moveToTarget(ta + Math.PI);
    else {
      let orbitAngle = ta + (Math.PI / 3) * this.orbitDir;
      this.moveToTarget(orbitAngle);
      this.aimTurret(target);
    }
  }

  draw() {
    if (this.dead) return;
    ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
    
    if (this === selectedShip) {
       ctx.strokeStyle = 'rgba(102, 204, 255, 0.6)'; ctx.lineWidth = 2;
       ctx.setLineDash([5, 5]);
       ctx.strokeRect(-this.totalWidth/2 - 10, -MOD_H/2 - 35, this.totalWidth + 20, MOD_H + 70);
       ctx.setLineDash([]);
    }
    
    this.extras.forEach(ext => {
      const module = this.modules[ext.index];
      if(module) drawModule(ctx, { type: ext.type, hp: module.hp }, ext.index * MOD_W - this.totalWidth/2, -MOD_H/2, false);
    });
    this.modules.forEach((mod, i) => drawModule(ctx, mod, i * MOD_W - this.totalWidth/2, -MOD_H/2, i === this.modules.length - 1));
    this.drawExtra(ctx);
    ctx.restore();
  }
  drawExtra(ctx) {}
  logic() {}
}

// ... (LaserShip, AssaultShip, MissileShip, SupplyShip 的定义与前版本完全一致,略) ...
class LaserShip extends Ship {
  constructor(x, y, isEnemy) { super(x, y, isEnemy, '#66ccff', ['engine', 'supply', 'core', 'hab', 'weapon_laser'], [{ index: 1, type: 'solar' }, { index: 3, type: 'radiator' }]); this.maxSpeed = 2.5; }
  logic() {
    const weaponFunctional = this.isModuleFunctional('weapon_laser');
    const mainGun = this.modules[4];
    let attackMultiplier = this.isModuleFunctional('solar') ? 1.0 : 0.8;
    let coolingRate = this.isModuleFunctional('radiator') ? 0.3 : 0.15;
    if (mainGun && mainGun.hp > 0 && mainGun.isOverheated) { mainGun.heat -= coolingRate; if (mainGun.heat <= 0) mainGun.isOverheated = false; }
    let target = null, minDist = 3500; 
    entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
    this.target = target;
    if (target) {
      this.aimTurret(target); this.dogfight(target, 800);
      if (weaponFunctional && !mainGun.isOverheated && Math.abs(normalizeAngle(angleTo(this, target) - this.angle)) < 0.35) {
        bullets.push(new Bullet(this.x, this.y, this.angle, this.isEnemy, 'laser_main', this, attackMultiplier));
        mainGun.heat = (mainGun.heat || 0) + 0.8; if (mainGun.heat >= 100) mainGun.isOverheated = true;
      }
    }
  }
  drawExtra(ctx) {
    if (this.isModuleFunctional('weapon_laser') && this.target) {
      drawTurret(ctx, -20, -MOD_H/2+2, this.turretAngle, 'laser_intercept'); drawTurret(ctx, -20, MOD_H/2-2, this.turretAngle, 'laser_intercept');
      drawTurret(ctx, 5, -MOD_H/2+2, this.turretAngle, 'laser_intercept'); drawTurret(ctx, 5, MOD_H/2-2, this.turretAngle, 'laser_intercept');
    }
  }
}
class AssaultShip extends Ship {
  constructor(x, y, isEnemy) { super(x, y, isEnemy, '#00ffcc', ['engine', 'supply', 'core', 'hab', 'missile', 'radar', 'railgun'], [{ index: 1, type: 'solar' }, { index: 2, type: 'solar' }, { index: 4, type: 'radiator' }]); this.railgunAmmo = 24; this.railgunCd = 0; this.maxSpeed = 4.5; }
  logic() {
    const weaponFunctional = this.isModuleFunctional('railgun');
    const radarFunctional = this.isModuleFunctional('radar');
    const detectRange = radarFunctional ? 5000 : 1500; 
    if (this.railgunCd > 0) this.railgunCd--;
    let target = null, minDist = detectRange;
    entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
    this.target = target;
    if (target) {
      this.aimTurret(target); this.dogfight(target, 400);
      if (weaponFunctional && this.railgunAmmo > 0 && this.railgunCd <= 0 && Math.abs(normalizeAngle(angleTo(this, target) - (this.angle + this.turretAngle))) < 0.15) {
        bullets.push(new Bullet(this.x, this.y, this.angle + this.turretAngle, this.isEnemy, 'railgun'));
        this.railgunAmmo--; this.railgunCd = 120;
      }
    }
  }
  drawExtra(ctx) { if (this.isModuleFunctional('railgun') && this.target) { drawTurret(ctx, 60, -MOD_H/2, this.turretAngle, 'railgun'); drawTurret(ctx, 60, MOD_H/2, this.turretAngle, 'railgun'); } }
}
class MissileShip extends Ship {
  constructor(x, y, isEnemy) { super(x, y, isEnemy, '#ff3366', ['engine', 'hab', 'missile', 'solar', 'missile', 'missile', 'core', 'missile', 'missile', 'missile', 'supply', 'supply']); this.maxAmmo = 80; this.ammo = 80; this.cd = 0; this.maxSpeed = 2.0; }
  logic() {
    if (this.cd > 0) this.cd--;
    if (this.ammo <= 0) { this.moveToTarget(this.angle + Math.PI); return; }
    let target = null, minDist = 4500;
    entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
    this.target = target;
    if (target) {
      this.dogfight(target, 1200);
      if (this.cd <= 0 && Math.abs(normalizeAngle(angleTo(this, target) - this.angle)) < 0.3) {
        bullets.push(new Bullet(this.x, this.y, this.angle, this.isEnemy, 'missile', this)); this.ammo--; this.cd = 80;
      }
    }
  }
  drawExtra(ctx) { ctx.save(); ctx.rotate(-this.angle); ctx.fillStyle = '#fff'; ctx.font = '12px monospace'; ctx.textAlign = 'center'; ctx.fillText(`弹:${this.ammo}`, 0, 45); ctx.restore(); }
}
class SupplyShip extends Ship {
  constructor(x, y, isEnemy) { super(x, y, isEnemy, '#ffaa00', ['engine', 'core', 'supply']); this.state = 'search'; this.friend = null; this.maxSpeed = 4.0; }
  logic() {
    if (this.state === 'search') {
      if (this.isModuleFunctional('supply')) this.friend = entities.find(e => e.isEnemy === this.isEnemy && !e.dead && e.ammo !== undefined && e.ammo < 20);
      else this.friend = null;
      if (this.friend) {
        this.moveToTarget(angleTo(this, this.friend));
        if (dist(this, this.friend) < 50) this.state = 'docking';
      } else this.moveToTarget(this.angle + (Math.random()-0.5)*0.1);
    } else if (this.state === 'docking') {
      this.vx = 0; this.vy = 0;
      if (!this.friend || this.friend.dead || !this.isModuleFunctional('supply')) { this.state = 'search'; return; }
      this.angle = angleTo(this, this.friend);
      if (this.friend.ammo < this.friend.maxAmmo) { this.friend.ammo += 0.5; if (Math.random() < 0.1) particles.push({x: (this.x+this.friend.x)/2, y: (this.y+this.friend.y)/2, vx:0, vy:0, life: 15, maxLife:15, color: '#00ff00', size: 2}); } 
      else this.state = 'search';
    }
  }
}

// 子弹类与主循环逻辑保持不变
class Bullet {
  constructor(x, y, angle, isEnemy, type, owner = null, multiplier = 1.0) {
    this.x = x; this.y = y; this.isEnemy = isEnemy; this.type = type; this.angle = angle;
    this.dead = false; this.life = 200; this.hp = 1;
    if (type === 'missile') { this.speed = 7; this.damage = 25 * multiplier; this.turnSpeed = 0.08; this.life = 800; this.target = null; this.hp = 12; }
    else if (type === 'railgun') { this.speed = 40; this.damage = 80 * multiplier; this.life = 200; }
    else if (type === 'laser_main') { this.speed = 30; this.damage = 4 * multiplier; this.life = 20; }
    else if (type === 'laser_pd') { this.speed = 35; this.damage = 6; this.life = 20; } 
    else if (type === 'pd_shell') { this.speed = 25; this.damage = 4; this.life = 30; }
    if (type.includes('laser') && owner) { this.vx = owner.vx + Math.cos(angle)*this.speed; this.vy = owner.vy + Math.sin(angle)*this.speed; }
    else { this.vx = Math.cos(angle)*this.speed; this.vy = Math.sin(angle)*this.speed; }
  }
  update() {
    if (this.dead) return;
    if (this.type === 'missile') {
      if (!this.target || this.target.dead) { let minDist = Infinity; entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; this.target = e; } } }); }
      if (this.target) { const ta = angleTo(this, this.target); const currentAngle = Math.atan2(this.vy, this.vx); const diff = normalizeAngle(ta - currentAngle); const newAngle = currentAngle + Math.sign(diff) * this.turnSpeed; this.vx = Math.cos(newAngle) * this.speed; this.vy = Math.sin(newAngle) * this.speed; this.angle = newAngle; }
    }
    this.x += this.vx; this.y += this.vy; this.life--;
    if (this.life <= 0 || this.x < 0 || this.x > WORLD_W || this.y < 0 || this.y > WORLD_H) this.dead = true;
    for (let e of entities) {
      if (!e.dead && e.isEnemy !== this.isEnemy) {
        if (this.type === 'laser_pd' || this.type === 'pd_shell') {
           for (let b of bullets) {
             if (!b.dead && b.isEnemy !== this.isEnemy && b.type === 'missile') {
               if (dist(this, b) < 30) { b.hp -= this.damage; this.dead = true; if (b.hp <= 0) { b.dead = true; for(let i=0; i<8; i++) particles.push({x:b.x, y:b.y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6, life:15, maxLife:15, color:'#fff', size:2}); } return; }
             }
           }
        } else if (this.type !== 'laser_pd' && this.type !== 'pd_shell') {
          if (dist(this, e) < e.radius) { e.takeDamage(this.x, this.y, this.damage); this.dead = true; }
        }
      }
    }
  }
  draw() {
    if (this.dead) return;
    ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
    if (this.type === 'missile') { ctx.fillStyle = '#ffaa00'; ctx.fillRect(-10, -3, 20, 6); ctx.fillStyle = 'rgba(255,50,0,0.8)'; ctx.beginPath(); ctx.moveTo(-10, -3); ctx.lineTo(-22, 0); ctx.lineTo(-10, 3); ctx.fill(); }
    else if (this.type === 'railgun') { ctx.fillStyle = '#00ffcc'; ctx.fillRect(-25, -3, 50, 6); }
    else if (this.type === 'laser_main') { ctx.fillStyle = 'rgba(102, 204, 255, 0.9)'; ctx.fillRect(-30, -2, 60, 4); }
    else if (this.type === 'laser_pd') { ctx.fillStyle = '#fff'; ctx.fillRect(-15, -1.5, 30, 3); }
    else if (this.type === 'pd_shell') { ctx.fillStyle = '#ffaa00'; ctx.fillRect(-4, -1.5, 8, 3); }
    ctx.restore();
  }
}

function startBattle() {
  document.getElementById('ui-layer').style.opacity = '0';
  setTimeout(() => { document.getElementById('ui-layer').style.display = 'none'; }, 300);
  gameState = 'playing'; entities = []; bullets = []; particles = []; selectedShip = null; uiEl.style.display = 'none';
  const genShips = (prefix, isEnemy, baseX) => {
    let yOffset = -500;
    for(let i=0; i<config[prefix+'Laser']; i++) { entities.push(new LaserShip(baseX, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
    for(let i=0; i<config[prefix+'Assault']; i++) { entities.push(new AssaultShip(baseX + 300, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
    for(let i=0; i<config[prefix+'Missile']; i++) { entities.push(new MissileShip(baseX, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
    for(let i=0; i<config[prefix+'Supply']; i++) { entities.push(new SupplyShip(baseX + 300, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
  };
  genShips('b', false, 1000); genShips('r', true, WORLD_W - 1000);
  cam.x = WORLD_W/2; cam.y = WORLD_H/2; cam.zoom = 0.15; loop();
}

function loop() {
  if (gameState !== 'playing') return;
  
  if (selectedShip && !selectedShip.dead) {
    if (!isDragging) {
      cam.x += (selectedShip.x - cam.x) * 0.05;
      cam.y += (selectedShip.y - cam.y) * 0.05;
    }
    updateTacticalUI();
  } else if (selectedShip && selectedShip.dead) {
    selectedShip = null; uiEl.style.display = 'none';
  }

  ctx.fillStyle = '#03050a'; ctx.fillRect(0, 0, W, H);
  ctx.save();
  ctx.translate(W / 2, H / 2); ctx.scale(cam.zoom, cam.zoom); ctx.translate(-cam.x, -cam.y); 

  ctx.strokeStyle = 'rgba(50, 100, 150, 0.1)';
  for(let i=0; i<WORLD_W; i+=400) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, WORLD_H); ctx.stroke(); }
  for(let i=0; i<WORLD_H; i+=400) { ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(WORLD_W, i); ctx.stroke(); }

  for (let e of entities) { e.update(); if (!e.dead) e.draw(); }
  for (let i = bullets.length - 1; i >= 0; i--) { bullets[i].update(); if (!bullets[i].dead) bullets[i].draw(); else bullets.splice(i, 1); }
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i]; p.x += p.vx; p.y += p.vy; p.life--;
    ctx.globalAlpha = Math.max(0, p.life / p.maxLife); ctx.fillStyle = p.color;
    ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill();
    if (p.life <= 0) particles.splice(i, 1);
  }
  ctx.globalAlpha = 1;
  ctx.restore(); 

  if (!entities.some(e => !e.isEnemy && !e.dead) || !entities.some(e => e.isEnemy && !e.dead)) {
    gameState = 'over';
    ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#fff'; ctx.font = 'bold 30px monospace'; ctx.textAlign = 'center';
    ctx.fillText('舰队交战结束', W/2, H/2); return;
  }
  requestAnimationFrame(loop);
}
</script>
</body>
</html>

Game Source: 深空舰队 - 极简战术版

Creator: CosmicRider48

Libraries: none

Complexity: complex (676 lines, 34.4 KB)

The full source code is displayed above on this page.

Remix Instructions

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