🎮ArcadeLab

星际雷霆 · 竖版卷轴飞行射击

by SuperOwl71
2238 lines66.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>
  :root {
    --accent: #4fc3f7;
    --danger: #ff5252;
    --gold: #ffd54f;
  }

  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #03040a;
    font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
    color: #e6f1ff;
    user-select: none;
    -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }

  #game-container {
    position: absolute;
    left: 0;
    top: 0;
    width: 540px;
    height: 960px;
    transform-origin: top left;
    overflow: hidden;
    background: #03040a;
    box-shadow: 0 0 80px rgba(0, 0, 0, 0.9);
  }

  canvas#game-canvas {
    position: absolute;
    inset: 0;
    display: block;
  }

  /* ===== HUD ===== */
  #hud {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    padding: max(27px, env(safe-area-inset-top, 0px)) max(27px, env(safe-area-inset-right, 0px)) max(27px, env(safe-area-inset-bottom, 0px)) max(27px, env(safe-area-inset-left, 0px));
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    pointer-events: none;
    z-index: 10;
  }

  .hud-panel {
    background: rgba(6, 12, 28, 0.55);
    border: 1px solid rgba(79, 195, 247, 0.35);
    border-radius: 12px;
    padding: 10px 16px;
    backdrop-filter: blur(6px);
    box-shadow: 0 0 18px rgba(79, 195, 247, 0.18);
  }

  .hud-label {
    font-size: 11px;
    letter-spacing: 2px;
    color: #8fb8d8;
    text-transform: uppercase;
  }

  .hud-value {
    font-size: 26px;
    font-weight: 700;
    color: #ffffff;
    text-shadow: 0 0 12px rgba(79, 195, 247, 0.8);
    font-variant-numeric: tabular-nums;
  }

  .hud-sub {
    font-size: 12px;
    color: #9be7ff;
    letter-spacing: 1px;
    margin-top: 2px;
  }

  #combo-value {
    color: #ffd54f;
    font-weight: 700;
    transition: opacity 0.2s ease;
  }

  #lives .hud-value {
    color: #ffd54f;
    text-shadow: 0 0 12px rgba(255, 213, 79, 0.8);
  }

  /* ===== 提示浮字 ===== */
  #toast {
    position: absolute;
    left: 50%;
    top: 22%;
    transform: translateX(-50%);
    font-size: 22px;
    font-weight: 700;
    letter-spacing: 2px;
    color: #ffffff;
    text-shadow: 0 0 16px rgba(79, 195, 247, 0.9);
    pointer-events: none;
    opacity: 0;
    z-index: 12;
    transition: opacity 0.25s ease;
    white-space: nowrap;
  }
  #toast.show { opacity: 1; }

  /* ===== 暂停按钮 ===== */
  #btn-pause {
    position: absolute;
    top: 74px;
    right: 22px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid rgba(79, 195, 247, 0.4);
    background: rgba(6, 12, 28, 0.55);
    color: #e6f1ff;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
    z-index: 15;
    backdrop-filter: blur(6px);
    box-shadow: 0 0 12px rgba(79, 195, 247, 0.25);
  }

  /* ===== 暂停遮罩 ===== */
  #pause-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 12px;
    background: rgba(3, 4, 10, 0.72);
    z-index: 18;
    backdrop-filter: blur(3px);
    cursor: pointer;
  }
  #pause-overlay.hidden { display: none; }
  #pause-overlay .pause-title {
    font-size: 42px;
    font-weight: 900;
    letter-spacing: 8px;
    color: #ffffff;
    text-shadow: 0 0 24px rgba(79, 195, 247, 0.8);
  }
  #pause-overlay .pause-hint {
    font-size: 14px;
    color: #8fb8d8;
    letter-spacing: 2px;
  }

  /* ===== 触控控件(默认隐藏,触屏设备显示) ===== */
  #joystick-base, #fire-btn { display: none; }
  body.touch #joystick-base { display: block; }
  body.touch #fire-btn { display: block; }

  #joystick-base {
    position: absolute;
    left: 26px;
    bottom: 30px;
    width: 118px;
    height: 118px;
    border-radius: 50%;
    border: 2px solid rgba(79, 195, 247, 0.4);
    background: rgba(6, 12, 28, 0.32);
    z-index: 15;
    touch-action: none;
  }
  #joystick-knob {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 52px;
    height: 52px;
    margin: -26px 0 0 -26px;
    border-radius: 50%;
    background: radial-gradient(circle at 40% 35%, rgba(122, 220, 255, 0.9), rgba(21, 101, 192, 0.85));
    box-shadow: 0 0 16px rgba(79, 195, 247, 0.6);
    pointer-events: none;
  }
  #fire-btn {
    position: absolute;
    right: 26px;
    bottom: 30px;
    width: 98px;
    height: 98px;
    border-radius: 50%;
    border: 2px solid rgba(255, 82, 82, 0.5);
    background: radial-gradient(circle at 40% 35%, rgba(255, 138, 128, 0.72), rgba(198, 40, 40, 0.68));
    color: #ffffff;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: 2px;
    z-index: 15;
    touch-action: none;
    box-shadow: 0 0 20px rgba(255, 82, 82, 0.4);
  }
  #fire-btn:active { transform: scale(0.92); }

  /* ===== 遮罩层(开始 / 结束) ===== */
  #overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 14px;
    background: radial-gradient(ellipse at center, rgba(10, 18, 40, 0.72) 0%, rgba(3, 4, 10, 0.92) 100%);
    z-index: 20;
    text-align: center;
    padding: 20px;
    transition: opacity 0.35s ease;
  }
  #overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  #overlay .title {
    font-size: clamp(40px, 8vw, 72px);
    font-weight: 900;
    letter-spacing: 6px;
    background: linear-gradient(180deg, #7adcff, #2f7bff);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    text-shadow: 0 0 40px rgba(47, 123, 255, 0.45);
    filter: drop-shadow(0 0 14px rgba(47, 123, 255, 0.5));
  }
  #overlay .subtitle {
    font-size: 15px;
    letter-spacing: 4px;
    color: #8fb8d8;
  }
  #overlay .final-score {
    font-size: 30px;
    font-weight: 800;
    color: #ffd54f;
    text-shadow: 0 0 20px rgba(255, 213, 79, 0.7);
  }
  #overlay .record {
    font-size: 14px;
    color: #9fe6c0;
  }

  #overlay .controls {
    margin-top: 8px;
    font-size: 13px;
    color: #b9d3e8;
    line-height: 1.9;
  }
  #overlay .controls b {
    color: #ffffff;
    background: rgba(79, 195, 247, 0.18);
    border: 1px solid rgba(79, 195, 247, 0.4);
    border-radius: 6px;
    padding: 2px 8px;
    margin: 0 2px;
    font-weight: 600;
  }
  #overlay .legend {
    margin-top: 6px;
    font-size: 12px;
    color: #8fb8d8;
    line-height: 1.8;
  }
  #overlay .legend span {
    display: inline-block;
    margin: 0 4px;
  }
  #overlay .legend .s { color: #ffd54f; }
  #overlay .legend .d { color: #ff8a80; }
  #overlay .legend .w { color: #7adcff; }
  #overlay .legend .li { color: #69f0ae; }
  #overlay .legend .sc { color: #9fe6c0; }
  #overlay .legend .bo { color: #7adcff; }

  #overlay .touch-hint { display: none; font-size: 13px; color: #b9d3e8; }
  body.touch #overlay .controls { display: none; }
  body.touch #overlay .touch-hint { display: block; }

  #btn-start {
    margin-top: 16px;
    font-size: 20px;
    font-weight: 700;
    letter-spacing: 3px;
    color: #03121f;
    background: linear-gradient(180deg, #7adcff, #2f7bff);
    border: none;
    border-radius: 40px;
    padding: 14px 46px;
    cursor: pointer;
    box-shadow: 0 0 28px rgba(79, 195, 247, 0.6);
    transition: transform 0.12s ease, box-shadow 0.2s ease;
  }
  #btn-start:hover {
    transform: translateY(-2px) scale(1.03);
    box-shadow: 0 0 38px rgba(79, 195, 247, 0.85);
  }
  #btn-start:active {
    transform: scale(0.97);
  }

</style>
</head>
<body>
  <div id="game-container">
    <canvas id="game-canvas"></canvas>

    <div id="hud">
      <div class="hud-panel">
        <div class="hud-label">分数 SCORE</div>
        <div class="hud-value" id="score">0</div>
        <div class="hud-sub" id="combo-value">连击 ×0</div>
      </div>
      <div class="hud-panel" id="weapon">
        <div class="hud-label">火力 LEVEL</div>
        <div class="hud-value" id="level-value">Lv.1</div>
        <div class="hud-sub" id="wingmen-value">僚机 ×0</div>
      </div>
      <div class="hud-panel" id="lives">
        <div class="hud-label">生命 LIVES</div>
        <div class="hud-value" id="lives-value">❤❤❤</div>
      </div>
    </div>

    <div id="toast"></div>
    <button id="btn-pause" title="暂停 (P)">⏸</button>

    <div id="joystick-base"><div id="joystick-knob"></div></div>
    <button id="fire-btn">火力</button>

    <div id="pause-overlay" class="hidden">
      <div class="pause-title">已暂停</div>
      <div class="pause-hint">按 P 键 / 点击此处继续</div>
    </div>

    <div id="overlay">
      <div class="title">星际雷霆</div>
      <div class="subtitle">STAR THUNDER · 竖版卷轴射击</div>
      <div class="controls">
        <b>W A S D</b> / <b>方向键</b> 移动 &nbsp;&nbsp; <b>空格</b> 射击 &nbsp;&nbsp; <b>P</b> 暂停<br>
        击落敌机 · 收集道具 · 挑战 Boss · 累积连击
      </div>
      <div class="touch-hint">左下摇杆移动 · 右下按钮射击</div>
      <div class="legend">
        <span class="s"><b>S</b> 火力升级</span>
        <span class="d"><b>D</b> 火力降级</span>
        <span class="w"><b>W</b> 僚机</span>
        <span class="li"><b>♥</b> 生命</span>
        <span class="sc"><b>+</b> 加分</span>
        <span class="bo"><b>✹</b> 清屏</span>
      </div>
      <button id="btn-start">开始游戏</button>
    </div>
  </div>

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

  /* ================= 基础设置 ================= */
  const canvas = document.getElementById("game-canvas");
  const ctx = canvas.getContext("2d");
  const container = document.getElementById("game-container");
  const scoreEl = document.getElementById("score");
  const livesEl = document.getElementById("lives-value");
  const levelEl = document.getElementById("level-value");
  const wingmenEl = document.getElementById("wingmen-value");
  const comboEl = document.getElementById("combo-value");
  const toastEl = document.getElementById("toast");
  const overlayEl = document.getElementById("overlay");
  const btnStart = document.getElementById("btn-start");
  const btnPause = document.getElementById("btn-pause");
  const pauseOverlayEl = document.getElementById("pause-overlay");
  const joystickBase = document.getElementById("joystick-base");
  const joystickKnob = document.getElementById("joystick-knob");
  const fireBtn = document.getElementById("fire-btn");

  const HEARTS = ["❤", "❤", "❤", "❤", "❤"];

  const PLAYER_BODY_A = "#1E90FF";
  const PLAYER_BODY_B = "#00BFFF";
  const LEVEL_HALO = {
    1: { color: "#4fc3f7", name: "Lv.1" },
    2: { color: "#26c6da", name: "Lv.2" },
    3: { color: "#b388ff", name: "Lv.3" }
  };

  const COMBO_WINDOW = 3.0;
  const MAX_PARTICLES = 500;   // 同屏粒子上限
  let particleScale = 1;        // 自适应粒子密度(卡顿时自动降低)

  // 固定逻辑分辨率(竖版 9:16),显示时等比缩放填满窗口(上下/左右留黑边)
  const LOGICAL_W = 540;
  const LOGICAL_H = 960;
  let W = LOGICAL_W, H = LOGICAL_H;
  let displayScale = 1;         // 显示缩放系数(= canvas 屏幕宽 / 逻辑宽)

  function resize() {
    const winW = window.innerWidth;
    const winH = window.innerHeight;
    const s = Math.min(winW / LOGICAL_W, winH / LOGICAL_H);
    displayScale = s;

    // 视口容器:等比缩放 + 居中(transform-origin 左上角)
    container.style.transform = "scale(" + s + ")";
    container.style.left = Math.round((winW - LOGICAL_W * s) / 2) + "px";
    container.style.top = Math.round((winH - LOGICAL_H * s) / 2) + "px";

    // 画布:内部逻辑坐标固定,显示尺寸随窗口缩放
    canvas.width = LOGICAL_W;
    canvas.height = LOGICAL_H;

    W = LOGICAL_W;
    H = LOGICAL_H;
  }
  window.addEventListener("resize", resize);
  window.addEventListener("orientationchange", resize);
  resize();

  const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
  if (isTouch) document.body.classList.add("touch");

  /* ================= 音效(WebAudio 合成,无外部文件) ================= */
  let audioCtx = null;
  function ensureAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch (e) { audioCtx = null; }
    }
    if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
  }
  function tone(freq, dur, type, vol, slideTo) {
    if (!audioCtx) return;
    try {
      const o = audioCtx.createOscillator();
      const g = audioCtx.createGain();
      o.type = type || "square";
      o.frequency.setValueAtTime(freq, audioCtx.currentTime);
      if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, audioCtx.currentTime + dur);
      g.gain.setValueAtTime(vol || 0.05, audioCtx.currentTime);
      g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + dur);
      o.connect(g).connect(audioCtx.destination);
      o.start();
      o.stop(audioCtx.currentTime + dur);
    } catch (e) { /* 忽略音频错误 */ }
  }
  const sfx = {
    shoot()   { tone(880, 0.07, "square", 0.025, 500); },        // 能量弹
    laser()   { tone(1200, 0.13, "sawtooth", 0.04, 300); },      // 菱形激光
    wheel()   { tone(320, 0.16, "triangle", 0.05, 150); },       // 旋转光轮
    homing()  { tone(500, 0.16, "square", 0.04, 900); },         // 追踪弹
    fan()     { tone(700, 0.08, "square", 0.04, 520); tone(900, 0.09, "square", 0.04, 620); }, // 扇形弹
    spiral()  { tone(420, 0.28, "sawtooth", 0.04, 190); },       // 螺旋弹
    explode() { tone(160, 0.28, "sawtooth", 0.07, 40); },
    powerup() { tone(520, 0.14, "triangle", 0.06); tone(780, 0.16, "triangle", 0.06, 1040); },
    downgrade(){ tone(400, 0.2, "sawtooth", 0.06, 160); },
    hit()     { tone(200, 0.25, "sawtooth", 0.08, 60); },
    combo()   { tone(660, 0.12, "square", 0.06, 990); },
    bossAlarm(){ tone(220, 0.5, "square", 0.08, 110); tone(330, 0.5, "square", 0.08, 165); },
    gameover(){ tone(300, 0.6, "sawtooth", 0.06, 60); }
  };

  /* ================= 游戏状态 ================= */
  let state = "menu"; // menu | playing | over
  let paused = false;
  let score = 0;
  let best = 0;
  let bossProgress = 0;
  const BOSS_EVERY = 500;
  let combo = 0;
  let comboTimer = 0;
  let comboPopupData = null;
  try { best = parseInt(localStorage.getItem("starThunderBest") || "0", 10) || 0; } catch (e) {}

  const player = {
    x: W / 2, y: H * 0.8,
    r: 16,
    speed: 340,
    lives: 3,
    level: 1,
    invincible: 0,
    fireCooldown: 0,
    alive: true
  };

  let bullets = [];
  let enemies = [];
  let enemyBullets = [];
  let powerups = [];
  let wingmen = [];
  let boss = null;
  let particles = [];
  let rings = [];
  let stars = [];
  let floatingTexts = [];

  let spawnTimer = 0;
  let spawnInterval = 1.15;
  let elapsed = 0;
  let shake = 0;
  let toastTimer = 0;
  let freezeTimer = 0;       // 顿帧(阶段切换)
  let phase2Popup = null;    // "Phase 2" 提示
  let bossAlertTimer = 0;    // 暴走红光强度

  /* ================= 输入(键盘) ================= */
  const keys = new Set();
  window.addEventListener("keydown", (e) => {
    const k = e.key.toLowerCase();
    if ([" ", "arrowup", "arrowdown", "arrowleft", "arrowright"].includes(k)) {
      e.preventDefault();
    }
    if (k === "p" || k === "escape") {
      if (state === "playing") togglePause();
      return;
    }
    keys.add(k);
    if ((k === " " || k === "enter") && state === "over") startGame();
    if ((k === " " || k === "enter") && state === "menu") startGame();
  });
  window.addEventListener("keyup", (e) => {
    keys.delete(e.key.toLowerCase());
  });
  window.addEventListener("blur", () => keys.clear());

  btnStart.addEventListener("click", () => {
    ensureAudio();
    if (state === "menu" || state === "over") startGame();
  });
  btnPause.addEventListener("click", () => {
    if (state === "playing") togglePause();
  });
  pauseOverlayEl.addEventListener("click", () => {
    if (state === "playing") togglePause();
  });

  /* ================= 触控输入 ================= */
  let moveVector = { x: 0, y: 0 };
  let fireHeld = false;
  let joyId = null;
  const JOY_RADIUS = 46;

  function joyCenter() {
    const r = joystickBase.getBoundingClientRect();
    return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
  }
  function joyUpdate(t) {
    const c = joyCenter();
    // 屏幕坐标 → 逻辑坐标(除以显示缩放系数,等价于 canvas.getBoundingClientRect() 换算)
    let dx = (t.clientX - c.x) / displayScale;
    let dy = (t.clientY - c.y) / displayScale;
    const d = Math.hypot(dx, dy);
    if (d > JOY_RADIUS) { dx = dx / d * JOY_RADIUS; dy = dy / d * JOY_RADIUS; }
    joystickKnob.style.transform = "translate(" + dx + "px," + dy + "px)";
    moveVector.x = dx / JOY_RADIUS;
    moveVector.y = dy / JOY_RADIUS;
  }
  function joyReset() {
    joyId = null;
    joystickKnob.style.transform = "translate(0px,0px)";
    moveVector.x = 0; moveVector.y = 0;
  }
  function findTouch(e, id) {
    for (let i = 0; i < e.touches.length; i++) {
      if (e.touches[i].identifier === id) return e.touches[i];
    }
    return null;
  }
  joystickBase.addEventListener("touchstart", (e) => {
    e.preventDefault();
    if (joyId !== null) return;
    const t = e.changedTouches[0];
    joyId = t.identifier;
    joyUpdate(t);
  }, { passive: false });
  joystickBase.addEventListener("touchmove", (e) => {
    e.preventDefault();
    const t = findTouch(e, joyId);
    if (t) joyUpdate(t);
  }, { passive: false });
  joystickBase.addEventListener("touchend", (e) => {
    for (let i = 0; i < e.changedTouches.length; i++) {
      if (e.changedTouches[i].identifier === joyId) joyReset();
    }
  });
  joystickBase.addEventListener("touchcancel", joyReset);

  fireBtn.addEventListener("touchstart", (e) => {
    e.preventDefault();
    ensureAudio();
    fireHeld = true;
  }, { passive: false });
  fireBtn.addEventListener("touchend", () => { fireHeld = false; });
  fireBtn.addEventListener("touchcancel", () => { fireHeld = false; });

  /* ================= 星空背景 ================= */
  function initStars() {
    stars = [];
    const count = Math.floor((W * H) / 9000);
    for (let i = 0; i < count; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H,
        r: Math.random() * 1.8 + 0.4,
        speed: Math.random() * 60 + 25,
        tw: Math.random() * Math.PI * 2
      });
    }
  }
  initStars();

  /* ================= 工具函数 ================= */
  const rand = (a, b) => a + Math.random() * (b - a);
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));

  function dist(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }
  function circlesHit(a, b) {
    const r = (a.r || 8) + (b.r || 8);
    return dist(a, b) < r;
  }

  function showToast(text, color) {
    toastEl.textContent = text;
    toastEl.style.color = color || "#ffffff";
    toastEl.classList.add("show");
    toastTimer = 1.8;
  }

  function addFloatingText(x, y, text, color, size) {
    floatingTexts.push({ x, y, text, color: color || "#ffffff", size: size || 16, life: 1.1, maxLife: 1.1 });
  }

  function addParticle(p) {
    if (particles.length >= MAX_PARTICLES) return;
    particles.push(p);
  }

  function burst(x, y, color, n, speed, size) {
    n = Math.round(n * particleScale);
    for (let i = 0; i < n; i++) {
      const ang = Math.random() * Math.PI * 2;
      const sp = rand(speed * 0.3, speed);
      addParticle({
        x, y,
        vx: Math.cos(ang) * sp,
        vy: Math.sin(ang) * sp,
        r: rand(size * 0.4, size),
        color,
        life: rand(0.3, 0.8),
        maxLife: 0.8,
        drag: 0.985
      });
    }
  }

  // 子弹命中:散射小碎片
  function sparkHit(x, y, color) {
    const n = Math.max(2, Math.round(4 * particleScale));
    for (let i = 0; i < n; i++) {
      const ang = Math.random() * Math.PI * 2;
      const sp = rand(70, 170);
      addParticle({
        x, y,
        vx: Math.cos(ang) * sp,
        vy: Math.sin(ang) * sp,
        r: rand(1, 2.4),
        color: color || "#ffffff",
        life: rand(0.2, 0.42),
        maxLife: 0.42,
        drag: 0.94
      });
    }
  }

  function addRing(x, y, color, maxR) {
    rings.push({ x, y, r: 4, maxR: maxR || 70, color, life: 0.5, maxLife: 0.5 });
  }

  function levelHalo() { return LEVEL_HALO[player.level] || LEVEL_HALO[1]; }

  function ddsFactor() { return 1 + score / 2000; }

  function comboMultiplier() {
    return Math.min(3, 1 + combo * 0.05);
  }

  function addScore(n) {
    score += n;
    bossProgress += n;
    maybeSpawnBoss();
  }

  function maybeSpawnBoss() {
    while (!boss && bossProgress >= BOSS_EVERY) {
      bossProgress -= BOSS_EVERY;
      spawnBoss();
    }
  }

  function triggerComboPopup() {
    comboPopupData = { text: "COMBO ×" + combo, life: 1.5, maxLife: 1.5 };
    shake = 8;
    sfx.combo();
  }

  /* ================= 弹幕发射 ================= */
  // 扇形弹:一次性射出 count 颗呈扇形
  function fireFan(x, y, count, spread, speed, color, kind) {
    const step = count > 1 ? spread / (count - 1) : 0;
    const base = Math.PI / 2;
    for (let i = 0; i < count; i++) {
      const ang = base - spread / 2 + step * i;
      enemyBullets.push({
        x, y, r: 5,
        vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed,
        kind: kind || "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
        color: color || "#7adcff"
      });
    }
  }

  // 螺旋弹:围绕中心旋转扩散
  function fireSpiral(x, y, count, radiusSpeed, angSpeed, color) {
    for (let i = 0; i < count; i++) {
      enemyBullets.push({
        x, y, r: 6,
        cx: x, cy: y,
        angle: (Math.PI * 2 / count) * i,
        radius: 10,
        radiusSpeed: radiusSpeed || 55,
        angSpeed: angSpeed || 2.4,
        kind: "wheel", pattern: "spiral", spin: rand(0, Math.PI * 2),
        color: color || "#ffd54f", life: 8
      });
    }
  }

  // 追踪弹:缓慢转向玩家
  function fireHoming(x, y, speed, color) {
    enemyBullets.push({
      x, y, r: 5,
      vx: 0, vy: speed || 150,
      turnRate: 3.0,
      kind: "laser", pattern: "homing", spin: 0,
      color: color || "#ff8a50", life: 3
    });
  }

  /* ================= 实体工厂 ================= */
  function spawnEnemy() {
    const diff = Math.min(1, score / 3000);
    const dds = ddsFactor();
    const speedMult = Math.min(dds, 2.0);
    const types = ["straight", "straight", "sine", "fast", "zigzag", "tank", "kamikaze", "transporter"];
    let type = types[Math.floor(Math.random() * types.length)];
    if (diff < 0.15 && (type === "tank" || type === "zigzag" || type === "kamikaze" || type === "transporter")) {
      type = "straight";
    }

    const base = {
      x: rand(W * 0.08, W * 0.92),
      y: -30,
      r: 16,
      hp: 1,
      speed: rand(90, 150) + diff * 90,
      type,
      phase: rand(0, Math.PI * 2),
      amp: rand(30, 90),
      shootTimer: rand(1, 3),
      flash: 0,
      worth: 100
    };

    switch (type) {
      case "sine":
        base.r = 14; base.speed = rand(70, 110) + diff * 60; base.worth = 150;
        break;
      case "fast":
        base.r = 12; base.speed = rand(220, 300) + diff * 100; base.worth = 200;
        break;
      case "zigzag":
        base.r = 14; base.speed = rand(130, 190) + diff * 70; base.worth = 180;
        base.vx = (Math.random() < 0.5 ? -1 : 1) * rand(90, 150);
        base.dirTimer = rand(0.6, 1.2);
        break;
      case "tank":
        base.r = 24; base.hp = 3; base.speed = rand(40, 70) + diff * 30; base.worth = 400;
        break;
      case "kamikaze":
        base.r = 13; base.hp = 1; base.speed = rand(300, 420); base.worth = 180;
        base.x = clamp(player.x + rand(-40, 40), 16, W - 16);
        base.aim = true;
        break;
      case "transporter":
        base.r = 20; base.hp = 2; base.worth = 250;
        base.vx = (Math.random() < 0.5 ? -1 : 1) * rand(80, 130);
        base.vy = rand(30, 55);
        base.shootTimer = null;
        break;
      default:
        base.r = 15; base.speed = rand(90, 150) + diff * 90; base.worth = 100;
    }

    base.hp = Math.max(1, Math.round(base.hp * dds));
    base.maxHp = base.hp;
    base.speed *= speedMult;
    if (base.vx !== undefined) base.vx *= speedMult;
    if (base.vy !== undefined) base.vy *= speedMult;

    enemies.push(base);
  }

  function spawnPowerup(x, y, forcedType) {
    let type = forcedType;
    if (!type) {
      const roll = Math.random();
      if (roll < 0.28) type = "score";
      else if (roll < 0.46) type = "upgrade";
      else if (roll < 0.56) type = "wingman";
      else if (roll < 0.66) type = "life";
      else if (roll < 0.74) type = "bomb";
      else if (roll < 0.90) type = "downgrade";
      else type = "score";
    }
    powerups.push({
      x, y, r: 12,
      type,
      vy: rand(70, 100),
      phase: rand(0, Math.PI * 2),
      life: 10
    });
  }

  function spawnBoss() {
    const dds = ddsFactor();
    const maxHp = Math.max(45, Math.round(45 * dds));
    boss = {
      x: W / 2, y: -90,
      r: 46,
      hp: maxHp, maxHp: maxHp,
      targetY: 110,
      phase: 1,          // 1 | 2
      entering: true,
      sineT: 0,
      ringTimer: 1.2,    // 圆形扩散弹
      aimTimer: 2.4,     // 瞄准三连射
      fanTimer: 1.1,     // 扇形密集弹
      dashSpeed: 420,    // 阶段2 冲刺速度
      dashState: "pause",// 冲刺状态机 dash | pause
      dashDir: 0,        // 冲刺锁定方向
      dashTimer: 0,      // 当前状态剩余时间
      laserState: "idle",// idle | charging | firing
      laserX: 0,
      laserTimer: 0,
      flash: 0
    };
    showToast("⚠ BOSS 来袭!", "#c9a0ff");
    sfx.bossAlarm();
    shake = 12;
  }

  function addWingmen() {
    wingmen = [];
    wingmen.push({ angle: 0, r: 10, x: player.x, y: player.y, fireCooldown: 0 });
    wingmen.push({ angle: Math.PI, r: 10, x: player.x, y: player.y, fireCooldown: 0 });
  }

  /* ================= 开始 / 暂停 / 重开 ================= */
  function togglePause() {
    paused = !paused;
    pauseOverlayEl.classList.toggle("hidden", !paused);
    btnPause.textContent = paused ? "▶" : "⏸";
  }

  function startGame() {
    ensureAudio();
    state = "playing";
    paused = false;
    pauseOverlayEl.classList.add("hidden");
    btnPause.textContent = "⏸";
    score = 0;
    bossProgress = 0;
    combo = 0;
    comboTimer = 0;
    comboPopupData = null;
    elapsed = 0;
    spawnTimer = 0.8;
    spawnInterval = 1.15;
    shake = 0;
    freezeTimer = 0;
    phase2Popup = null;
    bossAlertTimer = 0;

    player.x = W / 2;
    player.y = H * 0.8;
    player.lives = 3;
    player.level = 1;
    player.invincible = 0;
    player.fireCooldown = 0;
    player.alive = true;

    bullets = []; enemies = []; enemyBullets = []; powerups = [];
    wingmen = []; boss = null; particles = []; rings = []; floatingTexts = [];

    overlayEl.classList.add("hidden");
    updateHUD();
    showToast("战斗开始!", "#7adcff");
  }

  function gameOver() {
    state = "over";
    paused = false;
    pauseOverlayEl.classList.add("hidden");
    sfx.gameover();
    burst(player.x, player.y, "#00BFFF", 40, 320, 4);
    burst(player.x, player.y, "#ffffff", 20, 220, 3);
    addRing(player.x, player.y, "#00BFFF", 120);

    if (score > best) {
      best = score;
      try { localStorage.setItem("starThunderBest", String(best)); } catch (e) {}
    }

    document.querySelector("#overlay .title").textContent = "Game Over";
    document.querySelector("#overlay .subtitle").textContent = "GAME OVER";
    document.querySelector("#overlay .final-score")?.remove();
    document.querySelector("#overlay .record")?.remove();

    const controls = document.querySelector("#overlay .controls");
    const fs = document.createElement("div");
    fs.className = "final-score";
    fs.textContent = "本局得分 " + score;
    const rec = document.createElement("div");
    rec.className = "record";
    rec.textContent = "最高分 " + best;
    overlayEl.insertBefore(rec, controls);
    overlayEl.insertBefore(fs, rec);

    btnStart.textContent = "再来一局";
    overlayEl.classList.remove("hidden");
  }

  function backToMenu() {
    state = "menu";
    document.querySelector("#overlay .title").textContent = "星际雷霆";
    document.querySelector("#overlay .subtitle").textContent = "STAR THUNDER · 竖版卷轴射击";
    document.querySelector("#overlay .final-score")?.remove();
    document.querySelector("#overlay .record")?.remove();
    btnStart.textContent = "开始游戏";
    overlayEl.classList.remove("hidden");
  }

  function updateHUD() {
    scoreEl.textContent = String(score);
    livesEl.textContent = HEARTS.slice(0, Math.max(0, player.lives)).join("");
    levelEl.textContent = levelHalo().name;
    levelEl.style.color = levelHalo().color;
    wingmenEl.textContent = "僚机 ×" + wingmen.length;
    if (combo >= 2) {
      comboEl.textContent = "连击 ×" + combo + "  +" + Math.round((comboMultiplier() - 1) * 100) + "%";
      comboEl.style.opacity = "1";
    } else {
      comboEl.style.opacity = "0";
    }
  }

  /* ================= 更新逻辑 ================= */
  function update(dt) {
    if (state !== "playing") return;

    elapsed += dt;
    if (shake > 0) shake = Math.max(0, shake - dt * 30);
    if (toastTimer > 0) {
      toastTimer -= dt;
      if (toastTimer <= 0) toastEl.classList.remove("show");
    }

    if (combo > 0) {
      comboTimer -= dt;
      if (comboTimer <= 0) combo = 0;
    }

    if (comboPopupData) {
      comboPopupData.life -= dt;
      if (comboPopupData.life <= 0) comboPopupData = null;
    }

    /* --- 星空 --- */
    for (const s of stars) {
      s.y += s.speed * dt;
      s.tw += dt * 3;
      if (s.y > H + 5) { s.y = -5; s.x = Math.random() * W; }
    }

    /* --- 玩家移动(键盘 + 触控摇杆) --- */
    if (player.alive) {
      let dx = 0, dy = 0;
      const jm = Math.hypot(moveVector.x, moveVector.y);
      if (jm > 0.15) {
        dx = moveVector.x;
        dy = moveVector.y;
      } else {
        if (keys.has("a") || keys.has("arrowleft")) dx -= 1;
        if (keys.has("d") || keys.has("arrowright")) dx += 1;
        if (keys.has("w") || keys.has("arrowup")) dy -= 1;
        if (keys.has("s") || keys.has("arrowdown")) dy += 1;
        if (dx !== 0 && dy !== 0) { dx *= 0.7071; dy *= 0.7071; }
      }
      player.x = clamp(player.x + dx * player.speed * dt, 22, W - 22);
      player.y = clamp(player.y + dy * player.speed * dt, 40, H - 30);

      const halo = levelHalo().color;
      if (Math.random() < 0.9 * particleScale) {
        for (let n = 0; n < 2; n++) {
          const nx = player.x + (n === 0 ? -4 : 4);
          addParticle({
            x: nx + rand(-1, 1), y: player.y + player.r * 0.9,
            vx: rand(-10, 10), vy: rand(140, 260),
            r: rand(1.2, 2.8),
            color: Math.random() < 0.6 ? "#ffffff" : halo,
            life: rand(0.2, 0.45), maxLife: 0.45, drag: 0.96
          });
        }
      }

      player.fireCooldown -= dt;
      if ((fireHeld || keys.has(" ")) && player.fireCooldown <= 0) {
        fire();
        player.fireCooldown = 0.16;
      }

      const orbit = 34;
      for (const w of wingmen) {
        w.angle += dt * 2.4;
        w.x = player.x + Math.cos(w.angle) * orbit;
        w.y = player.y + Math.sin(w.angle) * orbit * 0.55;
        w.fireCooldown -= dt;
        if (w.fireCooldown <= 0) {
          w.fireCooldown = 0.4;
          bullets.push({ x: w.x, y: w.y - 8, vx: 0, vy: -460, r: 3, kind: "energy", pattern: "normal", spin: 0, color: "#7adcff", pierce: false });
          sfx.shoot();
        }
      }
    }

    if (player.invincible > 0) player.invincible -= dt;

    /* --- 敌人生成(Boss 战中减半) --- */
    spawnTimer -= dt;
    const diff = Math.min(1, score / 3000);
    spawnInterval = Math.max(0.34, 1.15 - diff * 0.8) * (boss ? 2 : 1);
    if (spawnTimer <= 0) {
      spawnEnemy();
      if (diff > 0.5 && Math.random() < 0.3) spawnEnemy();
      spawnTimer = spawnInterval * rand(0.8, 1.2);
    }

    /* --- 敌人更新 --- */
    for (let i = enemies.length - 1; i >= 0; i--) {
      const e = enemies[i];
      e.phase += dt * 3;
      if (e.flash > 0) e.flash -= dt;

      switch (e.type) {
        case "sine":
          e.y += e.speed * dt;
          e.x += Math.sin(e.phase) * 120 * dt;
          break;
        case "zigzag":
          e.y += e.speed * dt;
          e.dirTimer -= dt;
          if (e.dirTimer <= 0) { e.vx *= -1; e.dirTimer = rand(0.6, 1.2); }
          e.x += e.vx * dt;
          break;
        case "kamikaze":
          e.y += e.speed * dt;
          e.x += clamp(player.x - e.x, -120, 120) * dt;
          break;
        case "transporter":
          e.y += e.vy * dt;
          e.x += e.vx * dt;
          if (e.x < 16 || e.x > W - 16) e.vx *= -1;
          break;
        default:
          e.y += e.speed * dt;
      }
      e.x = clamp(e.x, 16, W - 16);

      // 敌方射击
      if ((e.type === "straight" || e.type === "tank") && e.shootTimer !== null) {
        e.shootTimer -= dt;
        if (e.shootTimer <= 0 && e.y < H * 0.6) {
          if (e.type === "tank") {
            // 精英:追踪激光
            fireHoming(e.x, e.y + e.r, 150, "#ff8a50");
            sfx.homing();
            e.shootTimer = 2.0;
          } else {
            // 普通:圆形能量弹
            enemyBullets.push({ x: e.x, y: e.y + e.r, r: 4, vx: 0, vy: 200, kind: "energy", pattern: "normal", spin: 0, color: "#7adcff" });
            sfx.shoot();
            e.shootTimer = rand(2, 3.4);
          }
        }
      }

      if (e.y > H + 40) { enemies.splice(i, 1); }
    }

    /* --- Boss 更新(多阶段动态 AI) --- */
    if (boss) {
      if (boss.flash > 0) boss.flash -= dt;
      if (bossAlertTimer > 0) bossAlertTimer -= dt;

      // 阶段切换:血量 <= 50%
      if (boss.phase === 1 && boss.hp <= boss.maxHp / 2) enterPhase2();

      if (boss.entering) {
        boss.y += 90 * dt;
        if (boss.y >= boss.targetY) { boss.y = boss.targetY; boss.entering = false; }
      } else if (boss.phase === 1) {
        // 第一阶段:缓慢 S 形正弦移动
        boss.sineT += dt * 0.9;
        boss.x = W / 2 + Math.sin(boss.sineT) * (W * 0.32);
        boss.y = boss.targetY + Math.sin(boss.sineT * 2) * 26;
        boss.x = clamp(boss.x, boss.r + 10, W - boss.r - 10);

        boss.ringTimer -= dt;
        if (boss.ringTimer <= 0) { bossRing(); boss.ringTimer = 1.5; }
        boss.aimTimer -= dt;
        if (boss.aimTimer <= 0) { bossAim(); boss.aimTimer = 2.2; }
      } else {
        // 第二阶段:暴走,水平冲刺追击(锁定方向 + 停顿,避免与玩家同步)
        boss.sineT += dt * 2;
        boss.y = boss.targetY + Math.sin(boss.sineT) * 12;

        boss.dashTimer -= dt;
        if (boss.dashState === "dash") {
          // 冲刺中:按锁定方向移动,不重新索敌
          boss.x += boss.dashDir * boss.dashSpeed * dt;
          if (boss.dashTimer <= 0) {
            boss.dashState = "pause";
            boss.dashTimer = 0.6;
          }
        } else {
          // 停顿期:横向静止,到点后重新锁定玩家方位冲刺
          if (boss.dashTimer <= 0) {
            boss.dashState = "dash";
            boss.dashDir = player.x >= boss.x ? 1 : -1;
            boss.dashTimer = 0.38;
          }
        }
        boss.x = clamp(boss.x, boss.r + 10, W - boss.r - 10);

        if (boss.laserState === "charging") {
          boss.laserTimer -= dt;
          if (boss.laserTimer <= 0) {
            boss.laserState = "firing";
            boss.laserTimer = 0.8;
            sfx.laser();
          }
        } else if (boss.laserState === "firing") {
          boss.laserTimer -= dt;
          if (player.alive && player.invincible <= 0 && Math.abs(player.x - boss.laserX) < 22 && player.y > boss.y - 12) {
            hurtPlayer();
          }
          if (boss.laserTimer <= 0) boss.laserState = "idle";
        } else {
          boss.fanTimer -= dt;
          if (boss.fanTimer <= 0) {
            if (Math.random() < 0.5) {
              bossFan();
              boss.fanTimer = 1.1;
            } else {
              boss.laserState = "charging";
              boss.laserX = player.x;
              boss.laserTimer = 1.0;
              sfx.homing();
              boss.fanTimer = 1.6;
            }
          }
        }
      }
    }

    /* --- 玩家子弹更新 --- */
    for (let i = bullets.length - 1; i >= 0; i--) {
      const b = bullets[i];
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      if (b.y < -20 || b.x < -20 || b.x > W + 20) bullets.splice(i, 1);
    }

    /* --- 敌方子弹更新(含追踪/螺旋弹道) --- */
    for (let i = enemyBullets.length - 1; i >= 0; i--) {
      const b = enemyBullets[i];
      b.spin = (b.spin || 0) + (b.kind === "wheel" ? dt * 12 : 0);

      if (b.pattern === "homing") {
        const desired = Math.atan2(player.y - b.y, player.x - b.x);
        const cur = Math.atan2(b.vy, b.vx);
        let d = desired - cur;
        while (d > Math.PI) d -= Math.PI * 2;
        while (d < -Math.PI) d += Math.PI * 2;
        const turn = clamp(d, -(b.turnRate || 3) * dt, (b.turnRate || 3) * dt);
        const na = cur + turn;
        const spd = Math.hypot(b.vx, b.vy) || 150;
        b.vx = Math.cos(na) * spd;
        b.vy = Math.sin(na) * spd;
        b.x += b.vx * dt;
        b.y += b.vy * dt;
      } else if (b.pattern === "spiral") {
        b.angle += b.angSpeed * dt;
        b.radius += b.radiusSpeed * dt;
        b.x = b.cx + Math.cos(b.angle) * b.radius;
        b.y = b.cy + Math.sin(b.angle) * b.radius;
      } else {
        b.x += (b.vx || 0) * dt;
        b.y += b.vy * dt;
      }

      if (b.life !== undefined) {
        b.life -= dt;
        if (b.life <= 0) { if (b.pattern === "homing") sparkHit(b.x, b.y, b.color); enemyBullets.splice(i, 1); continue; }
      }
      if (b.y > H + 40 || b.y < -40 || b.x < -40 || b.x > W + 40) enemyBullets.splice(i, 1);
    }

    /* --- 道具更新 --- */
    for (let i = powerups.length - 1; i >= 0; i--) {
      const p = powerups[i];
      p.y += p.vy * dt;
      p.phase += dt * 4;
      p.life -= dt;
      if (p.y > H + 20 || p.life <= 0) powerups.splice(i, 1);
    }

    /* --- 碰撞:子弹 vs 敌人 / Boss --- */
    for (let i = bullets.length - 1; i >= 0; i--) {
      const b = bullets[i];
      let removed = false;
      for (let j = enemies.length - 1; j >= 0; j--) {
        const e = enemies[j];
        if (circlesHit(b, e)) {
          e.hp -= 1;
          e.flash = 0.08;
          sparkHit(b.x, b.y, "#ffd54f");
          burst(b.x, b.y, "#ffd54f", 5, 140, 2.2);
          if (e.hp <= 0) destroyEnemy(e, j);
          if (!b.pierce) { removed = true; break; }
        }
      }
      if (!removed && boss && circlesHit(b, boss)) {
        boss.hp -= 1;
        boss.flash = 0.06;
        sparkHit(b.x, b.y, "#c9a0ff");
        burst(b.x, b.y, "#c9a0ff", 4, 120, 2);
        if (!b.pierce) removed = true;
        if (boss.hp <= 0) destroyBoss();
      }
      if (removed) bullets.splice(i, 1);
    }

    /* --- 碰撞:玩家 vs 敌人 / Boss / 敌方子弹 --- */
    if (player.alive && player.invincible <= 0) {
      for (let j = enemies.length - 1; j >= 0; j--) {
        const e = enemies[j];
        if (circlesHit(player, e)) {
          destroyEnemy(e, j);
          hurtPlayer();
          break;
        }
      }
      if (player.alive && boss && circlesHit(player, boss)) {
        hurtPlayer();
      }
    }
    if (player.alive && player.invincible <= 0) {
      for (let j = enemyBullets.length - 1; j >= 0; j--) {
        const b = enemyBullets[j];
        if (circlesHit(player, b)) {
          sparkHit(b.x, b.y, b.color);
          enemyBullets.splice(j, 1);
          hurtPlayer();
          break;
        }
      }
    }

    /* --- 碰撞:僚机 vs 敌人 / Boss / 敌方子弹 --- */
    if (player.alive) {
      for (let j = enemies.length - 1; j >= 0; j--) {
        const e = enemies[j];
        for (let k = wingmen.length - 1; k >= 0; k--) {
          if (circlesHit(e, wingmen[k])) {
            destroyEnemy(e, j);
            destroyWingman(k);
            break;
          }
        }
      }
      if (boss) {
        for (let k = wingmen.length - 1; k >= 0; k--) {
          if (circlesHit(boss, wingmen[k])) destroyWingman(k);
        }
      }
      for (let j = enemyBullets.length - 1; j >= 0; j--) {
        const b = enemyBullets[j];
        for (let k = wingmen.length - 1; k >= 0; k--) {
          if (circlesHit(b, wingmen[k])) {
            sparkHit(b.x, b.y, b.color);
            enemyBullets.splice(j, 1);
            destroyWingman(k);
            break;
          }
        }
      }
    }

    /* --- 碰撞:玩家 vs 道具 --- */
    if (player.alive) {
      for (let j = powerups.length - 1; j >= 0; j--) {
        const p = powerups[j];
        if (dist(player, p) < player.r + p.r + 6) {
          applyPowerup(p);
          powerups.splice(j, 1);
        }
      }
    }

    /* --- 粒子更新 --- */
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vx *= p.drag;
      p.vy *= p.drag;
      p.life -= dt;
      if (p.life <= 0) particles.splice(i, 1);
    }

    /* --- 冲击波环更新 --- */
    for (let i = rings.length - 1; i >= 0; i--) {
      const r = rings[i];
      r.r += (r.maxR - r.r) * 8 * dt;
      r.life -= dt;
      if (r.life <= 0) rings.splice(i, 1);
    }

    /* --- 飘字更新 --- */
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const f = floatingTexts[i];
      f.y -= 40 * dt;
      f.life -= dt;
      if (f.life <= 0) floatingTexts.splice(i, 1);
    }
  }

  /* ================= 战斗行为 ================= */
  function fire() {
    sfx.shoot();
    const py = player.y - player.r;
    const col = levelHalo().color;
    const pierce = player.level === 3;
    if (player.level === 1) {
      bullets.push({ x: player.x, y: py, vx: 0, vy: -520, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
    } else if (player.level === 2) {
      bullets.push({ x: player.x - 8, y: py, vx: 0, vy: -540, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
      bullets.push({ x: player.x + 8, y: py, vx: 0, vy: -540, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
    } else {
      bullets.push({ x: player.x - 12, y: py, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: true });
      bullets.push({ x: player.x, y: py - 4, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: "#ffffff", pierce: true });
      bullets.push({ x: player.x + 12, y: py, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: true });
    }
  }

  // 第一阶段:圆形扩散弹(12 发,向四周)
  function bossRing() {
    const count = 12;
    for (let i = 0; i < count; i++) {
      const ang = (Math.PI * 2 / count) * i;
      enemyBullets.push({
        x: boss.x, y: boss.y, r: 5,
        vx: Math.cos(ang) * 150, vy: Math.sin(ang) * 150,
        kind: "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
        color: "#7adcff", life: 6
      });
    }
    sfx.fan();
  }

  // 第一阶段:瞄准三连射
  function bossAim() {
    const ang = Math.atan2(player.y - boss.y, player.x - boss.x);
    for (let i = -1; i <= 1; i++) {
      const a = ang + i * 0.09;
      enemyBullets.push({
        x: boss.x, y: boss.y, r: 5,
        vx: Math.cos(a) * 280, vy: Math.sin(a) * 280,
        kind: "laser", pattern: "normal", spin: 0,
        color: "#ff8a50", life: 6
      });
    }
    sfx.homing();
  }

  // 第二阶段:扇形密集弹(15 发 / 30 度)
  function bossFan() {
    const ang = Math.atan2(player.y - boss.y, player.x - boss.x);
    const count = 15;
    const spread = Math.PI / 6;
    const step = spread / (count - 1);
    for (let i = 0; i < count; i++) {
      const a = ang - spread / 2 + step * i;
      enemyBullets.push({
        x: boss.x, y: boss.y, r: 4,
        vx: Math.cos(a) * 230, vy: Math.sin(a) * 230,
        kind: "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
        color: "#7adcff", life: 6
      });
    }
    sfx.fan();
  }

  // 进入第二阶段(暴走)
  function enterPhase2() {
    boss.phase = 2;
    boss.laserState = "idle";
    boss.laserTimer = 0;
    boss.fanTimer = 0.6;
    freezeTimer = 0.6;      // 顿帧
    phase2Popup = { life: 2.2, maxLife: 2.2 };
    bossAlertTimer = 2.0;
    shake = 18;
    sfx.bossAlarm();
    burst(boss.x, boss.y, "#ff5252", 40, 420, 4);
    addRing(boss.x, boss.y, "#ff5252", 220);
  }

  function destroyEnemy(e, index) {
    enemies.splice(index, 1);

    combo += 1;
    comboTimer = COMBO_WINDOW;
    const mult = comboMultiplier();
    const gained = Math.round(e.worth * mult);
    addScore(gained);

    sfx.explode();
    shake = 6;

    const palette = e.type === "tank"
      ? ["#8A2BE2", "#c9a0ff", "#ffd54f", "#ffffff"]
      : ["#ff5252", "#ff7043", "#ffd54f"];
    burst(e.x, e.y, palette[0], 18, 260, 3.5);
    burst(e.x, e.y, palette[1], 12, 200, 2.6);
    addRing(e.x, e.y, palette[0], 46);
    addFloatingText(e.x, e.y, "+" + gained + (combo >= 2 ? " ×" + mult.toFixed(1) : ""), "#ffd54f", 16);

    if (combo > 0 && combo % 10 === 0) triggerComboPopup();

    if (e.type === "transporter") {
      addFloatingText(e.x, e.y - 26, "3 个道具!", "#26c6da", 16);
      const goods = ["upgrade", "wingman", "life"];
      for (let g = 0; g < 3; g++) {
        spawnPowerup(e.x + rand(-36, 36), e.y + rand(-12, 12), goods[g]);
      }
    } else {
      const dropChance = e.type === "tank" ? 0.45 : 0.14;
      if (Math.random() < dropChance) spawnPowerup(e.x, e.y);
    }

    updateHUD();
  }

  function destroyBoss() {
    const dds = ddsFactor();
    score *= 2;
    shake = 30;
    sfx.explode();
    sfx.powerup();
    const bx = boss.x, by = boss.y;
    burst(bx, by, "#8A2BE2", 60, 420, 5);
    burst(bx, by, "#c9a0ff", 40, 320, 4);
    burst(bx, by, "#ffffff", 30, 260, 3);
    addRing(bx, by, "#8A2BE2", 180);
    addRing(bx, by, "#c9a0ff", 120);
    addFloatingText(bx, by - 20, "BOSS 击败!分数翻倍!", "#c9a0ff", 24);

    const sChance = Math.min(0.8, 0.4 + (dds - 1) * 0.5);
    for (let i = 0; i < 3; i++) {
      const r = Math.random();
      const type = r < sChance ? "upgrade" : (r < sChance + 0.25 ? "wingman" : "life");
      spawnPowerup(bx + rand(-50, 50), by + rand(-20, 30), type);
    }

    boss = null;
    updateHUD();
  }

  function destroyWingman(k) {
    const w = wingmen[k];
    wingmen.splice(k, 1);
    burst(w.x, w.y, "#00BFFF", 14, 220, 2.6);
    addRing(w.x, w.y, "#00BFFF", 40);
    sfx.hit();
    addFloatingText(w.x, w.y, "僚机损毁!", "#9be7ff", 14);
    updateHUD();
  }

  function hurtPlayer() {
    player.lives -= 1;
    player.level = 1;
    player.invincible = 2.2;
    combo = 0;
    shake = 14;
    sfx.hit();
    burst(player.x, player.y, levelHalo().color, 26, 280, 3.5);
    burst(player.x, player.y, "#ff5252", 16, 220, 3);
    addRing(player.x, player.y, "#ff5252", 90);
    addFloatingText(player.x, player.y - 30, "命中!", "#ff8a80", 18);
    updateHUD();

    if (player.lives <= 0) {
      player.alive = false;
      gameOver();
    }
  }

  function applyPowerup(p) {
    let msg, color;
    switch (p.type) {
      case "upgrade":
        player.level = Math.min(3, player.level + 1);
        msg = "火力升级 " + levelHalo().name + "!"; color = "#ffd54f";
        sfx.powerup();
        break;
      case "downgrade":
        player.level = Math.max(1, player.level - 1);
        msg = "火力降级 " + levelHalo().name; color = "#ff8a80";
        sfx.downgrade();
        break;
      case "wingman":
        if (wingmen.length < 2) {
          addWingmen();
          msg = "僚机出击!"; color = "#7adcff";
        } else {
          addScore(200);
          msg = "+200 分"; color = "#9fe6c0";
        }
        sfx.powerup();
        break;
      case "score":
        addScore(500);
        msg = "+500 分"; color = "#9fe6c0";
        sfx.powerup();
        break;
      case "life":
        player.lives = Math.min(5, player.lives + 1);
        msg = "+1 生命"; color = "#69f0ae";
        sfx.powerup();
        break;
      case "bomb":
        clearAllEnemies();
        msg = "清屏炸弹!"; color = "#7adcff";
        sfx.explode();
        break;
    }
    burst(p.x, p.y, color, 16, 200, 3);
    addRing(p.x, p.y, color, 56);
    addFloatingText(p.x, p.y, msg, color, 17);
    if (p.type !== "score" && p.type !== "bomb") showToast(msg, color);
    updateHUD();
  }

  function clearAllEnemies() {
    for (let i = enemies.length - 1; i >= 0; i--) {
      const e = enemies[i];
      addScore(e.worth);
      burst(e.x, e.y, "#7adcff", 16, 260, 3);
    }
    enemies = [];
    enemyBullets = [];
    shake = 20;
    addRing(player.x, player.y, "#7adcff", 160);
    sfx.explode();
    updateHUD();
  }

  /* ================= 渲染 ================= */
  // 子弹渲染:能量弹 / 菱形激光 / 旋转光轮
  function drawBullet(b) {
    if (b.kind === "laser") {
      // 菱形激光(红橙渐变,细长)
      ctx.save();
      ctx.translate(b.x, b.y);
      const ang = Math.atan2(b.vy || 0, b.vx || 0) + Math.PI / 2;
      ctx.rotate(ang);
      ctx.shadowColor = "#ff8a50";
      ctx.shadowBlur = 12;
      const len = b.r * 3.4, wd = b.r * 0.85;
      const g = ctx.createLinearGradient(0, -len / 2, 0, len / 2);
      g.addColorStop(0, "#ff8a50");
      g.addColorStop(0.5, "#ff5252");
      g.addColorStop(1, "#ff8a50");
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.moveTo(0, -len / 2);
      ctx.lineTo(wd, 0);
      ctx.lineTo(0, len / 2);
      ctx.lineTo(-wd, 0);
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = "#ffffff";
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.restore();
    } else if (b.kind === "wheel") {
      // 旋转光轮(半月形旋转)
      ctx.save();
      ctx.translate(b.x, b.y);
      ctx.rotate(b.spin || 0);
      ctx.shadowColor = b.color || "#ffd54f";
      ctx.shadowBlur = 12;
      ctx.fillStyle = b.color || "#ffd54f";
      ctx.beginPath();
      ctx.arc(0, 0, b.r, 0, Math.PI, false);
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = "#ffffff";
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.restore();
    } else {
      // 圆形能量弹(蓝白渐变 + 拖尾光晕)
      const mid = b.color || "#7adcff";
      ctx.save();
      ctx.translate(b.x, b.y);
      const ang = Math.atan2(b.vy || 0, b.vx || 0);
      ctx.rotate(ang);
      ctx.shadowColor = mid;
      ctx.shadowBlur = 14;
      ctx.globalCompositeOperation = "lighter";
      // 拖尾
      const tg = ctx.createLinearGradient(0, 0, -b.r * 3.5, 0);
      tg.addColorStop(0, "rgba(255,255,255,0.85)");
      tg.addColorStop(1, "rgba(122,220,255,0)");
      ctx.fillStyle = tg;
      ctx.beginPath();
      ctx.ellipse(0, 0, b.r * 3.5, b.r * 0.8, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalCompositeOperation = "source-over";
      // 核心
      const g = ctx.createRadialGradient(0, 0, 0, 0, 0, b.r);
      g.addColorStop(0, "#ffffff");
      g.addColorStop(0.5, mid);
      g.addColorStop(1, "rgba(122,220,255,0)");
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(0, 0, b.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }

  function drawShip(x, y, r, invincible) {
    if (invincible && Math.floor(invincible * 12) % 2 === 0) return;
    const halo = levelHalo().color;
    ctx.save();
    ctx.translate(x, y);

    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    ctx.globalAlpha = 0.35 + 0.2 * Math.sin(elapsed * 6);
    const aura = ctx.createRadialGradient(0, 0, r * 0.2, 0, 0, r * 2.3);
    aura.addColorStop(0, halo);
    aura.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = aura;
    ctx.beginPath();
    ctx.arc(0, 0, r * 2.3, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    ctx.shadowColor = halo;
    ctx.shadowBlur = 16;

    ctx.beginPath();
    ctx.moveTo(0, -r * 1.6);
    ctx.lineTo(r * 0.85, r * 0.45);
    ctx.lineTo(0, r * 1.05);
    ctx.lineTo(-r * 0.85, r * 0.45);
    ctx.closePath();

    const g = ctx.createLinearGradient(0, -r * 1.6, 0, r * 1.05);
    g.addColorStop(0, PLAYER_BODY_B);
    g.addColorStop(1, PLAYER_BODY_A);
    ctx.fillStyle = g;
    ctx.fill();
    ctx.strokeStyle = "rgba(255,255,255,0.9)";
    ctx.lineWidth = 1;
    ctx.stroke();

    ctx.shadowBlur = 0;
    ctx.beginPath();
    ctx.ellipse(0, -r * 0.4, r * 0.26, r * 0.5, 0, 0, Math.PI * 2);
    ctx.fillStyle = "rgba(255,255,255,0.95)";
    ctx.fill();

    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    const fl = r * 0.5 + rand(2, 8);
    const fg = ctx.createLinearGradient(0, r * 0.9, 0, r * 0.9 + fl);
    fg.addColorStop(0, "#ffffff");
    fg.addColorStop(0.4, halo);
    fg.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = fg;
    ctx.beginPath();
    ctx.moveTo(-r * 0.2, r * 0.9);
    ctx.lineTo(r * 0.2, r * 0.9);
    ctx.lineTo(0, r * 0.9 + fl);
    ctx.closePath();
    ctx.fill();
    ctx.restore();

    ctx.restore();
  }

  function drawWingman(w) {
    ctx.save();
    ctx.translate(w.x, w.y);
    ctx.shadowColor = "#7adcff";
    ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.moveTo(0, -12);
    ctx.lineTo(7, 6);
    ctx.lineTo(0, 3);
    ctx.lineTo(-7, 6);
    ctx.closePath();
    const g = ctx.createLinearGradient(0, -12, 0, 6);
    g.addColorStop(0, "#00BFFF");
    g.addColorStop(1, "#1E90FF");
    ctx.fillStyle = g;
    ctx.fill();
    ctx.strokeStyle = "rgba(255,255,255,0.9)";
    ctx.lineWidth = 1;
    ctx.stroke();
    ctx.restore();
  }

  function enemyStyle(type) {
    if (type === "tank") return { tier: "elite", c1: "#4a1280", c2: "#8A2BE2", glow: "#c9a0ff", core: "#e8d5ff" };
    if (type === "zigzag" || type === "transporter") return { tier: "medium", c1: "#8a6a00", c2: "#DAA520", glow: "#ffe08a", core: "#fff3c4" };
    return { tier: "small", c1: "#4a0000", c2: "#8B0000", glow: "#ff7a7a", core: "#ffb3b3" };
  }

  function drawEnemy(e) {
    const st = enemyStyle(e.type);
    ctx.save();
    ctx.translate(e.x, e.y);

    if (st.tier === "elite") {
      ctx.save();
      ctx.globalCompositeOperation = "lighter";
      ctx.globalAlpha = 0.45 + 0.25 * Math.sin(elapsed * 5 + e.phase);
      ctx.beginPath();
      ctx.arc(0, 0, e.r + 9, 0, Math.PI * 2);
      ctx.strokeStyle = st.glow;
      ctx.lineWidth = 1.5;
      ctx.stroke();
      ctx.restore();
    }

    if (st.tier === "medium") {
      ctx.save();
      ctx.rotate(e.phase * 2);
      ctx.strokeStyle = st.glow;
      ctx.lineWidth = 1.5;
      for (let a = 0; a < 3; a++) {
        ctx.rotate(Math.PI * 2 / 3);
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(e.r + 7, 0);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(e.r + 7, 0, 2.6, 0, Math.PI * 2);
        ctx.fillStyle = st.glow;
        ctx.fill();
      }
      ctx.restore();
    }

    if (e.flash > 0) {
      ctx.shadowColor = "#ffffff";
      ctx.shadowBlur = 24;
    } else {
      ctx.shadowColor = st.glow;
      ctx.shadowBlur = 10;
    }

    if (e.type === "transporter") {
      ctx.beginPath();
      ctx.roundRect(-e.r, -e.r * 0.5, e.r * 2, e.r * 1.1, 4);
      const tg = ctx.createLinearGradient(0, -e.r, 0, e.r);
      tg.addColorStop(0, st.c2);
      tg.addColorStop(1, st.c1);
      ctx.fillStyle = e.flash > 0 ? "#ffffff" : tg;
      ctx.fill();
      ctx.strokeStyle = st.glow;
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.fillStyle = st.c2;
      ctx.fillRect(-e.r * 0.7, -e.r * 0.2, e.r * 0.5, e.r * 0.5);
      ctx.fillRect(0.2 * e.r, -e.r * 0.2, e.r * 0.5, e.r * 0.5);
      ctx.shadowBlur = 0;
      ctx.beginPath();
      ctx.arc(0, 0, e.r * 0.2, 0, Math.PI * 2);
      ctx.fillStyle = st.core;
      ctx.fill();
      ctx.restore();
      return;
    }

    ctx.beginPath();
    ctx.moveTo(0, e.r * 1.3);
    ctx.lineTo(e.r, -e.r * 0.4);
    ctx.lineTo(e.r * 0.5, -e.r * 0.15);
    ctx.lineTo(0, -e.r * 0.6);
    ctx.lineTo(-e.r * 0.5, -e.r * 0.15);
    ctx.lineTo(-e.r, -e.r * 0.4);
    ctx.closePath();

    const g = ctx.createLinearGradient(0, -e.r, 0, e.r * 1.3);
    g.addColorStop(0, st.c2);
    g.addColorStop(1, st.c1);
    ctx.fillStyle = e.flash > 0 ? "#ffffff" : g;
    ctx.fill();
    ctx.strokeStyle = st.glow;
    ctx.lineWidth = 1;
    ctx.stroke();

    ctx.shadowBlur = 0;
    ctx.beginPath();
    ctx.arc(0, e.r * 0.15, e.r * 0.26, 0, Math.PI * 2);
    ctx.fillStyle = st.core;
    ctx.fill();

    if (e.type === "kamikaze") {
      ctx.strokeStyle = "rgba(255,255,255,0.9)";
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(0, 0, e.r + 6 + Math.sin(e.phase * 4) * 2, 0, Math.PI * 2);
      ctx.stroke();
    }

    if (e.type === "tank" && e.hp < e.maxHp) {
      const w = e.r * 1.6;
      ctx.fillStyle = "rgba(0,0,0,0.5)";
      ctx.fillRect(-w / 2, -e.r - 14, w, 5);
      ctx.fillStyle = "#c9a0ff";
      ctx.fillRect(-w / 2, -e.r - 14, w * (e.hp / e.maxHp), 5);
    }

    ctx.restore();
  }

  function drawBoss() {
    const b = boss;
    ctx.save();
    ctx.translate(b.x, b.y);

    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    ctx.globalAlpha = 0.4 + 0.25 * Math.sin(elapsed * 4);
    ctx.beginPath();
    ctx.arc(0, 0, b.r + 18, 0, Math.PI * 2);
    ctx.strokeStyle = "#c9a0ff";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.restore();

    ctx.shadowColor = b.flash > 0 ? "#ffffff" : "#8A2BE2";
    ctx.shadowBlur = b.flash > 0 ? 30 : 22;

    ctx.beginPath();
    ctx.moveTo(0, -b.r);
    ctx.lineTo(b.r, -b.r * 0.3);
    ctx.lineTo(b.r * 0.6, b.r * 0.2);
    ctx.lineTo(b.r * 0.9, b.r * 0.7);
    ctx.lineTo(0, b.r);
    ctx.lineTo(-b.r * 0.9, b.r * 0.7);
    ctx.lineTo(-b.r * 0.6, b.r * 0.2);
    ctx.lineTo(-b.r, -b.r * 0.3);
    ctx.closePath();

    const g = ctx.createLinearGradient(0, -b.r, 0, b.r);
    g.addColorStop(0, "#c9a0ff");
    g.addColorStop(0.5, "#8A2BE2");
    g.addColorStop(1, "#3d0a5e");
    ctx.fillStyle = b.flash > 0 ? "#ffffff" : g;
    ctx.fill();
    ctx.strokeStyle = "#e8d5ff";
    ctx.lineWidth = 1;
    ctx.stroke();

    ctx.shadowBlur = 0;
    ctx.beginPath();
    ctx.arc(0, 0, b.r * 0.36, 0, Math.PI * 2);
    ctx.fillStyle = "#ffd54f";
    ctx.fill();
    ctx.beginPath();
    ctx.arc(0, 0, b.r * 0.18, 0, Math.PI * 2);
    ctx.fillStyle = "#ffffff";
    ctx.fill();

    ctx.restore();
  }

  function drawLaser() {
    if (boss.laserState === "charging") {
      const on = Math.floor(elapsed * 12) % 2 === 0;
      ctx.save();
      ctx.globalAlpha = on ? 0.85 : 0.3;
      ctx.strokeStyle = "#ff5252";
      ctx.lineWidth = 3;
      ctx.setLineDash([10, 8]);
      ctx.beginPath();
      ctx.moveTo(boss.laserX, boss.y + boss.r * 0.4);
      ctx.lineTo(boss.laserX, H);
      ctx.stroke();
      ctx.restore();
    } else if (boss.laserState === "firing") {
      const bw = 26;
      ctx.save();
      ctx.globalCompositeOperation = "lighter";
      const g = ctx.createLinearGradient(boss.laserX - bw, 0, boss.laserX + bw, 0);
      g.addColorStop(0, "rgba(255,60,60,0)");
      g.addColorStop(0.5, "#ffffff");
      g.addColorStop(1, "rgba(255,60,60,0)");
      ctx.fillStyle = g;
      ctx.fillRect(boss.laserX - bw, boss.y - 10, bw * 2, H - boss.y + 10);
      ctx.shadowColor = "#ff5252";
      ctx.shadowBlur = 22;
      ctx.fillStyle = "rgba(255,82,82,0.55)";
      ctx.fillRect(boss.laserX - 7, boss.y - 10, 14, H - boss.y + 10);
      ctx.restore();
    }
  }

  function drawBossAlert() {
    const extra = bossAlertTimer > 0 ? bossAlertTimer : 0;
    const base = 0.16 + 0.12 * Math.sin(elapsed * 8);
    const intensity = clamp(base + extra * 0.35, 0, 0.7);
    const g = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.max(W, H) * 0.72);
    g.addColorStop(0, "rgba(255,0,0,0)");
    g.addColorStop(1, "rgba(255,30,30," + intensity + ")");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);
  }

  function drawBossBar() {
    const bw = Math.min(W * 0.6, 420);
    const x = (W - bw) / 2, y = 112;
    const segments = 2;                       // 两段血条(50% 为界)
    const segW = bw / segments;
    const hpPer = boss.maxHp / segments;

    ctx.fillStyle = "rgba(0,0,0,0.55)";
    ctx.fillRect(x - 4, y - 4, bw + 8, 20);

    for (let s = 0; s < segments; s++) {
      const frac = clamp((boss.hp - boss.maxHp + (s + 1) * hpPer) / hpPer, 0, 1);
      ctx.fillStyle = "rgba(255,255,255,0.12)";
      ctx.fillRect(x + s * segW, y, segW - 2, 12);
      if (frac > 0) {
        const g = ctx.createLinearGradient(0, y, 0, y + 12);
        if (s === 0) {
          g.addColorStop(0, "#c9a0ff");
          g.addColorStop(1, "#8A2BE2");
        } else {
          g.addColorStop(0, "#ff8a80");
          g.addColorStop(1, "#c62828");
        }
        ctx.fillStyle = g;
        ctx.fillRect(x + s * segW, y, (segW - 2) * frac, 12);
      }
    }
    ctx.fillStyle = boss.phase === 2 ? "#ff5252" : "#ffffff";
    ctx.font = "bold " + Math.round(W / 40) + "px 'Segoe UI', 'Microsoft YaHei', sans-serif";
    ctx.textAlign = "center";
    ctx.fillText(boss.phase === 2 ? "BOSS · PHASE 2" : "BOSS", W / 2, y - 10);
  }

  function drawPowerup(p) {
    const bob = Math.sin(p.phase) * 3;
    ctx.save();
    ctx.translate(p.x, p.y + bob);

    const colors = {
      upgrade: "#ffd54f", downgrade: "#ff8a80", wingman: "#7adcff",
      life: "#69f0ae", score: "#9fe6c0", bomb: "#7adcff"
    };
    const c = colors[p.type];
    ctx.shadowColor = c;
    ctx.shadowBlur = 18;

    ctx.beginPath();
    ctx.arc(0, 0, p.r, 0, Math.PI * 2);
    ctx.fillStyle = "rgba(10,18,40,0.85)";
    ctx.fill();
    ctx.strokeStyle = c;
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.shadowBlur = 0;
    ctx.fillStyle = c;
    ctx.font = "bold " + Math.round(W / 36) + "px 'Segoe UI', sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    const label = { upgrade: "S", downgrade: "D", wingman: "W", life: "♥", score: "+", bomb: "✹" }[p.type];
    ctx.fillText(label, 0, 1);

    ctx.restore();
  }

  function drawBackground() {
    const bg = ctx.createLinearGradient(0, 0, 0, H);
    bg.addColorStop(0, "#050818");
    bg.addColorStop(0.5, "#0a1030");
    bg.addColorStop(1, "#03040a");
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    const neb = ctx.createRadialGradient(W * 0.3, H * 0.25, 0, W * 0.3, H * 0.25, 300);
    neb.addColorStop(0, "rgba(47,90,180,0.14)");
    neb.addColorStop(1, "rgba(47,90,180,0)");
    ctx.fillStyle = neb;
    ctx.fillRect(0, 0, W, H);

    const neb2 = ctx.createRadialGradient(W * 0.8, H * 0.6, 0, W * 0.8, H * 0.6, 340);
    neb2.addColorStop(0, "rgba(120,40,160,0.12)");
    neb2.addColorStop(1, "rgba(120,40,160,0)");
    ctx.fillStyle = neb2;
    ctx.fillRect(0, 0, W, H);
    ctx.restore();

    for (const s of stars) {
      const a = 0.5 + 0.5 * Math.sin(s.tw);
      ctx.globalAlpha = a;
      ctx.fillStyle = "#cfe6ff";
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function render() {
    ctx.clearRect(0, 0, W, H);

    ctx.save();
    if (shake > 0) {
      ctx.translate(rand(-shake, shake) * 0.5, rand(-shake, shake) * 0.5);
    }

    drawBackground();

    for (const p of powerups) drawPowerup(p);

    for (const b of enemyBullets) drawBullet(b);
    for (const b of bullets) drawBullet(b);

    for (const e of enemies) drawEnemy(e);

    if (boss) drawBoss();
    if (boss) drawLaser();

    for (const w of wingmen) drawWingman(w);

    if (player.alive) {
      drawShip(player.x, player.y, player.r, player.invincible > 0);
      if (player.invincible > 0) {
        ctx.save();
        ctx.globalCompositeOperation = "lighter";
        ctx.beginPath();
        ctx.arc(player.x, player.y, player.r + 10, 0, Math.PI * 2);
        ctx.strokeStyle = "rgba(122,220,255," + (0.4 + 0.3 * Math.sin(elapsed * 20)) + ")";
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.restore();
      }
    }

    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    for (const p of particles) {
      ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
    ctx.globalAlpha = 1;

    for (const r of rings) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, r.life / r.maxLife);
      ctx.strokeStyle = r.color;
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(r.x, r.y, r.r, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();
    }

    for (const f of floatingTexts) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, f.life / f.maxLife);
      ctx.fillStyle = f.color;
      ctx.font = "bold " + Math.round((W / 30) * (f.size / 16)) + "px 'Segoe UI', 'Microsoft YaHei', sans-serif";
      ctx.textAlign = "center";
      ctx.shadowColor = f.color;
      ctx.shadowBlur = 8;
      ctx.fillText(f.text, f.x, f.y);
      ctx.restore();
    }

    ctx.restore();

    if (boss && boss.phase === 2) drawBossAlert();
    if (boss) drawBossBar();

    if (comboPopupData) {
      const cp = comboPopupData;
      const prog = 1 - cp.life / cp.maxLife;
      const scale = 0.5 + Math.min(1, prog * 5) * 0.7;
      const alpha = cp.life > 0.6 ? 1 : (cp.life / 0.6);
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.translate(W / 2, H * 0.35);
      ctx.scale(scale, scale);
      ctx.fillStyle = "#ffd54f";
      ctx.font = "bold " + Math.round(W / 16) + "px 'Segoe UI', 'Microsoft YaHei', sans-serif";
      ctx.textAlign = "center";
      ctx.shadowColor = "#ffd54f";
      ctx.shadowBlur = 30;
      ctx.fillText(cp.text, 0, 0);
      ctx.restore();
    }

    if (phase2Popup) {
      const pp = phase2Popup;
      const prog = 1 - pp.life / pp.maxLife;
      const scale = 1 + Math.min(1, prog * 4) * 0.6;
      const alpha = pp.life > 1.6 ? 1 : (pp.life / 1.6);
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.translate(W / 2, H * 0.4);
      ctx.scale(scale, scale);
      ctx.fillStyle = "#ff5252";
      ctx.font = "bold " + Math.round(W / 15) + "px 'Segoe UI', 'Microsoft YaHei', sans-serif";
      ctx.textAlign = "center";
      ctx.shadowColor = "#ff5252";
      ctx.shadowBlur = 40;
      ctx.fillText("PHASE 2", 0, 0);
      ctx.restore();
    }
  }

  /* ================= 主循环 ================= */
  let last = performance.now();
  let fpsAccum = 0, fpsFrames = 0, fps = 60;
  function loop(now) {
    let dt = (now - last) / 1000;
    last = now;
    if (dt > 0.05) dt = 0.05;

    // FPS 统计 + 自适应粒子密度(卡顿自动降低发射量)
    fpsAccum += dt;
    fpsFrames++;
    if (fpsAccum >= 0.5) {
      fps = fpsFrames / fpsAccum;
      fpsAccum = 0;
      fpsFrames = 0;
      if (fps < 45) particleScale = Math.max(0.3, particleScale - 0.15);
      else if (fps > 55) particleScale = Math.min(1, particleScale + 0.05);
    }

    if (phase2Popup) {
      phase2Popup.life -= dt;
      if (phase2Popup.life <= 0) phase2Popup = null;
    }

    if (freezeTimer > 0) {
      freezeTimer -= dt;   // 顿帧:冻结世界,仍渲染
    } else if (!paused) {
      update(dt);
    }
    render();
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);

  updateHUD();
  backToMenu();
})();
</script>
</body>
</html>

Game Source: 星际雷霆 · 竖版卷轴飞行射击

Creator: SuperOwl71

Libraries: none

Complexity: complex (2238 lines, 66.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-superowl71-msvxkrjx" to link back to the original. Then publish at arcadelab.ai/publish.