(() => {
  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const LOGICAL_W = Number(canvas.getAttribute("width")) || 480;
  const LOGICAL_H = Number(canvas.getAttribute("height")) || 720;
  function resizeCanvasForDisplay() {
    // Keep high-DPI rendering crisp without repainting 6.25x as many pixels.
    const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
    const width = Math.round(LOGICAL_W * dpr);
    const height = Math.round(LOGICAL_H * dpr);
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
    }
  }
  resizeCanvasForDisplay();
  window.addEventListener("resize", resizeCanvasForDisplay);
  const overlay = document.getElementById("overlay");
  const startButton = document.getElementById("startButton");
  const demoClearActions = document.getElementById("demoClearActions");
  const demoExitButton = document.getElementById("demoExitButton");
  const demoReplayButton = document.getElementById("demoReplayButton");
  const demoHpButton = document.getElementById("demoHpButton");
  const devTools = document.getElementById("devTools");
  const scoreEl = document.getElementById("score");
  const hpEl = document.getElementById("hp");
  const livesEl = document.getElementById("lives");
  const bombsEl = document.getElementById("bombs");
  const powerEl = document.getElementById("power");
  const powerStockEl = document.getElementById("powerStock");
  const hpBarFillEl = document.getElementById("hpBarFill");
  const casualtyFeedEl = document.getElementById("casualtyFeed");
  const casualtyStatsEl = document.getElementById("casualtyStats");

  const DEV_TOOLS = false;
  const W = LOGICAL_W;
  const H = LOGICAL_H;
  const keys = new Set();
  const player = { x: W / 2, y: H - 86, r: 6, hp: 100, maxHp: 100, lives: 3, bombs: 3, inv: 0, fire: 0, power: 1, powerStock: 0, respawn: 0, respawnMax: 0, hitPulse: 0 };
  const SCORE_1UP_FIRST = 60000;
  const SCORE_1UP_INTERVAL = 120000;
  const MAX_LIVES = 9;
  const STAGE1_DEMO = true;
  const assets = {
    sprites: loadImage("./assets/sprites.png"),
    extraSprites: loadImage("./assets/extra-enemies.png"),
    rabbitMidboss: loadImage("./assets/rabbit-midboss.png"),
    midPilot: loadImage("./assets/midboss-pilot.png?v=20260705-131912"),
    midPilotCrisis: loadImage("./assets/midboss-pilot-crisis.png?v=20260705-132538"),
    midPilotDefeat: loadImage("./assets/midboss-pilot-defeat.png?v=20260705-150202"),
    midPilotKilled: loadImage("./assets/midboss-kill-cutin.png?v=20260707-choice"),
    midPilotInterrogationNude: loadImage("./assets/midboss-interrogation-nude.png?v=20260705-jhzvb00001"),
    stage1MidInterrogationStart: loadImage('./assets/stage1-midboss-interrogation-start.png?v=20260717-stage1-mid-interrogation-start'),
    stage1MidInterrogationAssistant: loadImage('./assets/stage1-midboss-interrogation-assistant.png?v=20260718-stage1-mid-interrogation-assistant'),
    bg: loadImage("./assets/background.png"),
    pilot: loadImage("./assets/boss-pilot.png?v=20260705-145043"),
    pilotCrisis: loadImage("./assets/boss-pilot-crisis.png?v=20260705-145053"),
    pilotDefeat: loadImage("./assets/boss-pilot-defeat.png?v=20260705-145057"),
    bossKilled: loadImage("./assets/stage1-boss-kill-cutin-v2.png?v=20260709-stage1-boss-kill-v2"),
    stage1MidPilot: null,
    stage1MidPilotCrisis: null,
    stage1MidPilotDefeat: null,
    stage1MidPilotKilled: null,
    stage1Pilot: null,
    stage1PilotCrisis: null,
    stage1PilotDefeat: null,
    stage1BossKilled: null,
    stage2MidPilot: loadImage("./assets/stage2-midboss-pilot.png?v=20260707-stage2"),
    stage2MidPilotCrisis: loadImage("./assets/stage2-midboss-pilot-crisis.png?v=20260707-stage2"),
    stage2MidPilotDefeat: loadImage("./assets/stage2-midboss-pilot-defeat.png?v=20260707-stage2"),
    stage2MidPilotKilled: loadImage("./assets/stage2-midboss-kill-cutin.png?v=20260707-stage2"),
    stage3MidPilot: loadImage("./assets/stage3-midboss-pilot.png?v=20260708-stage3-midboss"),
    stage3MidPilotCrisis: loadImage("./assets/stage3-midboss-pilot-crisis.png?v=20260708-stage3-midboss"),
    stage3MidPilotDefeat: loadImage("./assets/stage3-midboss-pilot-defeat.png?v=20260708-stage3-midboss"),
    stage3MidPilotKilled: loadImage("./assets/stage3-midboss-kill-cutin.jpg?v=20260708-stage3-midboss"),
    stage2Pilot: loadImage("./assets/stage2-boss-pilot.png?v=20260707-stage2"),
    stage2PilotCrisis: loadImage("./assets/stage2-boss-pilot-crisis.png?v=20260707-stage2"),
    stage2PilotDefeat: loadImage("./assets/stage2-boss-pilot-defeat.png?v=20260707-stage2"),
    stage2BossKilled: loadImage("./assets/stage2-boss-kill-cutin.png?v=20260707-stage2kill1"),
    stage3Pilot: loadImage("./assets/stage3-boss-pilot.png?v=20260709-stage3-boss"),
    stage3PilotCrisis: loadImage("./assets/stage3-boss-pilot-crisis.png?v=20260709-stage3-boss"),
    stage3PilotDefeat: loadImage("./assets/stage3-boss-pilot-defeat.png?v=20260709-stage3-boss"),
    stage3BossKilled: loadImage("./assets/stage3-boss-kill-cutin.png?v=20260709-stage3-boss"),
    stage4MidPilot: loadImage("./assets/stage4-midboss-pilot.png?v=20260710-stage4-1"),
    stage4MidPilotCrisis: loadImage("./assets/stage4-midboss-pilot-crisis.png?v=20260710-stage4-1"),
    stage4MidPilotDefeat: loadImage("./assets/stage4-midboss-pilot-defeat.png?v=20260710-stage4-1"),
    stage4MidPilotKilled: loadImage("./assets/stage4-midboss-kill-cutin.png?v=20260710-stage4-1"),
    stage4Pilot: loadImage("./assets/stage4-boss-pilot.png?v=20260710-stage4-1"),
    stage4PilotCrisis: loadImage("./assets/stage4-boss-pilot-crisis.png?v=20260710-stage4-1"),
    stage4PilotDefeat: loadImage("./assets/stage4-boss-pilot-defeat.png?v=20260710-stage4-1"),
    stage4BossKilled: loadImage("./assets/stage4-boss-kill-cutin.png?v=20260710-stage4-1"),
    stage4Background: loadImage("./assets/stage4-ground-background.png?v=20260710-stage4-1"),
    stage2MidbossShip: loadImage("./assets/stage2-midboss-ship.png?v=20260708-stage2-destruction"),
    stage2BossShip: loadImage("./assets/stage2-boss-ship.png?v=20260708-stage2-destruction"),
    stage3MidbossShip: loadImage("./assets/stage3-midboss-ship.png?v=20260709-stage3-midboss-alpha2"),
    stage3BossShip: loadImage("./assets/stage3-boss-ship.png?v=20260709-stage3-shadowwing1"),
    stage3EnemyInterceptor: loadImage("./assets/stage3-enemy-interceptor.png?v=20260710-stage3-enemies1"),
    stage3EnemyStriker: loadImage("./assets/stage3-enemy-striker.png?v=20260710-stage3-enemies1"),
    stage3EnemyCarrier: loadImage("./assets/stage3-enemy-carrier.png?v=20260710-stage3-enemies1"),
    stage4MidbossShip: loadImage("./assets/stage4-midboss-ship.png?v=20260710-stage4-1"),
    stage4BossShip: loadImage("./assets/stage4-boss-ship.png?v=20260710-stage4-1"),
    stage4EnemyInterceptor: loadImage("./assets/stage4-enemy-interceptor.png?v=20260710-stage4-1"),
    stage4EnemyGunship: loadImage("./assets/stage4-enemy-gunship.png?v=20260710-stage4-1"),
    stage4EnemyBomber: loadImage("./assets/stage4-enemy-bomber.png?v=20260710-stage4-1"),
    stage5MidPilot: loadImage("./assets/stage5-midboss-pilot.png?v=20260711-stage5-1"),
    stage5MidPilotCrisis: loadImage("./assets/stage5-midboss-pilot-crisis.png?v=20260711-stage5-1"),
    stage5MidPilotDefeat: loadImage("./assets/stage5-midboss-pilot-defeat.png?v=20260711-stage5-1"),
    stage5MidPilotKilled: loadImage("./assets/stage5-midboss-kill-cutin.png?v=20260711-stage5-1"),
    stage5Pilot: loadImage("./assets/stage5-boss-pilot.png?v=20260711-stage5-1"),
    stage5PilotCrisis: loadImage("./assets/stage5-boss-pilot-crisis.png?v=20260711-stage5-1"),
    stage5PilotDefeat: loadImage("./assets/stage5-boss-pilot-defeat.png?v=20260711-stage5-1"),
    stage5BossKilled: loadImage("./assets/stage5-boss-kill-cutin.png?v=20260711-stage5-1"),
    stage5Background: loadImage("./assets/stage5-city-background.png?v=20260711-stage5-1"),
    stage5MidbossShip: loadImage("./assets/stage5-midboss-ship.png?v=20260711-stage5-1"),
    stage5BossShip: loadImage("./assets/stage5-boss-ship.png?v=20260711-stage5-1"),
    stage5EnemyInterceptor: loadImage("./assets/stage5-enemy-interceptor.png?v=20260711-stage5-1"),
    stage5EnemyJammer: loadImage("./assets/stage5-enemy-jammer.png?v=20260711-stage5-1"),
    stage5EnemyGunship: loadImage("./assets/stage5-enemy-gunship.png?v=20260711-stage5-1"),
    stage6MidPilot: loadImage("./assets/stage6-midboss-pilot.png?v=20260711-stage6-1"),
    stage6MidPilotCrisis: loadImage("./assets/stage6-midboss-pilot-crisis.png?v=20260711-stage6-1"),
    stage6MidPilotDefeat: loadImage("./assets/stage6-midboss-pilot-defeat.png?v=20260711-stage6-1"),
    stage6MidPilotKilled: loadImage("./assets/stage6-midboss-kill-cutin.png?v=20260711-stage6-1"),
    stage6Pilot: loadImage("./assets/stage6-boss-pilot.png?v=20260711-stage6-1"),
    stage6PilotCrisis: loadImage("./assets/stage6-boss-pilot-crisis.png?v=20260711-stage6-1"),
    stage6PilotDefeat: loadImage("./assets/stage6-boss-pilot-defeat.png?v=20260711-stage6-1"),
    stage6BossKilled: loadImage("./assets/stage6-boss-kill-cutin.png?v=20260711-stage6-1"),
    stage6Background: loadImage("./assets/stage6-military-base-background.png?v=20260711-stage6-1"),
    stage6MidbossShip: loadImage("./assets/stage6-midboss-ship.png?v=20260711-stage6-1"),
    stage6BossShip: loadImage("./assets/stage6-boss-ship.png?v=20260711-stage6-1"),
    stage6EnemyStriker: loadImage("./assets/stage6-enemy-striker.png?v=20260711-stage6-1"),
    stage6EnemyMortar: loadImage("./assets/stage6-enemy-mortar.png?v=20260711-stage6-1"),
    stage6EnemyCarrier: loadImage("./assets/stage6-enemy-carrier.png?v=20260711-stage6-1"),
    stage7MidPilot: loadImage("./assets/stage7-midboss-pilot.png?v=20260711-stage7-1"),
    stage7MidPilotCrisis: loadImage("./assets/stage7-midboss-pilot-crisis.png?v=20260711-stage7-1"),
    stage7MidPilotDefeat: loadImage("./assets/stage7-midboss-pilot-defeat.png?v=20260711-stage7-1"),
    stage7MidPilotKilled: loadImage("./assets/stage7-midboss-kill-cutin.png?v=20260711-stage7-1"),
    stage7Pilot: loadImage("./assets/stage7-boss-pilot.png?v=20260711-stage7-1"),
    stage7PilotCrisis: loadImage("./assets/stage7-boss-pilot-crisis.png?v=20260711-stage7-1"),
    stage7PilotDefeat: loadImage("./assets/stage7-boss-pilot-defeat.png?v=20260711-stage7-1"),
    stage7BossKilled: loadImage("./assets/stage7-boss-kill-cutin.png?v=20260711-stage7-1"),
    stage7MidbossShip: loadImage("./assets/stage7-midboss-ship.png?v=20260711-stage7-1"),
    stage7BossShip: loadImage("./assets/stage7-boss-ship.png?v=20260711-stage7-1"),
    stage7EnemySentinel: loadImage("./assets/stage7-enemy-sentinel.png?v=20260711-stage7-1"),
    stage7EnemyBreacher: loadImage("./assets/stage7-enemy-breacher.png?v=20260711-stage7-1"),
    stage7EnemyHive: loadImage("./assets/stage7-enemy-hive.png?v=20260711-stage7-1"),
    stage8MidPilot: loadImage("./assets/stage8-midboss-pilot.png?v=20260713-stage8-1"),
    stage8MidPilotCrisis: loadImage("./assets/stage8-midboss-pilot-crisis.png?v=20260713-stage8-1"),
    stage8MidPilotDefeat: loadImage("./assets/stage8-midboss-pilot-defeat.png?v=20260713-stage8-1"),
    stage8MidPilotKilled: loadImage("./assets/stage8-midboss-kill-cutin.png?v=20260713-stage8-1"),
    stage8Pilot: loadImage("./assets/stage8-boss-pilot.png?v=20260713-stage8-1"),
    stage8PilotCrisis: loadImage("./assets/stage8-boss-pilot-crisis.png?v=20260713-stage8-1"),
    stage8PilotDefeat: loadImage("./assets/stage8-boss-pilot-defeat.png?v=20260713-stage8-1"),
    stage8BossKilled: loadImage("./assets/stage8-boss-kill-cutin.png?v=20260713-stage8-1"),
    stage8MidbossShip: loadImage("./assets/stage8-midboss-ship.png?v=20260713-stage8-1"),
    stage8BossShip: loadImage("./assets/stage8-boss-ship.png?v=20260713-stage8-1"),
    stage8EnemySwitchback: loadImage("./assets/stage8-enemy-switchback.png?v=20260713-stage8-1"),
    stage8EnemyPolarity: loadImage("./assets/stage8-enemy-polarity.png?v=20260713-stage8-1"),
    stage8EnemyRelay: loadImage("./assets/stage8-enemy-relay.png?v=20260713-stage8-1"),
    stage7Background: loadImage("./assets/stage7-colony-outer-ring-background.png?v=20260713-stage78-pattern-2"),
    stage8Background: loadImage("./assets/stage8-colony-inner-transit-background.png?v=20260713-stage78-pattern-2"),
    stage7EclipseBlade: loadImage("./assets/stage7-eclipse-blade.png?v=20260713-stage78-pattern-2"),
    stage7CatPawMine: loadImage("./assets/stage7-cat-paw-mine.png?v=20260713-stage78-pattern-2"),
    stage8RouteBeacon: loadImage("./assets/stage8-route-beacon.png?v=20260713-stage78-pattern-2"),
    stage8FreightCapsule: loadImage("./assets/stage8-freight-capsule.png?v=20260713-stage78-pattern-2"),
    stage9MidPilot: loadImage("./assets/stage9-midboss-pilot.png?v=20260715-stage9-user-cutins-1"),
    stage9MidPilotCrisis: loadImage("./assets/stage9-midboss-pilot-crisis.png?v=20260715-stage9-user-cutins-1"),
    stage9MidPilotDefeat: loadImage("./assets/stage9-midboss-pilot-defeat.png?v=20260715-stage9-user-cutins-1"),
    stage9MidPilotKilled: loadImage("./assets/stage9-midboss-kill-cutin.jpeg?v=20260715-stage9-user-cutins-1"),
    stage9Pilot: loadImage("./assets/stage9-boss-pilot.png?v=20260715-stage9-user-cutins-1"),
    stage9PilotCrisis: loadImage("./assets/stage9-boss-pilot-crisis.png?v=20260715-stage9-user-cutins-1"),
    stage9PilotDefeat: loadImage("./assets/stage9-boss-pilot-defeat.png?v=20260715-stage9-user-cutins-1"),
    stage9BossKilled: loadImage("./assets/stage9-boss-kill-cutin.png?v=20260715-stage9-user-cutins-1"),
    stage9Background: loadImage("./assets/stage9-colony-core-background.png?v=20260715-stage9-1"),
    stage9EnemyVector: loadImage("./assets/stage9-enemy-vector.png?v=20260715-stage9-1"),
    stage9EnemyOrbiter: loadImage("./assets/stage9-enemy-orbiter.png?v=20260715-stage9-1"),
    stage9EnemyBastion: loadImage("./assets/stage9-enemy-bastion.png?v=20260715-stage9-1"),
    stage9MidbossShip: loadImage("./assets/stage9-midboss-ship.png?v=20260715-stage9-1"),
    stage9BossShip: loadImage("./assets/stage9-boss-ship.png?v=20260715-stage9-1"),
    stage9ConveyorDrone: loadImage("./assets/stage9-conveyor-drone.png?v=20260715-stage9-1"),
    stage9GravityCore: loadImage("./assets/stage9-gravity-core.png?v=20260715-stage9-1"),
    stage2EnemyScout: loadImage("./assets/stage2-enemy-scout.png?v=20260708-stage2-destruction"),
    stage2EnemyPrism: loadImage("./assets/stage2-enemy-prism.png?v=20260708-stage2-destruction"),
    stage2EnemyCarrier: loadImage("./assets/stage2-enemy-carrier.png?v=20260708-stage2-destruction"),
  };

  const frames = {
    player: { x: 0, y: 0, w: 627, h: 627 },
    drone: { x: 627, y: 0, w: 627, h: 627 },
    fighter: { x: 0, y: 627, w: 627, h: 627 },
    boss: { x: 627, y: 627, w: 627, h: 627 },
  };
  const extraFrames = {
    interceptor: { col: 0, row: 0 },
    shield: { col: 1, row: 0 },
    mineLayer: { col: 0, row: 1 },
    splitter: { col: 1, row: 1 },
  };
  const enemyStats = {
    drone: { r: 15, hp: 42, fire: 1.1, size: 58, sway: 18 },
    fighter: { r: 22, hp: 95, fire: 0.9, size: 86, sway: 35 },
    laser: { r: 24, hp: 160, fire: 1.65, size: 92, sway: 12 },
    interceptor: { r: 16, hp: 58, fire: 0.72, size: 64, sway: 65 },
    shield: { r: 31, hp: 230, fire: 0.85, size: 96, sway: 10 },
    mineLayer: { r: 26, hp: 135, fire: 1.25, size: 88, sway: 24 },
    splitter: { r: 18, hp: 74, fire: 1.05, size: 66, sway: 42 },
    starScout: { r: 17, hp: 72, fire: 0.95, size: 66, sway: 72 },
    prism: { r: 24, hp: 150, fire: 1.35, size: 84, sway: 22 },
    supplyCarrier: { r: 30, hp: 260, fire: 1.65, size: 104, sway: 12 },
    stage4Interceptor: { r: 17, hp: 86, fire: 0.82, size: 70, sway: 72 },
    stage4Gunship: { r: 27, hp: 190, fire: 1.4, size: 96, sway: 22 },
    stage4Bomber: { r: 34, hp: 330, fire: 1.75, size: 116, sway: 10 },
    stage5Interceptor: { r: 17, hp: 98, fire: 0.76, size: 72, sway: 78 },
    stage5Jammer: { r: 25, hp: 205, fire: 1.28, size: 92, sway: 28 },
    stage5Gunship: { r: 35, hp: 380, fire: 1.72, size: 122, sway: 12 },
    stage6Striker: { r: 18, hp: 112, fire: 0.82, size: 76, sway: 82 },
    stage6Mortar: { r: 28, hp: 245, fire: 1.45, size: 102, sway: 18 },
    stage6Carrier: { r: 37, hp: 460, fire: 1.9, size: 132, sway: 8 },
    stage7Sentinel: { r: 18, hp: 128, fire: 0.78, size: 78, sway: 88 },
    stage7Breacher: { r: 29, hp: 285, fire: 1.42, size: 106, sway: 16 },
    stage7Hive: { r: 38, hp: 520, fire: 1.86, size: 136, sway: 7 },
    stage8Switchback: { r: 18, hp: 142, fire: 0.76, size: 80, sway: 94 },
    stage8Polarity: { r: 30, hp: 320, fire: 1.36, size: 110, sway: 18 },
    stage8Relay: { r: 39, hp: 590, fire: 1.82, size: 140, sway: 8 },
    stage9Vector: { r: 18, hp: 156, fire: 0.74, size: 82, sway: 98 },
    stage9Orbiter: { r: 30, hp: 355, fire: 1.32, size: 112, sway: 20 },
    stage9Bastion: { r: 40, hp: 650, fire: 1.78, size: 144, sway: 7 },
  };

  let currentStage = 1;
  let state = "title";
  let last = 0;
  let time = 0;
  let score = 0;
  let nextScoreLife = SCORE_1UP_FIRST;
  let healAssistUsed = false;
  let boss = null;
  let midBoss = null;
  let midBossSpawned = false;
  let midBossDefeated = false;
  let midBossHalfDialogShown = false;
  let bossReadyTime = 48;
  let shake = 0;
  let flash = 0;
  let bgScroll = 0;
  let nextWave = 1;
  let bossIntroShown = false;
  let bossHalfDialogShown = false;
  let dialogStep = 0;
  let activeDialog = null;
  let alertScene = null;
  let nextPlayerBulletId = 1;
  let nextEnemyBulletId = 1;
  let playerBullets = [];
  let enemyBullets = [];
  let lasers = [];
  let stage4AttackFx = [];
  let stage5AttackFx = [];
  let stage6AttackFx = [];
  let stage7AttackFx = [];
  let stage8AttackFx = [];
  let stage9AttackFx = [];
  let enemies = [];
  let powerUps = [];
  let particles = [];
  // Continuous hits are visually/audio-throttled to keep dense firefights responsive.
  let lastMetalHitFxAt = -Infinity;
  const PARTICLE_SOFT_LIMIT = 160;
  const PARTICLE_HARD_LIMIT = 200;
  let wreckPieces = [];
  let destructionScene = null;
  let defeatChoice = null;
  let killCutinScene = null;
  let interrogationTarget = null;
  let interrogationStep = 0;
  let interrogationNotice = null;
  let killedPreviewTarget = null;
  const pointer = { x: -999, y: -999 };
  let reducedEffects = false;
  let smoothedRenderCost = 0;
  let slowRenderFrames = 0;
  let renderRecoveryFrames = 0;
  let renderedInterrogationLayout = null;
  let hudNextRenderAt = 0;

  function updateRenderQuality(renderCost) {
    smoothedRenderCost = smoothedRenderCost ? smoothedRenderCost * 0.92 + renderCost * 0.08 : renderCost;
    if (state !== "play") { slowRenderFrames = 0; renderRecoveryFrames = 0; return; }
    slowRenderFrames = smoothedRenderCost > 14 || renderCost > 22 ? Math.min(30, slowRenderFrames + 1) : Math.max(0, slowRenderFrames - 1);
    if (!reducedEffects && slowRenderFrames >= 6) { reducedEffects = true; renderRecoveryFrames = 0; }
    if (!reducedEffects) return;
    renderRecoveryFrames = smoothedRenderCost < 9.5 ? renderRecoveryFrames + 1 : 0;
    if (renderRecoveryFrames >= 240) { reducedEffects = false; slowRenderFrames = 0; renderRecoveryFrames = 0; }
  }

  function compactInPlace(list, keep) {
    let write = 0;
    for (let read = 0; read < list.length; read++) {
      const item = list[read];
      if (keep(item)) list[write++] = item;
    }
    list.length = write;
    return list;
  }


  let casualtyCount = 0;
  let enemyKillCount = 0;
  let crewKillCount = 0;
  let usedCrewNames = new Set();
  let casualtyFeed = [];
  let casualtyFeedFull = false;
  let shockCasualties = [];
  let nextShockIndex = 0;
  let lastShockEventId = -1;
  const crewRoster = [
    ["アリナ・セイル", 22], ["ミナト・レイ", 19], ["セリカ・ノーン", 24], ["カイ・ロウエン", 31], ["ノア・ユラ", 20],
    ["ユイナ・クラウ", 18], ["ラウル・ミゼ", 27], ["エリス・トーマ", 25], ["シオン・アベル", 21], ["マヤ・ルフト", 29],
    ["リオ・カナン", 23], ["ハルカ・ヴェル", 26], ["オルガ・ネイト", 34], ["ティア・ロゼ", 20], ["ナギ・サリス", 28],
    ["レン・ハーシェ", 32], ["ミオ・アルデ", 19], ["ルカ・フェイ", 24], ["イリス・バラン", 30], ["サナ・キリエ", 22],
    ["トウマ・ジル", 33], ["アヤメ・ルー", 21], ["ネリス・コート", 27], ["ユアン・ベルグ", 35], ["コハク・リード", 18],
    ["ラナ・ミスト", 23], ["グレン・アッシュ", 38], ["フィオ・ラザ", 20], ["カレン・スノウ", 26], ["イツキ・クロム", 29],
    ["メイ・オルト", 21], ["ザック・フェイン", 37], ["リサ・クライン", 24], ["アレン・ノーヴァ", 28], ["ヒナ・ラス", 19],
    ["セナ・ハルト", 25], ["キース・ヴァン", 36], ["モニカ・レイヴ", 31], ["ユリ・シェル", 22], ["エマ・シグ", 27],
    ["ノエル・アーク", 20], ["カナタ・オズ", 23], ["ミレイ・ザイル", 30], ["ルイス・グラム", 34], ["サクラ・レム", 18],
    ["アデル・フォーン", 28], ["リタ・バース", 25], ["ゼン・カルド", 39], ["ナナセ・ユール", 21], ["クロエ・エル", 24],
    ["リヒト・マイヤ", 32], ["ソフィ・ディア", 20], ["ガイ・ローデン", 41], ["ミリカ・ノル", 19], ["エト・サージ", 26],
    ["レナ・クォーツ", 22], ["バルト・エイム", 35], ["シェリル・ノア", 29], ["ハヤテ・リンド", 27], ["ニナ・オルフェ", 18]
  ];
  const shockDeathTimes = [30, 45, 60, 75, 88];
  const shockCrewRoster = crewRoster.filter((member) => member[1] <= 25);
  const fallbackNameSuffixes = ["アッシュ", "ノヴァ", "リゲル", "セラフ", "ヴェガ", "ルーン", "ミラージュ", "クロウ", "シグマ", "オルビス", "ネオン", "レイヴン", "シエル", "エクリプス", "ファントム", "ルシア", "カロン", "ステラ", "アイリス", "ゼファー"];
  const deathCausePool = ["爆死", "機体破砕", "コックピット直撃", "酸欠", "圧壊", "熱傷死", "誘爆", "減圧死", "推進炉融解", "操縦席焼損"];
  const enemyCrewSizes = { interceptor: 1, splitter: 1, fighter: 1, laser: 2, shield: 2, mineLayer: 3, starScout: 1, prism: 2, supplyCarrier: 4, stage4Interceptor: 1, stage4Gunship: 2, stage4Bomber: 4, stage5Interceptor: 1, stage5Jammer: 2, stage5Gunship: 5, stage6Striker: 1, stage6Mortar: 3, stage6Carrier: 6, stage7Sentinel: 1, stage7Breacher: 3, stage7Hive: 7, stage8Switchback: 1, stage8Polarity: 3, stage8Relay: 8, stage9Vector: 1, stage9Orbiter: 3, stage9Bastion: 9 };
  const audioFiles = {
    shot: "./assets/audio/shot.mp3",
    enemyHit: "./assets/audio/enemy-hit.mp3",
    bulletHit: "./assets/audio/bullet-hit.mp3",
    playerHit: "./assets/audio/player-hit.mp3",
    laser: "./assets/audio/laser.mp3",
    bossLaser: "./assets/audio/boss-laser-heavy-continuous.mp3",
    healSiren: "./assets/audio/heal-item-emergency-siren-loop.mp3",
    powerup: "./assets/audio/powerup.mp3",
  };
  const bgmFiles = {
    stage: "./assets/audio/stage-bgm-starfield-dash.mp3",
    boss: "./assets/audio/boss-bgm-starbreaking-route.mp3",
    stage2: "./assets/audio/stage2-bgm-mechanism-dash.mp3",
    boss2: "./assets/audio/stage2-boss-bgm-heavy-battle.mp3",
    stage3: "./assets/audio/stage3-bgm.mp3",
    boss3: "./assets/audio/stage3-boss-bgm.mp3",
    stage4: "./assets/audio/stage3-bgm.mp3",
    boss4: "./assets/audio/stage3-boss-bgm.mp3",
    stage5: "./assets/audio/stage3-bgm.mp3",
    boss5: "./assets/audio/stage3-boss-bgm.mp3",
    stage6: "./assets/audio/stage3-bgm.mp3",
    stage7: "./assets/audio/stage3-bgm.mp3",
    boss6: "./assets/audio/stage3-boss-bgm.mp3",
    boss7: "./assets/audio/stage3-boss-bgm.mp3",
    stage8: "./assets/audio/stage3-bgm.mp3",
    boss8: "./assets/audio/stage3-boss-bgm.mp3",
    stage9: "./assets/audio/stage3-bgm.mp3",
    boss9: "./assets/audio/stage3-boss-bgm.mp3",
    victory: "./assets/audio/victory-captured-officers.mp3",
    interrogation: "./assets/audio/interrogation-rusted-light.mp3",
  };
  const sounds = {};
  const musicTracks = {};
  const soundGate = {};
  let audioReady = false;
  let musicCtx = null;
  let musicTimer = null;
  let currentBgm = null;
  let musicStep = 0;

  function initAudio() {
    if (audioReady) return;
    for (const [name, src] of Object.entries(audioFiles)) {
      const audio = new Audio(src);
      audio.preload = "auto";
      audio.loop = name === "healSiren";
      audio.volume = {
        shot: 0.18,
        enemyHit: 0.34,
        bulletHit: 0.26,
        playerHit: 0.52,
        laser: 0.42,
        bossLaser: 0.82,
        healSiren: 0.58,
        powerup: 0.34,
      }[name] ?? 0.34;
      sounds[name] = audio;
    }
    for (const [name, src] of Object.entries(bgmFiles)) {
      if (STAGE1_DEMO && !["stage", "boss", "victory", "interrogation"].includes(name)) continue;
      const bgm = new Audio(src);
      bgm.preload = "auto";
      bgm.loop = true;
      bgm.volume = name.includes("boss") ? 0.68 : 0.62;
      musicTracks[name] = bgm;
    }
    audioReady = true;
  }

  function playSound(name, volume = 1, gap = 0.04) {
    if (!audioReady || !sounds[name]) return;
    const now = performance.now();
    if (soundGate[name] && now - soundGate[name] < gap * 1000) return;
    soundGate[name] = now;
    const audio = sounds[name].cloneNode();
    audio.volume = Math.min(1, sounds[name].volume * volume);
    audio.play().catch(() => {});
  }



  function startHealSiren() {
    if (!audioReady || !sounds.healSiren) return;
    const siren = sounds.healSiren;
    siren.loop = true;
    siren.volume = 0.58;
    if (siren.paused) {
      siren.currentTime = 0;
      siren.play().catch(() => {});
    }
  }

  function stopHealSiren() {
    const siren = sounds.healSiren;
    if (!siren) return;
    siren.pause();
    siren.currentTime = 0;
  }

  function syncHealSiren() {
    const activeHeal = powerUps.some((p) => p.type === "heal" && !p.dead && p.y < H + 42);
    if (activeHeal) startHealSiren();
    else stopHealSiren();
  }
  function ensureMusicContext() {
    if (!musicCtx) musicCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (musicCtx.state === "suspended") musicCtx.resume().catch(() => {});
  }

  function playTone(freq, duration = 0.12, type = "sine", volume = 0.08, delay = 0) {
    if (!audioReady) return;
    ensureMusicContext();
    const start = musicCtx.currentTime + delay;
    const osc = musicCtx.createOscillator();
    const gain = musicCtx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, start);
    gain.gain.setValueAtTime(0.0001, start);
    gain.gain.exponentialRampToValueAtTime(volume, start + 0.015);
    gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    osc.connect(gain).connect(musicCtx.destination);
    osc.start(start);
    osc.stop(start + duration + 0.03);
  }

  function playAlertTone(kind) {
    const base = kind === "boss" ? 210 : 260;
    playTone(base, 0.12, "sawtooth", 0.12, 0);
    playTone(base * 1.52, 0.12, "square", 0.075, 0.14);
  }

  function stopBgm() {
    if (musicTimer) clearInterval(musicTimer);
    musicTimer = null;
    for (const track of Object.values(musicTracks)) {
      track.pause();
      track.currentTime = 0;
    }
    currentBgm = null;
    musicStep = 0;
  }

  function startSynthBgm(mode) {
    ensureMusicContext();
    const stageNotes = [196, 246.94, 293.66, 329.63, 293.66, 246.94, 220, 261.63];
    const bossNotes = [110, 130.81, 146.83, 164.81, 146.83, 130.81, 98, 123.47];
    const tick = () => {
      if (!musicCtx || state === "title" || state === "over" || state === "clear") return;
      const isBossTrack = currentBgm && currentBgm.includes("boss");
      const notes = isBossTrack ? bossNotes : stageNotes;
      const i = musicStep % notes.length;
      const note = notes[i];
      playTone(note, isBossTrack ? 0.16 : 0.12, isBossTrack ? "sawtooth" : "triangle", isBossTrack ? 0.075 : 0.045);
      if (i % 2 === 0) playTone(note / 2, 0.22, "sine", isBossTrack ? 0.07 : 0.04);
      if (isBossTrack && i % 4 === 0) playTone(note * 2.01, 0.06, "square", 0.035);
      musicStep += 1;
    };
    tick();
    musicTimer = setInterval(tick, mode.includes("boss") ? 135 : 185);
  }

  function stageBgmMode() {
    return currentStage === 1 ? "stage" : `stage${currentStage}`;
  }

  function bossBgmMode() {
    return currentStage === 1 ? "boss" : `boss${currentStage}`;
  }

  function playBgm(mode) {
    if (!audioReady) return;
    const track = musicTracks[mode];
    if (currentBgm === mode && track && !track.paused) return;
    if (currentBgm === mode && musicTimer) return;
    stopBgm();
    currentBgm = mode;
    if (track) {
      track.currentTime = 0;
      track.volume = mode.includes("boss") ? 0.68 : 0.62;
      track.play().catch(() => startSynthBgm(mode));
      return;
    }
    startSynthBgm(mode);
  }
  function loadImage(src) {
    if (STAGE1_DEMO && /\/assets\/stage[2-9]/i.test(src)) return new Image();
    const img = new Image();
    img.src = src;
    return img;
  }


  function resetCasualtyFeed({ preserveTotals = false } = {}) {
    if (!preserveTotals) {
      casualtyCount = 0;
      enemyKillCount = 0;
      crewKillCount = 0;
      usedCrewNames = new Set();
      casualtyFeed = [];
      shockCasualties = [];
    }
    casualtyFeedFull = false;
    nextShockIndex = 0;
    lastShockEventId = -1;
    updateCasualtyFeed();
  }

  function updateCasualtyFeed() {
    if (casualtyStatsEl) {
      casualtyStatsEl.innerHTML = `<b>敵機撃破数 ${enemyKillCount}</b><b>乗員殺害数 ${crewKillCount}</b>`;
    }
    if (!casualtyFeedEl) return;
    casualtyFeedEl.classList.toggle("full", casualtyFeedFull);
    if (!casualtyFeed.length) {
      casualtyFeedEl.innerHTML = '<p class="empty">敵機撃破で乗員情報を受信</p>';
      return;
    }
    const shown = casualtyFeedFull ? casualtyFeed : casualtyFeed.slice(0, 7);
    casualtyFeedEl.innerHTML = shown.map((item) => `<p class="${item.cause === "ショック死" ? "shock" : item.action === "捕獲" ? "capture" : item.action === "撃破" ? "summary" : ""}">${item.text}</p>`).join("");
  }

  function selectCrewMember(pool, seed) {
    for (let i = 0; i < pool.length; i++) {
      const member = pool[(seed + i) % pool.length];
      if (!usedCrewNames.has(member[0])) {
        usedCrewNames.add(member[0]);
        return member;
      }
    }
    const base = pool[seed % pool.length];
    for (let i = 0; i < fallbackNameSuffixes.length; i++) {
      const name = `${base[0]}・${fallbackNameSuffixes[(seed + i) % fallbackNameSuffixes.length]}`;
      if (!usedCrewNames.has(name)) {
        usedCrewNames.add(name);
        return [name, base[1]];
      }
    }
    for (let a = 0; a < fallbackNameSuffixes.length; a++) {
      for (let b = 0; b < fallbackNameSuffixes.length; b++) {
        const name = `${base[0]}・${fallbackNameSuffixes[(seed + a) % fallbackNameSuffixes.length]}${fallbackNameSuffixes[(seed + b + 7) % fallbackNameSuffixes.length]}`;
        if (!usedCrewNames.has(name)) {
          usedCrewNames.add(name);
          return [name, base[1]];
        }
      }
    }
    const name = `${base[0]}・未登録兵`;
    usedCrewNames.add(name);
    return [name, base[1]];
  }

  function shouldApplyShockDeath() {
    if (nextShockIndex >= shockDeathTimes.length) return false;
    if (lastShockEventId === enemyKillCount) return false;
    if (time < shockDeathTimes[nextShockIndex]) return false;
    nextShockIndex += 1;
    lastShockEventId = enemyKillCount;
    return true;
  }

  function makeCrewCasualty(enemyType = "drone") {
    casualtyCount += 1;
    crewKillCount += 1;
    const isShock = shouldApplyShockDeath();
    const roster = selectCrewMember(isShock ? shockCrewRoster : crewRoster, casualtyCount - 1);
    const baseCause = deathCausePool[(casualtyCount + enemyType.length) % deathCausePool.length];
    const cause = isShock ? "ショック死" : baseCause;
    const item = { name: roster[0], age: roster[1], cause, enemyType, action: "戦死", text: `${roster[0]}（${roster[1]}）戦死・死因：${cause}。` };
    if (cause === "ショック死") shockCasualties.push(item);
    return item;
  }

  function recordCrewCasualty(enemyType = "drone", crewSize = enemyCrewSizes[enemyType] || 1) {
    enemyKillCount += 1;
    const casualties = [];
    for (let i = 0; i < crewSize; i++) casualties.push(makeCrewCasualty(enemyType));
    if (crewSize > 1) {
      casualtyFeed.unshift({ action: "撃破", text: `敵${enemyKillCount}機目撃破・乗員${crewSize}名死亡。` });
    }
    casualtyFeed.unshift(...casualties.reverse());
    updateCasualtyFeed();
  }

  function recordCapture(name, role, supportCrewSize = 0, unitLabel = "敵大型機") {
    enemyKillCount += 1;
    const support = [];
    for (let i = 0; i < supportCrewSize; i++) support.push(makeCrewCasualty("commandCrew"));
    const item = { name, role, action: "捕獲", text: `${name}（${role}）捕獲。${supportCrewSize ? `${unitLabel}随伴クルー${supportCrewSize}名死亡。` : ""}` };
    casualtyFeed.unshift(...support.reverse());
    casualtyFeed.unshift(item);
    updateCasualtyFeed();
  }

  function recordOfficerKilled(name, role, supportCrewSize = 0, unitLabel = "敵大型機") {
    enemyKillCount += 1;
    crewKillCount += 1;
    usedCrewNames.add(name);
    const support = [];
    for (let i = 0; i < supportCrewSize; i++) support.push(makeCrewCasualty("commandCrew"));
    casualtyFeed.unshift(...support.reverse());
    casualtyFeed.unshift({ name, role, cause: "爆死", action: "殺害", text: `${name}（${role}）殺害・死因：爆死。${supportCrewSize ? `${unitLabel}随伴クルー${supportCrewSize}名死亡。` : ""}` });
    updateCasualtyFeed();
  }

  function activeInterrogationTargets() {
    return interrogationTargets.filter((target) => target.stage === currentStage);
  }

  function interrogationTargetById(id) {
    return interrogationTargets.find((target) => target.id === id);
  }

  function interrogationTargetByRole(role, stage = currentStage) {
    return interrogationTargets.find((target) => target.stage === stage && target.role === role);
  }

  function resetOfficerProgress(stage = null) {
    for (const target of interrogationTargets) {
      if (stage !== null && target.stage !== stage) continue;
      target.status = "locked";
      target.done = false;
      target.killed = false;
      target.captured = false;
      target.interrogationProgress = 0;
      target.flags = [];
    }
  }

  function setOfficerOutcome(role, outcome) {
    const target = interrogationTargetByRole(role);
    if (!target) return;
    target.status = outcome === "kill" ? "killed" : "captured";
    target.killed = outcome === "kill";
    target.captured = outcome === "capture";
    if (target.killed) target.done = false;
  }
  function reset() {
    applyStageSetup(1);
    state = "play";
    time = 0;
    score = 0;
    nextScoreLife = SCORE_1UP_FIRST;
    healAssistUsed = false;
    resetCasualtyFeed();
    resetOfficerProgress();
    boss = null;
    midBoss = null;
    midBossSpawned = false;
    midBossDefeated = false;
    midBossHalfDialogShown = false;
    bossReadyTime = 48;
    shake = 0;
    flash = 0;
    bgScroll = 0;
    nextWave = 1;
    bossIntroShown = false;
    bossHalfDialogShown = false;
    dialogStep = 0;
    activeDialog = null;
    alertScene = null;
    nextPlayerBulletId = 1;
    nextEnemyBulletId = 1;
    player.x = W / 2;
    player.y = H - 86;
    player.hp = player.maxHp;
    player.lives = 3;
    player.bombs = 3;
    player.inv = 3.0;
    player.fire = 0;
    player.power = 1;
    player.powerStock = 0;
    player.respawn = 0;
    player.hitPulse = 0;
    healAssistUsed = false;
    playerBullets = [];
    enemyBullets = [];
    lasers = [];
    stage4AttackFx = [];
    stage5AttackFx = [];
    stage6AttackFx = [];
    stage7AttackFx = [];
    stage8AttackFx = [];
    stage9AttackFx = [];
    enemies = [];
    powerUps = [];
    stopHealSiren();
    particles = [];
    wreckPieces = [];
    destructionScene = null;
    overlay.classList.add("hidden");
    initAudio();
    playBgm(stageBgmMode());
    updateHud();
  }

  function updateHud() {
    const hudNow = performance.now();
    if (hudNow < hudNextRenderAt) return;
    hudNextRenderAt = hudNow + 50;
    scoreEl.textContent = Math.max(0, Math.floor(score)).toLocaleString();
    const hpValue = Math.ceil(Math.max(0, player.hp));
    const hpRate = Math.max(0, Math.min(1, player.hp / player.maxHp));
    hpEl.textContent = `${hpValue}/${player.maxHp}`;
    hpBarFillEl.style.width = `${hpRate * 100}%`;
    hpBarFillEl.style.backgroundColor = hpRate < 0.32 ? "#ff5f75" : hpRate < 0.62 ? "#ffd45f" : "#75ffd0";
    hpBarFillEl.parentElement.setAttribute("aria-valuenow", String(hpValue));
    livesEl.textContent = player.lives;
    bombsEl.textContent = player.bombs;
    powerEl.textContent = player.power;
    powerStockEl.textContent = `${player.powerStock}/30`;
  }

  let pilotName = "\u8fd1\u885b\u968a\u9577 \u30ea\u30f4\u30a3\u30a2\u30fb\u30ce\u30af\u30b9";
  let midPilotName = "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9";
  const playerPilotName = "自機パイロット";
  // 会話エディタが保存した内容を、同じブラウザ内のゲーム本体へ反映する。
  const INTERROGATION_EDITOR_STORAGE_KEY = "starfall-dialogue-runtime-v1";
  const interrogationEditorImageCache = new Map();
  let interrogationEffect = null;
  let publishedInterrogationScenes = window.STARFALL_INTERROGATION_SCENES || { version: 1, scenes: {} };

  async function loadPublishedInterrogationScenes() {
    try {
      const response = await fetch("./data/interrogation-scenes.json", { cache: "no-store" });
      if (response.ok) publishedInterrogationScenes = await response.json();
    } catch (_) {}
  }
  loadPublishedInterrogationScenes();
  if ("BroadcastChannel" in window) {
    const publishedSceneChannel = new BroadcastChannel("starfall-interrogation-publish");
    publishedSceneChannel.onmessage = () => loadPublishedInterrogationScenes();
  }

  function interrogationEditorSceneFor(target) {
    const published = publishedInterrogationScenes && publishedInterrogationScenes.scenes && publishedInterrogationScenes.scenes[target.id];
    if (published && Array.isArray(published.turns) && published.turns.length) return published;
    try {
      const saved = JSON.parse(localStorage.getItem(INTERROGATION_EDITOR_STORAGE_KEY) || "{}");
      const scene = saved && saved.scenes && saved.scenes[target.id];
      return scene && Array.isArray(scene.turns) && scene.turns.length ? scene : null;
    } catch (_) { return null; }
  }
  function interrogationEditorImage(source) {
    if (!source) return null;
    if (!interrogationEditorImageCache.has(source)) {
      const image = new Image();
      image.src = source.startsWith("assets/") ? `./${source}` : source;
      interrogationEditorImageCache.set(source, image);
    }
    return interrogationEditorImageCache.get(source);
  }

  function interrogationEditorTurnsFor(target) {
    const scene = interrogationEditorSceneFor(target);
    if (!scene) return null;
    return scene.turns.map((row) => {
      const speakerId = row.speaker || "player";
      const isTarget = speakerId === "target";
      const isAssistant = speakerId === "assistant";
      const isSilent = speakerId === "none";
      let asset = row.asset || "target";
      if (asset === "target") asset = target.asset;
      else if (asset === "start") asset = target.id === "stage1_mid" ? "stage1MidInterrogationStart" : target.asset;
      else if (asset === "assistant") asset = target.id === "stage1_mid" ? "stage1MidInterrogationAssistant" : target.asset;
      const imageData = row.customImage && row.customImage.dataUrl ? row.customImage.dataUrl : (row.imageData || (typeof row.asset === "string" && row.asset.startsWith("assets/") ? row.asset : null));
      const speaker = isSilent ? "" : (isTarget ? target.name : (isAssistant ? "\u5c0b\u554f\u88dc\u4f50\u5b98" : playerPilotName));
      const accent = isTarget ? target.accent : (isAssistant ? "#ffd87a" : "#9fe8ff");
      return { speaker, accent, asset, imageData, effect: row.effect || "none", sfx: row.sfx || "none", imageOffset: row.imageOffset || { x: 0, y: 0 }, stage: target.title, text: String(row.text || "") };
    });
  }

  function triggerInterrogationPresentation(turn) {
    interrogationEffect = { name: turn && turn.effect ? turn.effect : "none", started: performance.now() };
    if (turn && turn.sfx && turn.sfx !== "none") playSound(turn.sfx, 0.82, 0.02);
  }

  function applyInterrogationEditorBgm(target) {
    const scene = interrogationEditorSceneFor(target);
    const mode = scene && scene.bgm ? scene.bgm : "interrogation";
    if (mode === "none") stopBgm();
    else if (musicTracks[mode]) playBgm(mode);
  }
  function makeMidBossInterrogationLines() {
    return [
      { stage: "\u6355\u865c\u5f8c\u306e\u78ba\u8a8d", player: "\u540d\u524d\u3068\u6240\u5c5e\u3092\u8a00\u3048\u3002\u305d\u308c\u304c\u5206\u304b\u308c\u3070\u3001\u3053\u308c\u4ee5\u4e0a\u306e\u640d\u5bb3\u306f\u907f\u3051\u3089\u308c\u308b\u3002", enemy: "\u2026\u2026\u79c1\u306f\u8ca0\u3051\u305f\u3002\u3060\u304c\u3001\u90e8\u968a\u306e\u60c5\u5831\u307e\u3067\u306f\u8a00\u3048\u306a\u3044\u3002" },
      { stage: "\u4f5c\u6226\u8a0a\u554f", player: "\u4e00\u4eba\u3067\u8cac\u4efb\u3092\u8ca0\u3046\u5fc5\u8981\u306f\u306a\u3044\u3002\u6b21\u306e\u653b\u6483\u8a08\u753b\u3060\u3051\u3092\u6559\u3048\u308d\u3002", enemy: "\u2026\u2026\u88dc\u7d66\u7dda\u306e\u78ba\u4fdd\u304c\u6700\u512a\u5148\u3060\u3002\u305d\u308c\u304c\u5d29\u308c\u308c\u3070\u3001\u5f7c\u3089\u306f\u52d5\u3051\u306a\u3044\u3002" },
      { stage: "\u6700\u7d42\u78ba\u8a8d", player: "\u4ed6\u306b\u4f1d\u3048\u308b\u3079\u304d\u3053\u3068\u306f\uff1f\u3000\u5831\u5fa9\u306e\u9023\u9396\u3092\u6b62\u3081\u308b\u6a5f\u4f1a\u306f\u3001\u4eca\u3060\u3051\u3060\u3002", enemy: "\u2026\u2026\u6307\u63ee\u6240\u306f\u3001\u524d\u7dda\u304c\u5d29\u308c\u305f\u3089\u6c11\u9593\u533a\u753b\u3092\u5207\u308a\u6368\u3066\u308b\u3064\u3082\u308a\u3060\u3002\u305d\u308c\u3060\u3051\u306f\u2026\u2026\u9632\u3044\u3067\u3002" },
      { stage: "\u8a0a\u554f\u5b8c\u4e86", player: "\u8a18\u9332\u3057\u305f\u3002\u3042\u306a\u305f\u306f\u9694\u96e2\u533a\u753b\u3078\u79fb\u9001\u3059\u308b\u3002", enemy: "\u2026\u2026\u305b\u3081\u3066\u3001\u90e8\u968a\u306e\u307f\u3093\u306a\u306b\u306f\u751f\u304d\u308b\u9053\u3092\u6b8b\u3057\u3066\u3002" },
    ];
  }

  function makeStage1MidInterrogationLines() {
    const lines = makeMidBossInterrogationLines();
    lines[0] = {
      ...lines[0],
      asset: "stage1MidInterrogationStart",
      player: "\u30fb\u30fb\u30fb\u3044\u3064\u307e\u3067\u305d\u3093\u306a\u53cd\u6297\u7684\u306a\u76ee\u3064\u304d\u3067\u3044\u3089\u308c\u308b\u304b\u306a\uff1f",
      enemy: "\u79c1\u306f\u4f55\u3082\u55cb\u308b\u3064\u3082\u308a\u306f\u3042\u308a\u307e\u305b\u3093\u30fb\u30fb\u30fb\u4f55\u3092\u3055\u308c\u3066\u3082\u30fb\u30fb\u30fb",
    };
    lines[1] = {
      ...lines[1],
      asset: "stage1MidInterrogationStart",
      player: "\u305d\u306e\u65b9\u304c\u3053\u3063\u3061\u3082\u697d\u3057\u3081\u308b\u3055",
    };
    return lines;
  }

  function makeStage1MidInterrogationTurns() {
    const startAsset = "stage1MidInterrogationStart";
    return [
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "\u30fb\u30fb\u30fb\u3044\u3064\u307e\u3067\u305d\u3093\u306a\u53cd\u6297\u7684\u306a\u76ee\u3064\u304d\u3067\u3044\u3089\u308c\u308b\u304b\u306a\uff1f" },
      { speaker: "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9", accent: "#ff8bd8", asset: startAsset, text: "\u79c1\u306f\u4f55\u3082\u55cb\u308b\u3064\u3082\u308a\u306f\u3042\u308a\u307e\u305b\u3093\u30fb\u30fb\u30fb\u4f55\u3092\u3055\u308c\u3066\u3082\u30fb\u30fb\u30fb" },
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "\u305d\u306e\u65b9\u304c\u3053\u3063\u3061\u3082\u697d\u3057\u3081\u308b\u3055" },
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "\u5358\u5200\u76f4\u5165\u306b\u805e\u304f\u305e\u3002\u304a\u524d\u3089\u306e\u5973\u738b\u306f\u3069\u3053\u3060\uff1f" },
      { speaker: "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9", accent: "#ff8bd8", asset: startAsset, text: "\u30fb\u30fb\u30fb\u30fb\u30fb\u30fb" },
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "\u3075\u3093\u3001\u3060\u3093\u307e\u308a\u304b\u3002\u306a\u3089\u4ed5\u65b9\u306a\u3044\u306a\u30fb\u30fb\u30fb\u3084\u308c\u3063" },
      { speaker: "\u5c0b\u554f\u88dc\u4f50\u5b98", accent: "#ffd87a", asset: startAsset, text: "\u4e86\u89e3" },
      { speaker: "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9", accent: "#ff8bd8", asset: "stage1MidInterrogationAssistant", text: "\u304f\u3063\u30fb\u30fb\u30fb\uff01" },
      // Editable generic dialogue template. Replace only the SAMPLE TEXT strings as needed.
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "SAMPLE TEXT (PLAYER)" },
      { speaker: "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9", accent: "#ff8bd8", asset: startAsset, text: "SAMPLE TEXT (LILITH)" },
      { speaker: playerPilotName, accent: "#9fe8ff", asset: startAsset, text: "SAMPLE TEXT (PLAYER)" },
      { speaker: "\u5c0b\u554f\u88dc\u4f50\u5b98", accent: "#ffd87a", asset: startAsset, text: "SAMPLE TEXT (ASSISTANT)" },
      { speaker: "\u30ea\u30ea\u30b9\u30fb\u30df\u30b3\u30b9", accent: "#ff8bd8", asset: "stage1MidInterrogationAssistant", text: "SAMPLE TEXT (LILITH / IMAGE CHANGE)" },
    ];
  }

  function makeInterrogationLines(kind) {
    if (kind === "mid") {
      return makeMidBossInterrogationLines();
      return [
        { stage: "拘束尋問", player: "姫の居所について知っていることを渡せ。今なら穏便に済ませてやる。", enemy: "や、やだ……そんなこと言えるわけないでしょ……。" },
        { stage: "圧迫尋問", player: "断るなら、それなりの対応だな♪　強がりがいつまで続くか見せてもらう。", enemy: "ひっ……脅したって無駄よ。私は絶対に口を割らない……！" },
        { stage: "衣装破損差分", asset: "midPilotInterrogationNude", player: "まだ黙るか。なら次は、もっと逃げ場のない状況で聞く。", enemy: "うぅ……こんなの、尋問なんかじゃ……でも、姫さまのことだけは……。" },
        { stage: "尋問後差分", asset: "midPilotInterrogationNude", player: "今日はここまでだ。じゃあ次のやつを尋問するか。", enemy: "はぁ……はぁ……姫さま、ごめんなさい……。" },
      ];
    }
    return [
      { stage: "拘束尋問", player: "姫の居所についての情報を渡せ。近衛隊長なら通信経路を知っているはずだ。", enemy: "くっ……貴様なぞに、姫の影すら踏ませるものか。" },
      { stage: "圧迫尋問", player: "断るなら、それなりの対応だな♪　その忠誠心ごと揺さぶってやる。", enemy: "下劣な……！　どれほど辱められようと、私は屈しない……！" },
      { stage: "衣装破損差分", player: "まだ口を割らないか。いい目だ。だが、余裕は消えてきたな。", enemy: "はぁ……はぁ……姫だけは、絶対に……守る……。" },
      { stage: "尋問後差分", player: "今日はここまでだ。じゃあ次のやつを尋問するか。", enemy: "覚えていろ……この屈辱、必ず返す……。" },
    ];
  }

  const interrogationTargets = [
    {
      id: "stage1_mid",
      stage: 1,
      role: "mid",
      name: "リリス・ミコス",
      title: "中ボス機パイロット",
      asset: "stage1MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#ff8bd8",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage1MidPilotDefeat", killed: "stage1MidPilotKilled", nude: "midPilotInterrogationNude" },
      lines: makeStage1MidInterrogationLines(),
      turns: makeStage1MidInterrogationTurns(),
    },
    {
      id: "stage1_boss",
      stage: 1,
      role: "boss",
      name: "近衛隊長 リヴィア・ノクス",
      title: "近衛隊長",
      asset: "stage1PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#8fdcff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage1PilotDefeat", killed: "stage1BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage2_mid",
      stage: 2,
      role: "mid",
      name: "補給士官 エリカ・ヴェイル",
      title: "補給護衛隊パイロット",
      asset: "stage2MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#7dbbff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage2MidPilotDefeat", killed: "stage2MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage2_boss",
      stage: 2,
      role: "boss",
      name: "星翼隊長 ミミ・スターリィ",
      title: "星翼隊長",
      asset: "stage2PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#9fdcff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage2PilotDefeat", killed: "stage2BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage3_mid",
      stage: 3,
      role: "mid",
      name: "花冠士官 セラフィナ・ルミナ",
      title: "花冠隊迎撃士官",
      asset: "stage3MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#ff9bd7",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage3MidPilotDefeat", killed: "stage3MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage3_boss",
      stage: 3,
      role: "boss",
      name: "黒翼強襲隊長 アリア・ヴァルカ",
      title: "黒翼強襲隊長",
      asset: "stage3PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ffb15f",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage3PilotDefeat", killed: "stage3BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage4_mid",
      stage: 4,
      role: "mid",
      name: "レティシア・グレイヴ",
      title: "王都防空管制官",
      asset: "stage4MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#6fb9ff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage4MidPilotDefeat", killed: "stage4MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage4_boss",
      stage: 4,
      role: "boss",
      name: "フィーネ・エリュシオン",
      title: "第七白翼航空隊長",
      asset: "stage4PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#f4d36b",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage4PilotDefeat", killed: "stage4BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage5_mid",
      stage: 5,
      role: "mid",
      name: "ノア・エリュシオン",
      title: "第七白翼航空隊予備隊長",
      asset: "stage5MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#b79cff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["fiene_younger_sister"],
      images: { captured: "stage5MidPilotDefeat", killed: "stage5MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage5_boss",
      stage: 5,
      role: "boss",
      name: "ヴェロニカ・クロイツ",
      title: "王都中央防衛軍司令",
      asset: "stage5PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ff596f",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage5PilotDefeat", killed: "stage5BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage6_mid",
      stage: 6,
      role: "mid",
      name: "エイラ・カーティス",
      title: "アレス第七基地迎撃隊長",
      asset: "stage6MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#ff9b48",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage6MidPilotDefeat", killed: "stage6MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage6_boss",
      stage: 6,
      role: "boss",
      name: "ミラ・ヴァレンタイン",
      title: "軍事基地特務防衛司令",
      asset: "stage6PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ff5ab8",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: [],
      images: { captured: "stage6PilotDefeat", killed: "stage6BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage7_mid",
      stage: 7,
      role: "mid",
      name: "シオン・クロウリー",
      title: "アストラルウイング保安執行官",
      asset: "stage7MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#d7e9ff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_security"],
      images: { captured: "stage7MidPilotDefeat", killed: "stage7MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage7_boss",
      stage: 7,
      role: "boss",
      name: "レイナ・キャットハート",
      title: "コロニー第一内環防衛隊長",
      asset: "stage7PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ff82c8",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_inner_ring"],
      images: { captured: "stage7PilotDefeat", killed: "stage7BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage8_mid",
      stage: 8,
      role: "mid",
      name: "カレン・オルティス",
      title: "第二内環航路管制官",
      asset: "stage8MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#9cecff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_route_control"],
      images: { captured: "stage8MidPilotDefeat", killed: "stage8MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage8_boss",
      stage: 8,
      role: "boss",
      name: "イリーナ・フォルク",
      title: "第二内環機動防衛司令",
      asset: "stage8PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ff9d4d",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_magnetic_rail"],
      images: { captured: "stage8PilotDefeat", killed: "stage8BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
    {
      id: "stage9_mid",
      stage: 9,
      role: "mid",
      name: "\u30f4\u30a7\u30b9\u30d1\u30fb\u30ec\u30be\u30ca",
      title: "\u4e2d\u67a2\u642c\u9001\u7db2\u30c9\u30ed\u30fc\u30f3\u7ba1\u5236\u58eb\u5b98",
      asset: "stage9MidPilotDefeat",
      fallbackAsset: "midPilotDefeat",
      accent: "#d9a8ff",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_conveyor_control"],
      images: { captured: "stage9MidPilotDefeat", killed: "stage9MidPilotKilled" },
      lines: makeInterrogationLines("mid"),
    },
    {
      id: "stage9_boss",
      stage: 9,
      role: "boss",
      name: "\u30bb\u30ec\u30cd\u30fb\u30b0\u30e9\u30f4\u30a3\u30b9",
      title: "\u30b3\u30ed\u30cb\u30fc\u91cd\u529b\u7089\u7d71\u62ec\u57f7\u653f\u5b98",
      asset: "stage9PilotDefeat",
      fallbackAsset: "pilotDefeat",
      accent: "#ff8b5d",
      status: "locked",
      done: false,
      killed: false,
      captured: false,
      interrogationProgress: 0,
      flags: ["colony_gravity_core"],
      images: { captured: "stage9PilotDefeat", killed: "stage9BossKilled" },
      lines: makeInterrogationLines("boss"),
    },
  ];
  const dialogs = {
    midBossIntro: {
      image: "midPilot",
      next: "startMidBoss",
      lines: [
        { speaker: midPilotName, text: "あらら、こんなとこまで来ちゃダメですよ～♪" },
      ],
    },
    midBossHalf: {
      image: "midPilotCrisis",
      next: "resume",
      lines: [
        { speaker: midPilotName, text: "えっ、ウソ！？そんな機体でなんて攻撃力！？" },
      ],
    },
    midBossDefeat: {
      image: "midPilotDefeat",
      next: "resume",
      lines: [
        { speaker: midPilotName, text: "うっうっ・・・" },
      ],
    },
    preBoss: {
      image: "pilot",
      next: "startBoss",
      lines: [
        { speaker: pilotName, text: "\u8cb4\u69d8\u306e\u3088\u3046\u306a\u90aa\u306a\u8005\u3092\u59eb\u306b\u8fd1\u3065\u3051\u308b\u308f\u3051\u306b\u306f\u3044\u304b\u306a\u3044\u3063\uff01\uff01" },
        { speaker: pilotName, text: "\u3053\u3053\u3067\u5875\u3068\u306a\u308c\u3063\uff01\uff01" },
        { speaker: "SYSTEM", text: "WARNING: BOSS APPROACH" },
      ],
    },
    halfBoss: {
      image: "pilotCrisis",
      next: "resume",
      lines: [
        { speaker: pilotName, text: "\u304f\u3063\uff01\uff01" },
        { speaker: pilotName, text: "\u3053\u3093\u306a\u4e0b\u52a3\u306a\u8005\u306b\u79c1\u304c\u3053\u3053\u307e\u3067\u82e6\u6226\u3059\u308b\u306e\u304b\u3063\uff01\uff1f" },
        { speaker: pilotName, text: "\u59eb\u3060\u3051\u306f\u7d76\u5bfe\u306b\u5b88\u308b\u3063\uff01\uff01" },
      ],
    },
    defeatBoss: {
      image: "pilotDefeat",
      next: "clear",
      lines: [
        { speaker: pilotName, text: "くっ・・・貴様なぞに・・・" },
      ],
    },
  };
  const stageSetups = {
    1: {
      midName: "リリス・ミコス",
      bossName: "近衛隊長 リヴィア・ノクス",
      midTitle: "中ボス機パイロット",
      bossTitle: "近衛隊長",
      midAccent: "#ff8bd8",
      bossAccent: "#8fdcff",
      midIntro: ["あらら、こんなとこまで来ちゃダメですよ～♪"],
      midHalf: ["えっ、ウソ！？そんな機体でなんて攻撃力！？"],
      midDefeat: ["うっうっ・・・"],
      bossIntro: ["貴様のような邪な者を姫に近づけるわけにはいかないっ！！", "ここで塵となれっ！！", "WARNING: BOSS APPROACH"],
      bossHalf: ["くっ！！", "こんな下劣な者に私がここまで苦戦するのかっ！？", "姫だけは絶対に守るっ！！"],
      bossDefeat: ["くっ・・・貴様なぞに・・・"],
      midKillLine: "やだ……姫さま……助け……",
      bossKillLine: "姫……申し訳、ございません……",
      midHp: 9400,
      bossHp: 13000,
    },
    2: {
      midName: "補給士官 エリカ・ヴェイル",
      bossName: "星翼隊長 ミミ・スターリィ",
      midTitle: "補給護衛隊パイロット",
      bossTitle: "星翼隊長",
      midAccent: "#7dbbff",
      bossAccent: "#9fdcff",
      midIntro: ["ここから先は補給宙域よ。勝手に荒らされちゃ困るわ。", "悪いけど、あなたの航路はここで止める！"],
      midHalf: ["計算が合わない……そんな火力、聞いてないわよ！"],
      midDefeat: ["ごめん……管制、あとはお願い……"],
      bossIntro: ["みんなをいじめる悪い機体、見つけた！", "スターリィ隊、全機で押し返すよっ！", "WARNING: BOSS APPROACH"],
      bossHalf: ["やだ、シールドがもう半分！？", "でも泣かない……隊長だもん！"],
      bossDefeat: ["うぅ……まだ、飛べるのに……"],
      midKillLine: "管制……応答して……もう、見えない……",
      bossKillLine: "みんな……ごめんね……",
      midHp: 12600,
      bossHp: 16800,
    },
    3: {
      midName: "花冠士官 セラフィナ・ルミナ",
      bossName: "黒翼強襲隊長 アリア・ヴァルカ",
      midTitle: "花冠隊迎撃士官",
      bossTitle: "黒翼強襲隊長",
      midAccent: "#ff9bd7",
      bossAccent: "#ffb15f",
      midIntro: ["ふふっ、ここから先は花冠隊の管制圏よ。迷子なら今のうちに引き返しなさい。", "ルミナ-07、迎撃開始。派手に咲かせてあげる！"],
      midHalf: ["ちょ、ちょっと待って！　そんな乱暴な撃ち方、予定にないんだけど！？"],
      midDefeat: ["うそ……私の花冠隊が、こんな簡単に……"],
      bossIntro: ["へぇ、セラフィナを抜いたんだ。思ったよりやるじゃん。", "でもここからは私の狩場。黒翼隊、突撃するよ！", "WARNING: BOSS APPROACH"],
      bossHalf: ["くっ、装甲が……！　いいね、ここから本気で潰しに行く！"],
      bossDefeat: ["嘘でしょ……私が押し切られるなんて……"],
      midKillLine: "いや……まだ、終わりたく……ない……",
      bossKillLine: "やっ、やだっ!!あの人と離れたくっ・・・\nグブッ！",
      midHp: 11000,
      bossHp: 15000,
    },
    4: {
      midName: "レティシア・グレイヴ",
      bossName: "フィーネ・エリュシオン",
      midTitle: "王都防空管制官",
      bossTitle: "第七白翼航空隊長",
      midAccent: "#6fb9ff",
      bossAccent: "#f4d36b",
      midIntro: ["こちら王都防空管制、未確認機へ告ぐ。直ちに高度を落として投降しなさい。", "警告は一度だけよ。領空侵犯機を迎撃します！"],
      midHalf: ["回避計算が追いつかない……この低空で、なんて機動を！"],
      midDefeat: ["防空網が……突破される……！"],
      bossIntro: ["レティシアさんを倒したの、あなたなの……？", "第七白翼隊フィーネ、ここから先は絶対に通さない！", "WARNING: BOSS APPROACH"],
      bossHalf: ["う、嘘……！　地上のみんなが見てるのに……！", "まだ飛べる。白翼隊は、ここで退かない！"],
      bossDefeat: ["そんな……王都の空を、守れなかった……"],
      midKillLine: "管制塔……応答を……領空を、守れなくて……",
      bossKillLine: "やだ……まだ、帰れない……みんなが待ってるのに……",
      midHp: 14500,
      bossHp: 19500,
    },
    5: {
      midName: "ノア・エリュシオン",
      bossName: "ヴェロニカ・クロイツ",
      midTitle: "第七白翼航空隊予備隊長",
      bossTitle: "王都中央防衛軍司令",
      midAccent: "#b79cff",
      bossAccent: "#ff596f",
      midIntro: ["姉さまの機体反応が消えた……あなたがやったの？", "ノア・エリュシオン、ここで絶対に止める！"],
      midHalf: ["こんなはずじゃ……姉さまなら、もっと上手く戦えたのに！"],
      midDefeat: ["姉さま……私まで、負けちゃった……"],
      bossIntro: ["外縁防空隊を五つも抜いたか。見事だ。", "だが都市中枢は私の庭だ。ヴェロニカ・クロイツ、直々に処刑する。", "WARNING: BOSS APPROACH"],
      bossHalf: ["包囲網を正面から破るとは……ならば街区ごと封鎖するまで！"],
      bossDefeat: ["中央防衛網が……たった一機に、崩されるなんて……"],
      midKillLine: "姉さま……ごめん、追いつけなかった……",
      bossKillLine: "都市防衛権限を……最終移譲……ここまで、か……",
      midHp: 16800,
      bossHp: 23200,
    },
    6: {
      midName: "エイラ・カーティス",
      bossName: "ミラ・ヴァレンタイン",
      midTitle: "アレス第七基地迎撃隊長",
      bossTitle: "軍事基地特務防衛司令",
      midAccent: "#ff9b48",
      bossAccent: "#ff5ab8",
      midIntro: ["こちらアレス07、基地上空に侵入機を確認。悪いけど、ここから先へは行かせないよ！", "迎撃隊長エイラ・カーティス、全砲門開放。滑走路に近づく前に叩き落とす！"],
      midHalf: ["やるじゃない……！　だったら着弾点ごと空を埋めてあげる！"],
      midDefeat: ["くそっ……基地の防空線を、抜かれた……！"],
      bossIntro: ["基地の空が騒がしいと思ったら、ずいぶん派手に暴れてくれたね。", "特務防衛司令ミラ・ヴァレンタイン。あなたの機体、かわいく壊してあげる。", "WARNING: BOSS APPROACH"],
      bossHalf: ["本気でムカついてきた……！　格納庫も砲台も、全部まとめて空へ上げる！"],
      bossDefeat: ["うそ……私の基地が、たった一機に……？"],
      midKillLine: "まだ……滑走路は……守らないと……",
      bossKillLine: "やだ……ここで終わるの……？　まだ遊び足りないのに……",
      midHp: 30000,
      bossHp: 52000,
    },
    7: {
      midName: "シオン・クロウリー",
      bossName: "レイナ・キャットハート",
      midTitle: "アストラルウイング保安執行官",
      bossTitle: "コロニー第一内環防衛隊長",
      midAccent: "#d7e9ff",
      bossAccent: "#ff82c8",
      midIntro: ["侵入機を確認。ここは居住区へ通じる内環ゲートです。", "保安執行官シオン・クロウリー。静かに排除します。"],
      midHalf: ["火力評価を更新……危険度、最上位。ならば重力錨で航路ごと封じる。"],
      midDefeat: ["内環ゲートが……開いてしまう……"],
      bossIntro: ["シオンを倒して、ここまで来たんだ？　思ったよりずっと面白いじゃん。", "第一内環防衛隊長レイナ・キャットハート。コロニーの中で、好き勝手できると思わないで！", "WARNING: BOSS APPROACH"],
      bossHalf: ["あんた、本当にしつこい！　内壁も回廊も全部、私の武器なんだから！"],
      bossDefeat: ["うそ……内環防衛が、破られた……？"],
      midKillLine: "ゲート封鎖……失敗……住民を、守れなかった……",
      bossKillLine: "まだ……奥には行かせない……絶対に……",
      midHp: 36000,
      bossHp: 64000,
    },
    8: {
      midName: "カレン・オルティス",
      bossName: "イリーナ・フォルク",
      midTitle: "第二内環航路管制官",
      bossTitle: "第二内環機動防衛司令",
      midAccent: "#9cecff",
      bossAccent: "#ff9d4d",
      midIntro: ["侵入機、第二内環の転送軌道へ進入……お願い、今すぐ引き返して。", "航路管制官カレン・オルティス。軌道反転システムで、あなたを出口へ送り返します。"],
      midHalf: ["反転予測が追いつかない……なら、航路そのものを連続で切り替える！"],
      midDefeat: ["管制リンク喪失……ごめんなさい、軌道を守れなかった……"],
      bossIntro: ["カレンの管制網まで破ったか。だが、ここから先は通路ではない。私の戦場だ。", "第二内環機動防衛司令イリーナ・フォルク。磁気レール網で、その機体を解体する。", "WARNING: BOSS APPROACH"],
      bossHalf: ["まだ網を抜けるのか！　全レールを再配置、逃げ道ごと撃ち抜く！"],
      bossDefeat: ["第二内環が……私の防衛線が、崩れる……"],
      midKillLine: "管制席を……離れられない……みんなを、帰さないと……",
      bossKillLine: "まだ……内環は落ちていない……私が、止める……",
      midHp: 42000,
      bossHp: 76000,
    },
    9: {
      midName: "\u30f4\u30a7\u30b9\u30d1\u30fb\u30ec\u30be\u30ca",
      bossName: "\u30bb\u30ec\u30cd\u30fb\u30b0\u30e9\u30f4\u30a3\u30b9",
      midTitle: "\u4e2d\u67a2\u642c\u9001\u7db2\u30c9\u30ed\u30fc\u30f3\u7ba1\u5236\u58eb\u5b98",
      bossTitle: "\u30b3\u30ed\u30cb\u30fc\u91cd\u529b\u7089\u7d71\u62ec\u57f7\u653f\u5b98",
      midAccent: "#d9a8ff",
      bossAccent: "#ff8b5d",
      midIntro: ["\u30b3\u30ed\u30cb\u30fc\u4e2d\u67a2\u642c\u9001\u7db2\u3078\u306e\u4fb5\u5165\u3092\u78ba\u8a8d\u3002\u5168\u30c9\u30ed\u30fc\u30f3\u3001\u8fce\u6483\u8ecc\u9053\u3078\u3002", "\u30f4\u30a7\u30b9\u30d1\u30fb\u30ec\u30be\u30ca\u3002\u3053\u3053\u304b\u3089\u5148\u306f\u4e00\u6a5f\u305f\u308a\u3068\u3082\u901a\u3055\u306a\u3044\u3002"],
      midHalf: ["\u56de\u8ee2\u540c\u671f\u304c\u4e71\u308c\u305f\u2026\u2026\u306a\u3089\u3001\u5168\u6a5f\u3092\u76f4\u63a5\u3076\u3064\u3051\u308b\uff01"],
      midDefeat: ["\u642c\u9001\u7db2\u304c\u2026\u2026\u79c1\u306e\u6307\u63ee\u304b\u3089\u3001\u96e2\u308c\u3066\u3044\u304f\u2026\u2026"],
      bossIntro: ["\u5916\u74b0\u3082\u5185\u74b0\u3082\u8d8a\u3048\u3066\u3001\u3064\u3044\u306b\u91cd\u529b\u7089\u307e\u3067\u6765\u305f\u306e\u306d\u3002", "\u30bb\u30ec\u30cd\u30fb\u30b0\u30e9\u30f4\u30a3\u30b9\u3002\u30b3\u30ed\u30cb\u30fc\u306e\u5fc3\u81d3\u3054\u3068\u3001\u3042\u306a\u305f\u3092\u62bc\u3057\u6f70\u3059\u3002", "WARNING: BOSS APPROACH"],
      bossHalf: ["\u91cd\u529b\u4f4d\u76f8\u304c\u53cd\u8ee2\u3059\u308b\u2026\u2026\uff1f\u3000\u306a\u3089\u7089\u5fc3\u3092\u81e8\u754c\u307e\u3067\u958b\u304f\uff01"],
      bossDefeat: ["\u91cd\u529b\u7089\u304c\u2026\u2026\u9759\u304b\u306b\u3001\u306a\u3063\u3066\u3044\u304f\u2026\u2026"],
      midKillLine: "\u30c9\u30ed\u30fc\u30f3\u7fa4\u2026\u2026\u5e30\u6295\u3057\u3066\u2026\u2026\u79c1\u3092\u3001\u7f6e\u3044\u3066\u2026\u2026",
      bossKillLine: "\u7089\u5fc3\u505c\u6b62\u2026\u2026\u30b3\u30ed\u30cb\u30fc\u3092\u2026\u2026\u5b88\u308c\u306a\u304b\u3063\u305f\u2026\u2026",
      midHp: 50000,
      bossHp: 90000,
    },
  };

  const LAST_IMPLEMENTED_STAGE = 1;
  const stageRuntime = {
    1: { bossReadyTime: 48, midApproachTime: 32, bossApproachDelay: 36, label: "STAGE 1", color: "#9fdcff", assetPrefix: "stage1" },
    2: { bossReadyTime: 52, midApproachTime: 34, bossApproachDelay: 38, label: "STAGE 2", color: "#9fdcff", assetPrefix: "stage2" },
    3: { bossReadyTime: 54, midApproachTime: 34, bossApproachDelay: 40, label: "STAGE 3", color: "#ffd45f", assetPrefix: "stage3" },
    4: { bossReadyTime: 58, midApproachTime: 36, bossApproachDelay: 42, label: "STAGE 4 - GROUND FRONT", color: "#7fd7ff", assetPrefix: "stage4" },
    5: { bossReadyTime: 64, midApproachTime: 40, bossApproachDelay: 46, label: "STAGE 5 - NEON CAPITAL", color: "#cf9cff", assetPrefix: "stage5" },
    6: { bossReadyTime: 70, midApproachTime: 42, bossApproachDelay: 50, label: "STAGE 6 - ARES AIRBASE", color: "#ff9b48", assetPrefix: "stage6" },
    7: { bossReadyTime: 76, midApproachTime: 45, bossApproachDelay: 54, label: "STAGE 7 - COLONY OUTER RING", color: "#ff82c8", assetPrefix: "stage7" },
    8: { bossReadyTime: 80, midApproachTime: 47, bossApproachDelay: 56, label: "STAGE 8 - POLARITY TRANSIT", color: "#ff9d4d", assetPrefix: "stage8" },
    9: { bossReadyTime: 86, midApproachTime: 49, bossApproachDelay: 62, label: "STAGE 9 - GRAVITY CORE", color: "#b78cff", assetPrefix: "stage9" },
  };

  function rememberStage1Assets() {
    if (assets.stage1MidPilot) return;
    assets.stage1MidPilot = assets.midPilot;
    assets.stage1MidPilotCrisis = assets.midPilotCrisis;
    assets.stage1MidPilotDefeat = assets.midPilotDefeat;
    assets.stage1MidPilotKilled = assets.midPilotKilled;
    assets.stage1Pilot = assets.pilot;
    assets.stage1PilotCrisis = assets.pilotCrisis;
    assets.stage1PilotDefeat = assets.pilotDefeat;
    assets.stage1BossKilled = assets.bossKilled;
  }

  function runtimeForStage(stage = currentStage) {
    return stageRuntime[stage] || stageRuntime[1];
  }

  function applyStageAssetBundle(stage) {
    const prefix = runtimeForStage(stage).assetPrefix;
    assets.midPilot = assets[`${prefix}MidPilot`] || assets.stage1MidPilot;
    assets.midPilotCrisis = assets[`${prefix}MidPilotCrisis`] || assets.stage1MidPilotCrisis;
    assets.midPilotDefeat = assets[`${prefix}MidPilotDefeat`] || assets.stage1MidPilotDefeat;
    assets.midPilotKilled = assets[`${prefix}MidPilotKilled`] || assets.stage1MidPilotKilled;
    assets.pilot = assets[`${prefix}Pilot`] || assets.stage1Pilot;
    assets.pilotCrisis = assets[`${prefix}PilotCrisis`] || assets.stage1PilotCrisis;
    assets.pilotDefeat = assets[`${prefix}PilotDefeat`] || assets.stage1PilotDefeat;
    assets.bossKilled = assets[`${prefix}BossKilled`] || assets.stage1BossKilled;
  }

  function applyStageSetup(stage) {
    rememberStage1Assets();
    currentStage = stage;
    const setup = stageSetups[stage] || stageSetups[1];
    midPilotName = setup.midName;
    pilotName = setup.bossName;
    applyStageAssetBundle(stage);
    dialogs.midBossIntro.lines = setup.midIntro.map((text) => ({ speaker: midPilotName, text }));
    dialogs.midBossHalf.lines = setup.midHalf.map((text) => ({ speaker: midPilotName, text }));
    dialogs.midBossDefeat.lines = setup.midDefeat.map((text) => ({ speaker: midPilotName, text }));
    dialogs.preBoss.lines = setup.bossIntro.map((text) => ({ speaker: text.startsWith("WARNING") ? "SYSTEM" : pilotName, text }));
    dialogs.halfBoss.lines = setup.bossHalf.map((text) => ({ speaker: pilotName, text }));
    dialogs.defeatBoss.lines = setup.bossDefeat.map((text) => ({ speaker: pilotName, text }));
  }

  function stageSetup() {
    return stageSetups[currentStage] || stageSetups[1];
  }

  function midBossApproachTime() {
    return runtimeForStage().midApproachTime;
  }

  function bossApproachDelay() {
    return runtimeForStage().bossApproachDelay;
  }
  function startDialog(kind) {
    if (currentStage === 9) stage9AttackFx = [];
    if (currentStage === 8) stage8AttackFx = [];
    if (currentStage === 7) stage7AttackFx = [];
    if (currentStage === 6) stage6AttackFx = [];
    if (currentStage === 5) stage5AttackFx = [];
    if (currentStage === 4) stage4AttackFx = [];
    state = "dialog";
    activeDialog = dialogs[kind];
    dialogStep = 0;
    if (kind === "midBossIntro") {
      midBossSpawned = true;
      enemies = [];
      enemyBullets = [];
      lasers = [];
      clearPowerUps(true);
    }
    if (kind === "midBossHalf") {
      midBossHalfDialogShown = true;
      enemyBullets = [];
      lasers = [];
    }
    if (kind === "midBossDefeat") {
      enemyBullets = [];
      lasers = [];
    }
    if (kind === "preBoss") {
      bossIntroShown = true;
      enemies = [];
      enemyBullets = [];
      lasers = [];
      clearPowerUps(true);
    }
    if (kind === "halfBoss") {
      bossHalfDialogShown = true;
      enemyBullets = [];
      lasers = [];
    }
  }


  function startApproachAlert(kind, nextDialog) {
    if (currentStage === 9) stage9AttackFx = [];
    if (currentStage === 8) stage8AttackFx = [];
    if (currentStage === 7) stage7AttackFx = [];
    if (currentStage === 6) stage6AttackFx = [];
    if (currentStage === 5) stage5AttackFx = [];
    if (currentStage === 4) stage4AttackFx = [];
    state = "alert";
    alertScene = {
      kind,
      nextDialog,
      t: 0,
      duration: kind === "boss" ? 3.4 : 2.9,
      pulse: 0,
      title: kind === "boss" ? "WARNING: LARGE HOSTILE APPROACH" : "WARNING: FAST HOSTILE APPROACH",
      sub: kind === "boss" ? "BOSS SIGNATURE DETECTED" : "MID-BOSS SIGNATURE DETECTED",
    };
    enemies = [];
    enemyBullets = [];
    lasers = [];
    clearPowerUps(true);
    flash = Math.max(flash, 0.12);
    shake = Math.max(shake, 0.18);
    playAlertTone(kind);
  }

  function updateApproachAlert(dt) {
    if (!alertScene) return;
    time += dt;
    bgScroll = (bgScroll + 70 * dt) % H;
    shake = Math.max(0, shake - dt);
    flash = Math.max(0, flash - dt);
    alertScene.t += dt;
    alertScene.pulse -= dt;
    updateParticles(dt);
    if (alertScene.pulse <= 0) {
      alertScene.pulse = alertScene.kind === "boss" ? 0.48 : 0.58;
      playAlertTone(alertScene.kind);
      shake = Math.max(shake, alertScene.kind === "boss" ? 0.22 : 0.15);
      particles.push({ x: W / 2, y: 116, vx: 0, vy: 0, life: 0.28, maxLife: 0.28, color: alertScene.kind === "boss" ? "#ff4fd8" : "#ffdf70", type: "damageRing", size: alertScene.kind === "boss" ? 190 : 145 });
    }
    if (alertScene.t >= alertScene.duration) {
      const next = alertScene.nextDialog;
      alertScene = null;
      startDialog(next);
    }
  }
  function startMidBossDialog() {
    startApproachAlert("midBoss", "midBossIntro");
  }

  function startMidBossHalfDialog() {
    startDialog("midBossHalf");
  }

  function startMidBossDefeatDialog() {
    startDialog("midBossDefeat");
  }

  function startBossDialog() {
    startApproachAlert("boss", "preBoss");
  }

  function startBossHalfDialog() {
    startDialog("halfBoss");
  }

  function startBossDefeatDialog() {
    startDialog("defeatBoss");
  }

  function advanceDialog() {
    if (state !== "dialog" || !activeDialog) return;
    dialogStep += 1;
    if (dialogStep >= activeDialog.lines.length) {
      const next = activeDialog.next;
      activeDialog = null;
      alertScene = null;
      state = "play";
      if (next === "startMidBoss") startMidBoss();
      if (next === "startBoss") startBoss();
      if (next === "clear") {
        openInterrogationSelect();
      }
    }
  }

  function startDefeatChoice(kind) {
    state = "defeatChoice";
    defeatChoice = { kind, selected: "capture", t: 0 };
    enemyBullets = [];
    lasers = [];
    flash = Math.max(flash, 0.08);
    updateHud();
  }

  function defeatChoiceRects() {
    const y = H - 220;
    return [
      { action: "capture", label: "捕獲", x: 54, y, w: 166, h: 72 },
      { action: "kill", label: "殺害", x: W - 220, y, w: 166, h: 72 },
    ];
  }

  function chooseDefeatOutcome(action) {
    if (state !== "defeatChoice" || !defeatChoice) return;
    const kind = defeatChoice.kind;
    defeatChoice = null;
    if (kind === "mid") {
      if (action === "capture") {
        setOfficerOutcome("mid", "capture");
        recordCapture(midPilotName, stageSetup().midTitle, 2, "中ボス機");
        startMidBossDefeatDialog();
      } else {
        setOfficerOutcome("mid", "kill");
        recordOfficerKilled(midPilotName, stageSetup().midTitle, 2, "中ボス機");
        startKillCutin("mid", "resume");
      }
      return;
    }
    if (action === "capture") {
      setOfficerOutcome("boss", "capture");
      recordCapture(pilotName, stageSetup().bossTitle, 6, "ボス機");
      startBossDefeatDialog();
    } else {
      setOfficerOutcome("boss", "kill");
      recordOfficerKilled(pilotName, stageSetup().bossTitle, 6, "ボス機");
      startKillCutin("boss", "clear");
    }
  }

  function startKillCutin(kind, next) {
    state = "killCutin";
    killCutinScene = {
      kind,
      next,
      t: 0,
      duration: kind === "boss" ? 3.0 : 3.2,
      minWait: kind === "boss" ? 1.15 : 1.05,
      speaker: kind === "boss" ? pilotName : midPilotName,
      line: kind === "boss" ? stageSetup().bossKillLine : stageSetup().midKillLine,
    };
    enemyBullets = [];
    lasers = [];
    flash = Math.max(flash, 0.52);
    shake = Math.max(shake, 0.78);
    playKillExplosionSound(kind);
  }

  function advanceKillCutin() {
    if (state !== "killCutin" || !killCutinScene) return;
    if (killCutinScene.t < (killCutinScene.minWait || 0.8)) return;
    const next = killCutinScene.next;
    killCutinScene = null;
    if (next === "clear") openInterrogationSelect();
    else state = "play";
  }

  function clearPowerUps(keepHeal = false) {
    powerUps = keepHeal ? powerUps.filter((p) => p.type === "heal" && !p.dead) : [];
    syncHealSiren();
  }
  function openInterrogationSelect() {
    state = "interrogationSelect";
    initAudio();
    playBgm("victory");
    overlay.classList.add("hidden");
    interrogationTarget = null;
    interrogationStep = 0;
    interrogationNotice = null;
    killedPreviewTarget = null;
    enemyBullets = [];
    lasers = [];
    stage4AttackFx = [];
    stage5AttackFx = [];
    stage6AttackFx = [];
    stage7AttackFx = [];
    stage8AttackFx = [];
    stage9AttackFx = [];
    enemies = [];
    powerUps = [];
    stopHealSiren();
    casualtyFeedFull = true;
    updateCasualtyFeed();
    updateHud();
  }

  function startKilledPreview(target) {
    if (!target || !target.killed) return;
    killedPreviewTarget = target;
    interrogationNotice = null;
    state = "killedPreview";
    flash = Math.max(flash, 0.12);
    playTone(220, 0.08, "square", 0.05, 0);
    playTone(140, 0.16, "sawtooth", 0.04, 0.07);
  }

  function closeKilledPreview() {
    if (state !== "killedPreview") return;
    killedPreviewTarget = null;
    state = "interrogationSelect";
    flash = Math.max(flash, 0.08);
  }

  function startInterrogation(target) {
    if (target.killed) {
      startKilledPreview(target);
      return;
    }
    interrogationNotice = null;
    killedPreviewTarget = null;
    interrogationTarget = target;
    interrogationStep = 0;
    state = "interrogation";
    flash = Math.max(flash, 0.12);
    playTone(520, 0.08, "triangle", 0.07, 0);
    playTone(780, 0.08, "sine", 0.06, 0.07);
    playTone(1040, 0.12, "triangle", 0.055, 0.14);
    playTone(1560, 0.16, "sine", 0.045, 0.21);
    applyInterrogationEditorBgm(target);
    triggerInterrogationPresentation(interrogationTurnsFor(target)[0]);
  }

  function interrogationTurnsFor(target) {
    const editedTurns = interrogationEditorTurnsFor(target);
    if (editedTurns) return editedTurns;
    if (Array.isArray(target.turns)) return target.turns;
    return target.lines.flatMap((exchange) => {
      const entry = typeof exchange === "string" ? { player: exchange, enemy: exchange } : exchange;
      return [
        { speaker: playerPilotName, accent: "#9fe8ff", asset: entry.asset, stage: entry.stage, text: entry.player },
        { speaker: target.name, accent: target.accent, asset: entry.asset, stage: entry.stage, text: entry.enemy },
      ];
    });
  }

  function advanceInterrogation() {
    if (state !== "interrogation" || !interrogationTarget) return;
    const turns = interrogationTurnsFor(interrogationTarget);
    interrogationStep += 1;
    if (interrogationStep >= turns.length) {
      interrogationTarget.done = true;
      interrogationTarget.status = "interrogated";
      interrogationTarget.interrogationProgress = turns.length;
      if (interrogationEditorSceneFor(interrogationTarget)) playBgm("victory");
      interrogationTarget = null;
      interrogationStep = 0;
      state = "interrogationSelect";
      flash = Math.max(flash, 0.1);
    } else {
      triggerInterrogationPresentation(turns[interrogationStep]);
    }
  }

  function interrogationCardRects() {
    return activeInterrogationTargets().map((target, i) => ({ target, x: 38, y: 150 + i * 214, w: W - 76, h: 184 }));
  }

  function allInterrogationTargetsResolved() {
    return activeInterrogationTargets().every((target) => target.done || target.killed);
  }

  function nextStageButtonRect() {
    return { x: W / 2 - 142, y: H - 78, w: 284, h: 46 };
  }

  function goNextStage() {
    if (!allInterrogationTargetsResolved()) {
      interrogationNotice = { text: "未処理の捕虜がいます", t: 2.0 };
      playTone(180, 0.1, "square", 0.06, 0);
      return;
    }
    if (currentStage >= LAST_IMPLEMENTED_STAGE) {
      interrogationNotice = null;
      killedPreviewTarget = null;
      state = "clear";
      showDemoClearOverlay();
      return;
    }
    startStage(currentStage + 1, { preserveCasualtyTotals: true });
  }

  function startStage(stage, { preserveCasualtyTotals = false } = {}) {
    const runtime = stageRuntime[stage];
    if (!runtime || !stageSetups[stage]) throw new Error(`Stage ${stage} is not registered`);
    applyStageSetup(stage);
    state = "play";
    time = 0;
    healAssistUsed = false;
    resetCasualtyFeed({ preserveTotals: preserveCasualtyTotals });
    resetOfficerProgress(stage);
    boss = null;
    midBoss = null;
    midBossSpawned = false;
    midBossDefeated = false;
    midBossHalfDialogShown = false;
    bossReadyTime = runtime.bossReadyTime;
    shake = 0;
    flash = Math.max(flash, 0.18);
    bgScroll = 0;
    nextWave = 1;
    bossIntroShown = false;
    bossHalfDialogShown = false;
    dialogStep = 0;
    activeDialog = null;
    alertScene = null;
    defeatChoice = null;
    killCutinScene = null;
    interrogationTarget = null;
    interrogationNotice = null;
    killedPreviewTarget = null;
    player.x = W / 2;
    player.y = H - 86;
    player.hp = player.maxHp;
    player.bombs = Math.max(player.bombs, 3);
    player.inv = 3.0;
    player.fire = 0;
    player.respawn = 0;
    player.hitPulse = 0;
    playerBullets = [];
    enemyBullets = [];
    lasers = [];
    stage4AttackFx = [];
    stage5AttackFx = [];
    stage6AttackFx = [];
    stage7AttackFx = [];
    stage8AttackFx = [];
    stage9AttackFx = [];
    enemies = [];
    powerUps = [];
    stopHealSiren();
    particles = [];
    wreckPieces = [];
    destructionScene = null;
    overlay.classList.add("hidden");
    initAudio();
    playBgm(stageBgmMode());
    particles.push({ x: W / 2, y: 110, vx: 0, vy: -18, life: 1.4, maxLife: 1.4, color: runtime.color, type: "floatingText", text: runtime.label, size: stage >= 4 ? 25 : 30 });
    updateHud();
  }

  function startStageTwo(options = {}) { startStage(2, options); }
  function startStageThree(options = {}) { startStage(3, options); }
  function startStageFour(options = {}) { startStage(4, options); }
  function startStageFive(options = {}) { startStage(5, options); }
  function startStageSix(options = {}) { startStage(6, options); }

  function clearPlayfield() {
    playerBullets = [];
    enemyBullets = [];
    lasers = [];
    stage4AttackFx = [];
    stage5AttackFx = [];
    stage6AttackFx = [];
    stage7AttackFx = [];
    stage8AttackFx = [];
    stage9AttackFx = [];
    enemies = [];
    clearPowerUps();
    particles = [];
    flash = 0;
    shake = 0;
  }

  function prepareDevRun() {
    overlay.classList.add("hidden");
    initAudio();
    playBgm(stageBgmMode());
    state = "play";
    clearPlayfield();
    resetCasualtyFeed();
    player.x = W / 2;
    player.y = H - 86;
    player.hp = player.maxHp;
    player.lives = 3;
    player.bombs = 3;
    player.power = 5;
    player.powerStock = 0;
    player.respawn = 0;
    player.hitPulse = 0;
    player.inv = 3.0;
    player.fire = 0;
    healAssistUsed = false;
    resetCasualtyFeed();
    resetOfficerProgress(currentStage);
    boss = null;
    midBoss = null;
    midBossSpawned = false;
    midBossDefeated = false;
    midBossHalfDialogShown = false;
    bossReadyTime = 48;
    activeDialog = null;
    alertScene = null;
    wreckPieces = [];
    destructionScene = null;
    dialogStep = 0;
  }

  function devJump(target) {
    if (!DEV_TOOLS) return;
    const stageMatch = /^stage(\d+)$/.exec(target);
    if (stageMatch) {
      const stage = Number(stageMatch[1]);
      if (!stageRuntime[stage] || !stageSetups[stage]) return;
      if (stage >= 4) player.lives = Math.max(3, player.lives);
      startStage(stage);
      return;
    }
    if (target === "stage") applyStageSetup(1);
    prepareDevRun();
    if (target === "stage") {
      time = 0;
      nextWave = 1;
      bossIntroShown = false;
      bossHalfDialogShown = false;
    } else if (target === "midBoss") {
      time = midBossApproachTime();
      nextWave = 999;
      bossIntroShown = false;
      bossHalfDialogShown = false;
      midBossSpawned = false;
      midBossDefeated = false;
      midBossHalfDialogShown = false;
      bossReadyTime = 48;
      startMidBossDialog();
    } else if (target === "preBoss") {
      time = 48.05;
      bossIntroShown = false;
      bossHalfDialogShown = false;
      startBossDialog();
    } else if (target === "boss") {
      time = 49;
      bossIntroShown = true;
      bossHalfDialogShown = false;
      startBoss();
    } else if (target === "half") {
      time = 58;
      bossIntroShown = true;
      bossHalfDialogShown = false;
      startBoss();
      boss.hp = Math.floor(boss.maxHp * 0.5) - 1;
      startBossHalfDialog();
    } else if (target === "defeat") {
      time = 70;
      bossIntroShown = true;
      bossHalfDialogShown = true;
      boss = null;
      state = "destruction";
      enemyBullets = [];
      lasers = [];
      powerUps = [];
    stopHealSiren();
      startDestructionScene("boss", { x: W / 2, y: 112 }, "bossDefeat");
    }
    updateHud();
  }
  function showOverlay(title, body, button) {
    overlay.querySelector("h1").textContent = title;
    overlay.querySelector("p").textContent = body;
    startButton.textContent = button;
    startButton.hidden = false;
    demoClearActions.hidden = true;
    overlay.classList.remove("hidden");
  }

  function showDemoClearOverlay() {
    overlay.querySelector("h1").textContent = "Stage 1 Demo Clear";
    overlay.querySelector("p").textContent = "体験版はここまでです。製品版での続報をお待ちください。";
    startButton.hidden = true;
    demoClearActions.hidden = false;
    overlay.classList.remove("hidden");
  }

  function continueGame() {
    overlay.classList.add("hidden");
    initAudio();
    playBgm(boss ? bossBgmMode() : stageBgmMode());
    state = "play";
    player.lives = 3;
    player.hp = player.maxHp;
    player.bombs = Math.max(player.bombs, 3);
    player.power = 0;
    player.powerStock = 0;
    player.x = W / 2;
    player.y = H + 96;
    player.respawn = 2.25;
    player.respawnMax = 2.25;
    player.inv = 3.2;
    player.fire = 0.35;
    player.hitPulse = 0;
    healAssistUsed = false;
    enemyBullets = [];
    lasers = [];
    flash = Math.max(flash, 0.32);
    shake = Math.max(shake, 0.55);
    updateHud();
  }

  function spawnEnemy(type, x, y, vx, vy) {
    const stats = enemyStats[type] || enemyStats.drone;
    const laser = type === "laser";
    enemies.push({
      type,
      x,
      y,
      vx,
      vy,
      r: stats.r,
      hp: stats.hp,
      maxHp: stats.hp,
      fire: stats.fire,
      laserCharge: 0,
      laserReady: !laser,
      shieldFlash: 0,
      t: 0,
    });
  }


  function spawnStage2Wave() {
    const pattern = Math.floor(time / 4.0) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 7; i++) spawnEnemy("starScout", 44 + i * 65, -34 - i * 18, (i - 3) * 13, 92);
      spawnEnemy("prism", W / 2, -118, 0, 52);
    } else if (pattern === 1) {
      spawnEnemy("supplyCarrier", 104, -92, 22, 48);
      spawnEnemy("supplyCarrier", W - 104, -122, -22, 48);
      for (let i = 0; i < 4; i++) spawnEnemy("starScout", 84 + i * 104, -160 - i * 20, (i - 1.5) * 24, 118);
    } else if (pattern === 2) {
      for (let i = 0; i < 6; i++) spawnEnemy(i % 2 ? "prism" : "starScout", i % 2 ? W + 38 : -38, -34 - i * 30, i % 2 ? -132 : 132, 96);
    } else if (pattern === 3) {
      spawnEnemy("prism", 92, -76, 18, 58);
      spawnEnemy("prism", W - 92, -76, -18, 58);
      spawnEnemy("laser", W / 2, -135, 0, 48);
    } else if (pattern === 4) {
      for (let i = 0; i < 9; i++) spawnEnemy("starScout", 34 + i * 52, -28 - Math.abs(4 - i) * 24, 0, 112);
    } else if (pattern === 5) {
      spawnEnemy("supplyCarrier", W / 2, -118, 0, 54);
      spawnEnemy("prism", 82, -170, 34, 78);
      spawnEnemy("prism", W - 82, -170, -34, 78);
    } else if (pattern === 6) {
      spawnEnemy("supplyCarrier", 92, -118, 30, 54);
      spawnEnemy("prism", W / 2, -168, 0, 58);
      for (let i = 0; i < 5; i++) spawnEnemy("starScout", 58 + i * 86, -56 - i * 18, (i - 2) * 26, 124);
    } else if (pattern === 7) {
      spawnEnemy("laser", 76, -130, 16, 46);
      spawnEnemy("laser", W - 76, -130, -16, 46);
      spawnEnemy("supplyCarrier", W / 2, -196, 0, 50);
    } else if (pattern === 8) {
      for (let i = 0; i < 5; i++) spawnEnemy("starScout", 54 + i * 92, -40 - i * 34, Math.sin(i) * 70, 126);
      spawnEnemy("supplyCarrier", W / 2, -190, 0, 52);
    } else {
      spawnEnemy("laser", 84, -118, 12, 48);
      spawnEnemy("laser", W - 84, -148, -12, 48);
      for (let i = 0; i < 5; i++) spawnEnemy("prism", 54 + i * 94, -58 - i * 20, (i - 2) * 18, 86);
    }
  }

  function spawnStage2PostMidBossWave() {
    const pattern = Math.floor((time - 44) / 4.0) % 8;
    if (pattern === 0) {
      spawnEnemy("supplyCarrier", 94, -100, 26, 56);
      spawnEnemy("supplyCarrier", W - 94, -132, -26, 56);
      for (let i = 0; i < 6; i++) spawnEnemy("starScout", 45 + i * 78, -50 - i * 20, (i - 2.5) * 28, 126);
    } else if (pattern === 1) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 3 === 0 ? "prism" : "starScout", i % 2 ? W + 34 : -34, -28 - i * 28, i % 2 ? -158 : 158, 112);
    } else if (pattern === 2) {
      spawnEnemy("prism", W / 2, -96, 0, 58);
      spawnEnemy("laser", 118, -155, 10, 48);
      spawnEnemy("laser", W - 118, -155, -10, 48);
    } else if (pattern === 3) {
      for (let i = 0; i < 7; i++) spawnEnemy("starScout", 42 + i * 66, -36 - Math.abs(3 - i) * 32, 0, 132);
      spawnEnemy("supplyCarrier", W / 2, -170, 0, 60);
    } else if (pattern === 4) {
      spawnEnemy("prism", 86, -82, 36, 70);
      spawnEnemy("prism", W - 86, -82, -36, 70);
      spawnEnemy("supplyCarrier", W / 2, -150, 0, 54);
    } else if (pattern === 5) {
      spawnEnemy("supplyCarrier", 84, -112, 24, 56);
      spawnEnemy("supplyCarrier", W - 84, -142, -24, 56);
      spawnEnemy("laser", W / 2, -190, 0, 48);
    } else if (pattern === 6) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 2 ? "prism" : "starScout", 36 + i * 58, -42 - Math.abs(3.5 - i) * 24, (i - 3.5) * 18, 122);
    } else {
      for (let i = 0; i < 6; i++) spawnEnemy(i % 2 ? "laser" : "starScout", 54 + i * 74, -70 - i * 26, (i - 2.5) * 22, 94);
    }
  }

  function spawnStage3Wave() {
    const pattern = Math.floor(time / 4.0) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 5; i++) spawnEnemy(i % 2 ? "interceptor" : "starScout", 58 + i * 88, -38 - i * 18, (i - 2) * 18, 98);
      spawnEnemy("prism", W / 2, -116, 0, 54);
    } else if (pattern === 1) {
      spawnEnemy("shield", 112, -82, 18, 54);
      spawnEnemy("shield", W - 112, -82, -18, 54);
      for (let i = 0; i < 4; i++) spawnEnemy("drone", 84 + i * 104, -136 - i * 22, (i - 1.5) * 20, 118);
    } else if (pattern === 2) {
      spawnEnemy("laser", W / 2, -128, 0, 48);
      for (let i = 0; i < 6; i++) spawnEnemy("starScout", 44 + i * 78, -54 - Math.abs(2.5 - i) * 26, 0, 116);
    } else if (pattern === 3) {
      spawnEnemy("supplyCarrier", 96, -120, 24, 52);
      spawnEnemy("mineLayer", W - 106, -150, -24, 62);
    } else if (pattern === 4) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "fighter" : "interceptor", i % 2 ? W + 34 : -34, -36 - i * 26, i % 2 ? -142 : 142, 106);
    } else if (pattern === 5) {
      spawnEnemy("supplyCarrier", W / 2, -130, 0, 54);
      spawnEnemy("shield", 88, -164, 26, 50);
      spawnEnemy("shield", W - 88, -164, -26, 50);
    } else if (pattern === 6) {
      spawnEnemy("laser", 82, -126, 16, 48);
      spawnEnemy("laser", W - 82, -126, -16, 48);
      for (let i = 0; i < 5; i++) spawnEnemy(i % 2 ? "prism" : "interceptor", 64 + i * 88, -70 - i * 18, (i - 2) * 28, 120);
    } else {
      spawnEnemy("prism", 92, -86, 18, 62);
      spawnEnemy("prism", W - 92, -86, -18, 62);
      spawnEnemy("laser", W / 2, -160, 0, 46);
    }
  }

  function spawnStage3PostMidBossWave() {
    const pattern = Math.floor((time - 44) / 4.2) % 7;
    if (pattern === 0) {
      spawnEnemy("supplyCarrier", W / 2, -132, 0, 56);
      for (let i = 0; i < 6; i++) spawnEnemy(i % 2 ? "starScout" : "interceptor", 50 + i * 76, -46 - i * 18, (i - 2.5) * 24, 124);
    } else if (pattern === 1) {
      spawnEnemy("shield", 98, -92, 20, 52);
      spawnEnemy("shield", W - 98, -92, -20, 52);
      spawnEnemy("laser", W / 2, -142, 0, 48);
    } else if (pattern === 2) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "prism" : "drone", 44 + i * 66, -32 - Math.abs(3 - i) * 30, 0, 118);
    } else if (pattern === 3) {
      spawnEnemy("mineLayer", 92, -84, 32, 62);
      spawnEnemy("mineLayer", W - 92, -84, -32, 62);
      spawnEnemy("fighter", W / 2, -136, 0, 66);
    } else if (pattern === 4) {
      spawnEnemy("supplyCarrier", 96, -126, 26, 56);
      spawnEnemy("supplyCarrier", W - 96, -156, -26, 56);
      for (let i = 0; i < 5; i++) spawnEnemy("interceptor", 62 + i * 88, -64 - i * 18, (i - 2) * 28, 124);
    } else if (pattern === 5) {
      spawnEnemy("shield", W / 2, -112, 0, 50);
      spawnEnemy("laser", 86, -168, 18, 46);
      spawnEnemy("laser", W - 86, -168, -18, 46);
      for (let i = 0; i < 4; i++) spawnEnemy("prism", 86 + i * 104, -218 - i * 18, (i - 1.5) * 18, 88);
    } else {
      for (let i = 0; i < 6; i++) spawnEnemy(i % 2 ? "laser" : "starScout", 54 + i * 74, -70 - i * 24, (i - 2.5) * 20, 98);
    }
  }
  function spawnStage4Wave() {
    const pattern = Math.floor(time / 4.0) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage4Interceptor", 48 + i * 76, -42 - Math.abs(2.5 - i) * 28, (i - 2.5) * 18, 116);
    } else if (pattern === 1) {
      spawnEnemy("stage4Gunship", 104, -92, 24, 58);
      spawnEnemy("stage4Gunship", W - 104, -122, -24, 58);
      for (let i = 0; i < 4; i++) spawnEnemy("stage4Interceptor", 82 + i * 104, -172 - i * 18, (i - 1.5) * 24, 126);
    } else if (pattern === 2) {
      spawnEnemy("stage4Bomber", W / 2, -126, 0, 50);
      spawnEnemy("stage4Interceptor", 78, -178, 42, 128);
      spawnEnemy("stage4Interceptor", W - 78, -178, -42, 128);
    } else if (pattern === 3) {
      for (let i = 0; i < 5; i++) spawnEnemy("stage4Gunship", 58 + i * 91, -52 - i * 34, (i - 2) * 22, 82);
    } else if (pattern === 4) {
      spawnEnemy("stage4Bomber", 112, -102, 18, 54);
      spawnEnemy("stage4Bomber", W - 112, -144, -18, 54);
    } else if (pattern === 5) {
      for (let i = 0; i < 8; i++) spawnEnemy("stage4Interceptor", i % 2 ? W + 34 : -34, -34 - i * 26, i % 2 ? -156 : 156, 118);
    } else if (pattern === 6) {
      spawnEnemy("stage4Bomber", W / 2, -156, 0, 48);
      spawnEnemy("stage4Gunship", 86, -104, 28, 72);
      spawnEnemy("stage4Gunship", W - 86, -104, -28, 72);
    } else if (pattern === 7) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "stage4Gunship" : "stage4Interceptor", 42 + i * 66, -38 - Math.abs(3 - i) * 24, 0, 112);
    } else if (pattern === 8) {
      spawnEnemy("stage4Bomber", W / 2, -118, 0, 52);
      for (let i = 0; i < 5; i++) spawnEnemy("stage4Interceptor", 58 + i * 91, -204 - i * 18, (i - 2) * 30, 132);
    } else {
      spawnEnemy("stage4Gunship", 86, -84, 30, 66);
      spawnEnemy("stage4Gunship", W - 86, -84, -30, 66);
      spawnEnemy("stage4Bomber", W / 2, -178, 0, 50);
    }
  }

  function spawnStage4PostMidBossWave() {
    const pattern = Math.floor((time - 48) / 4.1) % 8;
    if (pattern === 0) {
      spawnEnemy("stage4Bomber", W / 2, -126, 0, 54);
      for (let i = 0; i < 6; i++) spawnEnemy("stage4Interceptor", 48 + i * 76, -52 - i * 18, (i - 2.5) * 26, 132);
    } else if (pattern === 1) {
      spawnEnemy("stage4Gunship", 92, -94, 26, 68);
      spawnEnemy("stage4Gunship", W - 92, -94, -26, 68);
      spawnEnemy("stage4Bomber", W / 2, -182, 0, 50);
    } else if (pattern === 2) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 3 === 0 ? "stage4Gunship" : "stage4Interceptor", i % 2 ? W + 32 : -32, -34 - i * 28, i % 2 ? -164 : 164, 122);
    } else if (pattern === 3) {
      spawnEnemy("stage4Bomber", 110, -116, 20, 56);
      spawnEnemy("stage4Bomber", W - 110, -152, -20, 56);
      for (let i = 0; i < 4; i++) spawnEnemy("stage4Interceptor", 88 + i * 102, -222 - i * 18, (i - 1.5) * 30, 136);
    } else if (pattern === 4) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage4Gunship", 46 + i * 78, -42 - Math.abs(2.5 - i) * 30, (i - 2.5) * 18, 90);
    } else if (pattern === 5) {
      spawnEnemy("stage4Bomber", W / 2, -132, 0, 52);
      spawnEnemy("stage4Gunship", 80, -194, 28, 72);
      spawnEnemy("stage4Gunship", W - 80, -194, -28, 72);
    } else if (pattern === 6) {
      for (let i = 0; i < 9; i++) spawnEnemy("stage4Interceptor", 32 + i * 52, -32 - Math.abs(4 - i) * 22, 0, 138);
      spawnEnemy("stage4Bomber", W / 2, -218, 0, 48);
    } else {
      spawnEnemy("stage4Bomber", 98, -132, 26, 58);
      spawnEnemy("stage4Bomber", W - 98, -162, -26, 58);
      for (let i = 0; i < 5; i++) spawnEnemy("stage4Gunship", 62 + i * 89, -232 - i * 20, (i - 2) * 22, 96);
    }
  }

  function spawnStage6Wave() {
    const pattern = Math.max(0, nextWave - 1) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 8; i++) spawnEnemy("stage6Striker", 34 + i * 59, -36 - Math.abs(3.5 - i) * 25, (i - 3.5) * 28, 142);
    } else if (pattern === 1) {
      spawnEnemy("stage6Mortar", 92, -92, 22, 64);
      spawnEnemy("stage6Mortar", W - 92, -126, -22, 64);
      for (let i = 0; i < 5; i++) spawnEnemy("stage6Striker", 55 + i * 92, -204 - i * 18, (i - 2) * 30, 148);
    } else if (pattern === 2) {
      spawnEnemy("stage6Carrier", W / 2, -146, 0, 48);
      for (let i = 0; i < 6; i++) spawnEnemy("stage6Striker", 46 + i * 78, -236 - i * 18, (i - 2.5) * 26, 152);
    } else if (pattern === 3) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "stage6Mortar" : "stage6Striker", i % 2 ? W + 36 : -36, -34 - i * 28, i % 2 ? -174 : 174, i % 3 === 0 ? 72 : 136);
    } else if (pattern === 4) {
      spawnEnemy("stage6Carrier", 104, -124, 20, 50);
      spawnEnemy("stage6Carrier", W - 104, -164, -20, 50);
    } else if (pattern === 5) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage6Mortar", 42 + i * 79, -42 - Math.abs(2.5 - i) * 34, (i - 2.5) * 18, 82);
    } else if (pattern === 6) {
      spawnEnemy("stage6Carrier", W / 2, -136, 0, 46);
      spawnEnemy("stage6Mortar", 78, -206, 24, 68);
      spawnEnemy("stage6Mortar", W - 78, -206, -24, 68);
    } else if (pattern === 7) {
      for (let i = 0; i < 10; i++) spawnEnemy("stage6Striker", 24 + i * 48, -34 - Math.abs(4.5 - i) * 22, 0, 148);
    } else if (pattern === 8) {
      spawnEnemy("stage6Carrier", W / 2, -126, 0, 48);
      for (let i = 0; i < 4; i++) spawnEnemy("stage6Mortar", 70 + i * 113, -224 - i * 24, (i - 1.5) * 22, 78);
    } else {
      spawnEnemy("stage6Carrier", 96, -148, 24, 52);
      spawnEnemy("stage6Carrier", W - 96, -186, -24, 52);
      for (let i = 0; i < 6; i++) spawnEnemy("stage6Striker", 45 + i * 78, -258 - i * 16, (i - 2.5) * 28, 154);
    }
  }

  function spawnStage6PostMidBossWave() {
    const pattern = Math.max(0, nextWave - 1) % 8;
    if (pattern === 0) {
      spawnEnemy("stage6Carrier", W / 2, -136, 0, 48);
      for (let i = 0; i < 10; i++) spawnEnemy("stage6Striker", 24 + i * 48, -58 - i * 20, (i - 4.5) * 26, 154);
    } else if (pattern === 1) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage6Mortar", 42 + i * 79, -44 - Math.abs(2.5 - i) * 32, (i - 2.5) * 18, 84);
      spawnEnemy("stage6Carrier", W / 2, -244, 0, 46);
    } else if (pattern === 2) {
      spawnEnemy("stage6Carrier", 96, -122, 26, 50);
      spawnEnemy("stage6Carrier", W - 96, -166, -26, 50);
      for (let i = 0; i < 6; i++) spawnEnemy("stage6Striker", 44 + i * 79, -248 - i * 20, (i - 2.5) * 32, 158);
    } else if (pattern === 3) {
      for (let i = 0; i < 12; i++) spawnEnemy("stage6Striker", i % 2 ? W + 32 : -32, -30 - i * 24, i % 2 ? -190 : 190, 144);
      spawnEnemy("stage6Mortar", W / 2, -238, 0, 70);
    } else if (pattern === 4) {
      spawnEnemy("stage6Carrier", W / 2, -122, 0, 46);
      for (let i = 0; i < 4; i++) spawnEnemy("stage6Mortar", 66 + i * 116, -206 - i * 24, (i - 1.5) * 24, 80);
    } else if (pattern === 5) {
      for (let i = 0; i < 9; i++) spawnEnemy(i % 4 === 0 ? "stage6Carrier" : "stage6Striker", 32 + i * 52, -38 - Math.abs(4 - i) * 28, 0, i % 4 === 0 ? 50 : 156);
    } else if (pattern === 6) {
      spawnEnemy("stage6Carrier", 90, -132, 24, 50);
      spawnEnemy("stage6Carrier", W - 90, -174, -24, 50);
      for (let i = 0; i < 7; i++) spawnEnemy("stage6Mortar", 38 + i * 67, -258 - i * 18, (i - 3) * 18, 86);
    } else {
      spawnEnemy("stage6Carrier", W / 2, -140, 0, 46);
      for (let i = 0; i < 12; i++) spawnEnemy("stage6Striker", 20 + i * 40, -242 - i * 15, (i - 5.5) * 22, 160);
    }
  }

  function spawnStage5Wave() {
    const pattern = Math.max(0, nextWave - 1) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 7; i++) spawnEnemy("stage5Interceptor", 42 + i * 66, -38 - Math.abs(3 - i) * 24, (i - 3) * 24, 128);
    } else if (pattern === 1) {
      spawnEnemy("stage5Jammer", 96, -88, 22, 66);
      spawnEnemy("stage5Jammer", W - 96, -118, -22, 66);
      for (let i = 0; i < 4; i++) spawnEnemy("stage5Interceptor", 80 + i * 106, -176 - i * 22, (i - 1.5) * 34, 138);
    } else if (pattern === 2) {
      spawnEnemy("stage5Gunship", W / 2, -132, 0, 52);
      for (let i = 0; i < 4; i++) spawnEnemy("stage5Interceptor", 56 + i * 122, -204 - i * 18, (i - 1.5) * 26, 142);
    } else if (pattern === 3) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage5Jammer", 48 + i * 77, -44 - Math.abs(2.5 - i) * 36, (i - 2.5) * 16, 82);
    } else if (pattern === 4) {
      spawnEnemy("stage5Gunship", 108, -112, 22, 56);
      spawnEnemy("stage5Gunship", W - 108, -152, -22, 56);
    } else if (pattern === 5) {
      for (let i = 0; i < 9; i++) spawnEnemy("stage5Interceptor", i % 2 ? W + 34 : -34, -32 - i * 24, i % 2 ? -174 : 174, 126);
    } else if (pattern === 6) {
      spawnEnemy("stage5Jammer", W / 2, -92, 0, 62);
      spawnEnemy("stage5Gunship", 86, -188, 26, 54);
      spawnEnemy("stage5Gunship", W - 86, -188, -26, 54);
    } else if (pattern === 7) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 3 === 0 ? "stage5Jammer" : "stage5Interceptor", 30 + i * 60, -38 - Math.abs(3.5 - i) * 30, 0, 132);
    } else if (pattern === 8) {
      spawnEnemy("stage5Gunship", W / 2, -124, 0, 50);
      for (let i = 0; i < 6; i++) spawnEnemy("stage5Interceptor", 46 + i * 78, -224 - i * 18, (i - 2.5) * 28, 146);
    } else {
      spawnEnemy("stage5Jammer", 82, -84, 24, 70);
      spawnEnemy("stage5Jammer", W - 82, -84, -24, 70);
      spawnEnemy("stage5Gunship", W / 2, -202, 0, 52);
    }
  }

  function spawnStage5PostMidBossWave() {
    const pattern = Math.max(0, nextWave - 1) % 8;
    if (pattern === 0) {
      spawnEnemy("stage5Gunship", W / 2, -126, 0, 54);
      for (let i = 0; i < 8; i++) spawnEnemy("stage5Interceptor", 30 + i * 60, -54 - i * 22, (i - 3.5) * 30, 142);
    } else if (pattern === 1) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage5Jammer", 42 + i * 79, -40 - Math.abs(2.5 - i) * 34, (i - 2.5) * 20, 86);
      spawnEnemy("stage5Gunship", W / 2, -232, 0, 50);
    } else if (pattern === 2) {
      spawnEnemy("stage5Gunship", 104, -118, 24, 58);
      spawnEnemy("stage5Gunship", W - 104, -158, -24, 58);
      for (let i = 0; i < 5; i++) spawnEnemy("stage5Interceptor", 54 + i * 93, -236 - i * 20, (i - 2) * 32, 150);
    } else if (pattern === 3) {
      for (let i = 0; i < 10; i++) spawnEnemy("stage5Interceptor", i % 2 ? W + 30 : -30, -28 - i * 26, i % 2 ? -184 : 184, 136);
      spawnEnemy("stage5Jammer", W / 2, -224, 0, 64);
    } else if (pattern === 4) {
      spawnEnemy("stage5Gunship", W / 2, -118, 0, 50);
      spawnEnemy("stage5Jammer", 82, -196, 24, 72);
      spawnEnemy("stage5Jammer", W - 82, -196, -24, 72);
    } else if (pattern === 5) {
      for (let i = 0; i < 9; i++) spawnEnemy(i % 4 === 0 ? "stage5Gunship" : "stage5Interceptor", 32 + i * 52, -36 - Math.abs(4 - i) * 28, 0, i % 4 === 0 ? 58 : 146);
    } else if (pattern === 6) {
      spawnEnemy("stage5Gunship", 96, -128, 26, 58);
      spawnEnemy("stage5Gunship", W - 96, -168, -26, 58);
      for (let i = 0; i < 6; i++) spawnEnemy("stage5Jammer", 46 + i * 78, -248 - i * 18, (i - 2.5) * 22, 90);
    } else {
      spawnEnemy("stage5Gunship", W / 2, -132, 0, 52);
      for (let i = 0; i < 10; i++) spawnEnemy("stage5Interceptor", 24 + i * 48, -222 - i * 16, (i - 4.5) * 24, 152);
    }
  }
  function spawnStage7Wave() {
    const pattern = Math.floor(time / 4.1) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 8; i++) spawnEnemy("stage7Sentinel", 32 + i * 60, -38 - Math.abs(3.5 - i) * 24, (i - 3.5) * 30, 148);
    } else if (pattern === 1) {
      spawnEnemy("stage7Breacher", 96, -104, 20, 66);
      spawnEnemy("stage7Breacher", W - 96, -138, -20, 66);
      for (let i = 0; i < 5; i++) spawnEnemy("stage7Sentinel", 58 + i * 91, -226 - i * 18, (i - 2) * 28, 152);
    } else if (pattern === 2) {
      spawnEnemy("stage7Hive", W / 2, -154, 0, 48);
      for (let i = 0; i < 6; i++) spawnEnemy("stage7Sentinel", 44 + i * 79, -248 - i * 16, (i - 2.5) * 24, 154);
    } else if (pattern === 3) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "stage7Breacher" : "stage7Sentinel", i % 2 ? W + 36 : -36, -34 - i * 30, i % 2 ? -176 : 176, i % 3 === 0 ? 72 : 144);
    } else if (pattern === 4) {
      spawnEnemy("stage7Hive", 102, -132, 18, 50);
      spawnEnemy("stage7Hive", W - 102, -172, -18, 50);
    } else if (pattern === 5) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage7Breacher", 42 + i * 79, -42 - Math.abs(2.5 - i) * 34, (i - 2.5) * 17, 82);
    } else if (pattern === 6) {
      spawnEnemy("stage7Hive", W / 2, -142, 0, 46);
      spawnEnemy("stage7Breacher", 76, -218, 24, 68);
      spawnEnemy("stage7Breacher", W - 76, -218, -24, 68);
    } else if (pattern === 7) {
      for (let i = 0; i < 11; i++) spawnEnemy("stage7Sentinel", 20 + i * 44, -34 - Math.abs(5 - i) * 21, 0, 154);
    } else if (pattern === 8) {
      spawnEnemy("stage7Hive", W / 2, -132, 0, 48);
      for (let i = 0; i < 4; i++) spawnEnemy("stage7Breacher", 70 + i * 113, -232 - i * 22, (i - 1.5) * 20, 80);
    } else {
      spawnEnemy("stage7Hive", 94, -152, 24, 52);
      spawnEnemy("stage7Hive", W - 94, -192, -24, 52);
      for (let i = 0; i < 7; i++) spawnEnemy("stage7Sentinel", 38 + i * 67, -268 - i * 16, (i - 3) * 28, 158);
    }
  }

  function spawnStage7PostMidBossWave() {
    const pattern = Math.floor(Math.max(0, time - 52) / 4.0) % 8;
    if (pattern === 0) {
      spawnEnemy("stage7Hive", W / 2, -142, 0, 47);
      for (let i = 0; i < 12; i++) spawnEnemy("stage7Sentinel", 18 + i * 41, -62 - i * 18, (i - 5.5) * 24, 160);
    } else if (pattern === 1) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage7Breacher", 40 + i * 80, -46 - Math.abs(2.5 - i) * 32, (i - 2.5) * 18, 86);
      spawnEnemy("stage7Hive", W / 2, -252, 0, 46);
    } else if (pattern === 2) {
      spawnEnemy("stage7Hive", 92, -128, 26, 50);
      spawnEnemy("stage7Hive", W - 92, -172, -26, 50);
      for (let i = 0; i < 7; i++) spawnEnemy("stage7Sentinel", 38 + i * 67, -258 - i * 18, (i - 3) * 32, 164);
    } else if (pattern === 3) {
      for (let i = 0; i < 14; i++) spawnEnemy("stage7Sentinel", i % 2 ? W + 34 : -34, -28 - i * 22, i % 2 ? -194 : 194, 150);
      spawnEnemy("stage7Breacher", W / 2, -248, 0, 72);
    } else if (pattern === 4) {
      spawnEnemy("stage7Hive", W / 2, -128, 0, 46);
      for (let i = 0; i < 5; i++) spawnEnemy("stage7Breacher", 58 + i * 91, -214 - i * 22, (i - 2) * 23, 84);
    } else if (pattern === 5) {
      for (let i = 0; i < 10; i++) spawnEnemy(i % 4 === 0 ? "stage7Hive" : "stage7Sentinel", 25 + i * 48, -42 - Math.abs(4.5 - i) * 27, 0, i % 4 === 0 ? 50 : 160);
    } else if (pattern === 6) {
      spawnEnemy("stage7Hive", 86, -136, 25, 50);
      spawnEnemy("stage7Hive", W - 86, -180, -25, 50);
      for (let i = 0; i < 7; i++) spawnEnemy("stage7Breacher", 34 + i * 69, -270 - i * 17, (i - 3) * 18, 88);
    } else {
      spawnEnemy("stage7Hive", W / 2, -146, 0, 46);
      for (let i = 0; i < 13; i++) spawnEnemy("stage7Sentinel", 16 + i * 38, -252 - i * 14, (i - 6) * 21, 166);
    }
  }

  function spawnStage8Wave() {
    const pattern = Math.floor(time / 4.0) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 9; i++) spawnEnemy("stage8Switchback", 24 + i * 54, -38 - Math.abs(4 - i) * 22, (i - 4) * 30, 154);
    } else if (pattern === 1) {
      spawnEnemy("stage8Polarity", 94, -112, 22, 68);
      spawnEnemy("stage8Polarity", W - 94, -146, -22, 68);
      for (let i = 0; i < 5; i++) spawnEnemy("stage8Switchback", 58 + i * 91, -236 - i * 18, (i - 2) * 26, 158);
    } else if (pattern === 2) {
      spawnEnemy("stage8Relay", W / 2, -158, 0, 48);
      for (let i = 0; i < 7; i++) spawnEnemy("stage8Switchback", 42 + i * 70, -252 - i * 15, (i - 3) * 22, 162);
    } else if (pattern === 3) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 3 === 0 ? "stage8Polarity" : "stage8Switchback", i % 2 ? W + 36 : -36, -42 - i * 28, i % 2 ? -184 : 184, i % 3 === 0 ? 76 : 150);
    } else if (pattern === 4) {
      spawnEnemy("stage8Relay", 102, -142, 18, 50);
      spawnEnemy("stage8Relay", W - 102, -184, -18, 50);
    } else if (pattern === 5) {
      for (let i = 0; i < 7; i++) spawnEnemy("stage8Polarity", 34 + i * 72, -44 - Math.abs(3 - i) * 30, (i - 3) * 18, 84);
    } else if (pattern === 6) {
      spawnEnemy("stage8Relay", W / 2, -146, 0, 47);
      spawnEnemy("stage8Polarity", 78, -224, 24, 72);
      spawnEnemy("stage8Polarity", W - 78, -224, -24, 72);
    } else if (pattern === 7) {
      for (let i = 0; i < 12; i++) spawnEnemy("stage8Switchback", 16 + i * 41, -34 - Math.abs(5.5 - i) * 19, 0, 164);
    } else if (pattern === 8) {
      spawnEnemy("stage8Relay", W / 2, -136, 0, 49);
      for (let i = 0; i < 5; i++) spawnEnemy("stage8Polarity", 52 + i * 99, -236 - i * 21, (i - 2) * 20, 86);
    } else {
      spawnEnemy("stage8Relay", 92, -154, 24, 52);
      spawnEnemy("stage8Relay", W - 92, -196, -24, 52);
      for (let i = 0; i < 8; i++) spawnEnemy("stage8Switchback", 26 + i * 64, -274 - i * 15, (i - 3.5) * 28, 166);
    }
  }

  function spawnStage8PostMidBossWave() {
    const pattern = Math.floor(Math.max(0, time - 54) / 3.9) % 8;
    if (pattern === 0) {
      spawnEnemy("stage8Relay", W / 2, -146, 0, 48);
      for (let i = 0; i < 14; i++) spawnEnemy("stage8Switchback", 10 + i * 36, -58 - i * 17, (i - 6.5) * 21, 168);
    } else if (pattern === 1) {
      for (let i = 0; i < 7; i++) spawnEnemy("stage8Polarity", 30 + i * 74, -48 - Math.abs(3 - i) * 29, (i - 3) * 17, 88);
      spawnEnemy("stage8Relay", W / 2, -258, 0, 46);
    } else if (pattern === 2) {
      spawnEnemy("stage8Relay", 88, -132, 26, 50);
      spawnEnemy("stage8Relay", W - 88, -176, -26, 50);
      for (let i = 0; i < 8; i++) spawnEnemy("stage8Switchback", 28 + i * 62, -268 - i * 17, (i - 3.5) * 30, 170);
    } else if (pattern === 3) {
      for (let i = 0; i < 16; i++) spawnEnemy("stage8Switchback", i % 2 ? W + 34 : -34, -30 - i * 21, i % 2 ? -202 : 202, 156);
      spawnEnemy("stage8Polarity", W / 2, -252, 0, 76);
    } else if (pattern === 4) {
      spawnEnemy("stage8Relay", W / 2, -132, 0, 47);
      for (let i = 0; i < 6; i++) spawnEnemy("stage8Polarity", 42 + i * 84, -220 - i * 20, (i - 2.5) * 22, 88);
    } else if (pattern === 5) {
      for (let i = 0; i < 11; i++) spawnEnemy(i % 4 === 0 ? "stage8Relay" : "stage8Switchback", 20 + i * 45, -44 - Math.abs(5 - i) * 24, 0, i % 4 === 0 ? 50 : 170);
    } else if (pattern === 6) {
      spawnEnemy("stage8Relay", 84, -140, 25, 50);
      spawnEnemy("stage8Relay", W - 84, -184, -25, 50);
      for (let i = 0; i < 8; i++) spawnEnemy("stage8Polarity", 24 + i * 66, -278 - i * 16, (i - 3.5) * 18, 90);
    } else {
      spawnEnemy("stage8Relay", W / 2, -150, 0, 47);
      for (let i = 0; i < 15; i++) spawnEnemy("stage8Switchback", 8 + i * 34, -262 - i * 13, (i - 7) * 20, 172);
    }
  }

  function spawnStage9Wave() {
    const pattern = Math.floor(time / 4.15) % 10;
    if (pattern === 0) {
      for (let i = 0; i < 8; i++) spawnEnemy("stage9Vector", 30 + i * 63, -42 - Math.abs(3.5 - i) * 25, (i - 3.5) * 28, 156);
    } else if (pattern === 1) {
      spawnEnemy("stage9Orbiter", 96, -128, 22, 70);
      spawnEnemy("stage9Orbiter", W - 96, -164, -22, 70);
      for (let i = 0; i < 4; i++) spawnEnemy("stage9Vector", 76 + i * 118, -238 - i * 22, (i - 1.5) * 24, 160);
    } else if (pattern === 2) {
      spawnEnemy("stage9Bastion", W / 2, -168, 0, 48);
      for (let i = 0; i < 6; i++) spawnEnemy("stage9Vector", 50 + i * 86, -270 - i * 18, (i - 2.5) * 22, 164);
    } else if (pattern === 3) {
      for (let i = 0; i < 8; i++) spawnEnemy(i % 3 === 0 ? "stage9Orbiter" : "stage9Vector", i % 2 ? W + 38 : -38, -46 - i * 31, i % 2 ? -180 : 180, i % 3 === 0 ? 74 : 154);
    } else if (pattern === 4) {
      spawnEnemy("stage9Bastion", 104, -150, 18, 50);
      spawnEnemy("stage9Bastion", W - 104, -196, -18, 50);
    } else if (pattern === 5) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage9Orbiter", 46 + i * 89, -48 - Math.abs(2.5 - i) * 34, (i - 2.5) * 18, 82);
    } else if (pattern === 6) {
      spawnEnemy("stage9Bastion", W / 2, -150, 0, 48);
      spawnEnemy("stage9Orbiter", 82, -230, 22, 72);
      spawnEnemy("stage9Orbiter", W - 82, -230, -22, 72);
    } else if (pattern === 7) {
      for (let i = 0; i < 10; i++) spawnEnemy("stage9Vector", 20 + i * 51, -38 - Math.abs(4.5 - i) * 21, 0, 164);
    } else if (pattern === 8) {
      spawnEnemy("stage9Bastion", W / 2, -142, 0, 49);
      for (let i = 0; i < 5; i++) spawnEnemy("stage9Orbiter", 62 + i * 105, -242 - i * 22, (i - 2) * 20, 84);
    } else {
      spawnEnemy("stage9Bastion", 98, -158, 22, 52);
      spawnEnemy("stage9Bastion", W - 98, -202, -22, 52);
      for (let i = 0; i < 7; i++) spawnEnemy("stage9Vector", 38 + i * 75, -286 - i * 17, (i - 3) * 26, 166);
    }
  }

  function spawnStage9PostMidBossWave() {
    const pattern = Math.floor(Math.max(0, time - 58) / 4.1) % 8;
    if (pattern === 0) {
      spawnEnemy("stage9Bastion", W / 2, -150, 0, 48);
      for (let i = 0; i < 10; i++) spawnEnemy("stage9Vector", 20 + i * 51, -64 - i * 19, (i - 4.5) * 21, 168);
    } else if (pattern === 1) {
      for (let i = 0; i < 6; i++) spawnEnemy("stage9Orbiter", 46 + i * 89, -52 - Math.abs(2.5 - i) * 32, (i - 2.5) * 18, 86);
      spawnEnemy("stage9Bastion", W / 2, -270, 0, 46);
    } else if (pattern === 2) {
      spawnEnemy("stage9Bastion", 94, -138, 24, 50);
      spawnEnemy("stage9Bastion", W - 94, -182, -24, 50);
      for (let i = 0; i < 7; i++) spawnEnemy("stage9Vector", 40 + i * 74, -276 - i * 18, (i - 3) * 28, 170);
    } else if (pattern === 3) {
      for (let i = 0; i < 11; i++) spawnEnemy("stage9Vector", i % 2 ? W + 36 : -36, -34 - i * 25, i % 2 ? -194 : 194, 158);
      spawnEnemy("stage9Orbiter", W / 2, -260, 0, 76);
    } else if (pattern === 4) {
      spawnEnemy("stage9Bastion", W / 2, -138, 0, 47);
      for (let i = 0; i < 5; i++) spawnEnemy("stage9Orbiter", 64 + i * 104, -226 - i * 22, (i - 2) * 20, 88);
    } else if (pattern === 5) {
      for (let i = 0; i < 9; i++) spawnEnemy(i % 4 === 0 ? "stage9Bastion" : "stage9Vector", 32 + i * 58, -48 - Math.abs(4 - i) * 27, 0, i % 4 === 0 ? 50 : 170);
    } else if (pattern === 6) {
      spawnEnemy("stage9Bastion", 88, -146, 24, 50);
      spawnEnemy("stage9Bastion", W - 88, -190, -24, 50);
      for (let i = 0; i < 6; i++) spawnEnemy("stage9Orbiter", 46 + i * 88, -286 - i * 18, (i - 2.5) * 18, 90);
    } else {
      spawnEnemy("stage9Bastion", W / 2, -154, 0, 47);
      for (let i = 0; i < 11; i++) spawnEnemy("stage9Vector", 12 + i * 49, -270 - i * 15, (i - 5) * 20, 172);
    }
  }
  const stageWaveHandlers = {
    2: spawnStage2Wave,
    3: spawnStage3Wave,
    4: spawnStage4Wave,
    5: spawnStage5Wave,
    6: spawnStage6Wave,
    7: spawnStage7Wave,
    8: spawnStage8Wave,
    9: spawnStage9Wave,
  };

  const stagePostMidWaveHandlers = {
    2: spawnStage2PostMidBossWave,
    3: spawnStage3PostMidBossWave,
    4: spawnStage4PostMidBossWave,
    5: spawnStage5PostMidBossWave,
    6: spawnStage6PostMidBossWave,
    7: spawnStage7PostMidBossWave,
    8: spawnStage8PostMidBossWave,
    9: spawnStage9PostMidBossWave,
  };

  function validateStageRegistry() {
    for (let stage = 1; stage <= LAST_IMPLEMENTED_STAGE; stage++) {
      if (!stageSetups[stage]) throw new Error(`Stage ${stage} setup is not registered`);
      if (!stageRuntime[stage]) throw new Error(`Stage ${stage} runtime is not registered`);
      if (stage > 1 && !stageWaveHandlers[stage]) throw new Error(`Stage ${stage} wave handler is not registered`);
      if (stage > 1 && !stagePostMidWaveHandlers[stage]) throw new Error(`Stage ${stage} post-mid wave handler is not registered`);
    }
  }

  validateStageRegistry();

  function spawnWave() {
    const handler = stageWaveHandlers[currentStage];
    if (handler) {
      handler();
      return;
    }
    const pattern = Math.floor(time / 4.0) % 11;
    if (pattern === 0) {
      for (let i = 0; i < 6; i++) spawnEnemy("drone", 52 + i * 75, -40 - i * 16, 0, 76);
      spawnEnemy("splitter", W / 2, -95, 0, 64);
    } else if (pattern === 1) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 3 === 0 ? "interceptor" : "drone", i % 2 ? W - 36 : 36, -30 - i * 36, i % 2 ? -72 : 72, 96);
    } else if (pattern === 2) {
      spawnEnemy("fighter", 118, -48, 24, 62);
      spawnEnemy("fighter", W - 118, -48, -24, 62);
      spawnEnemy("laser", W / 2, -108, 0, 46);
    } else if (pattern === 3) {
      for (let i = 0; i < 5; i++) spawnEnemy("drone", 70 + i * 85, -30, (i - 2) * 14, 92);
      spawnEnemy("shield", W / 2, -92, 0, 45);
    } else if (pattern === 4) {
      spawnEnemy("mineLayer", 105, -70, 24, 58);
      spawnEnemy("mineLayer", W - 105, -70, -24, 58);
      for (let i = 0; i < 4; i++) spawnEnemy("interceptor", 80 + i * 106, -150 - i * 18, (i - 1.5) * 22, 112);
    } else if (pattern === 5) {
      spawnEnemy("fighter", W / 2, -80, 0, 56);
      spawnEnemy("laser", W - 82, -130, -10, 45);
      spawnEnemy("splitter", 82, -120, 28, 74);
    } else if (pattern === 6) {
      for (let i = 0; i < 9; i++) spawnEnemy(i % 2 ? "splitter" : "drone", 34 + i * 52, -24 - Math.abs(4 - i) * 28, 0, 94);
      spawnEnemy("laser", W / 2, -92, 0, 48);
    } else if (pattern === 7) {
      spawnEnemy("laser", 86, -118, 12, 46);
      spawnEnemy("fighter", W / 2, -154, 0, 62);
      for (let i = 0; i < 4; i++) spawnEnemy("drone", 72 + i * 112, -54 - i * 18, (i - 1.5) * 22, 108);
    } else if (pattern === 8) {
      spawnEnemy("shield", W / 2, -92, 0, 48);
      spawnEnemy("mineLayer", 104, -148, 28, 58);
      spawnEnemy("mineLayer", W - 104, -148, -28, 58);
    } else if (pattern === 9) {
      spawnEnemy("shield", 96, -72, 10, 48);
      spawnEnemy("shield", W - 96, -72, -10, 48);
      spawnEnemy("mineLayer", W / 2, -130, 0, 60);
    } else {
      for (let i = 0; i < 8; i++) spawnEnemy("interceptor", 40 + i * 58, -42 - i * 22, (i % 2 ? -1 : 1) * 80, 118);
    }
  }

  function spawnPostMidBossWave() {
    const handler = stagePostMidWaveHandlers[currentStage];
    if (handler) {
      handler();
      return;
    }
    const pattern = Math.floor((time - 44) / 4.5) % 8;
    if (pattern === 0) {
      spawnEnemy("laser", 92, -130, 8, 47);
      spawnEnemy("laser", W - 92, -170, -8, 47);
      for (let i = 0; i < 5; i++) spawnEnemy("interceptor", 60 + i * 92, -54 - i * 20, (i - 2) * 26, 122);
    } else if (pattern === 1) {
      spawnEnemy("shield", W / 2, -105, 0, 50);
      for (let i = 0; i < 6; i++) spawnEnemy(i % 2 ? "splitter" : "drone", 42 + i * 82, -42 - Math.abs(2.5 - i) * 28, 0, 104);
    } else if (pattern === 2) {
      spawnEnemy("mineLayer", 90, -82, 34, 62);
      spawnEnemy("mineLayer", W - 90, -82, -34, 62);
      spawnEnemy("fighter", W / 2, -132, 0, 66);
    } else if (pattern === 3) {
      for (let i = 0; i < 7; i++) spawnEnemy("interceptor", i % 2 ? W + 30 : -30, -30 - i * 30, i % 2 ? -150 : 150, 108);
      spawnEnemy("laser", W / 2, -145, 0, 45);
    } else if (pattern === 4) {
      spawnEnemy("shield", 118, -92, 16, 52);
      spawnEnemy("shield", W - 118, -92, -16, 52);
      for (let i = 0; i < 4; i++) spawnEnemy("splitter", 88 + i * 116, -70 - i * 24, (i - 1.5) * 18, 86);
    } else if (pattern === 5) {
      spawnEnemy("laser", 90, -132, 18, 48);
      spawnEnemy("laser", W - 90, -132, -18, 48);
      spawnEnemy("fighter", W / 2, -172, 0, 66);
    } else if (pattern === 6) {
      for (let i = 0; i < 7; i++) spawnEnemy(i % 2 ? "splitter" : "interceptor", i % 2 ? W + 36 : -36, -38 - i * 28, i % 2 ? -146 : 146, 112);
      spawnEnemy("shield", W / 2, -150, 0, 50);
    } else {
      spawnEnemy("fighter", 112, -86, 34, 74);
      spawnEnemy("fighter", W - 112, -86, -34, 74);
      spawnEnemy("mineLayer", W / 2, -150, 0, 58);
      spawnEnemy("laser", W / 2 + Math.sin(time) * 120, -195, 0, 50);
    }
  }
  function startMidBoss() {
    midBossSpawned = true;
    midBoss = {
      x: W / 2,
      y: -88,
      r: 48,
      hp: stageSetup().midHp,
      maxHp: stageSetup().midHp,
      t: 0,
      fireA: 1.0,
      fireB: 2.0,
      fireC: 1.4,
      enter: 0,
    };
    enemies = [];
    enemyBullets.length = Math.min(enemyBullets.length, 24);
    lasers = [];
    shake = Math.max(shake, 0.12);
  }

  function spawnCarrotMissile(x, y, angle, speed = 112, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 122;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 8,
      color: "#ff9a36",
      hp,
      maxHp: hp,
      age: 0,
      kind: "carrot",
      turnRate: options.turnRate || 1.45,
      maxSpeed: scaledEnemySpeed(options.maxSpeed || 205),
      armTime: options.armTime || 0.58,
      spreadSide: options.spreadSide || 0,
    });
  }

  function carrotSalvo(source, side = 1, wide = false) {
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    const count = scaledCount(wide ? 10 : 8, wide ? 5 : 4);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const outward = side * (wide ? 1.32 : 0.92);
      const verticalFan = lane * (wide ? 0.145 : 0.11);
      const rackX = source.x + side * (70 + (i % 2) * 28) + lane * (wide ? 19 : 12);
      const rackY = source.y + 4 + i * (wide ? 15 : 12);
      spawnCarrotMissile(rackX, rackY, base + outward + verticalFan, 110 + i * 5, {
        armTime: wide ? 1.05 + i * 0.045 : 0.82 + i * 0.035,
        spreadSide: side,
        turnRate: wide ? 1.05 : 1.2,
        maxSpeed: wide ? 218 : 204,
        hp: wide ? 132 : 122,
      });
    }
  }

  function updateMidBoss(dt) {
    if (!midBoss) return;
    if (currentStage === 9) { updateStage9MidBoss(dt); return; }
    if (currentStage === 8) { updateStage8MidBoss(dt); return; }
    if (currentStage === 7) { updateStage7MidBoss(dt); return; }
    if (currentStage === 6) { updateStage6MidBoss(dt); return; }
    if (currentStage === 5) { updateStage5MidBoss(dt); return; }
    if (currentStage === 4) { updateStage4MidBoss(dt); return; }
    if (currentStage === 3) { updateStage3MidBoss(dt); return; }
    if (currentStage === 2) { updateStage2MidBoss(dt); return; }
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (128 - midBoss.y) * Math.min(1, dt * 1.7);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 1.35) * 92 + Math.sin(midBoss.t * 3.2) * 18;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.fireA <= 0 && midBoss.y > 72) {
      midBoss.fireA = scaledDelay(rage ? 1.08 : 1.42);
      carrotSalvo(midBoss, Math.sin(midBoss.t * 2.1) > 0 ? 1 : -1, false);
      playSound("laser", 0.55, 0.08);
    }
    if (midBoss.fireB <= 0 && midBoss.y > 72) {
      midBoss.fireB = scaledDelay(rage ? 1.8 : 2.55);
      carrotSalvo({ x: midBoss.x - 112, y: midBoss.y + 2 }, -1, true);
      carrotSalvo({ x: midBoss.x + 112, y: midBoss.y + 2 }, 1, true);
      ring(midBoss, rage ? 14 : 10, rage ? 96 : 78, midBoss.t, "#ffb6df", 4);
    }
  }

  function spawnStage2StarBullet(x, y, angle, speed = 128, color = "#9fdcff", options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 86;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 6,
      color,
      hp,
      maxHp: hp,
      age: 0,
      kind: options.kind || "star",
      damage: options.damage || 17,
      wobble: options.wobble || 0,
    });
  }

  function stage2ContainmentFan(source, count, color = "#8fdcff") {
    count = scaledCount(count, 4);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage2StarBullet(source.x + lane * 9, source.y + 28, base + lane * 0.12, 142 + Math.abs(lane) * 7, color, { r: 5.5, hp: 92, wobble: 0.55 });
    }
  }

  function stage2StarRing(source, count, speed, offset, color) {
    count = scaledCount(count, 5);
    for (let i = 0; i < count; i++) {
      const a = offset + Math.PI * 2 * i / count;
      spawnStage2StarBullet(source.x, source.y, a, speed + (i % 2) * 14, color, { r: i % 3 === 0 ? 7 : 5.5, hp: 74, wobble: 0.35 });
    }
  }

  function updateStage2MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (122 - midBoss.y) * Math.min(1, dt * 1.7);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 1.05) * 78 + Math.sin(midBoss.t * 2.8) * 16;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC = (midBoss.fireC || 1.2) - dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.fireA <= 0 && midBoss.y > 72) {
      midBoss.fireA = scaledDelay(rage ? 0.82 : 1.15);
      stage2ContainmentFan({ x: midBoss.x - 42, y: midBoss.y + 14 }, rage ? 9 : 7, "#8fdcff");
      stage2ContainmentFan({ x: midBoss.x + 42, y: midBoss.y + 14 }, rage ? 9 : 7, "#b7f7ff");
      playSound("laser", 0.5, 0.08);
    }
    if (midBoss.fireB <= 0 && midBoss.y > 72) {
      midBoss.fireB = scaledDelay(rage ? 1.45 : 2.05);
      const sweep = Math.sin(midBoss.t * 1.7) * 0.32;
      spawnLaser(midBoss.x - 76, midBoss.y + 42, Math.PI / 2 + sweep, 780, 8, { kind: "enemy", damage: 10, color: "#67f4ff", warmup: 0.42 });
      spawnLaser(midBoss.x + 76, midBoss.y + 42, Math.PI / 2 - sweep, 780, 8, { kind: "enemy", damage: 10, color: "#67f4ff", warmup: 0.42 });
    }
    if (midBoss.fireC <= 0 && midBoss.y > 72) {
      midBoss.fireC = scaledDelay(rage ? 2.05 : 2.8);
      stage2StarRing(midBoss, rage ? 18 : 13, rage ? 112 : 88, midBoss.t * 1.2, rage ? "#fff2a6" : "#9fdcff");
    }
  }

  function spawnStage3FlowerBit(x, y, angle, speed = 96, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 118;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 7,
      color: options.color || "#ff9bd7",
      hp,
      maxHp: hp,
      age: 0,
      kind: "stage3FlowerBit",
      baseAngle: angle,
      damage: options.damage || 18,
      armTime: options.armTime || 0.55,
      spinTime: options.spinTime || 0.72,
      spinRadius: options.spinRadius || 28,
      spinDir: options.spinDir || (Math.random() < 0.5 ? -1 : 1),
      dashSpeed: scaledEnemySpeed(options.dashSpeed || 245),
      dashAngle: null,
      dashLocked: false,
      turnRate: options.turnRate || 1.15,
      maxSpeed: scaledEnemySpeed(options.maxSpeed || 188),
    });
  }

  function spawnStage3PollenMine(x, y, angle, speed = 52, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 170;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 10,
      color: options.color || "#ffd76a",
      hp,
      maxHp: hp,
      age: 0,
      kind: "stage3PollenMine",
      damage: options.damage || 20,
      fuse: options.fuse || 1.85,
      burstCount: options.burstCount || 8,
    });
  }

  function stage3PetalPodSalvo(source, side = 1, rage = false) {
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    const count = scaledCount(rage ? 8 : 6, 3);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const launchAngle = base + side * (0.68 + Math.abs(lane) * 0.025) + lane * 0.055;
      spawnStage3FlowerBit(source.x + side * (68 + (i % 2) * 16), source.y + 18 + i * 6, launchAngle, 94 + i * 6, {
        armTime: rage ? 0.42 + i * 0.025 : 0.62 + i * 0.03,
        turnRate: rage ? 1.28 : 1.08,
        maxSpeed: rage ? 214 : 188,
        dashSpeed: rage ? 292 : 254,
        spinTime: rage ? 0.58 + i * 0.015 : 0.72 + i * 0.018,
        spinDir: side,
        hp: rage ? 128 : 112,
        color: i % 2 ? "#ff9bd7" : "#fff0a8",
      });
    }
  }

  function stage3BloomRing(source, rage = false) {
    const count = scaledCount(rage ? 18 : 13, 7);
    const offset = source.t * (rage ? -1.4 : 1.1);
    for (let i = 0; i < count; i++) {
      const a = offset + Math.PI * 2 * i / count;
      const color = i % 3 === 0 ? "#fff0a8" : "#ff9bd7";
      spawnStage3FlowerBit(source.x, source.y + 12, a, rage ? 118 : 92, {
        armTime: 1.2,
        turnRate: 0.22,
        maxSpeed: rage ? 134 : 108,
        dashSpeed: rage ? 218 : 188,
        spinTime: 0.92,
        spinDir: i % 2 ? -1 : 1,
        hp: 84,
        r: i % 3 === 0 ? 7.5 : 6.5,
        color,
      });
    }
  }

  function stage3PollenMineField(source, rage = false) {
    const count = scaledCount(rage ? 5 : 3, 2);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const px = source.x + lane * (rage ? 54 : 72) + Math.sin(source.t * 2 + i) * 18;
      const py = source.y + 54 + Math.abs(lane) * 12;
      spawnStage3PollenMine(px, py, Math.PI / 2 + lane * 0.12, rage ? 58 : 46, {
        fuse: rage ? 1.55 + i * 0.08 : 1.95 + i * 0.12,
        burstCount: scaledCount(rage ? 10 : 7, 5),
        color: i % 2 ? "#ffd76a" : "#ffb7ea",
      });
    }
  }

  function updateStage3MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (122 - midBoss.y) * Math.min(1, dt * 1.55);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.82) * 70 + Math.sin(midBoss.t * 2.35) * 14;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC = (midBoss.fireC || 1.8) - dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.fireA <= 0 && midBoss.y > 72) {
      midBoss.fireA = scaledDelay(rage ? 0.92 : 1.28);
      stage3PetalPodSalvo({ x: midBoss.x - 74, y: midBoss.y + 6 }, -1, rage);
      stage3PetalPodSalvo({ x: midBoss.x + 74, y: midBoss.y + 6 }, 1, rage);
      playSound("laser", 0.48, 0.08);
    }
    if (midBoss.fireB <= 0 && midBoss.y > 72) {
      midBoss.fireB = scaledDelay(rage ? 1.55 : 2.25);
      stage3BloomRing(midBoss, rage);
      particles.push({ x: midBoss.x, y: midBoss.y + 10, vx: 0, vy: 0, life: 0.35, maxLife: 0.35, color: rage ? "#fff0a8" : "#ff9bd7", type: "damageRing", size: rage ? 122 : 96 });
    }
    if (midBoss.fireC <= 0 && midBoss.y > 72) {
      midBoss.fireC = scaledDelay(rage ? 2.35 : 3.15);
      stage3PollenMineField(midBoss, rage);
    }
  }
  function spawnStage6Bullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const hp = options.hp || 56;
    const s = scaledEnemySpeed(speed);
    const bullet = {
      id: nextEnemyBulletId++, x, y,
      vx: Math.cos(angle) * s, vy: Math.sin(angle) * s,
      r: options.r || 5, color: options.color || "#ff9b48",
      hp, maxHp: hp, age: 0, kind,
      damage: options.damage || 18,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function burstStage6Shell(shell, heavy = false) {
    if (shell.dead) return;
    shell.dead = true;
    const count = scaledCount(shell.burstCount || (heavy ? 18 : 12), heavy ? 10 : 7);
    const offset = shell.id * 0.73 + shell.age;
    for (let i = 0; i < count; i++) {
      const speed = (heavy ? 154 : 124) + (i % 3) * 24;
      spawnStage6Bullet(shell.x, shell.y, speed, offset + i * Math.PI * 2 / count, "stage6Shard", {
        color: i % 2 ? "#ffc55f" : "#ff7648", r: heavy ? 5.2 : 4.5, hp: heavy ? 68 : 48, damage: heavy ? 22 : 16,
      });
    }
    stage6AttackFx.push({ type: "shock", x: shell.x, y: shell.y, t: 0, life: heavy ? 0.85 : 0.58, color: shell.color || "#ff9b48" });
    explode(shell.x, shell.y, shell.color || "#ff9b48", heavy ? 30 : 20);
  }

  function stage6MortarBarrage(source, rage = false, heavy = false) {
    const count = scaledCount(heavy ? (rage ? 9 : 7) : (rage ? 6 : 4), heavy ? 5 : 3);
    const leadX = Math.max(42, Math.min(W - 42, player.x + (Math.sin(source.t * 2.7) * (heavy ? 90 : 55))));
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const targetX = Math.max(34, Math.min(W - 34, leadX + lane * (heavy ? 58 : 72)));
      const targetY = Math.max(230, Math.min(H - 76, player.y - 70 + (i % 2) * 115));
      const fuse = (heavy ? 1.08 : 0.92) + i * 0.055;
      const x = source.x + lane * 12;
      const y = source.y + 30;
      const vx = (targetX - x) / fuse;
      const vy = (targetY - y) / fuse;
      const speed = Math.hypot(vx, vy) / (0.84 + enemyPressure() * 0.16);
      const angle = Math.atan2(vy, vx);
      spawnStage6Bullet(x, y, speed, angle, "stage6Shell", {
        color: heavy ? "#ff5f76" : "#ff9b48", r: heavy ? 9 : 7, hp: heavy ? 150 : 105,
        damage: heavy ? 28 : 20, fuse, burstCount: heavy ? (rage ? 20 : 16) : (rage ? 14 : 11), heavy,
      });
      stage6AttackFx.push({ type: "impact", x: targetX, y: targetY, t: 0, life: fuse + 0.32, hitAt: fuse, color: heavy ? "#ff5f76" : "#ffb04f" });
    }
  }

  function stage6RicochetVolley(source, rage = false) {
    const count = scaledCount(rage ? 11 : 8, 5);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const spread = (i - (count - 1) / 2) * (rage ? 0.115 : 0.14);
      spawnStage6Bullet(source.x, source.y + 28, rage ? 226 : 192, base + spread, "stage6Ricochet", {
        color: i % 2 ? "#ffe077" : "#ff9b48", r: 5.2, hp: 64, damage: 18, bounces: rage ? 2 : 1,
      });
    }
  }

  function stage6RammingMines(source, rage = false) {
    const count = scaledCount(rage ? 7 : 5, 4);
    for (let i = 0; i < count; i++) {
      const angle = i * Math.PI * 2 / count + source.t * 0.5;
      spawnStage6Bullet(source.x, source.y + 12, 35, angle, "stage6Mine", {
        color: i % 2 ? "#ffca58" : "#ff7548", r: 10, hp: rage ? 145 : 120, damage: 24,
        anchorX: source.x, anchorY: source.y + 20, orbitAngle: angle, orbitRadius: 72 + (i % 2) * 26,
        spinDir: i % 2 ? -1 : 1, armTime: 0.75 + i * 0.06, dashSpeed: scaledEnemySpeed(rage ? 310 : 270),
      });
    }
  }

  function stage6DroneEncirclement(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 8 : 6, 4);
    for (let i = 0; i < count; i++) {
      const angle = i * Math.PI * 2 / count;
      spawnStage6Bullet(source.x, source.y + 18, 30, angle, "stage6Drone", {
        color: i % 2 ? "#ff5ab8" : "#ff9b48", r: 12, hp: phase === 2 ? 190 : 150, damage: 22,
        orbitAngle: angle, orbitRadius: phase === 2 ? 142 : 120, phase, nextFire: 0.9 + i * 0.08, lifeTime: phase === 2 ? 5.8 : 4.9,
      });
    }
  }

  function burstStage6Core(core) {
    if (core.dead) return;
    core.dead = true;
    const phase = core.phase || 1;
    const rings = phase === 2 ? 3 : 2;
    for (let layer = 0; layer < rings; layer++) {
      const count = scaledCount((phase === 2 ? 20 : 16) + layer * 4, 10);
      for (let i = 0; i < count; i++) {
        spawnStage6Bullet(core.x, core.y, 116 + layer * 52, core.age * (layer % 2 ? -0.8 : 0.65) + i * Math.PI * 2 / count, "stage6ShockShard", {
          color: layer % 2 ? "#ff65c7" : "#ffa34f", r: 5 + layer, hp: 68, damage: 20,
        });
      }
    }
    stage6AttackFx.push({ type: "shock", x: core.x, y: core.y, t: 0, life: 1.15, color: "#ff5ab8" });
    explode(core.x, core.y, "#ff72ca", 46);
  }

  function stage6CompressionCore(source, phase = 1) {
    const core = spawnStage6Bullet(source.x, source.y + 42, 72, Math.PI / 2, "stage6Core", {
      color: "#ff5ab8", r: phase === 2 ? 20 : 17, hp: phase === 2 ? 260 : 220, damage: 30,
      phase, fuse: phase === 2 ? 1.35 : 1.55,
    });
    if (core) stage6AttackFx.push({ type: "core", id: core.id, x: core.x, y: core.y, t: 0, life: core.fuse, color: core.color });
  }

  function stage6ClusterRain(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 8 : 6, 4);
    for (let i = 0; i < count; i++) {
      const x = 36 + i * ((W - 72) / Math.max(1, count - 1));
      const targetY = 215 + ((i * 97 + Math.floor(source.t * 40)) % 330);
      const fuse = 0.95 + i * 0.075;
      const startY = -20 - i * 10;
      const speedFactor = 0.84 + enemyPressure() * 0.16;
      const fallSpeed = Math.max(150, (targetY - startY) / fuse / speedFactor);
      spawnStage6Bullet(x, startY, fallSpeed, Math.PI / 2, "stage6Cluster", {
        color: i % 2 ? "#ff5ab8" : "#ffb047", r: 8, hp: 105, damage: 22, fuse, burstCount: phase === 2 ? 10 : 8,
      });
      stage6AttackFx.push({ type: "impact", x, y: targetY, t: 0, life: fuse + 0.28, hitAt: fuse, color: i % 2 ? "#ff5ab8" : "#ffb047" });
    }
  }

  function updateStage6AttackFx(dt) {
    if (currentStage !== 6) {
      stage6AttackFx = [];
      return;
    }
    for (const fx of stage6AttackFx) fx.t += dt;
    stage6AttackFx = stage6AttackFx.filter((fx) => fx.t < fx.life);
  }

  function updateStage6MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (118 - midBoss.y) * Math.min(1, dt * 1.75);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.92) * 118 + Math.sin(midBoss.t * 2.8) * 16;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.y < 70) return;
    if (midBoss.fireA <= 0) {
      midBoss.fireA = scaledDelay(rage ? 1.9 : 2.45);
      stage6MortarBarrage(midBoss, rage, false);
    }
    if (midBoss.fireB <= 0) {
      midBoss.fireB = scaledDelay(rage ? 0.92 : 1.25);
      stage6RicochetVolley(midBoss, rage);
    }
    if (midBoss.fireC <= 0) {
      midBoss.fireC = scaledDelay(rage ? 2.7 : 3.45);
      stage6RammingMines(midBoss, rage);
    }
  }

  function updateStage6Boss(dt) {
    boss.t += dt;
    boss.y += (94 - boss.y) * Math.min(1, dt * 1.5);
    boss.x = W / 2 + Math.sin(boss.t * 0.55) * 102;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 2 ? 3.35 : 4.3);
      stage6DroneEncirclement(boss, phase);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 2 ? 3.65 : 4.75);
      stage6CompressionCore(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 2 ? 1.75 : 2.35);
      stage6ClusterRain(boss, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 2 ? 0.72 : 0.94);
      stage6RicochetVolley({ x: boss.x + Math.sin(boss.t * 2.2) * 64, y: boss.y + 34, t: boss.t }, phase === 2);
    }
  }

  function spawnStage7Bullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const hp = options.hp || 58;
    const s = scaledEnemySpeed(speed);
    const bullet = {
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 5,
      color: options.color || "#ff82c8",
      hp,
      maxHp: hp,
      age: 0,
      kind,
      damage: options.damage || 20,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function stage7BladeFan(source, rage = false) {
    const count = scaledCount(rage ? 11 : 8, rage ? 6 : 4);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage7Bullet(source.x + lane * 7, source.y + 24, rage ? 214 : 184, base + lane * (rage ? 0.105 : 0.13), "stage7Blade", {
        color: i % 2 ? "#ff82c8" : "#b8f6ff",
        r: 5.2,
        hp: 62,
        curve: (i % 2 ? 1 : -1) * (rage ? 0.58 : 0.42),
      });
    }
  }

  function stage7AnchorCage(source, rage = false) {
    const count = scaledCount(rage ? 8 : 6, 4);
    const radius = rage ? 170 : 138;
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count + source.t * 0.35;
      const anchorX = Math.max(34, Math.min(W - 34, player.x + Math.cos(a) * radius));
      const anchorY = Math.max(112, Math.min(H - 126, player.y + Math.sin(a) * radius * 0.72));
      const bullet = spawnStage7Bullet(source.x, source.y + 16, 24, a, "stage7Anchor", {
        color: i % 2 ? "#d7e9ff" : "#9d72ff",
        r: rage ? 10 : 9,
        hp: rage ? 138 : 116,
        damage: rage ? 28 : 22,
        anchorX,
        anchorY,
        lockTime: 0.9 + (i % 3) * 0.08,
        dashSpeed: scaledEnemySpeed(rage ? 286 : 248),
      });
      if (bullet) stage7AttackFx.push({ type: "anchor", x: anchorX, y: anchorY, t: 0, life: 1.25, color: bullet.color });
    }
  }

  function stage7DroneHalo(source, phase = 1, compact = false) {
    const count = scaledCount(compact ? (phase === 2 ? 7 : 5) : (phase === 2 ? 10 : 8), compact ? 4 : 6);
    const radius = compact ? 82 : 126;
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count;
      spawnStage7Bullet(source.x, source.y + 18, 18, a, "stage7Drone", {
        color: i % 2 ? "#ff82c8" : "#7eeeff",
        r: compact ? 7 : 8,
        hp: compact ? 86 : 108,
        damage: phase === 2 ? 24 : 19,
        centerX: source.x,
        centerY: source.y + 24,
        orbitAngle: a,
        orbitRadius: radius + (i % 2) * 22,
        spinDir: i % 2 ? 1 : -1,
        releaseAt: compact ? 0.82 + i * 0.055 : 1.1 + i * 0.07,
        dashSpeed: scaledEnemySpeed(phase === 2 ? 300 : 258),
      });
    }
    stage7AttackFx.push({ type: "halo", x: source.x, y: source.y + 24, t: 0, life: compact ? 1.0 : 1.45, color: phase === 2 ? "#ff82c8" : "#7eeeff" });
  }

  function stage7ShutterVolley(source, phase = 1) {
    const pairs = scaledCount(phase === 2 ? 5 : 4, 3);
    const gapY = Math.max(220, Math.min(H - 180, player.y - 34));
    for (let i = 0; i < pairs; i++) {
      const y = 170 + i * ((H - 310) / Math.max(1, pairs - 1));
      if (Math.abs(y - gapY) < 54 && i === Math.floor(pairs / 2)) continue;
      const inward = phase === 2 ? 126 : 102;
      spawnStage7Bullet(-20, y, inward, 0.08, "stage7Shutter", {
        color: i % 2 ? "#ff82c8" : "#c7d8ff",
        r: phase === 2 ? 13 : 11,
        hp: 155,
        damage: phase === 2 ? 30 : 24,
        side: -1,
      });
      spawnStage7Bullet(W + 20, y + 24, inward, Math.PI - 0.08, "stage7Shutter", {
        color: i % 2 ? "#c7d8ff" : "#ff82c8",
        r: phase === 2 ? 13 : 11,
        hp: 155,
        damage: phase === 2 ? 30 : 24,
        side: 1,
      });
      stage7AttackFx.push({ type: "shutter", y, t: 0, life: 0.7, color: i % 2 ? "#ff82c8" : "#7eeeff" });
    }
  }


  function stage7EclipseSquadron(source, rage = false) {
    const count = scaledCount(rage ? 7 : 5, 4);
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count - Math.PI / 2;
      spawnStage7Bullet(source.x, source.y + 18, 12, a, "stage7Eclipse", {
        color: i % 2 ? "#bb88ff" : "#e9f5ff", r: rage ? 15 : 13, hp: rage ? 132 : 110,
        damage: rage ? 30 : 24, center: source, orbitAngle: a, orbitRadius: 76 + (i % 2) * 24,
        spinDir: i % 2 ? 1 : -1, releaseAt: 1.08 + i * 0.09,
        dashSpeed: scaledEnemySpeed(rage ? 390 : 338), indestructible: true,
      });
    }
    stage7AttackFx.push({ type: "eclipse", x: source.x, y: source.y + 20, t: 0, life: 1.35, color: "#cda8ff" });
  }

  function stage7InkCurtain(source, rage = false) {
    const count = scaledCount(rage ? 13 : 9, 6);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage7Bullet(source.x + lane * 8, source.y + 30, rage ? 150 : 126, base + lane * 0.105, "stage7Ink", {
        color: i % 2 ? "#6b4b9f" : "#d7e9ff", r: 4.8, hp: 48, damage: rage ? 19 : 15,
        curve: (i % 2 ? 1 : -1) * 0.22,
      });
    }
  }

  function stage7PawMineField(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 6 : 4, 3);
    const safeLane = Math.max(58, Math.min(W - 58, player.x));
    for (let i = 0; i < count; i++) {
      let x = 58 + i * ((W - 116) / Math.max(1, count - 1));
      if (Math.abs(x - safeLane) < 52 && i === Math.floor(count / 2)) x = Math.max(42, Math.min(W - 42, x + 86));
      const mine = spawnStage7Bullet(x, -34 - (i % 2) * 46, phase === 2 ? 82 : 68, Math.PI / 2, "stage7PawMine", {
        color: i % 2 ? "#ff82c8" : "#f8f3ff", r: phase === 2 ? 18 : 16, hp: phase === 2 ? 170 : 145,
        damage: phase === 2 ? 32 : 26, stampAt: 1.95 + (i % 3) * 0.18, targetY: 184 + (i % 3) * 86, phase, wobble: i % 2 ? 1 : -1,
        indestructible: true,
      });
      if (mine) stage7AttackFx.push({ type: "paw", x, y: 184 + (i % 3) * 86, t: 0, life: mine.stampAt, color: mine.color });
    }
  }

  function stage7CatScratchVolley(source, phase = 1) {
    const slashes = scaledCount(phase === 2 ? 7 : 5, 4);
    const fromLeft = Math.floor(source.t * 2) % 2 === 0;
    for (let i = 0; i < slashes; i++) {
      const y = 150 + i * ((H - 300) / Math.max(1, slashes - 1));
      spawnStage7Bullet(fromLeft ? -28 : W + 28, y, phase === 2 ? 270 : 228, fromLeft ? 0.22 : Math.PI - 0.22, "stage7Scratch", {
        color: i % 2 ? "#ff82c8" : "#7eeeff", r: phase === 2 ? 8 : 7, hp: 86, damage: phase === 2 ? 26 : 21,
      });
      stage7AttackFx.push({ type: "scratch", fromLeft, y, t: 0, life: 0.58, color: i % 2 ? "#ff82c8" : "#7eeeff" });
    }
  }
  function updateStage7AttackFx(dt) {
    if (currentStage !== 7) {
      stage7AttackFx = [];
      return;
    }
    for (const fx of stage7AttackFx) fx.t += dt;
    stage7AttackFx = stage7AttackFx.filter((fx) => fx.t < fx.life).slice(-64);
  }

  function updateStage7MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (118 - midBoss.y) * Math.min(1, dt * 1.7);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.78) * 112 + Math.sin(midBoss.t * 2.5) * 14;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.y < 72) return;
    if (midBoss.fireA <= 0) {
      midBoss.fireA = scaledDelay(rage ? 2.55 : 3.25);
      stage7EclipseSquadron(midBoss, rage);
    }
    if (midBoss.fireB <= 0) {
      midBoss.fireB = scaledDelay(rage ? 1.12 : 1.48);
      stage7InkCurtain(midBoss, rage);
    }
    if (midBoss.fireC <= 0) {
      midBoss.fireC = scaledDelay(rage ? 4.1 : 5.2);
      stage7EclipseSquadron(midBoss, rage);
    }
  }

  function updateStage7Boss(dt) {
    boss.t += dt;
    boss.y += (94 - boss.y) * Math.min(1, dt * 1.5);
    boss.x = W / 2 + Math.sin(boss.t * 0.62) * 104;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 2 ? 2.45 : 3.2);
      stage7PawMineField(boss, phase);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 2 ? 1.65 : 2.2);
      stage7CatScratchVolley(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 2 ? 1.05 : 1.4);
      stage7InkCurtain({ x: boss.x, y: boss.y + 28, t: boss.t }, phase === 2);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 2 ? 4.3 : 5.4);
      stage7PawMineField(boss, phase);
    }
  }

  function spawnStage8Bullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const hp = options.hp || 64;
    const s = scaledEnemySpeed(speed);
    const bullet = {
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 5,
      color: options.color || "#ff9d4d",
      hp,
      maxHp: hp,
      age: 0,
      kind,
      damage: options.damage || 20,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function stage8SwitchbackVolley(source, rage = false) {
    const count = scaledCount(rage ? 13 : 9, rage ? 7 : 5);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage8Bullet(source.x + lane * 5, source.y + 22, rage ? 196 : 172, base + lane * (rage ? 0.11 : 0.14), "stage8Flip", {
        color: i % 2 ? "#ff9d4d" : "#9cecff",
        r: rage ? 5.8 : 5.2,
        hp: rage ? 78 : 66,
        flipAt: 0.68 + (i % 3) * 0.08,
        flipBias: lane * 0.035,
        damage: rage ? 24 : 19,
      });
    }
    stage8AttackFx.push({ type: "flip", x: source.x, y: source.y + 26, t: 0, life: 0.9, color: rage ? "#ff9d4d" : "#9cecff" });
  }

  function stage8PolarityNodes(source, rage = false) {
    const count = scaledCount(rage ? 7 : 5, 4);
    const radius = rage ? 164 : 132;
    const snapshotX = player.x;
    const snapshotY = player.y;
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count + source.t * 0.28;
      const targetX = Math.max(38, Math.min(W - 38, snapshotX + Math.cos(a) * radius));
      const targetY = Math.max(146, Math.min(H - 118, snapshotY + Math.sin(a) * radius * 0.68));
      const node = spawnStage8Bullet(source.x, source.y + 20, 28, a, "stage8PolarityNode", {
        color: i % 2 ? "#ff9d4d" : "#9cecff",
        r: rage ? 10 : 9,
        hp: rage ? 150 : 126,
        targetX,
        targetY,
        lockTime: 0.92 + (i % 2) * 0.12,
        burstCount: rage ? 8 : 6,
        damage: rage ? 28 : 22,
      });
      if (node) stage8AttackFx.push({ type: "node", x: targetX, y: targetY, t: 0, life: node.lockTime + 0.24, color: node.color });
    }
  }

  function stage8RailGrid(source, phase = 1) {
    const rows = scaledCount(phase === 2 ? 6 : 4, 3);
    const safeY = Math.max(190, Math.min(H - 150, player.y));
    for (let i = 0; i < rows; i++) {
      const y = 168 + i * ((H - 300) / Math.max(1, rows - 1));
      if (Math.abs(y - safeY) < 58 && i === Math.floor(rows / 2)) continue;
      const fromLeft = (i + Math.floor(source.t)) % 2 === 0;
      const speed = phase === 2 ? 238 : 196;
      spawnStage8Bullet(fromLeft ? -34 : W + 34, y, speed, fromLeft ? 0 : Math.PI, "stage8Rail", {
        color: i % 2 ? "#ff9d4d" : "#9cecff",
        r: phase === 2 ? 12 : 10,
        hp: 190,
        damage: phase === 2 ? 32 : 26,
        orientation: "horizontal",
      });
      stage8AttackFx.push({ type: "rail", x: fromLeft ? 0 : W, y, t: 0, life: 0.62, color: i % 2 ? "#ff9d4d" : "#9cecff", horizontal: true });
    }
    if (phase === 2) {
      const cols = scaledCount(4, 3);
      const safeX = player.x;
      for (let i = 0; i < cols; i++) {
        const x = 64 + i * ((W - 128) / Math.max(1, cols - 1));
        if (Math.abs(x - safeX) < 54 && i === Math.floor(cols / 2)) continue;
        spawnStage8Bullet(x, -34, 218, Math.PI / 2, "stage8Rail", {
          color: i % 2 ? "#9cecff" : "#ff9d4d",
          r: 11,
          hp: 190,
          damage: 30,
          orientation: "vertical",
        });
        stage8AttackFx.push({ type: "rail", x, y: 0, t: 0, life: 0.62, color: i % 2 ? "#9cecff" : "#ff9d4d", horizontal: false });
      }
    }
  }

  function stage8RelayScatter(source, phase = 1) {
    const rings = phase === 2 ? 2 : 1;
    for (let ringIndex = 0; ringIndex < rings; ringIndex++) {
      const count = scaledCount(phase === 2 ? 16 : 12, 8);
      for (let i = 0; i < count; i++) {
        const a = i * Math.PI * 2 / count + source.t * (ringIndex ? -0.7 : 0.55);
        spawnStage8Bullet(source.x, source.y + 24, phase === 2 ? 136 + ringIndex * 42 : 126, a, "stage8Spark", {
          color: (i + ringIndex) % 2 ? "#ff9d4d" : "#9cecff",
          r: 4.5,
          hp: 58,
          damage: phase === 2 ? 22 : 17,
          curve: ringIndex ? -0.36 : 0.28,
        });
      }
    }
  }


  function stage8BeaconConvoy(source, rage = false) {
    const count = scaledCount(rage ? 6 : 4, 3);
    const safeIndex = Math.max(0, Math.min(count - 1, Math.round((player.x - 48) / Math.max(1, (W - 96) / Math.max(1, count - 1)))));
    for (let i = 0; i < count; i++) {
      if (i === safeIndex && count > 3) continue;
      const x = 48 + i * ((W - 96) / Math.max(1, count - 1));
      const turnX = i % 2 ? Math.max(54, x - 92) : Math.min(W - 54, x + 92);
      const points = [{ x, y: 170 }, { x: turnX, y: 330 + (i % 2) * 54 }, { x: turnX + (i % 2 ? 54 : -54), y: H + 60 }];
      const beacon = spawnStage8Bullet(x, -34 - (i % 2) * 38, 0, Math.PI / 2, "stage8Beacon", {
        color: i % 2 ? "#ff9d4d" : "#9cecff", r: rage ? 15 : 13, hp: rage ? 128 : 108,
        damage: rage ? 28 : 23, route: points, routeIndex: 0, routeSpeed: scaledEnemySpeed(rage ? 214 : 184),
        nextSignal: 0,
      });
      if (beacon) stage8AttackFx.push({ type: "route", points: [{ x: beacon.x, y: beacon.y }, ...points], t: 0, life: 1.25, color: beacon.color });
    }
  }

  function stage8SignalSweep(source, rage = false) {
    const count = scaledCount(rage ? 11 : 8, 5);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage8Bullet(source.x, source.y + 24, rage ? 172 : 148, base + lane * 0.15, "stage8Signal", {
        color: i % 2 ? "#ffb769" : "#c7f7ff", r: 4.5, hp: 46, damage: rage ? 18 : 15,
        curve: (lane / Math.max(1, count)) * (rage ? 0.48 : 0.34),
      });
    }
  }

  function stage8FreightSwitchyard(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 4 : 3, 2);
    const safeSide = player.x < W / 2 ? 1 : -1;
    for (let i = 0; i < count; i++) {
      const fromLeft = (i % 2 ? 1 : -1) === safeSide ? i % 3 === 0 : i % 2 === 0;
      const startX = fromLeft ? -48 : W + 48;
      const laneY = 178 + i * 112;
      const middleX = fromLeft ? W * 0.7 : W * 0.3;
      const exitX = fromLeft ? W + 70 : -70;
      const points = [{ x: middleX, y: laneY }, { x: middleX, y: laneY + 138 }, { x: exitX, y: laneY + 210 }];
      const freight = spawnStage8Bullet(startX, laneY - 80, 0, 0, "stage8Freight", {
        color: i % 2 ? "#ff9d4d" : "#9cecff", r: phase === 2 ? 24 : 22, hp: phase === 2 ? 260 : 225,
        damage: phase === 2 ? 38 : 32, route: points, routeIndex: 0,
        routeSpeed: scaledEnemySpeed(phase === 2 ? 172 : 148), phase, cargoCount: phase === 2 ? 7 : 5,
      });
      if (freight) stage8AttackFx.push({ type: "route", points: [{ x: startX, y: laneY - 80 }, ...points], t: 0, life: 1.45, color: freight.color });
    }
  }

  function stage8CargoPulse(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 14 : 10, 7);
    const gap = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count + source.t * 0.24;
      if (Math.abs(Math.atan2(Math.sin(a - gap), Math.cos(a - gap))) < 0.26) continue;
      spawnStage8Bullet(source.x, source.y + 24, phase === 2 ? 146 : 124, a, "stage8Cargo", {
        color: i % 2 ? "#ff9d4d" : "#9cecff", r: 5.5, hp: 56, damage: phase === 2 ? 20 : 16,
        curve: i % 2 ? 0.12 : -0.12,
      });
    }
    stage8AttackFx.push({ type: "cargoBurst", x: source.x, y: source.y + 24, t: 0, life: 0.55, color: "#ffb769" });
  }
  function updateStage8AttackFx(dt) {
    if (currentStage !== 8) {
      stage8AttackFx = [];
      return;
    }
    for (const fx of stage8AttackFx) fx.t += dt;
    compactInPlace(stage8AttackFx, (fx) => fx.t < fx.life);
    if (stage8AttackFx.length > 64) stage8AttackFx.splice(0, stage8AttackFx.length - 64);
  }

  function updateStage8MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (116 - midBoss.y) * Math.min(1, dt * 1.65);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.72) * 118 + Math.sin(midBoss.t * 2.2) * 16;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.y < 72) return;
    if (midBoss.fireA <= 0) {
      midBoss.fireA = scaledDelay(rage ? 2.05 : 2.7);
      stage8BeaconConvoy(midBoss, rage);
    }
    if (midBoss.fireB <= 0) {
      midBoss.fireB = scaledDelay(rage ? 1.0 : 1.35);
      stage8SignalSweep(midBoss, rage);
    }
    if (midBoss.fireC <= 0) {
      midBoss.fireC = scaledDelay(rage ? 3.8 : 4.7);
      stage8BeaconConvoy(midBoss, rage);
    }
  }

  function updateStage8Boss(dt) {
    boss.t += dt;
    boss.y += (92 - boss.y) * Math.min(1, dt * 1.45);
    boss.x = W / 2 + Math.sin(boss.t * 0.58) * 108;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 2 ? 2.7 : 3.45);
      stage8FreightSwitchyard(boss, phase);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 2 ? 1.35 : 1.8);
      stage8CargoPulse(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 2 ? 4.6 : 5.6);
      stage8FreightSwitchyard(boss, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 2 ? 0.92 : 1.22);
      stage8CargoPulse({ x: boss.x, y: boss.y + 28, t: boss.t }, phase);
    }
  }

  function spawnStage9Bullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const hp = options.hp || 72;
    const s = scaledEnemySpeed(speed);
    const bullet = {
      id: nextEnemyBulletId++,
      x, y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 5,
      color: options.color || "#b78cff",
      hp,
      maxHp: hp,
      age: 0,
      kind,
      damage: options.damage || 20,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function stage9VectorFan(source, rage = false) {
    const count = scaledCount(rage ? 11 : 8, rage ? 7 : 5);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage9Bullet(source.x + lane * 4, source.y + 22, rage ? 202 : 178, base + lane * (rage ? 0.105 : 0.135), "stage9Curve", {
        color: i % 2 ? "#b78cff" : "#73f0ff",
        r: rage ? 5.8 : 5.1,
        hp: rage ? 78 : 66,
        curve: lane * (rage ? 0.018 : 0.014),
        damage: rage ? 23 : 19,
      });
    }
    stage9AttackFx.push({ type: "gate", x: source.x, y: source.y + 24, t: 0, life: 0.62, color: "#b78cff" });
  }

  function stage9ConveyorDrones(source, rage = false, requestedCount = 0) {
    const baseCount = requestedCount || (rage ? 8 : 6);
    const count = scaledCount(baseCount, rage ? 6 : 4);
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count + source.t * 0.2;
      const radius = 72 + (i % 2) * 30;
      const drone = spawnStage9Bullet(source.x + Math.cos(a) * 18, source.y + 24 + Math.sin(a) * 12, 0, 0, "stage9Drone", {
        color: i % 2 ? "#ff8ad8" : "#8ff7ff",
        r: rage ? 17 : 15,
        hp: rage ? 180 : 150,
        centerX: source.x,
        centerY: source.y + 28,
        orbitAngle: a,
        orbitTravel: 0,
        orbitRadius: radius,
        spinDir: i % 2 ? 1 : -1,
        dashSpeed: scaledEnemySpeed(rage ? 370 : 330),
        damage: rage ? 34 : 28,
      });
      if (drone) stage9AttackFx.push({ type: "orbit", x: drone.x, y: drone.y, t: 0, life: 1.25, color: drone.color });
    }
  }

  function stage9VectorGateWave(source, rage = false) {
    const rows = scaledCount(rage ? 5 : 4, 3);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (const side of [-1, 1]) {
      const gateX = Math.max(44, Math.min(W - 44, source.x + side * 116));
      for (let i = 0; i < rows; i++) {
        const offset = i - (rows - 1) / 2;
        spawnStage9Bullet(gateX, source.y + 30, rage ? 188 : 164, base + side * 0.24 + offset * 0.17, "stage9Arc", {
          color: side < 0 ? "#73f0ff" : "#ff8ad8",
          r: 6,
          hp: 82,
          curve: -side * (rage ? 0.42 : 0.32),
          damage: rage ? 25 : 20,
        });
      }
      stage9AttackFx.push({ type: "gate", x: gateX, y: source.y + 30, t: 0, life: 0.82, color: side < 0 ? "#73f0ff" : "#ff8ad8" });
    }
  }

  function stage9GravityCoreBurst(source, phase = 1) {
    const active = enemyBullets.reduce((n, b) => n + (b.kind === "stage9Core" && !b.dead ? 1 : 0), 0);
    if (active >= (phase === 2 ? 2 : 1)) return;
    const targetY = Math.max(218, Math.min(350, player.y - 150));
    const core = spawnStage9Bullet(source.x, source.y + 28, 72, Math.PI / 2, "stage9Core", {
      color: phase === 2 ? "#ff76c8" : "#b78cff",
      r: phase === 2 ? 31 : 27,
      hp: phase === 2 ? 360 : 310,
      targetY,
      burstAt: phase === 2 ? 1.72 : 1.95,
      phase,
      damage: phase === 2 ? 38 : 32,
      force: true,
    });
    if (core) stage9AttackFx.push({ type: "core", x: core.x, y: targetY, t: 0, life: core.burstAt, color: core.color });
  }

  function stage9DebrisFall(source, phase = 1) {
    const lanes = scaledCount(phase === 2 ? 9 : 7, 5);
    const gapLane = Math.max(0, Math.min(lanes - 1, Math.floor(player.x / W * lanes)));
    for (let i = 0; i < lanes; i++) {
      if (i === gapLane || (phase === 1 && i === gapLane - 1)) continue;
      const x = (i + 0.5) * W / lanes + Math.sin(source.t + i) * 13;
      const debris = spawnStage9Bullet(x, -40 - (i % 3) * 38, phase === 2 ? 226 : 196, Math.PI / 2, "stage9Debris", {
        color: i % 2 ? "#d9d3ff" : "#7b8ba5",
        r: phase === 2 ? 13 : 11,
        hp: 160,
        wobble: i % 2 ? 1 : -1,
        damage: phase === 2 ? 31 : 25,
      });
      if (debris) stage9AttackFx.push({ type: "debris", x, y: 0, t: 0, life: 0.58, color: "#ffd18a" });
    }
  }

  function stage9CorePulse(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 18 : 13, 9);
    const gap = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const a = i * Math.PI * 2 / count + source.t * 0.18;
      if (Math.abs(Math.atan2(Math.sin(a - gap), Math.cos(a - gap))) < 0.23) continue;
      spawnStage9Bullet(source.x, source.y + 26, phase === 2 ? 154 : 132, a, "stage9Arc", {
        color: i % 2 ? "#b78cff" : "#73f0ff",
        r: 5.2,
        hp: 58,
        curve: i % 2 ? 0.23 : -0.23,
        damage: phase === 2 ? 21 : 17,
      });
    }
  }

  function updateStage9AttackFx(dt) {
    if (currentStage !== 9) {
      stage9AttackFx = [];
      return;
    }
    for (const fx of stage9AttackFx) fx.t += dt;
    compactInPlace(stage9AttackFx, (fx) => fx.t < fx.life);
    if (stage9AttackFx.length > 48) stage9AttackFx.splice(0, stage9AttackFx.length - 48);
  }

  function updateStage9MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (116 - midBoss.y) * Math.min(1, dt * 1.65);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.68) * 116 + Math.sin(midBoss.t * 2.1) * 14;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.y < 72) return;
    if (midBoss.fireA <= 0) {
      midBoss.fireA = scaledDelay(rage ? 2.35 : 3.05);
      stage9ConveyorDrones(midBoss, rage);
    }
    if (midBoss.fireB <= 0) {
      midBoss.fireB = scaledDelay(rage ? 0.98 : 1.34);
      stage9VectorFan(midBoss, rage);
    }
    if (midBoss.fireC <= 0) {
      midBoss.fireC = scaledDelay(rage ? 3.55 : 4.5);
      stage9VectorGateWave(midBoss, rage);
    }
  }

  function updateStage9Boss(dt) {
    boss.t += dt;
    boss.y += (92 - boss.y) * Math.min(1, dt * 1.45);
    boss.x = W / 2 + Math.sin(boss.t * 0.54) * 104;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 2 ? 3.45 : 4.35);
      stage9GravityCoreBurst(boss, phase);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 2 ? 1.05 : 1.42);
      stage9CorePulse(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 2 ? 2.75 : 3.55);
      stage9DebrisFall(boss, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 2 ? 1.65 : 2.15);
      stage9VectorGateWave({ x: boss.x, y: boss.y + 18, t: boss.t }, phase === 2);
    }
  }
  function spawnStage5PatternBullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const hp = options.hp || 52;
    const s = scaledEnemySpeed(speed);
    const bullet = {
      id: nextEnemyBulletId++,
      x, y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 5,
      color: options.color || "#ba8cff",
      hp,
      maxHp: hp,
      age: 0,
      kind,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function stage5NeonDashBurst(source, rage = false) {
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    const count = scaledCount(rage ? 9 : 7, 3);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage5PatternBullet(source.x, source.y + 14, 178 + (i % 3) * 12, base + lane * 0.12, "stage5Arc", {
        color: i % 2 ? "#62f1ff" : "#c685ff",
        r: 4.2,
        hp: 42,
        curve: (i % 2 ? 1 : -1) * (rage ? 0.38 : 0.27),
      });
    }
  }

  function stage5JammerField(source, rage = false) {
    const count = scaledCount(rage ? 5 : 4, 2);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const targetX = Math.max(48, Math.min(W - 48, W / 2 + lane * (rage ? 82 : 94)));
      const targetY = 190 + (i % 2) * 92;
      const angle = Math.atan2(targetY - source.y, targetX - source.x);
      spawnStage5PatternBullet(source.x, source.y + 18, 82, angle, "stage5JammerNode", {
        color: i % 2 ? "#65f4ff" : "#bf82ff",
        r: 8,
        hp: rage ? 118 : 96,
        anchorX: targetX,
        anchorY: targetY,
        fuse: 0.92 + i * 0.08,
        lifeTime: rage ? 3.1 : 2.7,
        pulseCount: rage ? 10 : 8,
      });
    }
  }

  function burstStage5RoadOrb(b) {
    if (b.dead) return;
    b.dead = true;
    const count = scaledCount(b.rage ? 13 : 10, 5);
    const base = b.flip ? Math.PI : 0;
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage5PatternBullet(b.x, b.y, 172 + (i % 3) * 14, base + lane * 0.075, "stage5RoadShot", {
        color: i % 2 ? "#ff5f87" : "#7df4ff",
        r: 4,
        hp: 38,
      });
    }
    ring(b, b.rage ? 12 : 8, b.rage ? 126 : 106, b.age, "#d78cff", 4);
    explode(b.x, b.y, "#ffffff", 15);
  }

  function stage5GunshipSweep(source, rage = false) {
    const count = scaledCount(rage ? 4 : 3, 2);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage5PatternBullet(source.x + lane * 26, source.y + 26, 94 + i * 8, Math.PI / 2 + lane * 0.08, "stage5RoadOrb", {
        color: "#ff587d",
        r: 9,
        hp: rage ? 125 : 105,
        burstY: 250 + (i % 2) * 125,
        flip: i % 2 === 1,
        rage,
      });
    }
  }

  function stage5MirrorAssault(source, rage = false) {
    if (stage5AttackFx.some((fx) => fx.type === "mirror")) return;
    stage5AttackFx.push({
      type: "mirror",
      t: 0,
      life: rage ? 2.35 : 2.65,
      source,
      rage,
      volley: 0,
      copies: [
        { x: 72, y: 132 },
        { x: W / 2, y: 96 },
        { x: W - 72, y: 132 },
      ],
    });
  }

  function fireStage5MirrorVolley(fx) {
    fx.volley += 1;
    for (let i = 0; i < fx.copies.length; i++) {
      const copy = fx.copies[i];
      const base = Math.atan2(player.y - copy.y, player.x - copy.x);
      const count = scaledCount(fx.rage ? 8 : 6, 3);
      for (let lane = 0; lane < count; lane++) {
        const spread = (lane - (count - 1) / 2) * (fx.rage ? 0.13 : 0.15);
        spawnStage5PatternBullet(copy.x, copy.y + 16, 172 + fx.volley * 18, base + spread, "stage5Arc", {
          color: i === 1 ? "#ffffff" : i === 0 ? "#67f4ff" : "#c889ff",
          r: 4.3,
          hp: 44,
          curve: (i - 1) * 0.22 + (fx.volley % 2 ? 0.08 : -0.08),
        });
      }
      explode(copy.x, copy.y, i === 1 ? "#ffffff" : "#c889ff", 8);
    }
  }

  function stage5ClawSweep(source, rage = false) {
    const rows = scaledCount(rage ? 10 : 7, 4);
    for (const side of [-1, 1]) {
      for (let i = 0; i < rows; i++) {
        const x = side < 0 ? -12 : W + 12;
        const y = 126 + i * (rage ? 48 : 62);
        const angle = side < 0 ? 0.1 : Math.PI - 0.1;
        spawnStage5PatternBullet(x, y, rage ? 208 : 184, angle, "stage5Arc", {
          color: side < 0 ? "#67f4ff" : "#bd82ff",
          r: 5,
          hp: 58,
          curve: side * (rage ? 0.34 : 0.25),
        });
      }
    }
  }

  function stage5AlleyLasers(source, rage = false) {
    if (stage5AttackFx.some((fx) => fx.type === "alley")) return;
    const safeX = 120 + ((Math.floor(source.t * 0.7) % 3) * 120);
    stage5AttackFx.push({ type: "alley", t: 0, life: 1.75, fireAt: 0.72, fired: false, safeX, rage });
  }

  function stage5Lockdown(source, phase = 1) {
    if (stage5AttackFx.some((fx) => fx.type === "lockdown")) return;
    const safeIndex = Math.floor(source.t * 0.8) % 4;
    const columns = [62, 178, 302, 418].filter((_, i) => i !== safeIndex);
    const rows = phase === 2 ? [248, 414] : [322];
    stage5AttackFx.push({ type: "lockdown", t: 0, life: phase === 2 ? 2.15 : 1.95, fireAt: 0.88, fired: false, columns, rows, phase });
  }

  function stage5CrownDeployment(source, phase = 1) {
    if (enemyBullets.some((b) => b.kind === "stage5CrownNode" && !b.dead)) return;
    const count = scaledCount(phase === 2 ? 7 : 5, 3);
    for (let i = 0; i < count; i++) {
      const angle = -Math.PI / 2 + i * Math.PI * 2 / count;
      const node = spawnStage5PatternBullet(source.x, source.y, 0, angle, "stage5CrownNode", {
        color: i % 2 ? "#ff5578" : "#ffb05f",
        r: 8,
        hp: phase === 2 ? 150 : 125,
        orbitAngle: angle,
        orbitRadius: phase === 2 ? 128 : 108,
        phase,
        fireAt: 0.85 + i * 0.08,
        nextFire: 0.85 + i * 0.08,
        lifeTime: phase === 2 ? 5.4 : 4.8,
      });
      if (node) {
        node.x = source.x + Math.cos(angle) * 18;
        node.y = source.y + Math.sin(angle) * 12;
      }
    }
  }

  function stage5PulseRain(source, phase = 1) {
    const count = scaledCount(phase === 2 ? 18 : 13, 7);
    for (let i = 0; i < count; i++) {
      const x = 24 + ((i * 73 + Math.floor(source.t * 43)) % (W - 48));
      spawnStage5PatternBullet(x, -20 - (i % 4) * 18, 118 + (i % 3) * 16, Math.PI / 2, "stage5PulseDrop", {
        color: i % 3 === 0 ? "#ff5b76" : "#c786ff",
        r: i % 3 === 0 ? 6 : 4.5,
        hp: 44,
        delay: 0.28 + (i % 5) * 0.08,
      });
    }
  }

  function updateStage5AttackFx(dt) {
    if (currentStage !== 5) {
      stage5AttackFx = [];
      return;
    }
    for (const fx of stage5AttackFx) {
      fx.t += dt;
      if (fx.type === "mirror") {
        if (fx.volley === 0 && fx.t >= 0.55) fireStage5MirrorVolley(fx);
        if (fx.volley === 1 && fx.t >= 1.18) fireStage5MirrorVolley(fx);
        if (fx.rage && fx.volley === 2 && fx.t >= 1.75) fireStage5MirrorVolley(fx);
      } else if (fx.type === "alley" && !fx.fired && fx.t >= fx.fireAt) {
        fx.fired = true;
        const offsets = fx.rage ? [-170, -90, 90, 170] : [-150, -70, 70, 150];
        for (const offset of offsets) {
          const x = Math.max(24, Math.min(W - 24, fx.safeX + offset));
          spawnLaser(x, -12, Math.PI / 2, H + 70, fx.rage ? 7 : 6, { kind: "enemy", damage: 11, color: "#67f4ff", warmup: 0.12, life: 1.0, silent: true });
        }
        playSound("laser", 1.05, 0.08);
      } else if (fx.type === "lockdown" && !fx.fired && fx.t >= fx.fireAt) {
        fx.fired = true;
        for (const x of fx.columns) spawnLaser(x, -12, Math.PI / 2, H + 80, fx.phase === 2 ? 12 : 10, { kind: "boss", damage: fx.phase === 2 ? 28 : 24, color: "#ff4267", warmup: 0.1, life: 1.0, silent: true });
        for (const y of fx.rows) spawnLaser(-12, y, 0, W + 70, fx.phase === 2 ? 10 : 8, { kind: "boss", damage: 22, color: "#ff6a52", warmup: 0.1, life: 0.9, silent: true });
        playSound("bossLaser", 1.25, 0.1);
      }
    }
    stage5AttackFx = stage5AttackFx.filter((fx) => fx.t < fx.life);
  }

  function updateStage5MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (116 - midBoss.y) * Math.min(1, dt * 1.8);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 1.15) * 112 + Math.sin(midBoss.t * 3.4) * 18;
    midBoss.fireA -= dt;
    midBoss.fireB -= dt;
    midBoss.fireC -= dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) {
      startMidBossHalfDialog();
      return;
    }
    if (midBoss.y < 70) return;
    if (midBoss.fireA <= 0) {
      midBoss.fireA = scaledDelay(rage ? 2.05 : 2.65);
      stage5MirrorAssault(midBoss, rage);
    }
    if (midBoss.fireB <= 0) {
      midBoss.fireB = scaledDelay(rage ? 1.25 : 1.65);
      stage5ClawSweep(midBoss, rage);
    }
    if (midBoss.fireC <= 0) {
      midBoss.fireC = scaledDelay(rage ? 3.4 : 4.25);
      stage5AlleyLasers(midBoss, rage);
    }
  }

  function updateStage5Boss(dt) {
    boss.t += dt;
    boss.y += (92 - boss.y) * Math.min(1, dt * 1.55);
    boss.x = W / 2 + Math.sin(boss.t * 0.62) * 104;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 2 ? 3.25 : 4.25);
      stage5Lockdown(boss, phase);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 2 ? 3.0 : 3.8);
      stage5CrownDeployment(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 2 ? 1.02 : 1.38);
      stage5PulseRain(boss, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 2 ? 0.72 : 0.96);
      aimedBurst({ x: boss.x - 58, y: boss.y + 30 }, phase === 2 ? 8 : 6, 0.74, phase === 2 ? 222 : 196, "#ff6b82");
      aimedBurst({ x: boss.x + 58, y: boss.y + 30 }, phase === 2 ? 8 : 6, 0.74, phase === 2 ? 222 : 196, "#b58cff");
    }
  }
  function spawnStage4Missile(x, y, angle, speed = 92, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 112;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({
      id: nextEnemyBulletId++, x, y,
      vx: Math.cos(angle) * s, vy: Math.sin(angle) * s,
      r: options.r || 7, color: options.color || "#73d9ff",
      hp, maxHp: hp, age: 0, kind: "stage4Missile",
      damage: options.damage || 18,
      trackingTime: options.trackingTime || 0.72,
      turnRate: options.turnRate || 1.6,
      maxSpeed: scaledEnemySpeed(options.maxSpeed || 255),
    });
  }

  function spawnStage4FlakShell(x, y, targetX, targetY, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const speed = scaledEnemySpeed(options.speed || 88);
    const angle = Math.atan2(targetY - y, targetX - x);
    const hp = options.hp || 150;
    enemyBullets.push({
      id: nextEnemyBulletId++, x, y,
      vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
      r: options.r || 9, color: options.color || "#ffd36b",
      hp, maxHp: hp, age: 0, kind: "stage4Flak",
      damage: options.damage || 21,
      fuse: options.fuse || Math.max(0.75, Math.hypot(targetX - x, targetY - y) / speed),
      burstCount: options.burstCount || 9,
      burstSpeed: options.burstSpeed || 132,
    });
  }

  function burstStage4Flak(b) {
    if (b.dead) return;
    b.dead = true;
    const count = b.burstCount || 9;
    const offset = b.age * 1.7;
    for (let i = 0; i < count; i++) {
      enemyShot(b.x, b.y, b.burstSpeed || 132, offset + Math.PI * 2 * i / count, i % 2 ? "#ffd36b" : "#77ddff", 4.5, 48);
    }
    explode(b.x, b.y, b.color || "#ffd36b", 18);
    particles.push({ x: b.x, y: b.y, vx: 0, vy: 0, life: 0.3, maxLife: 0.3, color: b.color || "#ffd36b", type: "damageRing", size: 72 });
  }

  function stage4MissileVolley(source, rage = false, sideBias = 0) {
    const count = scaledCount(rage ? 7 : 5, 3);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const side = i % 2 ? 1 : -1;
      spawnStage4Missile(source.x + side * (30 + (i % 3) * 10), source.y + 24, base + lane * 0.11 + sideBias, 88 + i * 5, {
        trackingTime: rage ? 0.88 : 0.68,
        turnRate: rage ? 1.9 : 1.55,
        maxSpeed: rage ? 292 : 252,
        hp: rage ? 132 : 108,
        color: i % 2 ? "#73d9ff" : "#f4d36b",
      });
    }
  }

  function stage4FlakField(source, rage = false) {
    const count = scaledCount(rage ? 5 : 3, 2);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const targetX = Math.max(48, Math.min(W - 48, player.x + lane * (rage ? 64 : 86)));
      const targetY = H * (0.34 + (i % 3) * 0.08);
      spawnStage4FlakShell(source.x + lane * 24, source.y + 32, targetX, targetY, {
        speed: rage ? 104 : 86,
        burstCount: scaledCount(rage ? 11 : 8, 5),
        burstSpeed: rage ? 154 : 128,
        hp: rage ? 180 : 145,
        color: i % 2 ? "#ffd36b" : "#73d9ff",
      });
    }
  }

  function spawnStage4PatternBullet(x, y, speed, angle, kind, options = {}) {
    if (!canSpawnEnemyBullet(options)) return null;
    const s = scaledEnemySpeed(speed);
    const hp = options.hp || 64;
    const bullet = {
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * s,
      vy: Math.sin(angle) * s,
      r: options.r || 6,
      color: options.color || "#73d9ff",
      hp,
      maxHp: hp,
      age: 0,
      kind,
      damage: options.damage || 16,
      ...options,
    };
    enemyBullets.push(bullet);
    return bullet;
  }

  function stage4ScissorFormation(source, rage = false) {
    if (stage4AttackFx.some((fx) => fx.type === "scissorWave")) return;
    stage4AttackFx.push({
      type: "scissorWave",
      source,
      t: 0,
      life: rage ? 2.75 : 2.55,
      spawnAt: rage ? 0.34 : 0.44,
      spawned: false,
      rage,
      reverse: Math.sin(source.t * 1.37) < 0,
    });
    playTone(rage ? 620 : 520, 0.14, "sawtooth", 0.07);
  }

  function spawnStage4ScissorJets(fx) {
    const pairs = scaledCount(fx.rage ? 3 : 2, 1);
    for (let i = 0; i < pairs; i++) {
      const laneY = 112 + i * (fx.rage ? 72 : 92);
      const crossY = (fx.reverse ? 350 : 285) + i * 54;
      for (const side of [-1, 1]) {
        const startX = side < 0 ? -54 : W + 54;
        const targetX = side < 0 ? W + 72 : -72;
        const startY = fx.reverse ? laneY + 76 : laneY;
        const targetY = fx.reverse ? crossY - 86 : crossY + 120;
        const angle = Math.atan2(targetY - startY, targetX - startX);
        spawnStage4PatternBullet(startX, startY, fx.rage ? 408 : 360, angle, "stage4ScissorJet", {
          r: fx.rage ? 14 : 13,
          hp: fx.rage ? 390 : 310,
          damage: fx.rage ? 25 : 21,
          color: side < 0 ? "#75e7ff" : "#ffd56d",
          scissorSide: side,
          fireAt: 0.68 + i * 0.08,
          rage: fx.rage,
          force: i === 0,
        });
      }
    }
    playSound("laser", fx.rage ? 0.82 : 0.66, 0.1);
  }

  function fireStage4ScissorJet(jet) {
    const count = scaledCount(jet.rage ? 4 : 3, 2);
    const aim = Math.PI / 2 + jet.scissorSide * (jet.rage ? 0.56 : 0.5);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage4PatternBullet(jet.x, jet.y, jet.rage ? 226 : 194, aim + lane * 0.17, "stage4WingShot", {
        r: jet.rage ? 6 : 5.4,
        hp: jet.rage ? 84 : 66,
        damage: jet.rage ? 18 : 15,
        color: jet.color,
        force: i === Math.floor(count / 2),
      });
    }
    particles.push({ x: jet.x, y: jet.y, vx: 0, vy: 0, life: 0.28, maxLife: 0.28, color: jet.color, type: "damageRing", size: 28 });
  }

  function stage4MidBossWaveSequence(source, rage = false) {
    if (stage4AttackFx.some((fx) => fx.type === "midWave")) return;
    stage4AttackFx.push({
      type: "midWave",
      source,
      t: 0,
      life: rage ? 1.48 : 1.22,
      nextVolley: 0.04,
      volley: 0,
      volleys: scaledCount(rage ? 5 : 4, 3),
      rage,
    });
  }

  function fireStage4MidBossWave(fx) {
    if (!fx.source || !midBoss || midBoss.hp <= 0) return;
    const count = scaledCount(fx.rage ? 13 : 11, fx.rage ? 7 : 6);
    const spread = fx.rage ? 1.34 : 1.18;
    const volleyOffset = (fx.volley - (fx.volleys - 1) / 2) * (fx.rage ? 0.055 : 0.045);
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      const angle = Math.PI / 2 + volleyOffset + lane * spread / Math.max(1, count - 1);
      spawnStage4PatternBullet(fx.source.x, fx.source.y + 36, (fx.rage ? 146 : 124) + fx.volley * 8, angle, "stage4WaveBullet", {
        r: fx.rage ? 5.4 : 4.8,
        hp: fx.rage ? 68 : 52,
        damage: fx.rage ? 14 : 11,
        color: fx.volley % 2 ? "#ffd56d" : "#75e7ff",
        wavePhase: i * 0.65 + fx.volley,
        force: i === Math.floor(count / 2),
      });
    }
    playTone(300 + fx.volley * 34, 0.06, "triangle", 0.035);
  }

  function stage4MidBossLaserBarrage(source, rage = false) {
    if (stage4AttackFx.some((fx) => fx.type === "midLaserBarrage")) return;
    stage4AttackFx.push({
      type: "midLaserBarrage",
      source,
      t: 0,
      life: rage ? 2.1 : 1.72,
      nextShot: 0.05,
      shot: 0,
      total: scaledCount(rage ? 8 : 5, 3),
      rage,
    });
    playSound("laser", rage ? 0.88 : 0.7, 0.12);
  }

  function fireStage4MidBossLaser(fx) {
    if (!fx.source || !midBoss || midBoss.hp <= 0) return;
    const offsets = [-0.78, 0.7, -0.48, 0.42, -0.2, 0.17, -0.64, 0.57, -0.34, 0.31];
    const index = fx.shot % offsets.length;
    const side = fx.shot % 2 ? 1 : -1;
    const angle = Math.PI / 2 + offsets[index] + Math.sin(fx.source.t * 1.8 + fx.shot) * 0.07;
    spawnLaser(fx.source.x + side * 62, fx.source.y + 24, angle, 820, fx.rage ? 7 : 6, {
      kind: "enemy",
      damage: fx.rage ? 14 : 11,
      color: side < 0 ? "#73e7ff" : "#ffd466",
      warmup: fx.rage ? 0.28 : 0.36,
      life: fx.rage ? 0.76 : 0.84,
      silent: true,
    });
    playTone(185 + (fx.shot % 4) * 42, 0.08, "sawtooth", 0.045);
  }
  function stage4SiegeTurrets() {
    return enemyBullets.filter((b) => b.kind === "stage4SiegeTurret" && !b.dead);
  }

  function stage4SiegeDeployment(source, phase = 1) {
    if (stage4AttackFx.some((fx) => fx.type === "siegeFrame")) return false;
    const anchors = phase === 1
      ? [{ x: 78, y: 158 }, { x: W - 78, y: 158 }, { x: 96, y: 354 }, { x: W - 96, y: 354 }]
      : [{ x: 68, y: 146 }, { x: W - 68, y: 146 }, { x: 82, y: 392 }, { x: W - 82, y: 392 }];
    const turretIds = [];
    const lifeTime = phase === 1 ? 5.25 : 6.55;
    for (let i = 0; i < anchors.length; i++) {
      const anchor = anchors[i];
      const side = i % 2 === 0 ? -1 : 1;
      const turret = spawnStage4PatternBullet(source.x + side * 24, source.y + 18, 0, Math.PI / 2, "stage4SiegeTurret", {
        r: phase === 1 ? 18 : 20,
        hp: phase === 1 ? 620 : 820,
        damage: phase === 1 ? 24 : 28,
        color: side < 0 ? "#72e3ff" : "#ffd15e",
        anchorX: anchor.x,
        anchorY: anchor.y,
        deployTime: 0.62 + i * 0.07,
        fireAt: 0.92 + i * (phase === 1 ? 0.42 : 0.34),
        lifeTime,
        turretIndex: i,
        phase,
        fired: false,
        force: true,
      });
      if (turret) turretIds.push(turret.id);
    }
    stage4AttackFx.push({
      type: "siegeFrame",
      source,
      x: source.x,
      y: source.y,
      t: 0,
      life: lifeTime,
      phase,
      turretIds,
      finaleAt: phase === 1 ? 2.9 : 2.55,
      finaleFired: false,
      rotateAt: phase === 2 ? 3.35 : 99,
      rotated: false,
      secondFinaleAt: phase === 2 ? 5.45 : 99,
      secondFinaleFired: false,
    });
    source.siegePulse = 1.25;
    playSound("bossLaser", phase === 1 ? 0.76 : 0.94, 0.18);
    return true;
  }

  function fireStage4SiegeTurret(turret, finale = false) {
    if (!turret || turret.dead) return;
    const count = scaledCount(finale ? (turret.phase === 2 ? 10 : 8) : (turret.phase === 2 ? 8 : 6), finale ? 4 : 3);
    const aim = Math.atan2(player.y - turret.y, player.x - turret.x);
    const spread = finale ? 0.25 : 0.19;
    for (let i = 0; i < count; i++) {
      const lane = i - (count - 1) / 2;
      spawnStage4PatternBullet(turret.x, turret.y, finale ? 238 : 205, aim + lane * spread, "stage4SiegeShot", {
        r: finale ? 6.6 : 5.8,
        hp: finale ? 96 : 74,
        damage: finale ? 20 : 17,
        color: turret.color,
        curve: (turret.turretIndex % 2 ? 1 : -1) * (finale ? 0.12 : 0.07),
        force: i === Math.floor(count / 2),
      });
    }
    turret.recoil = 1;
    particles.push({ x: turret.x, y: turret.y, vx: 0, vy: 0, life: 0.34, maxLife: 0.34, color: turret.color, type: "damageRing", size: finale ? 42 : 30 });
    playTone(finale ? 175 : 230 + turret.turretIndex * 34, finale ? 0.14 : 0.09, "square", finale ? 0.075 : 0.05);
  }

  function updateStage4AttackFx(dt) {
    if (currentStage !== 4) {
      stage4AttackFx = [];
      return;
    }
    for (const fx of stage4AttackFx) {
      fx.t += dt;
      if (fx.type === "scissorWave") {
        if (!fx.spawned && fx.t >= fx.spawnAt) {
          fx.spawned = true;
          if (midBoss && midBoss.hp > 0) spawnStage4ScissorJets(fx);
        }
      } else if (fx.type === "midWave") {
        while (fx.volley < fx.volleys && fx.t >= fx.nextVolley) {
          fireStage4MidBossWave(fx);
          fx.volley += 1;
          fx.nextVolley += fx.rage ? 0.2 : 0.24;
        }
      } else if (fx.type === "midLaserBarrage") {
        while (fx.shot < fx.total && fx.t >= fx.nextShot) {
          fireStage4MidBossLaser(fx);
          fx.shot += 1;
          fx.nextShot += fx.rage ? 0.13 : 0.17;
        }
      } else if (fx.type === "siegeFrame") {
        if (fx.source) {
          fx.x = fx.source.x;
          fx.y = fx.source.y;
        }
        const turrets = enemyBullets.filter((b) => fx.turretIds.includes(b.id) && !b.dead);
        if (!fx.finaleFired && fx.t >= fx.finaleAt) {
          fx.finaleFired = true;
          for (const turret of turrets) fireStage4SiegeTurret(turret, true);
          if (fx.source) fx.source.siegePulse = 0.95;
        }
        if (!fx.rotated && fx.t >= fx.rotateAt) {
          fx.rotated = true;
          const rotatedAnchors = [
            { x: W - 82, y: 392 }, { x: 82, y: 392 },
            { x: W - 68, y: 146 }, { x: 68, y: 146 },
          ];
          for (const turret of turrets) {
            const anchor = rotatedAnchors[turret.turretIndex] || rotatedAnchors[0];
            turret.anchorX = anchor.x;
            turret.anchorY = anchor.y;
            turret.fired = false;
            turret.fireAt = turret.age + 0.52 + turret.turretIndex * 0.26;
          }
          playSound("laser", 0.78, 0.12);
        }
        if (!fx.secondFinaleFired && fx.t >= fx.secondFinaleAt) {
          fx.secondFinaleFired = true;
          for (const turret of turrets) fireStage4SiegeTurret(turret, true);
          if (fx.source) fx.source.siegePulse = 1.1;
        }
      }
    }
    stage4AttackFx = stage4AttackFx.filter((fx) => fx.t < fx.life);
  }

  function updateStage4MidBoss(dt) {
    midBoss.t += dt;
    midBoss.enter += dt;
    midBoss.y += (118 - midBoss.y) * Math.min(1, dt * 1.5);
    midBoss.x = W / 2 + Math.sin(midBoss.t * 0.78) * 82 + Math.sin(midBoss.t * 2.15) * 14;
    midBoss.fireA -= dt;
    midBoss.fireB = (midBoss.fireB || 0) - dt;
    midBoss.fireC = (midBoss.fireC || 0) - dt;
    const rage = midBoss.hp / midBoss.maxHp < 0.5;
    if (!midBossHalfDialogShown && rage) { startMidBossHalfDialog(); return; }
    if (midBoss.fireA <= 0 && midBoss.y > 70) {
      midBoss.fireA = scaledDelay(rage ? 2.7 : 3.3);
      stage4ScissorFormation(midBoss, rage);
    }
    if (midBoss.fireB <= 0 && midBoss.y > 70) {
      midBoss.fireB = scaledDelay(rage ? 1.55 : 2.05);
      stage4MidBossWaveSequence(midBoss, rage);
    }
    if (midBoss.fireC <= 0 && midBoss.y > 70) {
      midBoss.fireC = scaledDelay(rage ? 3.15 : 4.15);
      stage4MidBossLaserBarrage(midBoss, rage);
    }
  }

  function updateStage4Boss(dt) {
    boss.t += dt;
    boss.y += (94 - boss.y) * Math.min(1, dt * 1.35);
    boss.x = W / 2 + Math.sin(boss.t * 0.42) * 58 + Math.sin(boss.t * 1.22) * 13;
    boss.siegePulse = Math.max(0, (boss.siegePulse || 0) - dt);
    boss.fireA -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) { startBossHalfDialog(); return; }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0 && boss.y > 36) {
      if (stage4SiegeDeployment(boss, phase)) {
        boss.fireA = scaledDelay(phase === 1 ? 5.35 : 6.55);
      } else {
        boss.fireA = 0.15;
      }
    }
  }
  function spawnStage2EnergyOrb(x, y, phase = 1) {
    if (!canSpawnEnemyBullet({ force: true })) return;
    const speed = scaledEnemySpeed(phase === 1 ? 78 : 92);
    const hp = phase === 1 ? 2600 : 3400;
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.sin(boss.t * 1.7) * (phase === 1 ? 14 : 22),
      vy: speed,
      r: phase === 1 ? 31 : 36,
      color: phase === 1 ? "#9ff8ff" : "#ffec8a",
      hp,
      maxHp: hp,
      age: 0,
      kind: "stage2EnergyOrb",
      damage: phase === 1 ? 24 : 30,
      burstY: H * (phase === 1 ? 0.48 : 0.52),
      burstCount: scaledCount(phase === 1 ? 16 : 22, 10),
      burstLength: phase === 1 ? 210 : 250,
      burstWidth: phase === 1 ? 8 : 11,
    });
    playSound("laser", 0.9, 0.08);
  }

  function burstStage2EnergyOrb(b) {
    if (b.dead) return;
    b.dead = true;
    const count = b.burstCount || 12;
    const offset = b.age * 2.3;
    for (let i = 0; i < count; i++) {
      const a = offset + Math.PI * 2 * i / count;
      spawnLaser(b.x, b.y, a, b.burstLength || 160, b.burstWidth || 7, {
        kind: "bossBurst",
        damage: 20,
        color: i % 2 ? "#ff8df0" : "#9ff8ff",
        warmup: 0.02,
        life: 0.95,
        silent: true,
      });
    }
    explode(b.x, b.y, b.color || "#ffffff", 34);
    particles.push({ x: b.x, y: b.y, vx: 0, vy: 0, life: 0.42, maxLife: 0.42, color: b.color || "#ffffff", type: "damageRing", size: 138 });
    flash = Math.max(flash, 0.12);
    shake = Math.max(shake, 0.18);
    playSound("bossLaser", 0.9, 0.18);
  }
  function spawnStage3ShadowDrone(x, y, angle, options = {}) {
    if (!canSpawnEnemyBullet(options)) return;
    const hp = options.hp || 180;
    enemyBullets.push({
      id: nextEnemyBulletId++,
      x,
      y,
      vx: Math.cos(angle) * 42,
      vy: Math.sin(angle) * 42,
      r: options.r || 12,
      color: options.color || "#8fdcff",
      hp,
      maxHp: hp,
      age: 0,
      kind: "stage3ShadowDrone",
      damage: options.damage || 22,
      anchorX: options.anchorX ?? x,
      anchorY: options.anchorY ?? y,
      orbitAngle: angle,
      orbitRadius: options.orbitRadius || 72,
      spinDir: options.spinDir || 1,
      armTime: options.armTime || 1.05,
      dashSpeed: options.dashSpeed || 250,
    });
  }

  function stage3BossDroneWave(source, phase = 1) {
    const count = scaledCount(phase === 1 ? 5 : 7, 3);
    const radius = phase === 1 ? 78 : 98;
    for (let i = 0; i < count; i++) {
      const side = i % 2 ? 1 : -1;
      const a = -Math.PI / 2 + (i - (count - 1) / 2) * 0.42;
      spawnStage3ShadowDrone(source.x + Math.cos(a) * 52, source.y + 28 + Math.sin(a) * 18, a, {
        anchorX: source.x + side * 32,
        anchorY: source.y + 26,
        orbitRadius: radius + (i % 3) * 9,
        spinDir: side,
        armTime: phase === 1 ? 1.05 + i * 0.05 : 0.82 + i * 0.035,
        dashSpeed: scaledEnemySpeed(phase === 1 ? 238 : 282),
        hp: phase === 1 ? 165 : 205,
        r: phase === 1 ? 11.5 : 12.5,
        color: i % 2 ? "#8fdcff" : "#ffffff",
      });
    }
    playSound("laser", 0.75, 0.08);
  }

  function stage3BossWingShardVolley(source, phase = 1) {
    const count = scaledCount(phase === 1 ? 7 : 10, 4);
    const left = Math.sin(source.t * 1.3) > 0;
    const startX = left ? -18 : W + 18;
    for (let i = 0; i < count; i++) {
      const y = 70 + i * ((H * 0.56) / Math.max(1, count - 1));
      const aimY = player.y - 120 + Math.sin(source.t + i) * 58;
      const targetX = player.x + (i - (count - 1) / 2) * 16;
      const a = Math.atan2(aimY - y, targetX - startX);
      enemyShot(startX, y, phase === 1 ? 138 : 162, a, i % 2 ? "#8fdcff" : "#ffb15f", 5.6, 74, {
        kind: "stage3WingShard",
        damage: phase === 1 ? 15 : 18,
        force: i < 2,
      });
    }
  }

  function stage3BossCrossLasers(source, phase = 1) {
    const warmup = phase === 1 ? 0.46 : 0.36;
    const width = phase === 1 ? 8 : 10;
    const leftY = 118 + Math.sin(source.t * 1.2) * 42;
    const rightY = 118 + Math.cos(source.t * 1.2) * 42;
    const leftAngle = Math.atan2(H + 40 - leftY, W * 0.78);
    const rightAngle = Math.atan2(H + 40 - rightY, -W * 0.78);
    spawnLaser(0, leftY, leftAngle, 860, width, { kind: "bossBurst", damage: phase === 1 ? 18 : 22, color: "#ffb15f", warmup, life: 1.05 });
    spawnLaser(W, rightY, rightAngle, 860, width, { kind: "bossBurst", damage: phase === 1 ? 18 : 22, color: "#8fdcff", warmup, life: 1.05 });
  }

  function updateStage3Boss(dt) {
    boss.t += dt;
    boss.y += (94 - boss.y) * Math.min(1, dt * 1.45);
    boss.x = W / 2 + Math.sin(boss.t * 0.54) * 78 + Math.sin(boss.t * 1.45) * 18;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD = (boss.fireD || 1.6) - dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 1 ? 2.25 : 1.62);
      stage3BossDroneWave(boss, phase);
      particles.push({ x: boss.x, y: boss.y + 18, vx: 0, vy: 0, life: 0.36, maxLife: 0.36, color: phase === 1 ? "#8fdcff" : "#ffb15f", type: "damageRing", size: phase === 1 ? 116 : 146 });
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 1 ? 1.12 : 0.78);
      stage3BossWingShardVolley(boss, phase);
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 1 ? 3.55 : 2.72);
      stage3BossCrossLasers(boss, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 1 ? 1.85 : 1.42);
      const count = phase === 1 ? 12 : 16;
      ring({ x: boss.x, y: boss.y + 24 }, count, phase === 1 ? 92 : 112, boss.t * (phase === 1 ? 0.9 : -1.35), phase === 1 ? "#8fdcff" : "#ffb15f", 4.3);
    }
  }
  function updateStage2Boss(dt) {
    boss.t += dt;
    boss.y += (92 - boss.y) * Math.min(1, dt * 1.5);
    boss.x = W / 2 + Math.sin(boss.t * 0.66) * 82;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    boss.fireD = (boss.fireD || 2.0) - dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 1 ? 0.52 : 0.36);
      stage2StarRing(boss, phase === 1 ? 20 : 28, phase === 1 ? 96 : 124, boss.t * (phase === 1 ? 1.35 : -1.9), phase === 1 ? "#9fdcff" : "#fff2a6");
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 1 ? 1.25 : 0.88);
      const laneCount = scaledCount(phase === 1 ? 5 : 7, 3);
      for (let i = 0; i < laneCount; i++) {
        const x = 48 + i * ((W - 96) / (laneCount - 1));
        spawnStage2StarBullet(x, -22, Math.PI / 2 + Math.sin(boss.t + i) * 0.12, 150 + i * 6, i % 2 ? "#b8f7ff" : "#ffd7f4", { r: 7, hp: 96, wobble: 0.7 });
      }
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 1 ? 4.05 : 3.1);
      const side = Math.sin(boss.t * 1.9) >= 0 ? 1 : -1;
      spawnStage2EnergyOrb(boss.x + side * 96, boss.y + 54, phase);
      if (phase === 2) spawnStage2EnergyOrb(boss.x - side * 96, boss.y + 62, phase);
    }
    if (boss.fireD <= 0) {
      boss.fireD = scaledDelay(phase === 1 ? 2.2 : 1.7);
      aimedBurst({ x: boss.x - 86, y: boss.y + 18 }, phase === 1 ? 5 : 7, 0.58, 220, "#7df4ff");
      aimedBurst({ x: boss.x + 86, y: boss.y + 18 }, phase === 1 ? 5 : 7, 0.58, 220, "#7df4ff");
    }
  }
  function startBoss() {
    playBgm(bossBgmMode());
    boss = {
      x: W / 2,
      y: -92,
      r: 60,
      hp: stageSetup().bossHp,
      maxHp: stageSetup().bossHp,
      t: 0,
      fireA: 0,
      fireB: 1.2,
      fireC: currentStage === 2 ? 0.7 : 2.4,
      fireD: 1.8,
    };
    enemies = [];
    enemyBullets.length = Math.min(enemyBullets.length, 45);
    lasers = [];
  }

  function playerShot(props) {
    playerBullets.push({ id: nextPlayerBulletId++, ...props });
  }

  function firePlayer(dt) {
    player.fire -= dt;
    if (player.fire > 0) return;
    player.fire = Math.max(0.052, 0.096 - player.power * 0.007);
    const power = player.power;
    const damage = 6 + power * 2;
    playerShot({ x: player.x - 12, y: player.y - 25, vx: -18, vy: -590, r: 4, damage, kind: "bolt" });
    playerShot({ x: player.x + 12, y: player.y - 25, vx: 18, vy: -590, r: 4, damage, kind: "bolt" });
    if (power >= 2) playerShot({ x: player.x, y: player.y - 32, vx: 0, vy: -675, r: 5, damage: damage + 4, kind: "pierce" });
    if (power >= 3) {
      playerShot({ x: player.x - 23, y: player.y - 18, vx: -78, vy: -545, r: 3.5, damage: damage - 1, kind: "side" });
      playerShot({ x: player.x + 23, y: player.y - 18, vx: 78, vy: -545, r: 3.5, damage: damage - 1, kind: "side" });
    }
    playSound("shot", 1, 0.065);
    if (power >= 5) {
      playerShot({ x: player.x - 4, y: player.y - 34, vx: -34, vy: -720, r: 4.5, damage: damage + 6, kind: "pierce" });
      playerShot({ x: player.x + 4, y: player.y - 34, vx: 34, vy: -720, r: 4.5, damage: damage + 6, kind: "pierce" });
    }
  }

  function enemyPressure() {
    if (player.respawn > 0 || player.inv > 1.2) return 0.45;
    return [0.48, 0.58, 0.72, 0.86, 1.0, 1.12][Math.max(0, Math.min(5, player.power))] || 0.72;
  }

  function scaledCount(count, min = 1) {
    return Math.max(min, Math.round(count * enemyPressure()));
  }

  function scaledDelay(base) {
    const p = enemyPressure();
    return base * (1.6 - Math.min(1.12, p) * 0.5);
  }

  function scaledEnemySpeed(speed) {
    const p = enemyPressure();
    return speed * (0.84 + p * 0.16);
  }

  function enemyBulletBudget() {
    const p = Math.max(0, Math.min(5, player.power));
    const base = currentStage === 9 ? [68, 84, 102, 122, 146, 172][p] : currentStage === 8 ? [72, 90, 110, 132, 156, 184][p] : currentStage === 7 ? [78, 98, 120, 146, 174, 204][p] : currentStage === 6 ? [82, 104, 132, 164, 198, 236][p] : currentStage === 2 ? [58, 72, 92, 118, 148, 176][p] : [76, 92, 116, 144, 176, 210][p];
    if (player.respawn > 0 || player.inv > 1.2) return Math.min(base, currentStage === 9 ? 58 : currentStage === 8 ? 60 : currentStage === 7 ? 64 : currentStage === 2 ? 54 : 70);
    return base;
  }

  function canSpawnEnemyBullet(extra = {}) {
    return extra.force || enemyBullets.length < enemyBulletBudget();
  }

  function enemyShot(x, y, speed, angle, color = "#ff5bb8", size = 5, hp = 38, extra = {}) {
    if (!canSpawnEnemyBullet(extra)) return;
    const pressure = enemyPressure();
    if (!extra.force && Math.random() > Math.min(0.98, pressure + 0.1)) return;
    const s = scaledEnemySpeed(speed);
    enemyBullets.push({ id: nextEnemyBulletId++, x, y, vx: Math.cos(angle) * s, vy: Math.sin(angle) * s, r: size, color, hp, maxHp: hp, age: 0, ...extra });
  }
  function spawnLaser(x, y, angle, length = 850, width = 13, options = {}) {
    const kind = options.kind || "enemy";
    if (!options.silent) playSound(kind === "boss" ? "bossLaser" : "laser", kind === "boss" ? 1 : Math.min(1.5, width / 14), kind === "boss" ? 0.45 : 0.18);
    lasers.push({
      x,
      y,
      angle,
      length,
      width,
      kind,
      damage: options.damage ?? (kind === "boss" ? 35 : 12),
      color: options.color || (kind === "boss" ? "#ff4fd8" : "#67f4ff"),
      life: options.life || 1.75,
      maxLife: options.life || 1.75,
      warmup: options.warmup ?? 0.55,
    });
  }

  function pointToSegmentDistance(px, py, ax, ay, bx, by) {
    const dx = bx - ax;
    const dy = by - ay;
    const len2 = dx * dx + dy * dy || 1;
    const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2));
    const x = ax + dx * t;
    const y = ay + dy * t;
    return Math.hypot(px - x, py - y);
  }

  function crossBurst(source, speed = 150, color = "#ffb86a") {
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    for (let i = -1; i <= 1; i++) {
      enemyShot(source.x, source.y, speed, base + i * 0.33, color, 4, 38);
    }
    enemyShot(source.x, source.y, speed * 0.78, base + Math.PI / 2, color, 4, 34);
    enemyShot(source.x, source.y, speed * 0.78, base - Math.PI / 2, color, 4, 34);
  }

  function spiralBurst(source, count, speed, color) {
    count = scaledCount(count, 4);
    for (let i = 0; i < count; i++) {
      enemyShot(source.x, source.y, speed + (i % 3) * 12, source.t * 2.2 + i * Math.PI * 2 / count, color, 4, 42);
    }
  }

  function mineShot(source) {
    const a = Math.atan2(player.y - source.y, player.x - source.x) + (Math.random() - 0.5) * 0.7;
    enemyShot(source.x, source.y, 42, a, "#74ffd5", 7, 74, { kind: "mine", fuse: 1.25 + Math.random() * 0.55 });
  }

  function splitterShot(source) {
    const a = Math.atan2(player.y - source.y, player.x - source.x);
    enemyShot(source.x, source.y, 105, a, "#ff8f63", 6, 58, { kind: "splitter", fuse: 1.05 });
  }

  function aimedBurst(source, count, spread, speed, color) {
    count = scaledCount(count, 1);
    const base = Math.atan2(player.y - source.y, player.x - source.x);
    const start = base - spread * 0.5;
    for (let i = 0; i < count; i++) {
      const a = count === 1 ? base : start + (spread * i) / (count - 1);
      enemyShot(source.x, source.y, speed, a, color, 4, 34);
    }
  }

  function ring(source, count, speed, offset, color, size = 5) {
    count = scaledCount(count, 4);
    for (let i = 0; i < count; i++) enemyShot(source.x, source.y, speed, offset + (Math.PI * 2 * i) / count, color, size, size >= 5 ? 48 : 36);
  }

  function playBombSound() {
    playSound("laser", 1.55, 0.01);
    playSound("playerHit", 1.35, 0.01);
    playTone(72, 0.42, "sawtooth", 0.18, 0);
    playTone(118, 0.34, "square", 0.13, 0.03);
    playTone(360, 0.2, "triangle", 0.11, 0.08);
    playTone(720, 0.16, "square", 0.075, 0.16);
  }

  function playKillExplosionSound(kind) {
    const heavy = kind === "boss";
    playSound("enemyHit", heavy ? 2.2 : 1.8, 0);
    playSound("playerHit", heavy ? 1.45 : 1.18, 0.01);
    playSound("bulletHit", heavy ? 1.75 : 1.35, 0.01);
    playSound("laser", heavy ? 1.15 : 0.85, 0.02);
    playTone(42, heavy ? 0.72 : 0.58, "sawtooth", heavy ? 0.28 : 0.22, 0);
    playTone(66, heavy ? 0.62 : 0.48, "square", heavy ? 0.2 : 0.15, 0.035);
    playTone(96, 0.32, "sawtooth", 0.16, 0.12);
    playTone(180, 0.26, "triangle", 0.12, 0.2);
    playTone(38, 0.86, "sine", heavy ? 0.2 : 0.16, 0.24);
  }

  function bomb() {
    if (player.bombs <= 0 || player.respawn > 0 || state !== "play") return;
    player.bombs -= 1;
    player.inv = 2.6;
    flash = Math.max(flash, 0.62);
    shake = Math.max(shake, 1.08);
    playBombSound();

    particles.push({ x: player.x, y: player.y, vx: 0, vy: 0, life: 0.95, maxLife: 0.72, color: "#eaffff", type: "bombRing", size: 760 });
    particles.push({ x: player.x, y: player.y, vx: 0, vy: 0, life: 0.48, maxLife: 0.48, color: "#77f5ff", type: "bombRing", size: 520 });
    particles.push({ x: W / 2, y: H / 2, vx: 0, vy: 0, life: 0.32, maxLife: 0.32, color: "#ffffff", type: "bombFlash", size: 1 });
    particles.push({ x: player.x, y: player.y - 28, vx: 0, vy: -48, life: 0.82, maxLife: 0.82, color: "#ffffff", type: "floatingText", text: "BOMB", size: 30 });

    for (let i = 0; i < 96; i++) {
      const a = (Math.PI * 2 * i) / 96;
      const s = 210 + (i % 9) * 34;
      particles.push({ x: player.x, y: player.y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.48 + Math.random() * 0.22, maxLife: 0.7, color: i % 3 ? "#bff9ff" : "#ffffff", type: "shard", size: 3 });
    }

    for (const b of enemyBullets) {
      if (Math.random() < 0.42) explode(b.x, b.y, b.color || "#fff0a8", 6);
    }
    for (const l of lasers) {
      for (let i = 0; i < 7; i++) {
        const t = i / 6;
        const x = l.x + Math.cos(l.angle) * l.length * t;
        const y = l.y + Math.sin(l.angle) * l.length * t;
        explode(x, y, "#ff9bed", 9);
      }
    }
    for (const e of enemies) {
      score += Math.floor(({ stage9Bastion: 2850, stage9Orbiter: 1480, stage9Vector: 740, stage8Relay: 2500, stage8Polarity: 1320, stage8Switchback: 680, stage7Hive: 2200, stage7Breacher: 1180, stage7Sentinel: 620, stage6Carrier: 1900, stage6Mortar: 1050, stage6Striker: 560, stage5Gunship: 1500, stage5Jammer: 880, stage5Interceptor: 480, stage4Bomber: 1250, stage4Gunship: 820, stage4Interceptor: 440, supplyCarrier: 980, prism: 620, starScout: 360, laser: 900, shield: 1100, mineLayer: 760, splitter: 420, interceptor: 360, fighter: 650 }[e.type] || 220) * 0.65);
      explode(e.x, e.y, e.type === "stage9Bastion" ? "#ff8ad8" : e.type === "stage9Orbiter" ? "#b78cff" : e.type === "stage9Vector" ? "#73f0ff" : e.type === "stage8Relay" ? "#ff9d4d" : e.type === "stage8Polarity" ? "#9cecff" : e.type === "stage8Switchback" ? "#dff7ff" : e.type === "stage7Hive" ? "#9d72ff" : e.type === "stage7Breacher" ? "#d7e9ff" : e.type === "stage7Sentinel" ? "#7eeeff" : e.type === "stage6Carrier" ? "#ff5ab8" : e.type === "stage6Mortar" ? "#ffb24f" : e.type === "stage6Striker" ? "#ff9b48" : e.type === "stage5Gunship" ? "#ff596f" : ["stage5Jammer", "stage5Interceptor"].includes(e.type) ? "#b98cff" : e.type === "stage4Bomber" ? "#f4d36b" : ["stage4Gunship", "stage4Interceptor"].includes(e.type) ? "#73d9ff" : ["starScout", "prism", "supplyCarrier"].includes(e.type) ? "#9fdcff" : e.type === "laser" ? "#ff7ee7" : e.type === "shield" ? "#d3a2ff" : "#ff7777", e.type === "stage9Bastion" ? 48 : e.type === "stage9Orbiter" ? 36 : e.type === "stage9Vector" ? 27 : e.type === "stage8Relay" ? 46 : e.type === "stage8Polarity" ? 34 : e.type === "stage8Switchback" ? 26 : e.type === "stage7Hive" ? 44 : e.type === "stage7Breacher" ? 32 : e.type === "stage7Sentinel" ? 25 : e.type === "stage6Carrier" ? 42 : e.type === "stage6Mortar" ? 31 : e.type === "stage6Striker" ? 24 : e.type === "stage5Gunship" ? 38 : e.type === "stage5Jammer" ? 28 : e.type === "stage4Bomber" ? 34 : e.type === "stage4Gunship" ? 26 : e.type === "supplyCarrier" ? 34 : e.type === "shield" ? 30 : 22);
      if (Math.random() < ({ stage9Bastion: 0.99, stage9Orbiter: 0.92, stage9Vector: 0.64, stage8Relay: 0.98, stage8Polarity: 0.9, stage8Switchback: 0.62, stage7Hive: 0.98, stage7Breacher: 0.88, stage7Sentinel: 0.58, stage6Carrier: 0.98, stage6Mortar: 0.86, stage6Striker: 0.56, stage5Gunship: 0.96, stage5Jammer: 0.82, stage5Interceptor: 0.52, stage4Bomber: 0.94, stage4Gunship: 0.78, stage4Interceptor: 0.48 }[e.type] || 0.25)) spawnPowerUp(e.x, e.y, ["stage9Bastion", "stage9Orbiter", "stage8Relay", "stage8Polarity", "stage7Hive", "stage7Breacher", "stage6Carrier", "stage6Mortar", "stage5Gunship", "stage5Jammer", "stage4Bomber", "stage4Gunship"].includes(e.type) ? 2 : 1);
      recordCrewCasualty(e.type);
      e.dead = true;
    }

    enemyBullets = [];
    lasers = [];
    stage4AttackFx = [];
    stage5AttackFx = [];
    stage6AttackFx = [];
    stage7AttackFx = [];
    stage8AttackFx = [];
    stage9AttackFx = [];
    enemies = [];

    if (midBoss) {
      midBoss.hp -= Math.ceil(midBoss.maxHp * 0.1);
      particles.push({ x: midBoss.x, y: midBoss.y, vx: 0, vy: 0, life: 0.5, maxLife: 0.5, color: "#ff97eb", type: "damageRing", size: midBoss.r * 2.2 });
      metalHit(midBoss.x, midBoss.y, 1.8, "enemyHit");
      if (midBoss.hp <= 0) {
        score += 5000;
        const defeated = { x: midBoss.x, y: midBoss.y };
        for (let i = 0; i < 3; i++) spawnPowerUp(midBoss.x + (i - 1) * 34, midBoss.y + 16, 2);
        midBoss = null;
        midBossDefeated = true;
        state = "destruction";
        powerUps = powerUps.filter((p) => p.y > 0);
        syncHealSiren();
        nextWave = time + 2.8;
        bossReadyTime = time + bossApproachDelay();
        startDestructionScene("midBoss", defeated, "midBossDefeat");
      }
    }
    if (boss) {
      const wasAboveHalf = boss.hp / boss.maxHp > 0.5;
      boss.hp -= Math.ceil(boss.maxHp * 0.1);
      particles.push({ x: boss.x, y: boss.y, vx: 0, vy: 0, life: 0.56, maxLife: 0.56, color: "#ff5be4", type: "damageRing", size: boss.r * 2.1 });
      metalHit(boss.x, boss.y, 2.1, "enemyHit");
      if (boss.hp <= 0) {
        score += 12000;
        const defeated = { x: boss.x, y: boss.y };
        boss = null;
        state = "destruction";
        powerUps = [];
        stopHealSiren();
        startDestructionScene("boss", defeated, "bossDefeat");
      } else if (wasAboveHalf && boss.hp / boss.maxHp <= 0.5 && !bossHalfDialogShown) {
        flash = Math.min(flash, 0.28);
        startBossHalfDialog();
      }
    }
    updateHud();
  }
  function hitPlayer(damage = 20) {
    if (player.inv > 0 || player.respawn > 0 || state !== "play") return;
    playSound("playerHit", Math.min(1.5, 0.85 + damage / 45), 0.12);
    player.hp -= damage;
    player.power = Math.max(0, player.power - (damage >= 20 ? 1 : 0));
    player.inv = damage >= 20 ? 1.05 : 0.58;
    player.hitPulse = Math.min(1, 0.38 + damage / 45);
    flash = Math.max(flash, damage >= 20 ? 0.18 : 0.1);
    shake = Math.max(shake, damage >= 20 ? 0.62 : 0.32);
    enemyBullets = enemyBullets.filter((b) => dist(b, player) > 130);
    playerDamageBurst(player.x, player.y, player.hp <= 0, damage);
    if (player.hp <= 0) losePlayerShip();
    updateHud();
  }

  function playerDamageBurst(x, y, fatal = false, damage = 20) {
    particles.push({ x, y, vx: 0, vy: 0, life: fatal ? 0.55 : 0.36, maxLife: fatal ? 0.55 : 0.36, color: fatal ? "#ff415f" : "#ffdf70", type: "damageRing", size: fatal ? 96 : 62 });
    particles.push({ x, y, vx: 0, vy: 0, life: 0.22, maxLife: 0.22, color: "#ffffff", type: "impact", size: fatal ? 46 : 34 });
    explode(x, y, fatal ? "#ff4868" : "#ffd76b", fatal ? 58 : 34);
    for (let i = 0; i < (fatal ? 28 : 16); i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 95 + Math.random() * (fatal ? 280 : 170);
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.28 + Math.random() * 0.25, maxLife: 0.55, color: Math.random() > 0.45 ? "#ffec9c" : "#ff5270", type: "shard", size: fatal ? 3 : 2 });
    }
  }

  function losePlayerShip() {
    player.lives -= 1;
    if (player.lives <= 0) {
      state = "over";
      stopBgm();
      showOverlay("Mission Failed", "コンテニューでこの場から再開できます。", "コンテニュー");
      updateHud();
      return;
    }
    player.hp = player.maxHp;
    player.power = 0;
    player.powerStock = 0;
    player.x = W / 2;
    player.y = H + 96;
    player.respawn = 2.25;
    player.respawnMax = 2.25;
    player.hitPulse = 0;
    player.inv = 3.2;
    player.fire = 0.35;
    healAssistUsed = false;
    enemyBullets = [];
    lasers = [];
    flash = Math.max(flash, 0.3);
    shake = Math.max(shake, 0.55);
  }
  function explode(x, y, color, n = 16) {
    if (reducedEffects) n = Math.max(3, Math.ceil(n * 0.45));
    else if (particles.length >= PARTICLE_SOFT_LIMIT) n = Math.ceil(n * 0.42);
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 40 + Math.random() * 180;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.35 + Math.random() * 0.45, maxLife: 0.8, color, type: "spark" });
    }
  }

  function metalHit(x, y, strength = 1, soundName = "enemyHit") {
    shake = Math.max(shake, 0.035 * strength);

    // Every hit gets an inexpensive, high-contrast confirmation tick.
    // The expensive burst and sound are still merged during rapid fire.
    particles.push({ x, y, vx: 0, vy: 0, life: 0.09, maxLife: 0.09, color: "#ffffff", type: "hitTick", size: 7 + strength * 5 });
    const interval = reducedEffects || particles.length >= PARTICLE_SOFT_LIMIT ? 0.075 : 0.035;
    if (time - lastMetalHitFxAt < interval) return;
    lastMetalHitFxAt = time;

    playSound(soundName, Math.min(1.4, 0.55 + strength * 0.22), soundName === "bulletHit" ? 0.025 : 0.035);
    particles.push({ x, y, vx: 0, vy: 0, life: 0.18, maxLife: 0.18, color: "#ffffff", type: "impact", size: 15 + strength * 8 });
    const fullShards = 9 + Math.floor(strength * 5);
    const shards = reducedEffects || particles.length >= PARTICLE_SOFT_LIMIT ? Math.max(3, Math.floor(fullShards * 0.45)) : fullShards;
    for (let i = 0; i < shards; i++) {
      const a = -Math.PI / 2 + (Math.random() - 0.5) * 2.3;
      const s = 145 + Math.random() * 265;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s + 70, life: 0.22 + Math.random() * 0.28, maxLife: 0.5, color: Math.random() > 0.5 ? "#ffd36a" : "#f4fbff", type: "shard", size: 1.5 + Math.random() * 2.2 });
    }
  }
  function awardLife(text = "1UP") {
    if (player.lives >= MAX_LIVES) return;
    player.lives += 1;
    flash = Math.max(flash, 0.24);
    playSound("powerup", 1.25, 0.05);
    particles.push({ x: player.x, y: player.y - 38, vx: 0, vy: -40, life: 1.0, maxLife: 1.0, color: "#ffef8a", type: "floatingText", text, size: 24 });
    explode(player.x, player.y - 26, "#ffef8a", 42);
  }

  function checkScoreRewards() {
    while (score >= nextScoreLife) {
      nextScoreLife += SCORE_1UP_INTERVAL;
      awardLife("SCORE 1UP");
    }
  }

  function spawnPowerUp(x, y, value = 1) {
    powerUps.push({ type: "power", x, y, vx: (Math.random() - 0.5) * 36, vy: -35, r: value > 1 ? 13 : 10, value, t: 0 });
  }

  function spawnHealAssist() {
    healAssistUsed = true;
    const x = Math.max(54, Math.min(W - 54, player.x + (Math.random() - 0.5) * 170));
    powerUps.push({ type: "heal", x, y: -30, vx: (W / 2 - x) * 0.06, vy: 42, r: 15, value: 45, t: 0 });
    particles.push({ x: W / 2, y: 82, vx: 0, vy: -24, life: 1.1, maxLife: 1.1, color: "#ffef8a", type: "floatingText", text: "REPAIR SUPPORT", size: 18 });
  }

  function maybeSpawnHealAssist() {
    if (healAssistUsed || player.respawn > 0 || state !== "play") return;
    if (player.hp > 0 && player.hp < player.maxHp * 0.5 && !powerUps.some((p) => p.type === "heal")) spawnHealAssist();
  }
  function dist(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }

  function overlaps(a, b, pad = 0) {
    const rr = a.r + b.r + pad;
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return dx * dx + dy * dy < rr * rr;
  }

  function update(dt) {
    if (state === "alert") {
      updateApproachAlert(dt);
      updateHud();
      return;
    }
    if (state === "killCutin") {
      time += dt;
      bgScroll = (bgScroll + 70 * dt) % H;
      shake = Math.max(0, shake - dt);
      flash = Math.max(0, flash - dt);
      if (killCutinScene) killCutinScene.t += dt;
      updateParticles(dt);
      updateHud();
      return;
    }
    if (state === "destruction") {
      time += dt;
      bgScroll = (bgScroll + 70 * dt) % H;
      shake = Math.max(0, shake - dt);
      flash = Math.max(0, flash - dt);
      updateDestructionScene(dt);
      updateHud();
      return;
    }
    if (state === "defeatChoice") {
      if (defeatChoice) defeatChoice.t += dt;
      shake = Math.max(0, shake - dt);
      flash = Math.max(0, flash - dt * 1.8);
      updateHud();
      return;
    }
    if (state === "interrogationSelect" || state === "interrogation") {
      if (state === "interrogationSelect" && interrogationNotice) {
        interrogationNotice.t -= dt;
        if (interrogationNotice.t <= 0) interrogationNotice = null;
      }
      shake = Math.max(0, shake - dt);
      flash = Math.max(0, flash - dt * 1.8);
      updateHud();
      return;
    }
    if (state !== "play") return;
    time += dt;
    bgScroll = (bgScroll + 70 * dt) % H;
    shake = Math.max(0, shake - dt);
    flash = Math.max(0, flash - dt);
    player.inv = Math.max(0, player.inv - dt);
    player.hitPulse = Math.max(0, player.hitPulse - dt);
    maybeSpawnHealAssist();

    if (player.respawn > 0) {
      player.respawn = Math.max(0, player.respawn - dt);
      const total = player.respawnMax || 2.25;
      const progress = 1 - player.respawn / total;
      const eased = 1 - Math.pow(1 - progress, 2.35);
      player.x = W / 2;
      player.y = (H + 96) + (H - 86 - (H + 96)) * eased;
      if (player.respawn <= 0) {
        player.y = H - 86;
        player.respawnMax = 0;
      }
    } else {
      const slow = keys.has("Shift");
      const speed = slow ? 170 : 285;
      let dx = 0;
      let dy = 0;
      if (keys.has("ArrowLeft") || keys.has("a")) dx -= 1;
      if (keys.has("ArrowRight") || keys.has("d")) dx += 1;
      if (keys.has("ArrowUp") || keys.has("w")) dy -= 1;
      if (keys.has("ArrowDown") || keys.has("s")) dy += 1;
      const len = Math.hypot(dx, dy) || 1;
      player.x = Math.max(24, Math.min(W - 24, player.x + (dx / len) * speed * dt));
      player.y = Math.max(48, Math.min(H - 34, player.y + (dy / len) * speed * dt));
      firePlayer(dt);
    }

    if (!boss && !midBoss && !midBossSpawned && time >= midBossApproachTime()) {
      startMidBossDialog();
    }
    if (!boss && !midBoss && time > nextWave && (time < 44 || (midBossDefeated && time < bossReadyTime - 2))) {
      spawnWave();
      if (midBossDefeated) spawnPostMidBossWave();
      nextWave = time + (midBossDefeated ? 2.25 : Math.max(2.6, 4.5 - time * 0.025));
    }
    if (!boss && !midBoss && midBossDefeated && time >= bossReadyTime) {
      if (!bossIntroShown) startBossDialog();
      else startBoss();
    }

    updateEnemies(dt);
    updateMidBoss(dt);
    updateBoss(dt);
    updateStage4AttackFx(dt);
    updateStage5AttackFx(dt);
    updateStage6AttackFx(dt);
    updateStage7AttackFx(dt);
    updateStage8AttackFx(dt);
    updateStage9AttackFx(dt);
    updateLasers(dt);
    moveBullets(dt);
    updateParticles(dt);
    updatePowerUps(dt);
    collisions();
    cleanup();
    checkScoreRewards();
    updateHud();
  }

  function updateEnemies(dt) {
    for (const e of enemies) {
      e.t += dt;
      const stats = enemyStats[e.type] || enemyStats.drone;
      e.x += (e.vx + Math.sin(e.t * 3) * stats.sway) * dt;
      e.y += e.vy * dt;
      if (e.type === "interceptor") e.vx += Math.sin(e.t * 5.4) * 46 * dt;
      if (e.type === "shield") e.y += Math.sin(e.t * 2.1) * 12 * dt;
      e.fire -= dt;
      e.shieldFlash = Math.max(0, (e.shieldFlash || 0) - dt);
      e.laserCharge = Math.max(0, (e.laserCharge || 0) - dt);
      if (e.fire <= 0 && e.y > 20 && e.y < H - 120) {
        if (e.type === "stage9Vector") {
          e.fire = scaledDelay(0.78);
          stage9VectorFan(e, false);
        } else if (e.type === "stage9Orbiter") {
          e.fire = scaledDelay(1.52);
          stage9ConveyorDrones(e, false, 3);
          aimedBurst(e, 3, 0.34, 176, "#b78cff");
        } else if (e.type === "stage9Bastion") {
          e.fire = scaledDelay(1.96);
          stage9CorePulse(e, 1);
          if (Math.floor(e.t) % 2 === 0) stage9VectorGateWave(e, false);
        } else if (e.type === "stage8Switchback") {
          e.fire = scaledDelay(0.76);
          stage8SwitchbackVolley(e, false);
        } else if (e.type === "stage8Polarity") {
          e.fire = scaledDelay(1.42);
          stage8PolarityNodes(e, false);
          aimedBurst(e, 3, 0.34, 178, "#9cecff");
        } else if (e.type === "stage8Relay") {
          e.fire = scaledDelay(1.9);
          stage8RelayScatter(e, 1);
          if (Math.floor(e.t) % 2 === 0) stage8SwitchbackVolley(e, false);
        } else if (e.type === "stage7Sentinel") {
          e.fire = scaledDelay(0.78);
          stage7BladeFan(e, false);
        } else if (e.type === "stage7Breacher") {
          e.fire = scaledDelay(1.48);
          stage7AnchorCage(e, false);
          aimedBurst(e, 3, 0.34, 172, "#d7e9ff");
        } else if (e.type === "stage7Hive") {
          e.fire = scaledDelay(1.95);
          stage7DroneHalo(e, 1, true);
          ring(e, 9, 78, e.t, "#9d72ff", 4);
        } else if (e.type === "stage6Striker") {
          e.fire = scaledDelay(0.82);
          const base = Math.atan2(player.y - e.y, player.x - e.x);
          for (let i = -1; i <= 1; i++) spawnStage6Bullet(e.x, e.y + 20, 202, base + i * 0.16, "stage6Ricochet", { color: i ? "#ffb24f" : "#fff0a0", r: 4.8, hp: 55, bounces: 1 });
        } else if (e.type === "stage6Mortar") {
          e.fire = scaledDelay(1.5);
          stage6MortarBarrage(e, false, false);
        } else if (e.type === "stage6Carrier") {
          e.fire = scaledDelay(1.95);
          stage6RammingMines(e, false);
          aimedBurst(e, 5, 0.52, 168, "#ff8b55");
        } else if (e.type === "stage5Interceptor") {
          e.fire = scaledDelay(0.76);
          stage5NeonDashBurst(e, false);
        } else if (e.type === "stage5Jammer") {
          e.fire = scaledDelay(1.38);
          stage5JammerField(e, false);
          aimedBurst(e, 3, 0.3, 154, "#67f4ff");
        } else if (e.type === "stage5Gunship") {
          e.fire = scaledDelay(1.82);
          stage5GunshipSweep(e, false);
          aimedBurst(e, 5, 0.48, 168, "#ff6684");
        } else if (e.type === "stage4Interceptor") {
          e.fire = scaledDelay(0.82);
          aimedBurst(e, scaledCount(4, 2), 0.42, 188, "#73d9ff");
        } else if (e.type === "stage4Gunship") {
          e.fire = scaledDelay(1.45);
          stage4MissileVolley(e, false, Math.sin(e.t) * 0.1);
        } else if (e.type === "stage4Bomber") {
          e.fire = scaledDelay(1.85);
          stage4FlakField(e, false);
          aimedBurst(e, 3, 0.32, 150, "#f4d36b");
        } else if (e.type === "starScout") {
          e.fire = scaledDelay(0.92);
          stage2ContainmentFan(e, 5, "#9fdcff");
        } else if (e.type === "prism") {
          e.fire = scaledDelay(1.32);
          stage2StarRing(e, 10, 92, e.t * 1.8, "#ffd7f4");
          aimedBurst(e, 3, 0.24, 150, "#fff2a6");
        } else if (e.type === "supplyCarrier") {
          e.fire = scaledDelay(1.68);
          stage2StarRing(e, 12, 70, -e.t, "#b8f7ff");
          spawnEnemy("starScout", e.x + Math.sin(e.t) * 36, e.y - 16, (Math.random() - 0.5) * 44, 108);
        } else if (e.type === "fighter") {
          e.fire = scaledDelay(1.15);
          aimedBurst(e, 5, 0.7, 165, "#ff6cc8");
          ring(e, 10, 92, e.t, "#9fe8ff", 4);
        } else if (e.type === "laser") {
          e.fire = scaledDelay(2.8);
          e.laserCharge = 0.78;
          e.laserReady = true;
          const angle = Math.atan2(player.y - e.y, player.x - e.x);
          spawnLaser(e.x, e.y + 12, angle, 880, 6, { kind: "enemy", damage: 12, color: "#67f4ff", warmup: 0.62 });
        } else if (e.type === "interceptor") {
          e.fire = scaledDelay(0.72);
          crossBurst(e, 188, "#ffb86a");
        } else if (e.type === "shield") {
          e.fire = scaledDelay(1.05);
          spiralBurst(e, 14, 88, "#d3a2ff");
          aimedBurst(e, 3, 0.28, 142, "#fff07a");
        } else if (e.type === "mineLayer") {
          e.fire = scaledDelay(1.25);
          mineShot(e);
          if (Math.floor(e.t * 2) % 2 === 0) ring(e, 8, 56, e.t, "#74ffd5", 4);
        } else if (e.type === "splitter") {
          e.fire = scaledDelay(1.05);
          splitterShot(e);
        } else {
          e.fire = scaledDelay(1.55);
          aimedBurst(e, 3, 0.36, 180, "#ffde6a");
        }
      }
    }
  }
  function updateBoss(dt) {
    if (!boss) return;
    if (currentStage === 9) { updateStage9Boss(dt); return; }
    if (currentStage === 8) { updateStage8Boss(dt); return; }
    if (currentStage === 7) { updateStage7Boss(dt); return; }
    if (currentStage === 6) { updateStage6Boss(dt); return; }
    if (currentStage === 5) { updateStage5Boss(dt); return; }
    if (currentStage === 4) { updateStage4Boss(dt); return; }
    if (currentStage === 3) { updateStage3Boss(dt); return; }
    if (currentStage === 2) { updateStage2Boss(dt); return; }
    boss.t += dt;
    boss.y += (90 - boss.y) * Math.min(1, dt * 1.5);
    boss.x = W / 2 + Math.sin(boss.t * 0.72) * 95;
    boss.fireA -= dt;
    boss.fireB -= dt;
    boss.fireC -= dt;
    if (!bossHalfDialogShown && boss.hp / boss.maxHp <= 0.5) {
      startBossHalfDialog();
      return;
    }
    const phase = boss.hp / boss.maxHp < 0.48 ? 2 : 1;
    if (boss.fireA <= 0) {
      boss.fireA = scaledDelay(phase === 1 ? 0.42 : 0.28);
      const offset = boss.t * (phase === 1 ? 1.4 : -2.2);
      ring(boss, phase === 1 ? 18 : 24, phase === 1 ? 108 : 128, offset, phase === 1 ? "#ff5bb8" : "#f7d35d", phase === 1 ? 5 : 4);
    }
    if (boss.fireB <= 0) {
      boss.fireB = scaledDelay(phase === 1 ? 1.4 : 0.95);
      aimedBurst({ x: boss.x - 48, y: boss.y + 26 }, 6, 0.8, 210, "#7df4ff");
      aimedBurst({ x: boss.x + 48, y: boss.y + 26 }, 6, 0.8, 210, "#7df4ff");
    }
    if (boss.fireC <= 0) {
      boss.fireC = scaledDelay(phase === 1 ? 4.2 : 3.1);
      const y = boss.y + 58;
      const offset = phase === 1 ? 46 : 58;
      const width = phase === 1 ? 17 : 22;
      spawnLaser(boss.x - offset, y, Math.PI / 2, 930, width, { kind: "boss", damage: 35, color: "#ff4fd8", warmup: 0.52 });
      spawnLaser(boss.x + offset, y, Math.PI / 2, 930, width, { kind: "boss", damage: 35, color: "#ff4fd8", warmup: 0.52 });
    }
  }

  function updateLasers(dt) {
    for (const l of lasers) {
      l.life -= dt;
      l.x += Math.cos(l.angle) * 10 * dt;
      l.y += Math.sin(l.angle) * 10 * dt;
    }
  }
  function moveBullets(dt) {
    for (const b of playerBullets) {
      b.x += b.vx * dt;
      b.y += b.vy * dt;
    }
    for (const b of enemyBullets) {
      b.age = (b.age || 0) + dt;
      if (b.kind === "stage9Drone") {
        const turnRate = 5.2;
        if ((b.orbitTravel || 0) < Math.PI * 2) {
          b.orbitTravel = (b.orbitTravel || 0) + turnRate * dt;
          b.orbitAngle += (b.spinDir || 1) * turnRate * dt;
          const targetX = (b.centerX || b.x) + Math.cos(b.orbitAngle) * (b.orbitRadius || 82);
          const targetY = (b.centerY || b.y) + Math.sin(b.orbitAngle) * (b.orbitRadius || 82) * 0.5;
          b.vx = (targetX - b.x) * 7.4;
          b.vy = (targetY - b.y) * 7.4;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
            stage9AttackFx.push({ type: "dash", x: b.x, y: b.y, t: 0, life: 0.42, color: b.color, angle: b.dashAngle });
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(330), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(860) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
        if (b.age > 3.8) b.dead = true;
      } else if (b.kind === "stage9Core") {
        if (b.y < (b.targetY || 270) && b.age < (b.burstAt || 1.9)) {
          b.vy = Math.max(18, ((b.targetY || 270) - b.y) * 2.2);
          b.vx *= 0.9;
        } else {
          b.vx *= 0.88;
          b.vy *= 0.82;
        }
        if (!b.burstDone && b.age >= (b.burstAt || 1.9)) {
          b.burstDone = true;
          b.dead = true;
          const phase = b.phase || 1;
          const count = scaledCount(phase === 2 ? 24 : 18, 13);
          const gapAngle = Math.atan2(player.y - b.y, player.x - b.x);
          for (let i = 0; i < count; i++) {
            const a = i * Math.PI * 2 / count + b.age * 0.34;
            const gap = Math.abs(Math.atan2(Math.sin(a - gapAngle), Math.cos(a - gapAngle)));
            if (gap < 0.24) continue;
            spawnStage9Bullet(b.x, b.y, phase === 2 ? 196 : 164, a, "stage9Arc", {
              color: i % 3 === 0 ? "#ff8ad8" : (i % 2 ? "#73f0ff" : "#b78cff"),
              r: phase === 2 ? 6 : 5.3,
              hp: phase === 2 ? 78 : 64,
              curve: i % 2 ? 0.26 : -0.26,
              damage: phase === 2 ? 26 : 21,
              force: true,
            });
          }
          stage9AttackFx.push({ type: "coreBurst", x: b.x, y: b.y, t: 0, life: 0.72, color: b.color });
          explode(b.x, b.y, b.color, 22);
        }
      } else if (b.kind === "stage9Curve" || b.kind === "stage9Arc") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
        if (b.age > 4.6) b.dead = true;
      } else if (b.kind === "stage9Debris") {
        b.vx += Math.sin(b.age * 6.4 + b.id) * scaledEnemySpeed(18) * dt * (b.wobble || 1);
        b.vy += scaledEnemySpeed(15) * dt;
        if (b.age > 4.5) b.dead = true;
      } else if (b.kind === "stage8Beacon" || b.kind === "stage8Freight") {
        const target = b.route?.[b.routeIndex || 0];
        if (!target) {
          b.dead = true;
        } else {
          const dx = target.x - b.x;
          const dy = target.y - b.y;
          const dist = Math.hypot(dx, dy);
          const speed = b.routeSpeed || scaledEnemySpeed(170);
          if (dist < Math.max(8, speed * dt * 1.2)) {
            b.x = target.x;
            b.y = target.y;
            b.routeIndex = (b.routeIndex || 0) + 1;
            if (b.kind === "stage8Beacon") {
              const burst = scaledCount(3, 2);
              for (let i = 0; i < burst; i++) {
                spawnStage8Bullet(b.x, b.y, 112 + i * 18, Math.PI / 2 + (i - (burst - 1) / 2) * 0.42, "stage8Signal", {
                  color: b.color, r: 4.2, hp: 42, damage: 14, curve: i % 2 ? 0.2 : -0.2,
                });
              }
            } else if (b.routeIndex >= 2 && !b.unloaded) {
              b.unloaded = true;
              const cargo = scaledCount(b.cargoCount || 5, 4);
              for (let i = 0; i < cargo; i++) {
                const a = Math.PI / 2 + (i - (cargo - 1) / 2) * 0.34;
                spawnStage8Bullet(b.x, b.y, 126 + (i % 2) * 28, a, "stage8Cargo", {
                  color: i % 2 ? "#ff9d4d" : "#9cecff", r: 5.2, hp: 54, damage: b.phase === 2 ? 21 : 17,
                  curve: i % 2 ? 0.14 : -0.14,
                });
              }
              stage8AttackFx.push({ type: "cargoBurst", x: b.x, y: b.y, t: 0, life: 0.55, color: b.color });
            }
            if (b.routeIndex >= b.route.length) b.dead = true;
          } else {
            b.vx = dx / Math.max(1, dist) * speed;
            b.vy = dy / Math.max(1, dist) * speed;
          }
        }
      } else if (b.kind === "stage8Signal" || b.kind === "stage8Cargo") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage7Eclipse") {
        if (b.age < (b.releaseAt || 1.1)) {
          b.orbitAngle += (b.spinDir || 1) * dt * 5.7;
          const centerX = b.center?.x ?? b.x;
          const centerY = (b.center?.y ?? b.y) + 22;
          const targetX = centerX + Math.cos(b.orbitAngle) * (b.orbitRadius || 86);
          const targetY = centerY + Math.sin(b.orbitAngle) * (b.orbitRadius || 86) * 0.48;
          b.vx = (targetX - b.x) * 8;
          b.vy = (targetY - b.y) * 8;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(350), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(920) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
        if (b.age > 3.25) b.dead = true;
      } else if (b.kind === "stage7PawMine") {
        const targetY = b.targetY || 220;
        if (b.age < (b.stampAt || 2)) {
          b.vx = Math.sin(b.age * 4.4 + b.id) * 18 * (b.wobble || 1);
          b.vy = (targetY - b.y) * 2.4;
        } else if (!b.stamped) {
          b.stamped = true;
          const pattern = [{ x: 0, y: 0, r: 6.5 }, { x: -22, y: -17, r: 4.6 }, { x: -8, y: -29, r: 4.6 }, { x: 8, y: -29, r: 4.6 }, { x: 22, y: -17, r: 4.6 }];
          for (const part of pattern) {
            const angle = Math.atan2(player.y - b.y + part.y, player.x - b.x + part.x);
            spawnStage7Bullet(b.x + part.x, b.y + part.y, b.phase === 2 ? 188 : 158, angle, "stage7PawPellet", {
              color: b.color, r: part.r, hp: 58, damage: b.phase === 2 ? 22 : 18,
            });
          }
          stage7AttackFx.push({ type: "pawBurst", x: b.x, y: b.y, t: 0, life: 0.55, color: b.color });
          explode(b.x, b.y, b.color, 14);
          b.dead = true;
        }
      } else if (b.kind === "stage7Ink") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage7Scratch" || b.kind === "stage7PawPellet") {
        if (b.age > 4) b.dead = true;
      } else       if (b.kind === "stage8Flip") {
        if (!b.flipped && b.age >= (b.flipAt || 0.72)) {
          const angle = Math.atan2(player.y - b.y, player.x - b.x) + (b.flipBias || 0);
          const speed = Math.max(scaledEnemySpeed(205), Math.hypot(b.vx, b.vy) * 1.12);
          b.vx = Math.cos(angle) * speed;
          b.vy = Math.sin(angle) * speed;
          b.flipped = true;
          stage8AttackFx.push({ type: "flipBurst", x: b.x, y: b.y, t: 0, life: 0.34, color: b.color });
        }
      } else if (b.kind === "stage8PolarityNode") {
        if (b.age < (b.lockTime || 0.95)) {
          b.vx = ((b.targetX || b.x) - b.x) * 6.8;
          b.vy = ((b.targetY || b.y) - b.y) * 6.8;
        } else if (!b.burstDone) {
          b.burstDone = true;
          const count = scaledCount(b.burstCount || 6, 4);
          for (let i = 0; i < count; i++) {
            const a = i * Math.PI * 2 / count + b.age;
            spawnStage8Bullet(b.x, b.y, 176, a, "stage8Spark", { color: i % 2 ? "#ff9d4d" : "#9cecff", r: 4.6, hp: 62, damage: 18, curve: i % 2 ? 0.32 : -0.32 });
          }
          stage8AttackFx.push({ type: "nodeBurst", x: b.x, y: b.y, t: 0, life: 0.5, color: b.color });
          b.dead = true;
        }
      } else if (b.kind === "stage8Spark") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage8Rail") {
        if (b.age > 4.2) b.dead = true;
      } else if (b.kind === "stage7Anchor") {
        if (b.age < (b.lockTime || 0.9)) {
          b.vx = ((b.anchorX || b.x) - b.x) * 6.6;
          b.vy = ((b.anchorY || b.y) - b.y) * 6.6;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(270), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(680) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
        if (b.age > 3.4) b.dead = true;
      } else if (b.kind === "stage7Drone") {
        if (b.age < (b.releaseAt || 1.1)) {
          b.orbitAngle += (b.spinDir || 1) * dt * 4.2;
          const targetX = (b.centerX || b.x) + Math.cos(b.orbitAngle) * (b.orbitRadius || 110);
          const targetY = (b.centerY || b.y) + Math.sin(b.orbitAngle) * (b.orbitRadius || 110) * 0.48;
          b.vx = (targetX - b.x) * 6.2;
          b.vy = (targetY - b.y) * 6.2;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(270), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(740) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
        if (b.age > 3.8) b.dead = true;
      } else if (b.kind === "stage7Blade") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage7Shutter") {
        b.vy += scaledEnemySpeed(18) * dt;
        if (b.age > 4.6) b.dead = true;
      } else if (b.kind === "stage6Mine") {
        const armTime = b.armTime || 0.8;
        if (b.age < armTime) {
          b.orbitAngle += (b.spinDir || 1) * dt * 5.1;
          const targetX = (b.anchorX || b.x) + Math.cos(b.orbitAngle) * (b.orbitRadius || 78);
          const targetY = (b.anchorY || b.y) + Math.sin(b.orbitAngle) * (b.orbitRadius || 78) * 0.44;
          b.vx = (targetX - b.x) * 6.1;
          b.vy = (targetY - b.y) * 6.1;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(280), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(760) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
        if (b.age > 3.4) b.dead = true;
      } else if (b.kind === "stage6Drone") {
        if (!boss || currentStage !== 6) {
          b.dead = true;
        } else {
          b.orbitAngle += dt * (b.phase === 2 ? 1.25 : 0.96);
          const targetX = boss.x + Math.cos(b.orbitAngle) * b.orbitRadius;
          const targetY = boss.y + 34 + Math.sin(b.orbitAngle) * b.orbitRadius * 0.52;
          b.vx = (targetX - b.x) * 6.4;
          b.vy = (targetY - b.y) * 6.4;
          if (b.age >= (b.nextFire || 0.9)) {
            b.nextFire += b.phase === 2 ? 0.58 : 0.76;
            const base = Math.atan2(player.y - b.y, player.x - b.x);
            const count = scaledCount(b.phase === 2 ? 5 : 3, 2);
            for (let i = 0; i < count; i++) spawnStage6Bullet(b.x, b.y, b.phase === 2 ? 214 : 184, base + (i - (count - 1) / 2) * 0.17, "stage6Shard", { color: b.color, r: 4.4, hp: 44 });
          }
          if (b.age > (b.lifeTime || 5)) b.dead = true;
        }
      } else if (b.kind === "stage6Core") {
        b.vx *= 0.982;
        b.vy *= 0.992;
      } else if (b.kind === "stage6Ricochet") {
        b.vx += Math.sin(b.age * 7.5 + b.id) * scaledEnemySpeed(7) * dt;
      } else if (b.kind === "stage5Arc") {
        const speed = Math.hypot(b.vx, b.vy);
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage5JammerNode") {
        if (b.age < (b.fuse || 0.9)) {
          b.vx = ((b.anchorX || b.x) - b.x) * 4.8;
          b.vy = ((b.anchorY || b.y) - b.y) * 4.8;
        } else {
          b.vx = Math.sin(b.age * 3 + b.id) * 8;
          b.vy = Math.cos(b.age * 2.4 + b.id) * 6;
          b.nextPulse = b.nextPulse ?? b.fuse;
          b.pulseIndex = b.pulseIndex || 0;
          if (b.pulseIndex < 3 && b.age >= b.nextPulse) {
            b.pulseIndex += 1;
            b.nextPulse += 0.62;
            ring(b, b.pulseCount || 8, 96 + b.pulseIndex * 14, b.age + b.pulseIndex * 0.22, b.color, 4);
            if (b.pulseIndex === 2) aimedBurst(b, 5, 0.55, 176, "#ffffff");
          }
        }
        if (b.age > (b.lifeTime || 2.7)) b.dead = true;
      } else if (b.kind === "stage5RoadOrb") {
        b.vx += Math.sin(b.age * 4 + b.id) * scaledEnemySpeed(12) * dt;
      } else if (b.kind === "stage5CrownNode") {
        if (!boss || currentStage !== 5) {
          b.dead = true;
        } else {
          b.orbitAngle += dt * (b.phase === 2 ? 1.15 : 0.88);
          const targetX = boss.x + Math.cos(b.orbitAngle) * b.orbitRadius;
          const targetY = boss.y + 24 + Math.sin(b.orbitAngle) * b.orbitRadius * 0.5;
          b.vx = (targetX - b.x) * 6.2;
          b.vy = (targetY - b.y) * 6.2;
          if (b.age >= (b.nextFire || 0.9)) {
            b.nextFire += b.phase === 2 ? 0.62 : 0.78;
            aimedBurst(b, b.phase === 2 ? 6 : 4, b.phase === 2 ? 0.56 : 0.42, b.phase === 2 ? 210 : 184, b.color);
            spawnStage5PatternBullet(b.x, b.y, b.phase === 2 ? 148 : 126, b.orbitAngle + Math.PI / 2, "stage5Arc", { color: "#ffdf9a", r: 4, hp: 40, curve: 0.22 });
          }
          if (b.age > (b.lifeTime || 4.8)) b.dead = true;
        }
      } else if (b.kind === "stage5PulseDrop") {
        if (b.age >= (b.delay || 0.3)) {
          b.vy = Math.min(scaledEnemySpeed(320), b.vy + scaledEnemySpeed(170) * dt);
          b.vx += Math.sin(b.age * 7 + b.id) * scaledEnemySpeed(18) * dt;
        } else {
          b.vy *= 0.94;
        }
      } else if (b.kind === "stage4ScissorJet") {
        if (!b.released && b.age >= (b.fireAt || 0.68)) {
          b.released = true;
          fireStage4ScissorJet(b);
        }
        if (b.age > 2.65) b.dead = true;
      } else if (b.kind === "stage4WingShot") {
        b.vx += Math.sin(b.age * 7 + b.id) * scaledEnemySpeed(8) * dt;
      } else if (b.kind === "stage4SiegeTurret") {
        b.recoil = Math.max(0, (b.recoil || 0) - dt * 3.2);
        const deploy = Math.max(0.01, b.deployTime || 0.7);
        const settle = Math.max(0, Math.min(1, b.age / deploy));
        const hoverX = (b.anchorX || b.x) + Math.sin(b.age * 2.1 + b.turretIndex) * 5;
        const hoverY = (b.anchorY || b.y) + Math.cos(b.age * 1.8 + b.turretIndex) * 4;
        const pull = settle < 1 ? 6.6 : 4.8;
        b.vx = (hoverX - b.x) * pull;
        b.vy = (hoverY - b.y) * pull;
        if (!b.fired && b.age >= (b.fireAt || 1)) {
          b.fired = true;
          fireStage4SiegeTurret(b, false);
        }
        if (b.age > (b.lifeTime || 5.3)) {
          b.dead = true;
          explode(b.x, b.y, b.color, 10);
        }
      } else if (b.kind === "stage4SiegeShot") {
        const angle = Math.atan2(b.vy, b.vx) + (b.curve || 0) * dt;
        const speed = Math.hypot(b.vx, b.vy);
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage4Missile") {
        const current = Math.atan2(b.vy, b.vx);
        let desired = current;
        if (b.age < (b.trackingTime || 0.72)) desired = Math.atan2(player.y - b.y, player.x - b.x);
        let diff = ((desired - current + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
        const maxTurn = (b.turnRate || 1.6) * dt;
        diff = Math.max(-maxTurn, Math.min(maxTurn, diff));
        const angle = current + diff;
        const speed = Math.min(b.maxSpeed || scaledEnemySpeed(255), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(145) * dt);
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      } else if (b.kind === "stage3ShadowDrone") {
        const armTime = b.armTime || 1.05;
        if (b.age < armTime) {
          b.orbitAngle += (b.spinDir || 1) * dt * 4.6;
          const t = Math.max(0, Math.min(1, b.age / armTime));
          const targetX = (b.anchorX || b.x) + Math.cos(b.orbitAngle) * (b.orbitRadius || 72);
          const targetY = (b.anchorY || b.y) + Math.sin(b.orbitAngle) * ((b.orbitRadius || 72) * 0.42) + t * 28;
          b.vx = (targetX - b.x) * 5.6;
          b.vy = (targetY - b.y) * 5.6;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(250), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(700) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
      } else if (b.kind === "stage3WingShard") {
        b.vx += Math.sin(b.age * 6 + b.id) * 10 * dt;
      } else       if (b.kind === "stage3FlowerBit") {
        const armTime = b.armTime || 0.55;
        const spinTime = b.spinTime || 0.72;
        const current = Math.atan2(b.vy, b.vx);
        if (b.age < spinTime) {
          const t = Math.max(0, Math.min(1, b.age / spinTime));
          const spinAngle = (b.baseAngle ?? current) + (b.spinDir || 1) * Math.PI * 2 * t;
          const swirlSpeed = scaledEnemySpeed(58 + 26 * Math.sin(t * Math.PI));
          const sinkSpeed = scaledEnemySpeed(22 + 24 * t);
          b.vx = Math.cos(spinAngle) * swirlSpeed;
          b.vy = Math.sin(spinAngle) * swirlSpeed + sinkSpeed;
        } else {
          if (!b.dashLocked) {
            b.dashAngle = Math.atan2(player.y - b.y, player.x - b.x);
            b.dashLocked = true;
          }
          const speed = Math.min(b.dashSpeed || scaledEnemySpeed(245), Math.hypot(b.vx, b.vy) + scaledEnemySpeed(620) * dt);
          b.vx = Math.cos(b.dashAngle) * speed;
          b.vy = Math.sin(b.dashAngle) * speed;
        }
      } else if (b.kind === "stage2EnergyOrb") {
        b.vx += Math.sin(b.age * 4 + b.id) * 18 * dt;
        if (b.y >= (b.burstY || H * 0.5)) {
          burstStage2EnergyOrb(b);
          continue;
        }
      } else if (b.kind === "carrot") {
        const armed = b.age >= (b.armTime || 0.55);
        const speed = Math.min(b.maxSpeed || 205, Math.hypot(b.vx, b.vy) + (armed ? 34 : 16) * dt);
        const current = Math.atan2(b.vy, b.vx);
        let desired;
        if (armed) {
          desired = Math.atan2(player.y - b.y, player.x - b.x);
        } else {
          const driftY = 135 + Math.sin(b.id * 1.7) * 54;
          const driftX = W / 2 + (b.spreadSide || 1) * (245 + Math.sin(b.id) * 64);
          desired = Math.atan2(driftY - b.y, driftX - b.x);
        }
        let diff = ((desired - current + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
        const turnBoost = armed ? 1.75 : 0.62;
        const maxTurn = (b.turnRate || 1.35) * turnBoost * dt;
        diff = Math.max(-maxTurn, Math.min(maxTurn, diff));
        const angle = current + diff;
        b.vx = Math.cos(angle) * speed;
        b.vy = Math.sin(angle) * speed;
      }
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      if (b.kind === "stage6Shell") {
        if (b.age >= (b.fuse || 1)) burstStage6Shell(b, !!b.heavy);
      } else if (b.kind === "stage6Ricochet") {
        if (b.x < 12 && b.vx < 0 && b.bounces > 0) { b.x = 12; b.vx = Math.abs(b.vx); b.bounces -= 1; }
        if (b.x > W - 12 && b.vx > 0 && b.bounces > 0) { b.x = W - 12; b.vx = -Math.abs(b.vx); b.bounces -= 1; }
        if (b.y < 44 && b.vy < 0 && b.bounces > 0) { b.y = 44; b.vy = Math.abs(b.vy); b.bounces -= 1; }
      } else if (b.kind === "stage6Core") {
        if (b.age >= (b.fuse || 1.5)) burstStage6Core(b);
      } else if (b.kind === "stage6Cluster") {
        if (b.age >= (b.fuse || 1)) {
          b.dead = true;
          const count = scaledCount(b.burstCount || 8, 5);
          const base = Math.atan2(player.y - b.y, player.x - b.x);
          for (let i = 0; i < count; i++) spawnStage6Bullet(b.x, b.y, 136 + (i % 2) * 44, base + (i - (count - 1) / 2) * 0.22, "stage6Shard", { color: i % 2 ? "#ff5ab8" : "#ffb047", r: 4.8, hp: 48 });
          stage6AttackFx.push({ type: "shock", x: b.x, y: b.y, t: 0, life: 0.48, color: b.color });
          explode(b.x, b.y, b.color, 18);
        }
      } else if (b.kind === "stage5RoadOrb") {
        if (b.y >= (b.burstY || H * 0.5)) burstStage5RoadOrb(b);
      } else if (b.kind === "stage4Flak") {
        b.vx *= 0.995;
        b.vy *= 0.995;
        if (b.age > b.fuse) burstStage4Flak(b);
      } else if (b.kind === "stage3PollenMine") {
        b.vx *= 0.988;
        b.vy *= 0.988;
        if (b.age > b.fuse) {
          b.dead = true;
          const burstCount = b.burstCount || 8;
          for (let i = 0; i < burstCount; i++) {
            enemyShot(b.x, b.y, 118, i * Math.PI * 2 / burstCount + b.age * 0.8, i % 2 ? "#ffd76a" : "#ff9bd7", 4.2, 36);
          }
          explode(b.x, b.y, b.color || "#ffd76a", 12);
        }
      } else if (b.kind === "mine") {
        b.vx *= 0.992;
        b.vy *= 0.992;
        if (b.age > b.fuse) {
          b.dead = true;
          for (let i = 0; i < 14; i++) enemyShot(b.x, b.y, 112, i * Math.PI * 2 / 14 + b.age, "#74ffd5", 4, 32);
          explode(b.x, b.y, "#74ffd5", 10);
        }
      } else if (b.kind === "splitter" && b.age > b.fuse) {
        b.dead = true;
        const base = Math.atan2(player.y - b.y, player.x - b.x);
        for (let i = -2; i <= 2; i++) enemyShot(b.x, b.y, 155, base + i * 0.32, "#ff8f63", 4, 34);
        explode(b.x, b.y, "#ffb48a", 6);
      }
    }
  }

  function updateParticles(dt) {
    for (const p of particles) {
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.life -= dt;
      p.vx *= 0.98;
      p.vy *= 0.98;
    }
  }


  function startDestructionScene(kind, source, nextAction) {
    if (currentStage === 9) stage9AttackFx = [];
    if (currentStage === 8) stage8AttackFx = [];
    if (currentStage === 7) stage7AttackFx = [];
    if (currentStage === 6) stage6AttackFx = [];
    if (currentStage === 5) stage5AttackFx = [];
    if (currentStage === 4) stage4AttackFx = [];
    const isBoss = kind === "boss";
    const stage = currentStage;
    const size = stage === 9 ? (isBoss ? 312 : 238) : stage === 8 ? (isBoss ? 296 : 226) : stage === 7 ? (isBoss ? 282 : 218) : stage === 6 ? (isBoss ? 262 : 204) : stage === 5 ? (isBoss ? 246 : 186) : stage === 4 ? (isBoss ? 236 : 174) : stage === 3 ? (isBoss ? 228 : 168) : stage === 2 ? (isBoss ? 214 : 150) : (isBoss ? 188 : 104);
    destructionScene = {
      kind,
      stage,
      x: source.x,
      y: source.y,
      t: 0,
      duration: isBoss ? 2.45 : 1.85,
      nextAction,
      burst: 0,
      flashAt: 0,
    };
    wreckPieces = [];
    const count = isBoss ? 9 : 6;
    for (let i = 0; i < count; i++) {
      const a = -Math.PI / 2 + (i - (count - 1) / 2) * (isBoss ? 0.34 : 0.42) + (Math.random() - 0.5) * 0.28;
      const speed = (isBoss ? 62 : 78) + Math.random() * (isBoss ? 110 : 90);
      wreckPieces.push({
        kind,
        stage,
        x: source.x + (Math.random() - 0.5) * size * 0.45,
        y: source.y + (Math.random() - 0.5) * size * 0.35,
        vx: Math.cos(a) * speed,
        vy: Math.sin(a) * speed + (isBoss ? 66 : 48),
        rot: (Math.random() - 0.5) * 1.2,
        vr: (Math.random() - 0.5) * 3.8,
        size: size * (0.26 + Math.random() * 0.22),
        alpha: 1,
        clipX: Math.random(),
        clipY: Math.random(),
      });
    }
    explode(source.x, source.y, isBoss ? "#ffffff" : "#ffb6df", isBoss ? 120 : 78);
    explode(source.x, source.y + (isBoss ? 42 : 22), isBoss ? "#ff5be9" : "#8ff6ff", isBoss ? 76 : 48);
    flash = Math.max(flash, isBoss ? 0.32 : 0.22);
    shake = Math.max(shake, isBoss ? 0.62 : 0.44);
    playSound("enemyHit", isBoss ? 1.6 : 1.25, 0);
  }

  function updateDestructionScene(dt) {
    if (!destructionScene) return;
    destructionScene.t += dt;
    destructionScene.burst -= dt;
    destructionScene.flashAt -= dt;
    if (destructionScene.burst <= 0) {
      destructionScene.burst = destructionScene.kind === "boss" ? 0.13 : 0.16;
      const radius = destructionScene.kind === "boss" ? 118 : 62;
      const bx = destructionScene.x + (Math.random() - 0.5) * radius * 1.8;
      const by = destructionScene.y + (Math.random() - 0.5) * radius * 1.3 + destructionScene.t * 12;
      explode(bx, by, Math.random() > 0.5 ? "#ffffff" : "#ff77db", destructionScene.kind === "boss" ? 34 : 22);
      metalHit(bx, by, destructionScene.kind === "boss" ? 1.7 : 1.3, "enemyHit");
      shake = Math.max(shake, destructionScene.kind === "boss" ? 0.42 : 0.26);
    }
    if (destructionScene.flashAt <= 0) {
      destructionScene.flashAt = 0.36 + Math.random() * 0.18;
      flash = Math.max(flash, 0.08);
    }
    for (const p of wreckPieces) {
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 42 * dt;
      p.rot += p.vr * dt;
      p.vx *= 0.992;
      p.vy *= 0.992;
      p.alpha = Math.max(0, 1 - destructionScene.t / destructionScene.duration);
    }
    updateLasers(dt);
    moveBullets(dt);
    updateParticles(dt);
    cleanup();
    if (destructionScene.t >= destructionScene.duration) {
      const next = destructionScene.nextAction;
      destructionScene = null;
      wreckPieces = [];
      if (next === "midBossDefeat") startDefeatChoice("mid");
      if (next === "bossDefeat") startDefeatChoice("boss");
    }
  }

  function collectPowerUp(p) {
    p.dead = true;
    if (p.type === "heal") {
      const before = player.hp;
      player.hp = Math.min(player.maxHp, player.hp + (p.value || 45));
      score += 1000;
      flash = Math.max(flash, 0.18);
      playSound("powerup", 1.15, 0.05);
      particles.push({ x: player.x, y: player.y - 34, vx: 0, vy: -38, life: 0.95, maxLife: 0.95, color: "#ffef8a", type: "floatingText", text: `HP +${Math.ceil(player.hp - before)}`, size: 22 });
      explode(player.x, player.y - 18, "#ffef8a", 32);
      updateHud();
      return;
    }
    if (player.power >= 5) {
      player.powerStock += p.value;
      score += 450 * p.value;
      while (player.powerStock >= 30) {
        player.powerStock -= 30;
        if (player.bombs < 50) player.bombs += 1;
        flash = Math.max(flash, 0.2);
        particles.push({ x: player.x, y: player.y - 26, vx: 0, vy: -34, life: 0.9, maxLife: 0.9, color: "#7dffcf", type: "floatingText", text: "BOMB +1", size: 20 });
        explode(player.x, player.y - 28, "#7dffcf", 34);
      }
    } else {
      player.power = Math.min(5, player.power + p.value);
      score += 300 * p.value;
    }
    flash = Math.max(flash, 0.08);
    playSound("powerup", 1, 0.08);
    explode(player.x, player.y - 16, "#7dffcf", 12 + p.value * 4);
    updateHud();
  }
  function updatePowerUps(dt) {
    for (const p of powerUps) {
      p.t += dt;
      if (p.type === "heal") {
        p.vy = Math.min(92, p.vy + 42 * dt);
        p.x += p.vx * dt + Math.sin(p.t * 2.4) * 10 * dt;
      } else {
        p.vy = Math.min(118, p.vy + 150 * dt);
        p.x += p.vx * dt + Math.sin(p.t * 5) * 18 * dt;
      }
      p.y += p.vy * dt;
      if (dist(p, player) < p.r + player.r + 12) collectPowerUp(p);
    }
  }
  function collisions() {
    const bulletCellSize = 48;
    const enemyBulletGrid = new Map();
    const bulletCellKey = (x, y) => x * 64 + y;
    for (const eb of enemyBullets) {
      if (eb.dead || eb.indestructible) continue;
      const key = bulletCellKey(Math.floor(eb.x / bulletCellSize), Math.floor(eb.y / bulletCellSize));
      let cell = enemyBulletGrid.get(key);
      if (!cell) enemyBulletGrid.set(key, cell = []);
      cell.push(eb);
    }

    // Enemy bodies use the same spatial partition as enemy bullets, avoiding
    // a full player-bullet × enemy scan every frame.
    const enemyGrid = new Map();
    for (const enemy of enemies) {
      if (enemy.dead) continue;
      const key = bulletCellKey(Math.floor(enemy.x / bulletCellSize), Math.floor(enemy.y / bulletCellSize));
      let cell = enemyGrid.get(key);
      if (!cell) enemyGrid.set(key, cell = []);
      cell.push(enemy);
    }

    for (const b of playerBullets) {
      if (b.dead) continue;
      const cx = Math.floor(b.x / bulletCellSize);
      const cy = Math.floor(b.y / bulletCellSize);
      let enemyBulletHit = false;
      for (let oy = -1; oy <= 1 && !enemyBulletHit; oy++) {
        for (let ox = -1; ox <= 1 && !enemyBulletHit; ox++) {
          const cell = enemyBulletGrid.get(bulletCellKey(cx + ox, cy + oy));
          if (!cell) continue;
          for (const eb of cell) {
            if (!eb.dead && overlaps(b, eb, 2) && !b.hitEnemyBulletIds?.has(eb.id)) {
              (b.hitEnemyBulletIds ||= new Set()).add(eb.id);
              eb.hp -= b.damage;
              metalHit(b.x, b.y, 0.55, "bulletHit");
              if (eb.hp <= 0) {
                eb.dead = true;
                score += 12;
                explode(eb.x, eb.y, eb.color, 8);
              }
              enemyBulletHit = true;
              break;
            }
          }
        }
      }
      const nearbyEnemies = [];
      // Some large ships overlap more than one adjacent 48px cell.
      for (let oy = -2; oy <= 2; oy++) {
        for (let ox = -2; ox <= 2; ox++) {
          const cell = enemyGrid.get(bulletCellKey(cx + ox, cy + oy));
          if (cell) nearbyEnemies.push(...cell);
        }
      }
      for (const e of nearbyEnemies) {
        if (overlaps(b, e)) {
          b.dead = true;
          if (e.type === "laser" && !e.laserReady) {
            e.shieldFlash = 0.18;
            metalHit(b.x, b.y, 1.2, "enemyHit");
            break;
          }
          e.hp -= b.damage;
          metalHit(b.x, b.y, e.type === "laser" ? 1.5 : e.type === "fighter" ? 1.4 : 1, "enemyHit");
          if (e.hp <= 0 && !e.dead) {
            e.dead = true;
            recordCrewCasualty(e.type);
            score += { stage9Bastion: 2850, stage9Orbiter: 1480, stage9Vector: 740, stage8Relay: 2500, stage8Polarity: 1320, stage8Switchback: 680, stage7Hive: 2200, stage7Breacher: 1180, stage7Sentinel: 620, stage6Carrier: 1900, stage6Mortar: 1050, stage6Striker: 560, stage5Gunship: 1500, stage5Jammer: 880, stage5Interceptor: 480, stage4Bomber: 1250, stage4Gunship: 820, stage4Interceptor: 440, supplyCarrier: 980, prism: 620, starScout: 360, laser: 900, shield: 1100, mineLayer: 760, splitter: 420, interceptor: 360, fighter: 650 }[e.type] || 220;
            explode(e.x, e.y, e.type === "stage9Bastion" ? "#ff8ad8" : e.type === "stage9Orbiter" ? "#b78cff" : e.type === "stage9Vector" ? "#73f0ff" : e.type === "stage8Relay" ? "#ff9d4d" : e.type === "stage8Polarity" ? "#9cecff" : e.type === "stage8Switchback" ? "#dff7ff" : e.type === "stage7Hive" ? "#9d72ff" : e.type === "stage7Breacher" ? "#d7e9ff" : e.type === "stage7Sentinel" ? "#7eeeff" : e.type === "stage6Carrier" ? "#ff5ab8" : e.type === "stage6Mortar" ? "#ffb24f" : e.type === "stage6Striker" ? "#ff9b48" : e.type === "stage5Gunship" ? "#ff596f" : ["stage5Jammer", "stage5Interceptor"].includes(e.type) ? "#b98cff" : e.type === "stage4Bomber" ? "#f4d36b" : ["stage4Gunship", "stage4Interceptor"].includes(e.type) ? "#73d9ff" : ["starScout", "prism", "supplyCarrier"].includes(e.type) ? "#9fdcff" : e.type === "laser" ? "#ff7ee7" : e.type === "fighter" ? "#d38cff" : "#ff7777", e.type === "stage9Bastion" ? 48 : e.type === "stage9Orbiter" ? 36 : e.type === "stage9Vector" ? 27 : e.type === "stage8Relay" ? 46 : e.type === "stage8Polarity" ? 34 : e.type === "stage8Switchback" ? 26 : e.type === "stage7Hive" ? 44 : e.type === "stage7Breacher" ? 32 : e.type === "stage7Sentinel" ? 25 : e.type === "stage6Carrier" ? 42 : e.type === "stage6Mortar" ? 31 : e.type === "stage6Striker" ? 24 : e.type === "stage5Gunship" ? 38 : e.type === "stage5Jammer" ? 28 : e.type === "stage4Bomber" ? 34 : e.type === "stage4Gunship" ? 26 : e.type === "supplyCarrier" ? 30 : e.type === "laser" ? 30 : e.type === "fighter" ? 24 : 14);
            if (Math.random() < ({ stage9Bastion: 0.99, stage9Orbiter: 0.92, stage9Vector: 0.64, stage8Relay: 0.98, stage8Polarity: 0.9, stage8Switchback: 0.62, stage7Hive: 0.98, stage7Breacher: 0.88, stage7Sentinel: 0.58, stage6Carrier: 0.98, stage6Mortar: 0.86, stage6Striker: 0.56, stage5Gunship: 0.96, stage5Jammer: 0.82, stage5Interceptor: 0.52, stage4Bomber: 0.94, stage4Gunship: 0.78, stage4Interceptor: 0.48, supplyCarrier: 0.9, prism: 0.68, starScout: 0.45, laser: 0.95, shield: 0.95, mineLayer: 0.75, splitter: 0.55, interceptor: 0.42, fighter: 0.85 }[e.type] || 0.38)) spawnPowerUp(e.x, e.y, ["stage9Bastion", "stage9Orbiter", "stage8Relay", "stage8Polarity", "stage7Hive", "stage7Breacher", "stage6Carrier", "stage6Mortar", "stage5Gunship", "stage5Jammer", "stage4Bomber", "stage4Gunship", "supplyCarrier", "laser", "fighter", "shield", "mineLayer"].includes(e.type) ? 2 : 1);
          }
          break;
        }
      }
      if (midBoss && !b.dead && overlaps(b, midBoss)) {
        b.dead = true;
        midBoss.hp -= b.damage;
        metalHit(b.x, b.y, 1.45, "enemyHit");
        score += 5;
        if (Math.random() < 0.05) spawnPowerUp(b.x, b.y, 1);
        if (midBoss.hp <= 0) {
          score += 5000;
          const defeated = { x: midBoss.x, y: midBoss.y };
          for (let i = 0; i < 3; i++) spawnPowerUp(midBoss.x + (i - 1) * 34, midBoss.y + 16, 2);
          midBoss = null;
          midBossDefeated = true;
          state = "destruction";
          enemyBullets = enemyBullets.filter((eb) => eb.kind !== "carrot" || Math.random() < 0.18);
          nextWave = time + 2.8;
          bossReadyTime = time + bossApproachDelay();
          startDestructionScene("midBoss", defeated, "midBossDefeat");
        }
      }
      if (boss && !b.dead && overlaps(b, boss)) {
        b.dead = true;
        boss.hp -= b.damage;
        metalHit(b.x, b.y, 1.7, "enemyHit");
        if (Math.random() < 0.035) spawnPowerUp(b.x, b.y, 1);
        score += 4;
        if (boss.hp <= 0) {
          score += 12000;
          const defeated = { x: boss.x, y: boss.y };
          boss = null;
          state = "destruction";
          enemyBullets = [];
          lasers = [];
          powerUps = [];
    stopHealSiren();
          startDestructionScene("boss", defeated, "bossDefeat");
        }
      }
    }
    for (const p of powerUps) if (!p.dead && dist(p, player) < p.r + player.r + 12) collectPowerUp(p);
    for (const b of enemyBullets) if (!b.dead && overlaps(b, player)) hitPlayer(b.damage || 20);
    if (midBoss && overlaps(midBoss, player, 8)) hitPlayer(35);
    for (const l of lasers) {
      if (l.life < l.maxLife - l.warmup) {
        const ex = l.x + Math.cos(l.angle) * l.length;
        const ey = l.y + Math.sin(l.angle) * l.length;
        if (pointToSegmentDistance(player.x, player.y, l.x, l.y, ex, ey) < player.r + l.width * 0.38) hitPlayer(l.damage || 12);
      }
    }
    for (const e of enemies) if (overlaps(e, player, 6)) {
      e.dead = true;
      hitPlayer(35);
    }
  }

  function cleanup() {
    compactInPlace(playerBullets, (b) => !b.dead && b.y > -40 && b.x > -40 && b.x < W + 40);
    compactInPlace(enemyBullets, (b) => !b.dead && b.y > -70 && b.y < H + 70 && b.x > -70 && b.x < W + 70);
    compactInPlace(lasers, (l) => l.life > 0);
    compactInPlace(enemies, (e) => !e.dead && e.y < H + 80 && e.x > -90 && e.x < W + 90);
    compactInPlace(powerUps, (p) => !p.dead && p.y < H + 42 && p.x > -40 && p.x < W + 40);
    syncHealSiren();
    compactInPlace(particles, (p) => p.life > 0);
    if (particles.length > PARTICLE_HARD_LIMIT) particles.splice(0, particles.length - PARTICLE_HARD_LIMIT);
  }

  function draw() {
    const shakeX = shake > 0 ? (Math.random() - 0.5) * shake * 18 : 0;
    const shakeY = shake > 0 ? (Math.random() - 0.5) * shake * 18 : 0;
    if (shake > 0) ctx.translate(shakeX, shakeY);
    drawBackground();
    drawDestructionCore();
    drawWreckPieces();
    drawParticles();
    drawEnemies();
    drawMidBoss();
    drawBoss();
    drawStage4AttackFx();
    drawStage5AttackFx();
    drawStage6AttackFx();
    drawStage7AttackFx();
    drawStage8AttackFx();
    drawStage9AttackFx();
    drawBullets();
    drawLasers();
    drawPowerUps();
    drawPlayer();
    if (shake > 0) ctx.translate(-shakeX, -shakeY);

    drawHitOverlay();
    if (state === "alert") drawApproachAlert();
    if (state === "dialog") drawDialog();
    if (state === "defeatChoice") drawDefeatChoice();
    if (state === "killCutin") drawKillCutin();
    if (state === "killedPreview") drawKilledPreview();
    if (state === "interrogationSelect") drawInterrogationSelect();
    if (state === "interrogation") drawInterrogation();
    if (flash > 0) {
      ctx.save();
      ctx.globalAlpha = Math.min(0.86, flash * 1.05);
      ctx.fillStyle = "#dffcff";
      ctx.fillRect(0, 0, W, H);
      ctx.restore();
    }
  }


  function drawStage4GroundBackground() {
    ctx.fillStyle = "#17372e";
    ctx.fillRect(0, 0, W, H);
    const img = assets.stage4Background;
    if (img && img.complete && img.naturalWidth && img.naturalHeight) {
      const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight) * 1.34;
      const bw = img.naturalWidth * scale;
      const bh = img.naturalHeight * scale;
      const overflowX = Math.max(0, bw - W);
      const overflowY = Math.max(0, bh - H);
      const panX = (Math.sin(time * 0.09) * 0.5 + 0.5) * overflowX;
      const panY = (Math.sin(time * 0.045 - Math.PI / 2) * 0.5 + 0.5) * overflowY;
      ctx.drawImage(img, -panX, -panY, bw, bh);
    } else {
      const g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "#60877a");
      g.addColorStop(0.5, "#315b49");
      g.addColorStop(1, "#18392e");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, W, H);
    }
    ctx.save();
    const haze = ctx.createLinearGradient(0, 0, 0, H);
    haze.addColorStop(0, "rgba(184, 225, 235, 0.18)");
    haze.addColorStop(0.42, "rgba(255, 255, 255, 0.02)");
    haze.addColorStop(1, "rgba(4, 28, 23, 0.2)");
    ctx.fillStyle = haze;
    ctx.fillRect(0, 0, W, H);
    ctx.globalAlpha = 0.08;
    ctx.fillStyle = "#dff7ff";
    for (let i = 0; i < 6; i++) {
      const x = ((i * 137 + time * (8 + i)) % (W + 180)) - 90;
      const y = 80 + i * 116 + Math.sin(time * 0.12 + i) * 24;
      ctx.beginPath();
      ctx.ellipse(x, y, 86, 26, -0.18, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  function drawStage6MilitaryBaseBackground() {
    ctx.fillStyle = "#080b0b";
    ctx.fillRect(0, 0, W, H);
    const img = assets.stage6Background;
    if (img && img.complete && img.naturalWidth && img.naturalHeight) {
      const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight);
      const bw = Math.ceil(img.naturalWidth * scale);
      const bh = Math.ceil(img.naturalHeight * scale);
      const x = Math.floor((W - bw) / 2);
      const scroll = (time * 48) % (bh * 2);
      const startTile = Math.floor((-scroll - bh) / bh);
      const endTile = Math.ceil((H + bh) / bh);
      for (let i = startTile; i <= endTile; i++) {
        const y = Math.floor(i * bh + scroll) - 1;
        const drawH = bh + 3;
        if (Math.abs(i) % 2 === 1) {
          ctx.save();
          ctx.translate(0, y + drawH);
          ctx.scale(1, -1);
          ctx.drawImage(img, x, 0, bw, drawH);
          ctx.restore();
        } else ctx.drawImage(img, x, y, bw, drawH);
      }
    } else {
      const g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "#20271f");
      g.addColorStop(0.5, "#151914");
      g.addColorStop(1, "#080b09");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, W, H);
    }
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    ctx.globalAlpha = 0.16;
    ctx.strokeStyle = "#ff9b48";
    ctx.lineWidth = 2;
    const marker = (time * 72) % 190;
    for (let y = -190 + marker; y < H + 190; y += 190) {
      ctx.setLineDash([18, 22]);
      ctx.beginPath();
      ctx.moveTo(W * 0.18, y);
      ctx.lineTo(W * 0.18, y + 120);
      ctx.moveTo(W * 0.82, y + 18);
      ctx.lineTo(W * 0.82, y + 138);
      ctx.stroke();
    }
    ctx.setLineDash([]);
    ctx.restore();
  }

  function drawStage5CityBackground() {
    ctx.fillStyle = "#02040b";
    ctx.fillRect(0, 0, W, H);
    const img = assets.stage5Background;
    if (img && img.complete && img.naturalWidth && img.naturalHeight) {
      const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight);
      const bw = Math.ceil(img.naturalWidth * scale);
      const bh = Math.ceil(img.naturalHeight * scale);
      const x = Math.floor((W - bw) / 2);
      const scroll = (time * 44) % (bh * 2);
      const startTile = Math.floor((-scroll - bh) / bh);
      const endTile = Math.ceil((H + bh) / bh);
      for (let i = startTile; i <= endTile; i++) {
        const y = Math.floor(i * bh + scroll) - 1;
        const drawH = bh + 3;
        if (Math.abs(i) % 2 === 1) {
          ctx.save();
          ctx.translate(0, y + drawH);
          ctx.scale(1, -1);
          ctx.drawImage(img, x, 0, bw, drawH);
          ctx.restore();
        } else ctx.drawImage(img, x, y, bw, drawH);
      }
    } else {
      const g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "#17102b");
      g.addColorStop(0.52, "#071525");
      g.addColorStop(1, "#02040a");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, W, H);
    }
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    ctx.globalAlpha = 0.12;
    ctx.strokeStyle = "#a56cff";
    ctx.lineWidth = 2;
    const roadOffset = (time * 84) % 180;
    for (let y = -180 + roadOffset; y < H + 180; y += 180) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W, y + 68);
      ctx.stroke();
    }
    ctx.restore();
  }
  function drawGeneratedScrollingBackground(img, speed, tint) {
    if (!img || !img.complete || !img.naturalWidth || !img.naturalHeight) return false;
    const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight);
    const bw = Math.ceil(img.naturalWidth * scale);
    const bh = Math.ceil(img.naturalHeight * scale);
    const x = Math.floor((W - bw) / 2);
    const scroll = (time * speed) % (bh * 2);
    const startTile = Math.floor((-scroll - bh) / bh);
    const endTile = Math.ceil((H + bh) / bh);
    for (let i = startTile; i <= endTile; i++) {
      const y = Math.floor(i * bh + scroll) - 1;
      const drawH = bh + 3;
      if (Math.abs(i) % 2 === 1) {
        ctx.save();
        ctx.translate(0, y + drawH);
        ctx.scale(1, -1);
        ctx.drawImage(img, x, 0, bw, drawH);
        ctx.restore();
      } else ctx.drawImage(img, x, y, bw, drawH);
    }
    const shade = ctx.createLinearGradient(0, 0, 0, H);
    shade.addColorStop(0, tint);
    shade.addColorStop(0.48, "rgba(0,0,0,0.02)");
    shade.addColorStop(1, "rgba(0,0,0,0.28)");
    ctx.fillStyle = shade;
    ctx.fillRect(0, 0, W, H);
    return true;
  }
  function drawStage8TransitBackground() {
    if (drawGeneratedScrollingBackground(assets.stage8Background, 44, "rgba(12, 28, 38, 0.12)")) return;
    const base = ctx.createLinearGradient(0, 0, 0, H);
    base.addColorStop(0, "#10161d");
    base.addColorStop(0.52, "#050a10");
    base.addColorStop(1, "#020406");
    ctx.fillStyle = base;
    ctx.fillRect(0, 0, W, H);
    const scroll = (time * 128) % 144;
    ctx.save();
    for (let y = -144 + scroll; y < H + 144; y += 144) {
      const depth = Math.max(0.25, Math.min(1, (y + 180) / (H + 180)));
      const inset = 38 + (1 - depth) * 96;
      ctx.fillStyle = "rgba(30, 37, 44, " + (0.48 + depth * 0.26) + ")";
      ctx.fillRect(0, y, inset, 104);
      ctx.fillRect(W - inset, y, inset, 104);
      ctx.strokeStyle = y % 288 < 144 ? "rgba(156, 236, 255, 0.46)" : "rgba(255, 157, 77, 0.44)";
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(inset, y);
      ctx.lineTo(inset - 30, y + 104);
      ctx.moveTo(W - inset, y);
      ctx.lineTo(W - inset + 30, y + 104);
      ctx.stroke();
      ctx.fillStyle = y % 288 < 144 ? "rgba(156, 236, 255, 0.78)" : "rgba(255, 157, 77, 0.78)";
      for (const x of [18, W - 26]) ctx.fillRect(x, y + 22, 8, 52);
    }
    ctx.globalCompositeOperation = "screen";
    ctx.strokeStyle = "rgba(220, 247, 255, 0.18)";
    ctx.lineWidth = 2;
    const ringOffset = (time * 96) % 172;
    for (let y = -172 + ringOffset; y < H + 172; y += 172) {
      ctx.beginPath();
      ctx.ellipse(W / 2, y + 58, W * 0.62, 72, 0, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.strokeStyle = "rgba(255, 157, 77, 0.2)";
    for (let i = 0; i < 5; i++) {
      const x = 70 + i * ((W - 140) / 4);
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(W / 2 + (x - W / 2) * 0.35, H);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawStage7ColonyBackground() {
    if (drawGeneratedScrollingBackground(assets.stage7Background, 40, "rgba(24, 12, 38, 0.14)")) return;
    const base = ctx.createLinearGradient(0, 0, 0, H);
    base.addColorStop(0, "#0b0f1c");
    base.addColorStop(0.5, "#050814");
    base.addColorStop(1, "#02040a");
    ctx.fillStyle = base;
    ctx.fillRect(0, 0, W, H);

    const scroll = (time * 112) % 128;
    ctx.save();
    for (let y = -128 + scroll; y < H + 128; y += 128) {
      const depth = Math.max(0.3, Math.min(1, (y + 160) / (H + 160)));
      const inset = 46 + (1 - depth) * 88;
      ctx.fillStyle = "rgba(20, 28, 48, " + (0.42 + depth * 0.28) + ")";
      ctx.fillRect(0, y, inset, 92);
      ctx.fillRect(W - inset, y, inset, 92);
      ctx.strokeStyle = y % 256 < 128 ? "rgba(126, 238, 255, 0.34)" : "rgba(255, 130, 200, 0.32)";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(inset, y);
      ctx.lineTo(inset - 24, y + 92);
      ctx.moveTo(W - inset, y);
      ctx.lineTo(W - inset + 24, y + 92);
      ctx.stroke();
      ctx.fillStyle = y % 256 < 128 ? "rgba(126, 238, 255, 0.72)" : "rgba(255, 130, 200, 0.7)";
      ctx.fillRect(16, y + 20, 7, 42);
      ctx.fillRect(W - 23, y + 20, 7, 42);
    }

    ctx.globalCompositeOperation = "screen";
    const rail = ctx.createLinearGradient(0, 0, W, 0);
    rail.addColorStop(0, "rgba(126,238,255,0.18)");
    rail.addColorStop(0.2, "rgba(126,238,255,0)");
    rail.addColorStop(0.8, "rgba(255,130,200,0)");
    rail.addColorStop(1, "rgba(255,130,200,0.18)");
    ctx.fillStyle = rail;
    ctx.fillRect(0, 0, W, H);

    ctx.strokeStyle = "rgba(215, 233, 255, 0.16)";
    ctx.lineWidth = 2;
    const ribOffset = (time * 84) % 150;
    for (let y = -150 + ribOffset; y < H + 150; y += 150) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W * 0.23, y + 42);
      ctx.lineTo(W * 0.77, y + 42);
      ctx.lineTo(W, y);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawStage9CoreBackground() {
    ctx.save();
    ctx.fillStyle = "#03020a";
    ctx.fillRect(0, 0, W, H);
    const img = assets.stage9Background;
    if (img && img.complete && img.naturalWidth && img.naturalHeight) {
      const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight) * 1.12;
      const drawW = img.naturalWidth * scale;
      const drawH = img.naturalHeight * scale;
      const overflowX = Math.max(0, drawW - W);
      const overflowY = Math.max(0, drawH - H);
      const panX = overflowX * (0.5 + Math.sin(time * 0.075) * 0.42);
      const panY = overflowY * (0.5 + Math.sin(time * 0.052 - Math.PI / 2) * 0.38);
      ctx.drawImage(img, -panX, -panY, drawW, drawH);
    } else {
      const fallback = ctx.createLinearGradient(0, 0, 0, H);
      fallback.addColorStop(0, "#171027");
      fallback.addColorStop(0.52, "#080713");
      fallback.addColorStop(1, "#02020a");
      ctx.fillStyle = fallback;
      ctx.fillRect(0, 0, W, H);
    }
    const shade = ctx.createLinearGradient(0, 0, 0, H);
    shade.addColorStop(0, "rgba(3, 2, 10, 0.2)");
    shade.addColorStop(0.55, "rgba(3, 2, 10, 0.03)");
    shade.addColorStop(1, "rgba(3, 2, 10, 0.34)");
    ctx.fillStyle = shade;
    ctx.fillRect(0, 0, W, H);
    ctx.globalCompositeOperation = "screen";
    ctx.strokeStyle = "rgba(183, 140, 255, 0.12)";
    ctx.lineWidth = 2;
    const ringY = 190 + Math.sin(time * 0.36) * 12;
    ctx.beginPath();
    ctx.ellipse(W / 2, ringY, 155 + Math.sin(time * 0.22) * 9, 48, 0, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();
  }

  function drawBackground() {
    if (currentStage === 9) {
      drawStage9CoreBackground();
      return;
    }
    if (currentStage === 8) {
      drawStage8TransitBackground();
      return;
    }
    if (currentStage === 7) {
      drawStage7ColonyBackground();
      return;
    }
    if (currentStage === 6) {
      drawStage6MilitaryBaseBackground();
      return;
    }
    if (currentStage === 5) {
      drawStage5CityBackground();
      return;
    }
    if (currentStage === 4) {
      drawStage4GroundBackground();
      return;
    }
    ctx.fillStyle = "#020712";
    ctx.fillRect(0, 0, W, H);
    const img = assets.bg;
    if (img.complete && img.naturalWidth && img.naturalHeight) {
      const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight);
      const bw = Math.ceil(img.naturalWidth * scale);
      const bh = Math.ceil(img.naturalHeight * scale);
      const x = Math.floor((W - bw) / 2);
      const scroll = (time * 28) % (bh * 2);
      const startTile = Math.floor((-scroll - bh) / bh);
      const endTile = Math.ceil((H + bh) / bh);
      for (let i = startTile; i <= endTile; i++) {
        const y = Math.floor(i * bh + scroll);
        const drawY = y - 1;
        const drawH = bh + 2;
        if (Math.abs(i) % 2 === 1) {
          ctx.save();
          ctx.translate(0, drawY + drawH);
          ctx.scale(1, -1);
          ctx.drawImage(img, x, 0, bw, drawH);
          ctx.restore();
        } else {
          ctx.drawImage(img, x, drawY, bw, drawH);
        }
      }
    } else {
      const g = ctx.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "#071426");
      g.addColorStop(0.52, "#05101e");
      g.addColorStop(1, "#02060d");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, W, H);
    }
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (let i = 0; i < 42; i++) {
      const x = (i * 83 + Math.sin(i * 7.1) * 22) % W;
      const y = (i * 151 + time * (18 + (i % 5) * 7)) % H;
      const a = 0.16 + (i % 4) * 0.055;
      ctx.fillStyle = `rgba(170, 235, 255, ${a})`;
      ctx.fillRect(x, y, i % 7 === 0 ? 2 : 1, i % 7 === 0 ? 2 : 1);
    }
    ctx.restore();
  }

  function drawSprite(frame, x, y, w, h, alpha = 1, img = assets.sprites) {
    ctx.save();
    ctx.globalAlpha = alpha;
    if (img && img.complete && img.naturalWidth && frame) {
      ctx.drawImage(img, frame.x, frame.y, frame.w, frame.h, x - w / 2, y - h / 2, w, h);
    } else {
      ctx.fillStyle = "rgba(143, 246, 255, 0.82)";
      ctx.beginPath();
      ctx.moveTo(x, y - h / 2);
      ctx.lineTo(x + w * 0.42, y + h / 2);
      ctx.lineTo(x, y + h * 0.24);
      ctx.lineTo(x - w * 0.42, y + h / 2);
      ctx.closePath();
      ctx.fill();
    }
    ctx.restore();
  }
  function drawPlayer() {
    if (player.respawn > 0 && player.y > H + 44) return;
    const blink = player.inv > 0 && player.respawn <= 0 && Math.floor(player.inv * 14) % 2 === 0;
    const alpha = blink ? 0.48 : 1;
    if (player.respawn > 0) {
      // No entry effect: the ship simply rises from below the screen.
    } else if (player.inv > 0) {
      ctx.save();
      ctx.globalCompositeOperation = "screen";
      ctx.strokeStyle = "rgba(255, 224, 112, 0.68)";
      ctx.lineWidth = 2.2;
      ctx.shadowBlur = 16;
      ctx.shadowColor = "#ffdf70";
      ctx.beginPath();
      ctx.arc(player.x, player.y, 26 + Math.sin(time * 14) * 2, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();
    }
    drawSprite(frames.player, player.x, player.y, 76, 76, alpha);
    ctx.beginPath();
    ctx.arc(player.x, player.y, player.r, 0, Math.PI * 2);
    ctx.fillStyle = "#e8ffff";
    ctx.fill();
  }

  function drawHitOverlay() {
    if (player.hitPulse <= 0) return;
    const a = Math.min(0.52, player.hitPulse * 0.62);
    ctx.save();
    ctx.globalAlpha = a;
    ctx.fillStyle = "#ff1f4f";
    ctx.fillRect(0, 0, W, H);
    ctx.globalAlpha = Math.min(0.9, player.hitPulse * 1.2);
    ctx.strokeStyle = "#ffffff";
    ctx.lineWidth = 6;
    ctx.shadowBlur = 24;
    ctx.shadowColor = "#ff335f";
    ctx.strokeRect(8, 8, W - 16, H - 16);
    ctx.font = "900 26px Segoe UI, sans-serif";
    ctx.textAlign = "center";
    ctx.fillStyle = "#ffffff";
    ctx.fillText(player.hp <= 0 ? "SHIP LOST" : "HIT!", W / 2, 104);
    ctx.textAlign = "left";
    ctx.restore();
  }
  function getExtraFrame(type) {
    const grid = extraFrames[type];
    const img = assets.extraSprites;
    if (!grid || !img.complete || !img.naturalWidth || !img.naturalHeight) return null;
    const cellW = img.naturalWidth / 2;
    const cellH = img.naturalHeight / 2;
    return { x: grid.col * cellW, y: grid.row * cellH, w: cellW, h: cellH };
  }



  function drawGeneratedStage2Ship(img, x, y, size) {
    ctx.drawImage(img, x - size / 2, y - size / 2, size, size);
  }
  function drawStage2Enemy(e) {
    const img = e.type === "starScout" ? assets.stage2EnemyScout : e.type === "prism" ? assets.stage2EnemyPrism : assets.stage2EnemyCarrier;
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      drawGeneratedStage2Ship(img, e.x, e.y, size);
      return;
    }
    const fallbackType = e.type === "starScout" ? "interceptor" : e.type === "prism" ? "shield" : "mineLayer";
    const extraFrame = getExtraFrame(fallbackType);
    const frame = extraFrame || (fallbackType === "interceptor" ? frames.fighter : frames.boss);
    const source = extraFrame ? assets.extraSprites : assets.sprites;
    ctx.save();
    ctx.shadowBlur = e.type === "supplyCarrier" ? 24 : 18;
    ctx.shadowColor = e.type === "supplyCarrier" ? "#fff2a6" : e.type === "prism" ? "#ffd7f4" : "#9fdcff";
    drawSprite(frame, e.x, e.y, size, size, 1, source);
    ctx.globalCompositeOperation = "screen";
    ctx.translate(e.x, e.y);
    ctx.rotate(Math.sin(e.t * 2.2) * 0.06);
    if (e.type === "starScout") {
      ctx.strokeStyle = "rgba(159, 220, 255, 0.86)";
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(-size * 0.34, 0);
      ctx.lineTo(0, -size * 0.28);
      ctx.lineTo(size * 0.34, 0);
      ctx.stroke();
      ctx.fillStyle = "rgba(255, 242, 166, 0.9)";
      ctx.beginPath();
      ctx.arc(0, size * 0.22, 4 + Math.sin(e.t * 10) * 1.2, 0, Math.PI * 2);
      ctx.fill();
    } else if (e.type === "prism") {
      ctx.strokeStyle = "rgba(255, 215, 244, 0.92)";
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(0, 0, size * 0.34 + Math.sin(e.t * 6) * 2, 0, Math.PI * 2);
      ctx.stroke();
      ctx.fillStyle = "rgba(255, 255, 255, 0.88)";
      ctx.beginPath();
      ctx.arc(0, 0, 5, 0, Math.PI * 2);
      ctx.fill();
    } else {
      ctx.strokeStyle = "rgba(159, 220, 255, 0.75)";
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(-size * 0.48, -size * 0.08);
      ctx.lineTo(size * 0.48, -size * 0.08);
      ctx.stroke();
      ctx.fillStyle = "rgba(255, 242, 166, 0.9)";
      for (let side of [-1, 1]) {
        ctx.beginPath();
        ctx.arc(side * size * 0.28, size * 0.24, 6 + Math.sin(e.t * 8 + side) * 1.5, 0, Math.PI * 2);
        ctx.fill();
      }
    }
    ctx.restore();
  }

  function stage9EnemyImage(type) {
    if (type === "stage9Vector") return assets.stage9EnemyVector;
    if (type === "stage9Orbiter") return assets.stage9EnemyOrbiter;
    return assets.stage9EnemyBastion;
  }

  function drawStage9Enemy(e) {
    const img = stage9EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage9Bastion" ? 30 : e.type === "stage9Orbiter" ? 22 : 16;
      ctx.shadowColor = e.type === "stage9Bastion" ? "#ff8ad8" : e.type === "stage9Orbiter" ? "#b78cff" : "#73f0ff";
      drawStage7ShipImage(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    drawSprite(e.type === "stage9Bastion" ? frames.boss : frames.fighter, e.x, e.y, size, size);
  }

  function stage8EnemyImage(type) {
    if (type === "stage8Switchback") return assets.stage8EnemySwitchback;
    if (type === "stage8Polarity") return assets.stage8EnemyPolarity;
    return assets.stage8EnemyRelay;
  }

  function drawStage8Enemy(e) {
    const img = stage8EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage8Relay" ? 30 : e.type === "stage8Polarity" ? 23 : 17;
      ctx.shadowColor = e.type === "stage8Relay" ? "#ff9d4d" : e.type === "stage8Polarity" ? "#9cecff" : "#dff7ff";
      drawStage7ShipImage(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    drawSprite(e.type === "stage8Relay" ? frames.boss : frames.fighter, e.x, e.y, size, size);
  }

  function stage7EnemyImage(type) {
    if (type === "stage7Sentinel") return assets.stage7EnemySentinel;
    if (type === "stage7Breacher") return assets.stage7EnemyBreacher;
    return assets.stage7EnemyHive;
  }

  function drawStage7ShipImage(img, x, y, size) {
    const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
    const drawW = ratio > 1 ? size : size * ratio;
    const drawH = ratio > 1 ? size / ratio : size;
    ctx.drawImage(img, x - drawW / 2, y - drawH / 2, drawW, drawH);
  }

  function drawStage7Enemy(e) {
    const img = stage7EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage7Hive" ? 28 : e.type === "stage7Breacher" ? 22 : 16;
      ctx.shadowColor = e.type === "stage7Hive" ? "#9d72ff" : e.type === "stage7Breacher" ? "#ffb047" : "#7eeeff";
      drawStage7ShipImage(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    drawSprite(e.type === "stage7Hive" ? frames.boss : frames.fighter, e.x, e.y, size, size);
  }

  function stage6EnemyImage(type) {
    if (type === "stage6Striker") return assets.stage6EnemyStriker;
    if (type === "stage6Mortar") return assets.stage6EnemyMortar;
    return assets.stage6EnemyCarrier;
  }

  function drawStage6ShipImage(img, x, y, size) {
    const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
    const drawW = ratio > 1 ? size : size * ratio;
    const drawH = ratio > 1 ? size / ratio : size;
    ctx.drawImage(img, x - drawW / 2, y - drawH / 2, drawW, drawH);
  }

  function drawStage6Enemy(e) {
    const img = stage6EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage6Carrier" ? 26 : e.type === "stage6Mortar" ? 20 : 15;
      ctx.shadowColor = e.type === "stage8Relay" ? "#ff9d4d" : e.type === "stage8Polarity" ? "#9cecff" : e.type === "stage8Switchback" ? "#dff7ff" : e.type === "stage7Hive" ? "#9d72ff" : e.type === "stage7Breacher" ? "#d7e9ff" : e.type === "stage7Sentinel" ? "#7eeeff" : e.type === "stage6Carrier" ? "#ff5ab8" : e.type === "stage6Mortar" ? "#ffb24f" : "#ff9b48";
      drawStage6ShipImage(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    drawSprite(e.type === "stage6Carrier" ? frames.boss : frames.fighter, e.x, e.y, size, size);
  }

  function stage5EnemyImage(type) {
    if (type === "stage5Interceptor") return assets.stage5EnemyInterceptor;
    if (type === "stage5Jammer") return assets.stage5EnemyJammer;
    return assets.stage5EnemyGunship;
  }

  function drawStage5Enemy(e) {
    const img = stage5EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage5Gunship" ? 24 : 16;
      ctx.shadowColor = e.type === "stage5Gunship" ? "#ff596f" : e.type === "stage5Jammer" ? "#bd82ff" : "#67f4ff";
      drawGeneratedStage2Ship(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    drawSprite(e.type === "stage5Gunship" ? frames.boss : frames.fighter, e.x, e.y, size, size);
  }
  function stage4EnemyImage(type) {
    if (type === "stage4Interceptor") return assets.stage4EnemyInterceptor;
    if (type === "stage4Gunship") return assets.stage4EnemyGunship;
    return assets.stage4EnemyBomber;
  }

  function drawStage4Enemy(e) {
    const img = stage4EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      ctx.save();
      ctx.shadowBlur = e.type === "stage4Bomber" ? 22 : 14;
      ctx.shadowColor = e.type === "stage8Relay" ? "#ff9d4d" : e.type === "stage8Polarity" ? "#9cecff" : e.type === "stage8Switchback" ? "#dff7ff" : e.type === "stage7Hive" ? "#9d72ff" : e.type === "stage7Breacher" ? "#d7e9ff" : e.type === "stage7Sentinel" ? "#7eeeff" : e.type === "stage6Carrier" ? "#ff5ab8" : e.type === "stage6Mortar" ? "#ffb24f" : e.type === "stage6Striker" ? "#ff9b48" : e.type === "stage5Gunship" ? "#ff596f" : ["stage5Jammer", "stage5Interceptor"].includes(e.type) ? "#b98cff" : e.type === "stage4Bomber" ? "#f4d36b" : "#73d9ff";
      drawGeneratedStage2Ship(img, e.x, e.y, size);
      ctx.restore();
      return;
    }
    const fallback = e.type === "stage4Bomber" ? frames.boss : frames.fighter;
    drawSprite(fallback, e.x, e.y, size, size);
  }

  function stage3EnemyImage(type) {
    if (["drone", "interceptor", "starScout", "splitter"].includes(type)) return assets.stage3EnemyInterceptor;
    if (["fighter", "laser", "prism"].includes(type)) return assets.stage3EnemyStriker;
    return assets.stage3EnemyCarrier;
  }

  function drawStage3Enemy(e) {
    const img = stage3EnemyImage(e.type);
    const size = (enemyStats[e.type] || enemyStats.drone).size;
    if (img && img.complete && img.naturalWidth) {
      drawGeneratedStage2Ship(img, e.x, e.y, size);
      return;
    }
    const extraFrame = getExtraFrame(e.type);
    const frame = extraFrame || (e.type === "laser" ? frames.fighter : frames[e.type]);
    drawSprite(frame, e.x, e.y, size, size, 1, extraFrame ? assets.extraSprites : assets.sprites);
  }

  function drawStage3LaserStatus(e) {
    ctx.save();
    if (!e.laserReady || e.shieldFlash > 0) {
      ctx.globalCompositeOperation = "screen";
      ctx.strokeStyle = e.shieldFlash > 0 ? "#ffffff" : "rgba(255, 88, 224, 0.55)";
      ctx.lineWidth = e.shieldFlash > 0 ? 4 : 2;
      ctx.shadowBlur = 20;
      ctx.shadowColor = "#ff4fd8";
      ctx.beginPath();
      ctx.arc(e.x, e.y, e.r + 12 + Math.sin(e.t * 8) * 2, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.globalCompositeOperation = "screen";
    ctx.shadowBlur = 18;
    ctx.shadowColor = "#ff4fd8";
    ctx.strokeStyle = e.laserCharge > 0 ? "#b9ffff" : "rgba(103, 244, 255, 0.48)";
    ctx.lineWidth = e.laserCharge > 0 ? 4 : 2;
    ctx.beginPath();
    ctx.moveTo(e.x - 22, e.y + 18);
    ctx.lineTo(e.x + 22, e.y + 18);
    ctx.stroke();
    ctx.restore();
  }

  function drawEnemies() {
    for (const e of enemies) {
      if (currentStage === 9 && e.type.startsWith("stage9")) {
        drawStage9Enemy(e);
      } else if (currentStage === 8 && e.type.startsWith("stage8")) {
        drawStage8Enemy(e);
      } else if (currentStage === 7 && e.type.startsWith("stage7")) {
        drawStage7Enemy(e);
      } else if (currentStage === 6 && e.type.startsWith("stage6")) {
        drawStage6Enemy(e);
      } else if (currentStage === 5 && e.type.startsWith("stage5")) {
        drawStage5Enemy(e);
      } else if (currentStage === 4 && e.type.startsWith("stage4")) {
        drawStage4Enemy(e);
      } else if (currentStage === 3) {
        drawStage3Enemy(e);
        if (e.type === "laser") drawStage3LaserStatus(e);
      } else if (["starScout", "prism", "supplyCarrier"].includes(e.type)) {
        drawStage2Enemy(e);
      } else {
        const extraFrame = getExtraFrame(e.type);
        const frame = extraFrame || (e.type === "laser" ? frames.fighter : frames[e.type]);
        const size = (enemyStats[e.type] || enemyStats.drone).size;
        drawSprite(frame, e.x, e.y, size, size, 1, extraFrame ? assets.extraSprites : assets.sprites);
        if (e.type === "laser") {
          ctx.save();
          if (!e.laserReady || e.shieldFlash > 0) {
            ctx.globalCompositeOperation = "screen";
            ctx.strokeStyle = e.shieldFlash > 0 ? "#ffffff" : "rgba(255, 88, 224, 0.55)";
            ctx.lineWidth = e.shieldFlash > 0 ? 4 : 2;
            ctx.shadowBlur = 20;
            ctx.shadowColor = "#ff4fd8";
            ctx.beginPath();
            ctx.arc(e.x, e.y, e.r + 12 + Math.sin(e.t * 8) * 2, 0, Math.PI * 2);
            ctx.stroke();
          }
          ctx.globalCompositeOperation = "screen";
          ctx.shadowBlur = 18;
          ctx.shadowColor = "#ff4fd8";
          ctx.strokeStyle = e.laserCharge > 0 ? "#b9ffff" : "rgba(103, 244, 255, 0.48)";
          ctx.lineWidth = e.laserCharge > 0 ? 4 : 2;
          ctx.beginPath();
          ctx.moveTo(e.x - 22, e.y + 18);
          ctx.lineTo(e.x + 22, e.y + 18);
          ctx.stroke();
          ctx.restore();
        }
      }
      const hpw = e.type === "stage9Bastion" ? 90 : e.type === "stage9Orbiter" ? 68 : e.type === "stage9Vector" ? 48 : e.type === "stage8Relay" ? 86 : e.type === "stage8Polarity" ? 66 : e.type === "stage8Switchback" ? 46 : e.type === "stage7Hive" ? 82 : e.type === "stage7Breacher" ? 64 : e.type === "stage7Sentinel" ? 44 : e.type === "stage6Carrier" ? 78 : e.type === "stage6Mortar" ? 60 : e.type === "stage6Striker" ? 42 : e.type === "stage5Gunship" ? 72 : e.type === "stage5Jammer" ? 56 : e.type === "stage5Interceptor" ? 40 : e.type === "stage4Bomber" ? 68 : e.type === "stage4Gunship" ? 54 : e.type === "stage4Interceptor" ? 38 : e.type === "supplyCarrier" ? 62 : e.type === "prism" ? 48 : e.type === "starScout" ? 36 : e.type === "shield" ? 58 : e.type === "mineLayer" ? 50 : e.type === "laser" ? 48 : e.type === "fighter" ? 42 : 28;
      ctx.fillStyle = "rgba(255,255,255,0.22)";
      ctx.fillRect(e.x - hpw / 2, e.y + e.r + 12, hpw, 3);
      ctx.fillStyle = "#ff708f";
      ctx.fillRect(e.x - hpw / 2, e.y + e.r + 12, hpw * Math.max(0, e.hp / e.maxHp), 3);
    }
  }

  function drawStage2MidBossShip() {
    const x = midBoss.x;
    const y = midBoss.y;
    if (assets.stage2MidbossShip.complete && assets.stage2MidbossShip.naturalWidth) {
      const size = 150 * (1 + Math.sin(midBoss.t * 6.5) * 0.025);
      ctx.save();
      ctx.shadowBlur = 24;
      ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#fff2a6" : "#7dbbff";
      drawGeneratedStage2Ship(assets.stage2MidbossShip, x, y, size);
      ctx.restore();
      return;
    }
    const pulse = 1 + Math.sin(midBoss.t * 6.5) * 0.025;
    const extraFrame = getExtraFrame("shield") || getExtraFrame("mineLayer");
    ctx.save();
    ctx.shadowBlur = 26;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#fff2a6" : "#7dbbff";
    if (extraFrame) drawSprite(extraFrame, x, y, 150 * pulse, 150 * pulse, 1, assets.extraSprites);
    else drawSprite(frames.boss, x, y, 154 * pulse, 154 * pulse);
    ctx.globalCompositeOperation = "screen";
    ctx.translate(x, y);
    ctx.strokeStyle = "rgba(159, 220, 255, 0.84)";
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.moveTo(-78, -6);
    ctx.lineTo(-118, 24);
    ctx.moveTo(78, -6);
    ctx.lineTo(118, 24);
    ctx.stroke();
    ctx.strokeStyle = "rgba(255, 242, 166, 0.92)";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(0, 0, 52 + Math.sin(midBoss.t * 7) * 3, 0, Math.PI * 2);
    ctx.stroke();
    ctx.fillStyle = "rgba(255, 242, 166, 0.9)";
    for (let side of [-1, 1]) {
      ctx.beginPath();
      ctx.arc(side * 62, 42, 8 + Math.sin(midBoss.t * 12 + side) * 2, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  function drawStage3MidBossShip() {
    const x = midBoss.x;
    const y = midBoss.y;
    if (assets.stage3MidbossShip.complete && assets.stage3MidbossShip.naturalWidth) {
      const size = 168 * (1 + Math.sin(midBoss.t * 5.8) * 0.02);
      ctx.save();
      ctx.shadowBlur = 28;
      ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#fff0a8" : "#ff9bd7";
      drawGeneratedStage2Ship(assets.stage3MidbossShip, x, y, size);
      ctx.restore();
      return;
    }
    const pulse = 1 + Math.sin(midBoss.t * 5.8) * 0.02;
    ctx.save();
    ctx.shadowBlur = 28;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#fff0a8" : "#ff9bd7";
    drawSprite(frames.boss, x, y, 164 * pulse, 164 * pulse);
    ctx.globalCompositeOperation = "screen";
    ctx.translate(x, y);
    ctx.strokeStyle = "rgba(255, 155, 215, 0.82)";
    ctx.lineWidth = 4;
    for (let i = 0; i < 8; i++) {
      const a = midBoss.t * 0.9 + i * Math.PI / 4;
      const px = Math.cos(a) * 74;
      const py = Math.sin(a) * 52;
      ctx.beginPath();
      ctx.ellipse(px, py, 17, 8, a, 0, Math.PI * 2);
      ctx.stroke();
    }
    ctx.restore();
  }
  function drawStage2BossShip() {
    const x = boss.x;
    const y = boss.y;
    if (assets.stage2BossShip.complete && assets.stage2BossShip.naturalWidth) {
      const size = 214 * (1 + Math.sin(boss.t * 5.2) * 0.018);
      ctx.save();
      ctx.shadowBlur = 30;
      ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#fff2a6" : "#9fdcff";
      drawGeneratedStage2Ship(assets.stage2BossShip, x, y, size);
      ctx.restore();
      return;
    }
    const pulse = 1 + Math.sin(boss.t * 5.2) * 0.018;
    ctx.save();
    ctx.shadowBlur = 30;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#fff2a6" : "#9fdcff";
    drawSprite(frames.boss, x, y, 210 * pulse, 210 * pulse);
    ctx.globalCompositeOperation = "screen";
    ctx.translate(x, y);
    for (let side of [-1, 1]) {
      ctx.strokeStyle = side < 0 ? "rgba(159, 220, 255, 0.8)" : "rgba(255, 215, 244, 0.82)";
      ctx.lineWidth = 5;
      ctx.beginPath();
      ctx.moveTo(side * 38, -22);
      ctx.lineTo(side * 128, 18);
      ctx.lineTo(side * 78, 56);
      ctx.stroke();
      ctx.fillStyle = "rgba(255, 242, 166, 0.92)";
      ctx.beginPath();
      ctx.arc(side * 90, 28, 10 + Math.sin(boss.t * 9 + side) * 2, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.strokeStyle = "rgba(255, 242, 166, 0.9)";
    ctx.lineWidth = 3;
    ctx.beginPath();
    for (let i = 0; i < 10; i++) {
      const rr = i % 2 ? 24 : 48;
      const a = -Math.PI / 2 + i * Math.PI / 5 + boss.t * 0.5;
      const px = Math.cos(a) * rr;
      const py = Math.sin(a) * rr - 6;
      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
  }
  function drawStage3BossShip() {
    const x = boss.x;
    const y = boss.y;
    if (assets.stage3BossShip.complete && assets.stage3BossShip.naturalWidth) {
      const size = 228 * (1 + Math.sin(boss.t * 5.0) * 0.016);
      ctx.save();
      ctx.shadowBlur = 34;
      ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ffb15f" : "#8fdcff";
      drawGeneratedStage2Ship(assets.stage3BossShip, x, y, size);
      ctx.globalCompositeOperation = "screen";
      ctx.translate(x, y);
      ctx.strokeStyle = boss.hp / boss.maxHp < 0.48 ? "rgba(255, 177, 95, 0.82)" : "rgba(143, 220, 255, 0.78)";
      ctx.lineWidth = 3;
      for (let side of [-1, 1]) {
        ctx.beginPath();
        ctx.moveTo(side * 72, -28);
        ctx.lineTo(side * 124, 10 + Math.sin(boss.t * 3 + side) * 8);
        ctx.lineTo(side * 82, 42);
        ctx.stroke();
      }
      ctx.restore();
      return;
    }
    const pulse = 1 + Math.sin(boss.t * 5.0) * 0.016;
    ctx.save();
    ctx.shadowBlur = 34;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ffb15f" : "#8fdcff";
    drawSprite(frames.boss, x, y, 216 * pulse, 216 * pulse);
    ctx.restore();
  }
  function drawStage9MidBossShip() {
    const size = 238 * (1 + Math.sin(midBoss.t * 4.6) * 0.014);
    ctx.save();
    ctx.shadowBlur = enemyBullets.length > 90 ? 16 : 44;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#ff8ad8" : "#b78cff";
    if (assets.stage9MidbossShip.complete && assets.stage9MidbossShip.naturalWidth) drawStage7ShipImage(assets.stage9MidbossShip, midBoss.x, midBoss.y, size);
    else drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    ctx.restore();
  }

  function drawStage9BossShip() {
    const size = 312 * (1 + Math.sin(boss.t * 3.8) * 0.012);
    ctx.save();
    ctx.shadowBlur = enemyBullets.length > 90 ? 18 : 54;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ff8ad8" : "#b78cff";
    if (assets.stage9BossShip.complete && assets.stage9BossShip.naturalWidth) drawStage7ShipImage(assets.stage9BossShip, boss.x, boss.y, size);
    else drawSprite(frames.boss, boss.x, boss.y, size, size);
    ctx.restore();
  }

  function drawStage9AttackFx() {
    if (currentStage !== 9 || !stage9AttackFx.length) return;
    const lowFx = enemyBullets.length > 82;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage9AttackFx) {
      const p = Math.max(0, Math.min(1, fx.t / fx.life));
      ctx.globalAlpha = Math.max(0, 0.78 * (1 - p));
      ctx.strokeStyle = fx.color || "#b78cff";
      ctx.fillStyle = fx.color || "#b78cff";
      ctx.shadowColor = fx.color || "#b78cff";
      ctx.shadowBlur = lowFx ? 4 : 22;
      if (fx.type === "gate") {
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.ellipse(fx.x, fx.y, 22 + p * 25, 9 + p * 10, p * 1.8, 0, Math.PI * 2);
        ctx.stroke();
        if (!lowFx) {
          for (let i = 0; i < 4; i++) {
            const a = i * Math.PI / 2 + p * 2.4;
            ctx.beginPath();
            ctx.moveTo(fx.x + Math.cos(a) * 14, fx.y + Math.sin(a) * 8);
            ctx.lineTo(fx.x + Math.cos(a) * 35, fx.y + Math.sin(a) * 18);
            ctx.stroke();
          }
        }
      } else if (fx.type === "orbit") {
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 8 + p * 24, 0, Math.PI * 2);
        ctx.stroke();
      } else if (fx.type === "core") {
        ctx.globalAlpha = 0.3 + Math.sin(fx.t * 13) * 0.12;
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 34 + Math.sin(fx.t * 8) * 6, 0, Math.PI * 2);
        ctx.stroke();
        if (!lowFx) {
          ctx.setLineDash([9, 8]);
          ctx.beginPath();
          ctx.arc(fx.x, fx.y, 58 - p * 20, 0, Math.PI * 2);
          ctx.stroke();
          ctx.setLineDash([]);
        }
      } else if (fx.type === "debris") {
        ctx.globalAlpha = 0.5 * (1 - p);
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(fx.x, 0);
        ctx.lineTo(fx.x, 76 + p * 120);
        ctx.stroke();
      } else if (fx.type === "dash") {
        ctx.lineWidth = 7 - p * 5;
        ctx.beginPath();
        ctx.moveTo(fx.x, fx.y);
        ctx.lineTo(fx.x - Math.cos(fx.angle) * (48 + p * 82), fx.y - Math.sin(fx.angle) * (48 + p * 82));
        ctx.stroke();
      } else if (fx.type === "coreBurst") {
        ctx.lineWidth = 5 - p * 3;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 18 + p * 108, 0, Math.PI * 2);
        ctx.stroke();
        if (!lowFx) {
          for (let i = 0; i < 12; i++) {
            const a = i * Math.PI / 6 + p * 0.3;
            ctx.beginPath();
            ctx.moveTo(fx.x + Math.cos(a) * (22 + p * 28), fx.y + Math.sin(a) * (22 + p * 28));
            ctx.lineTo(fx.x + Math.cos(a) * (42 + p * 92), fx.y + Math.sin(a) * (42 + p * 92));
            ctx.stroke();
          }
        }
      }
    }
    ctx.restore();
  }

  function drawStage8MidBossShip() {
    const size = 226 * (1 + Math.sin(midBoss.t * 4.7) * 0.014);
    ctx.save();
    ctx.shadowBlur = 42;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#ff9d4d" : "#9cecff";
    if (assets.stage8MidbossShip.complete && assets.stage8MidbossShip.naturalWidth) drawStage7ShipImage(assets.stage8MidbossShip, midBoss.x, midBoss.y, size);
    else drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    ctx.restore();
  }

  function drawStage8BossShip() {
    const size = 296 * (1 + Math.sin(boss.t * 4.0) * 0.012);
    ctx.save();
    ctx.shadowBlur = 52;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ff9d4d" : "#9cecff";
    if (assets.stage8BossShip.complete && assets.stage8BossShip.naturalWidth) drawStage7ShipImage(assets.stage8BossShip, boss.x, boss.y, size);
    else drawSprite(frames.boss, boss.x, boss.y, size, size);
    ctx.restore();
  }

  function drawStage8AttackFx() {
    if (currentStage !== 8 || !stage8AttackFx.length) return;
    const lowFx = enemyBullets.length > 96;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage8AttackFx) {
      const p = Math.max(0, Math.min(1, fx.t / fx.life));
      ctx.globalAlpha = Math.max(0, 0.78 * (1 - p));
      ctx.strokeStyle = fx.color || "#9cecff";
      ctx.shadowColor = fx.color || "#9cecff";
      ctx.shadowBlur = lowFx ? 6 : 24;
      if (fx.type === "route") {
        ctx.globalAlpha = 0.58 * (1 - p);
        ctx.lineWidth = 4;
        ctx.setLineDash(lowFx ? [] : [14, 10]);
        ctx.beginPath();
        for (let i = 0; i < fx.points.length; i++) {
          const point = fx.points[i];
          if (i === 0) ctx.moveTo(point.x, point.y);
          else ctx.lineTo(point.x, point.y);
        }
        ctx.stroke();
        ctx.setLineDash([]);
        if (!lowFx) {
          for (let i = 1; i < fx.points.length; i++) {
            const point = fx.points[i];
            ctx.beginPath();
            ctx.arc(point.x, point.y, 10 + p * 6, 0, Math.PI * 2);
            ctx.stroke();
          }
        }
      } else if (fx.type === "cargoBurst") {
        ctx.globalAlpha = 0.72 * (1 - p);
        ctx.lineWidth = 5;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 16 + p * 62, 0, Math.PI * 2);
        ctx.stroke();
      } else if (fx.type === "node" || fx.type === "nodeBurst") {
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, fx.type === "node" ? 38 - p * 22 : 18 + p * 58, 0, Math.PI * 2);
        ctx.stroke();
        if (!lowFx) {
          for (let i = 0; i < 4; i++) {
            const a = i * Math.PI / 2 + p * 1.2;
            ctx.beginPath();
            ctx.moveTo(fx.x + Math.cos(a) * 14, fx.y + Math.sin(a) * 14);
            ctx.lineTo(fx.x + Math.cos(a) * 34, fx.y + Math.sin(a) * 34);
            ctx.stroke();
          }
        }
      } else if (fx.type === "rail") {
        ctx.lineWidth = 5 - p * 3;
        ctx.setLineDash([18, 14]);
        ctx.beginPath();
        if (fx.horizontal) {
          ctx.moveTo(0, fx.y);
          ctx.lineTo(W, fx.y);
        } else {
          ctx.moveTo(fx.x, 0);
          ctx.lineTo(fx.x, H);
        }
        ctx.stroke();
        ctx.setLineDash([]);
      } else {
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 12 + p * 54, -0.7, Math.PI * 1.3);
        ctx.stroke();
      }
    }
    ctx.restore();
  }

  function drawStage7MidBossShip() {
    const size = 218 * (1 + Math.sin(midBoss.t * 4.8) * 0.014);
    ctx.save();
    ctx.shadowBlur = 40;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#9d72ff" : "#d7e9ff";
    if (assets.stage7MidbossShip.complete && assets.stage7MidbossShip.naturalWidth) drawStage7ShipImage(assets.stage7MidbossShip, midBoss.x, midBoss.y, size);
    else drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    ctx.restore();
  }

  function drawStage7BossShip() {
    const size = 282 * (1 + Math.sin(boss.t * 4.2) * 0.012);
    ctx.save();
    ctx.shadowBlur = 48;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ff82c8" : "#7eeeff";
    if (assets.stage7BossShip.complete && assets.stage7BossShip.naturalWidth) drawStage7ShipImage(assets.stage7BossShip, boss.x, boss.y, size);
    else drawSprite(frames.boss, boss.x, boss.y, size, size);
    ctx.restore();
  }

  function drawStage7AttackFx() {
    if (currentStage !== 7 || !stage7AttackFx.length) return;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage7AttackFx) {
      const p = Math.max(0, Math.min(1, fx.t / fx.life));
      ctx.strokeStyle = fx.color || "#7eeeff";
      ctx.shadowColor = fx.color || "#7eeeff";
      ctx.shadowBlur = 22;
      if (fx.type === "eclipse") {
        ctx.globalAlpha = 0.62 * (1 - p);
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.ellipse(fx.x, fx.y, 72 + p * 38, 34 + p * 18, p * 1.2, 0, Math.PI * 2);
        ctx.stroke();
      } else if (fx.type === "paw" || fx.type === "pawBurst") {
        ctx.globalAlpha = 0.72 * (1 - p);
        ctx.lineWidth = 3;
        const rr = fx.type === "paw" ? 22 - p * 8 : 20 + p * 46;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, rr, 0, Math.PI * 2);
        ctx.stroke();
        for (const toe of [[-18,-18],[-6,-28],[6,-28],[18,-18]]) {
          ctx.beginPath();
          ctx.arc(fx.x + toe[0], fx.y + toe[1], Math.max(4, rr * 0.24), 0, Math.PI * 2);
          ctx.stroke();
        }
      } else if (fx.type === "scratch") {
        ctx.globalAlpha = 0.58 * (1 - p);
        ctx.lineWidth = 4;
        ctx.beginPath();
        if (fx.fromLeft) {
          ctx.moveTo(0, fx.y - 34);
          ctx.lineTo(W, fx.y + 72);
        } else {
          ctx.moveTo(W, fx.y - 34);
          ctx.lineTo(0, fx.y + 72);
        }
        ctx.stroke();
      } else if (fx.type === "anchor") {
        ctx.globalAlpha = 0.75 * (1 - p);
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 34 - p * 18, 0, Math.PI * 2);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(fx.x - 24, fx.y);
        ctx.lineTo(fx.x + 24, fx.y);
        ctx.moveTo(fx.x, fx.y - 24);
        ctx.lineTo(fx.x, fx.y + 24);
        ctx.stroke();
      } else if (fx.type === "halo") {
        ctx.globalAlpha = 0.55 * (1 - p);
        ctx.lineWidth = 5;
        ctx.beginPath();
        ctx.ellipse(fx.x, fx.y, 64 + p * 96, 28 + p * 42, 0, 0, Math.PI * 2);
        ctx.stroke();
      } else if (fx.type === "shutter") {
        ctx.globalAlpha = 0.6 * (1 - p);
        ctx.lineWidth = 8 - p * 5;
        ctx.beginPath();
        ctx.moveTo(0, fx.y);
        ctx.lineTo(W, fx.y);
        ctx.stroke();
      }
    }
    ctx.restore();
  }

  function drawStage6MidBossShip() {
    const size = 204 * (1 + Math.sin(midBoss.t * 5.1) * 0.016);
    ctx.save();
    ctx.shadowBlur = 36;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#ff5f76" : "#ff9b48";
    if (assets.stage6MidbossShip.complete && assets.stage6MidbossShip.naturalWidth) drawStage6ShipImage(assets.stage6MidbossShip, midBoss.x, midBoss.y, size);
    else drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    ctx.restore();
  }

  function drawStage6BossShip() {
    const size = 262 * (1 + Math.sin(boss.t * 4.1) * 0.013);
    ctx.save();
    ctx.shadowBlur = 44;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ff9b48" : "#ff5ab8";
    if (assets.stage6BossShip.complete && assets.stage6BossShip.naturalWidth) drawStage6ShipImage(assets.stage6BossShip, boss.x, boss.y, size);
    else drawSprite(frames.boss, boss.x, boss.y, size, size);
    ctx.restore();
  }

  function drawStage6AttackFx() {
    if (currentStage !== 6 || !stage6AttackFx.length) return;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage6AttackFx) {
      if (fx.type === "impact") {
        const remain = Math.max(0, fx.hitAt - fx.t);
        const charge = 1 - Math.min(1, remain / Math.max(0.01, fx.hitAt));
        const radius = 34 - charge * 14;
        ctx.globalAlpha = 0.28 + charge * 0.6;
        ctx.strokeStyle = fx.color;
        ctx.shadowColor = fx.color;
        ctx.shadowBlur = 18;
        ctx.lineWidth = 2 + charge * 2;
        ctx.setLineDash([7, 6]);
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, radius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.beginPath();
        ctx.moveTo(fx.x - radius - 8, fx.y);
        ctx.lineTo(fx.x + radius + 8, fx.y);
        ctx.moveTo(fx.x, fx.y - radius - 8);
        ctx.lineTo(fx.x, fx.y + radius + 8);
        ctx.stroke();
      } else if (fx.type === "shock") {
        const p = Math.min(1, fx.t / fx.life);
        ctx.globalAlpha = (1 - p) * 0.9;
        ctx.strokeStyle = fx.color;
        ctx.shadowColor = fx.color;
        ctx.shadowBlur = 28;
        ctx.lineWidth = 7 * (1 - p) + 2;
        ctx.beginPath();
        ctx.arc(fx.x, fx.y, 16 + p * 118, 0, Math.PI * 2);
        ctx.stroke();
      } else if (fx.type === "core") {
        const core = enemyBullets.find((b) => b.id === fx.id && !b.dead);
        if (!core) continue;
        const p = Math.min(1, fx.t / fx.life);
        ctx.globalAlpha = 0.35 + p * 0.55;
        ctx.strokeStyle = fx.color;
        ctx.shadowColor = fx.color;
        ctx.shadowBlur = 26;
        ctx.lineWidth = 3;
        for (let i = 0; i < 3; i++) {
          ctx.beginPath();
          ctx.arc(core.x, core.y, 28 + i * 14 + Math.sin(fx.t * 10 + i) * 5, 0, Math.PI * 2);
          ctx.stroke();
        }
      }
    }
    ctx.setLineDash([]);
    ctx.restore();
  }

  function drawStage5MidBossShip() {
    const size = 186 * (1 + Math.sin(midBoss.t * 5.4) * 0.018);
    ctx.save();
    ctx.shadowBlur = 34;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#ff7b9c" : "#b98cff";
    if (assets.stage5MidbossShip.complete && assets.stage5MidbossShip.naturalWidth) drawGeneratedStage2Ship(assets.stage5MidbossShip, midBoss.x, midBoss.y, size);
    else drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    ctx.restore();
  }

  function drawStage5BossShip() {
    const size = 246 * (1 + Math.sin(boss.t * 4.4) * 0.014);
    ctx.save();
    ctx.shadowBlur = 40;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ffb05f" : "#ff526e";
    if (assets.stage5BossShip.complete && assets.stage5BossShip.naturalWidth) drawGeneratedStage2Ship(assets.stage5BossShip, boss.x, boss.y, size);
    else drawSprite(frames.boss, boss.x, boss.y, size, size);
    ctx.restore();
  }

  function drawStage5AttackFx() {
    if (currentStage !== 5 || !stage5AttackFx.length) return;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage5AttackFx) {
      if (fx.type === "mirror") {
        const alpha = Math.max(0, Math.min(1, fx.t / 0.45)) * Math.max(0.2, 1 - fx.t / fx.life);
        for (let i = 0; i < fx.copies.length; i++) {
          const copy = fx.copies[i];
          const img = assets.stage5MidbossShip;
          ctx.save();
          ctx.globalAlpha = (i === 1 ? 0.45 : 0.25) * alpha;
          ctx.shadowBlur = 24;
          ctx.shadowColor = i === 0 ? "#67f4ff" : i === 1 ? "#ffffff" : "#bd82ff";
          if (img && img.complete && img.naturalWidth) drawGeneratedStage2Ship(img, copy.x, copy.y, i === 1 ? 102 : 92);
          ctx.strokeStyle = i === 0 ? "#67f4ff" : i === 1 ? "#ffffff" : "#bd82ff";
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.arc(copy.x, copy.y, 36 + Math.sin(fx.t * 12 + i) * 5, 0, Math.PI * 2);
          ctx.stroke();
          ctx.restore();
        }
      } else if (fx.type === "alley" && !fx.fired) {
        ctx.globalAlpha = 0.35 + Math.sin(fx.t * 24) * 0.16;
        ctx.strokeStyle = "#67f4ff";
        ctx.shadowColor = "#67f4ff";
        ctx.shadowBlur = 18;
        ctx.lineWidth = 3;
        const offsets = fx.rage ? [-170, -90, 90, 170] : [-150, -70, 70, 150];
        for (const offset of offsets) {
          const x = Math.max(24, Math.min(W - 24, fx.safeX + offset));
          ctx.beginPath();
          ctx.moveTo(x, 0);
          ctx.lineTo(x, H);
          ctx.stroke();
        }
      } else if (fx.type === "lockdown" && !fx.fired) {
        const charge = Math.max(0, Math.min(1, fx.t / fx.fireAt));
        ctx.globalAlpha = 0.28 + charge * 0.5;
        ctx.strokeStyle = "#ff4e69";
        ctx.shadowColor = "#ff344f";
        ctx.shadowBlur = 20;
        ctx.lineWidth = 2 + charge * 3;
        for (const x of fx.columns) {
          ctx.beginPath();
          ctx.moveTo(x, 0);
          ctx.lineTo(x, H);
          ctx.stroke();
        }
        ctx.strokeStyle = "#ff835f";
        for (const y of fx.rows) {
          ctx.beginPath();
          ctx.moveTo(0, y);
          ctx.lineTo(W, y);
          ctx.stroke();
        }
      }
    }
    ctx.restore();
  }
  function drawStage4MidBossShip() {
    const size = 174 * (1 + Math.sin(midBoss.t * 5.2) * 0.018);
    ctx.save();
    ctx.shadowBlur = 30;
    ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#f4d36b" : "#73d9ff";
    if (assets.stage4MidbossShip.complete && assets.stage4MidbossShip.naturalWidth) {
      drawGeneratedStage2Ship(assets.stage4MidbossShip, midBoss.x, midBoss.y, size);
    } else {
      drawSprite(frames.boss, midBoss.x, midBoss.y, size, size);
    }
    ctx.restore();
  }

  function drawStage4AttackFx() {
    if (currentStage !== 4 || !stage4AttackFx.length) return;
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const fx of stage4AttackFx) {
      if (fx.type === "scissorWave" && fx.t < fx.spawnAt) {
        const charge = Math.max(0, Math.min(1, fx.t / fx.spawnAt));
        const pairs = fx.rage ? 3 : 2;
        ctx.save();
        ctx.globalAlpha = 0.3 + charge * 0.62;
        ctx.shadowBlur = 22;
        for (let i = 0; i < pairs; i++) {
          const y = 112 + i * (fx.rage ? 72 : 92) + (fx.reverse ? 76 : 0);
          for (const side of [-1, 1]) {
            const color = side < 0 ? "#75e7ff" : "#ffd56d";
            const edgeX = side < 0 ? 0 : W;
            ctx.fillStyle = color;
            ctx.shadowColor = color;
            ctx.save();
            ctx.translate(edgeX, y);
            ctx.scale(side, 1);
            ctx.beginPath();
            ctx.moveTo(0, -14);
            ctx.lineTo(42 + charge * 30, -7);
            ctx.lineTo(58 + charge * 36, 0);
            ctx.lineTo(42 + charge * 30, 7);
            ctx.lineTo(0, 14);
            ctx.closePath();
            ctx.fill();
            ctx.restore();
          }
        }
        ctx.restore();
      } else if (fx.type === "siegeFrame") {
        const turrets = enemyBullets.filter((b) => fx.turretIds.includes(b.id) && !b.dead);
        if (!turrets.length) continue;
        ctx.save();
        ctx.globalAlpha = 0.26 + Math.sin(fx.t * 5) * 0.06;
        ctx.lineWidth = 3;
        for (const turret of turrets) {
          ctx.strokeStyle = turret.color;
          ctx.shadowColor = turret.color;
          ctx.shadowBlur = 14;
          ctx.beginPath();
          ctx.moveTo(fx.x, fx.y + 18);
          ctx.lineTo(turret.x, turret.y);
          ctx.stroke();
        }
        if (turrets.length >= 3) {
          ctx.globalAlpha = 0.16;
          ctx.strokeStyle = "#ffffff";
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(turrets[0].x, turrets[0].y);
          for (let i = 1; i < turrets.length; i++) ctx.lineTo(turrets[i].x, turrets[i].y);
          ctx.closePath();
          ctx.stroke();
        }
        ctx.globalAlpha = 0.72;
        for (const turret of turrets) {
          const remaining = Math.max(0, (turret.fireAt || 0) - turret.age);
          if (turret.fired || remaining > 0.42) continue;
          const charge = 1 - remaining / 0.42;
          const size = 26 + charge * 12;
          ctx.strokeStyle = "#ffffff";
          ctx.shadowColor = turret.color;
          ctx.shadowBlur = 22;
          ctx.lineWidth = 2 + charge * 2;
          for (let i = 0; i < 4; i++) {
            const sx = i % 2 ? 1 : -1;
            const sy = i < 2 ? -1 : 1;
            ctx.beginPath();
            ctx.moveTo(turret.x + sx * size, turret.y + sy * (size - 8));
            ctx.lineTo(turret.x + sx * size, turret.y + sy * size);
            ctx.lineTo(turret.x + sx * (size - 8), turret.y + sy * size);
            ctx.stroke();
          }
        }
        ctx.restore();
      }
    }
    ctx.restore();
  }
  function drawStage4BossShip() {
    const size = 236 * (1 + Math.sin(boss.t * 4.6) * 0.014);
    const siege = Math.max(0, boss.siegePulse || 0);
    ctx.save();
    if (siege > 0) {
      const pulse = Math.min(1, siege * 2.3);
      ctx.save();
      ctx.translate(boss.x, boss.y + 18);
      ctx.globalCompositeOperation = "screen";
      ctx.globalAlpha = 0.42 + pulse * 0.46;
      ctx.shadowBlur = 30;
      ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#ffd56d" : "#75e7ff";
      ctx.fillStyle = "#ffffff";
      ctx.beginPath();
      ctx.moveTo(0, -26 - pulse * 8);
      ctx.lineTo(22 + pulse * 7, 0);
      ctx.lineTo(0, 26 + pulse * 8);
      ctx.lineTo(-22 - pulse * 7, 0);
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = boss.hp / boss.maxHp < 0.48 ? "#ffd56d" : "#75e7ff";
      ctx.lineWidth = 4;
      for (let i = 0; i < 4; i++) {
        ctx.rotate(Math.PI / 2);
        ctx.beginPath();
        ctx.moveTo(34, 0);
        ctx.lineTo(68 + pulse * 18, 0);
        ctx.stroke();
      }
      ctx.restore();
    }
    ctx.shadowBlur = 36;
    ctx.shadowColor = boss.hp / boss.maxHp < 0.48 ? "#f4d36b" : "#73d9ff";
    if (assets.stage4BossShip.complete && assets.stage4BossShip.naturalWidth) {
      drawGeneratedStage2Ship(assets.stage4BossShip, boss.x, boss.y, size);
    } else {
      drawSprite(frames.boss, boss.x, boss.y, size, size);
    }
    ctx.restore();
  }
  function drawMidBoss() {
    if (!midBoss) return;
    ctx.save();
    if (currentStage === 9) {
      drawStage9MidBossShip();
    } else if (currentStage === 8) {
      drawStage8MidBossShip();
    } else if (currentStage === 7) {
      drawStage7MidBossShip();
    } else if (currentStage === 6) {
      drawStage6MidBossShip();
    } else if (currentStage === 5) {
      drawStage5MidBossShip();
    } else if (currentStage === 4) {
      drawStage4MidBossShip();
    } else if (currentStage === 3) {
      drawStage3MidBossShip();
    } else if (currentStage === 2) {
      drawStage2MidBossShip();
    } else {
      const pulse = 1 + Math.sin(midBoss.t * 7) * 0.025;
      if (assets.rabbitMidboss.complete && assets.rabbitMidboss.naturalWidth) {
        const size = 138 * pulse;
        ctx.shadowBlur = 24;
        ctx.shadowColor = midBoss.hp / midBoss.maxHp < 0.5 ? "#ff7ad5" : "#8ff6ff";
        ctx.drawImage(assets.rabbitMidboss, midBoss.x - size / 2, midBoss.y - size / 2, size, size);
      } else {
        ctx.fillStyle = "#f7fbff";
        ctx.beginPath();
        ctx.ellipse(midBoss.x, midBoss.y, 48, 58, 0, 0, Math.PI * 2);
        ctx.fill();
      }
    }
    ctx.shadowBlur = 0;
    ctx.fillStyle = "rgba(255,255,255,0.25)";
    ctx.fillRect(72, 46, W - 144, 7);
    ctx.fillStyle = currentStage === 9 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#ff8ad8" : "#b78cff") : currentStage === 8 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#ff9d4d" : "#9cecff") : currentStage === 7 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#9d72ff" : "#d7e9ff") : currentStage === 6 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#ff5f76" : "#ff9b48") : currentStage === 5 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#ff6f93" : "#b98cff") : currentStage === 4 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#f4d36b" : "#73d9ff") : currentStage === 3 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#fff0a8" : "#ff9bd7") : currentStage === 2 ? (midBoss.hp / midBoss.maxHp < 0.5 ? "#fff2a6" : "#7dbbff") : (midBoss.hp / midBoss.maxHp < 0.5 ? "#ff8bd8" : "#8ff6ff");
    ctx.fillRect(72, 46, (W - 144) * Math.max(0, midBoss.hp / midBoss.maxHp), 7);
    ctx.fillStyle = "rgba(255, 246, 255, 0.86)";
    ctx.font = "800 12px Segoe UI, sans-serif";
    ctx.textAlign = "center";
    ctx.fillText(currentStage === 9 ? "CONVEYOR CONTROLLER" : currentStage === 8 ? "POLARITY CONTROLLER" : currentStage === 7 ? "GRAVITY WARDEN" : currentStage === 6 ? "ARES BREAKER" : currentStage === 5 ? "NEON LYNX" : currentStage === 4 ? "AEGIS RAVEN" : currentStage === 3 ? "BLOOM CARRIER" : currentStage === 2 ? "SUPPLY GUARDIAN" : "BUNNY MID-BOSS", W / 2, 42);
    ctx.textAlign = "left";
    ctx.restore();
  }
  function drawBoss() {
    if (!boss) return;
    if (currentStage === 9) drawStage9BossShip();
    else if (currentStage === 8) drawStage8BossShip();
    else if (currentStage === 7) drawStage7BossShip();
    else if (currentStage === 6) drawStage6BossShip();
    else if (currentStage === 5) drawStage5BossShip();
    else if (currentStage === 4) drawStage4BossShip();
    else if (currentStage === 3) drawStage3BossShip();
    else if (currentStage === 2) drawStage2BossShip();
    else drawSprite(frames.boss, boss.x, boss.y, 188, 188);
    ctx.fillStyle = "rgba(255,255,255,0.25)";
    ctx.fillRect(58, 18, W - 116, 8);
    ctx.fillStyle = currentStage === 9 ? (boss.hp / boss.maxHp < 0.48 ? "#ff8ad8" : "#b78cff") : currentStage === 8 ? (boss.hp / boss.maxHp < 0.48 ? "#ff9d4d" : "#9cecff") : currentStage === 7 ? (boss.hp / boss.maxHp < 0.48 ? "#ff82c8" : "#7eeeff") : currentStage === 6 ? (boss.hp / boss.maxHp < 0.48 ? "#ff9b48" : "#ff5ab8") : currentStage === 5 ? (boss.hp / boss.maxHp < 0.48 ? "#ffb05f" : "#ff526e") : currentStage === 4 ? (boss.hp / boss.maxHp < 0.48 ? "#f4d36b" : "#73d9ff") : (boss.hp / boss.maxHp < 0.48 ? "#ffd45f" : "#ff5bb8");
    ctx.fillRect(58, 18, (W - 116) * Math.max(0, boss.hp / boss.maxHp), 8);
  }

  function drawBullets() {
    ctx.save();
    const totalActionObjects = playerBullets.length + enemyBullets.length + enemies.length + particles.length;
    const lowBulletFx = reducedEffects || totalActionObjects > 190 || enemyBullets.length > (currentStage === 9 ? 82 : currentStage === 8 ? 88 : 110);
    const stage9LowFx = currentStage === 9 && enemyBullets.length > 82;
    const stage8LowFx = currentStage === 8 && enemyBullets.length > 88;
    for (const b of playerBullets) {
      ctx.shadowBlur = b.kind === "pierce" ? 18 : 12;
      ctx.shadowColor = "#79ecff";
      if (lowBulletFx) {
        ctx.shadowBlur = 4;
        ctx.fillStyle = "#e9ffff";
      } else {
        const g = ctx.createLinearGradient(b.x, b.y + b.r * 3, b.x, b.y - b.r * 3);
        g.addColorStop(0, "#3bd4ff");
        g.addColorStop(0.45, "#e9ffff");
        g.addColorStop(1, "#ffffff");
        ctx.fillStyle = g;
      }
      ctx.beginPath();
      ctx.ellipse(b.x, b.y, b.r * 0.9, b.r * 2.35, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 0.28;
      ctx.fillStyle = "#9df4ff";
      ctx.beginPath();
      ctx.ellipse(b.x, b.y + 11, b.r * 0.6, b.r * 3.2, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 1;
    }
    for (const b of enemyBullets) {
      if (["stage9Drone", "stage9Core", "stage9Curve", "stage9Arc", "stage9Debris"].includes(b.kind)) {
        const r = b.r || 7;
        const a = Math.atan2(b.vy, b.vx);
        if (stage9LowFx && (b.kind === "stage9Curve" || b.kind === "stage9Arc" || b.kind === "stage9Debris")) {
          ctx.shadowBlur = 0;
          ctx.globalCompositeOperation = "source-over";
          ctx.fillStyle = b.color;
          ctx.beginPath();
          if (b.kind === "stage9Debris") {
            ctx.save();
            ctx.translate(b.x, b.y);
            ctx.rotate(b.age * 3.4 * (b.wobble || 1));
            ctx.moveTo(0, -r * 1.35);
            ctx.lineTo(r * 0.9, r * 0.7);
            ctx.lineTo(-r * 0.75, r);
            ctx.closePath();
            ctx.fill();
            ctx.restore();
          } else {
            ctx.ellipse(b.x, b.y, r * 1.45, r * 0.66, a, 0, Math.PI * 2);
            ctx.fill();
          }
          continue;
        }
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.kind === "stage9Drone" ? b.age * 5.2 : b.kind === "stage9Core" ? b.age * 1.8 : b.kind === "stage9Debris" ? b.age * 3.4 * (b.wobble || 1) : a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 6 : (b.kind === "stage9Core" ? 28 : 18);
        ctx.shadowColor = b.color;
        if (b.kind === "stage9Drone" && assets.stage9ConveyorDrone.complete && assets.stage9ConveyorDrone.naturalWidth) {
          const size = r * 3.5;
          ctx.drawImage(assets.stage9ConveyorDrone, -size / 2, -size / 2, size, size);
        } else if (b.kind === "stage9Core" && assets.stage9GravityCore.complete && assets.stage9GravityCore.naturalWidth) {
          const size = r * 2.75;
          ctx.drawImage(assets.stage9GravityCore, -size / 2, -size / 2, size, size);
          ctx.strokeStyle = "rgba(255,255,255,0.78)";
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.arc(0, 0, r * (1.45 + Math.sin(b.age * 10) * 0.12), 0, Math.PI * 2);
          ctx.stroke();
        } else if (b.kind === "stage9Debris") {
          ctx.globalCompositeOperation = "source-over";
          ctx.fillStyle = b.color;
          ctx.strokeStyle = "rgba(255, 209, 138, 0.72)";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.moveTo(0, -r * 1.45);
          ctx.lineTo(r, r * 0.68);
          ctx.lineTo(-r * 0.82, r);
          ctx.closePath();
          ctx.fill();
          ctx.stroke();
        } else {
          ctx.fillStyle = b.color;
          ctx.strokeStyle = "rgba(255,255,255,0.82)";
          ctx.lineWidth = 1.2;
          ctx.beginPath();
          ctx.ellipse(0, 0, r * 1.65, r * 0.72, 0, 0, Math.PI * 2);
          ctx.fill();
          ctx.stroke();
        }
        ctx.restore();
        continue;
      }
      if (["stage8Beacon", "stage8Freight", "stage8Signal", "stage8Cargo"].includes(b.kind)) {
        const r = b.r || 7;
        const a = Math.atan2(b.vy, b.vx);
        if (stage8LowFx && (b.kind === "stage8Signal" || b.kind === "stage8Cargo")) {
          ctx.shadowBlur = 0;
          ctx.globalCompositeOperation = "source-over";
          ctx.fillStyle = b.color;
          ctx.beginPath();
          ctx.ellipse(b.x, b.y, b.kind === "stage8Cargo" ? r * 1.45 : r * 1.55, b.kind === "stage8Cargo" ? r * 0.7 : r * 0.62, a, 0, Math.PI * 2);
          ctx.fill();
          continue;
        }
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate((b.kind === "stage8Beacon" || b.kind === "stage8Freight") ? a + Math.PI / 2 : a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 7 : 20;
        ctx.shadowColor = b.color;
        if (b.kind === "stage8Beacon" && assets.stage8RouteBeacon.complete && assets.stage8RouteBeacon.naturalWidth) {
          const size = r * 3.6;
          ctx.drawImage(assets.stage8RouteBeacon, -size / 2, -size / 2, size, size);
        } else if (b.kind === "stage8Freight" && assets.stage8FreightCapsule.complete && assets.stage8FreightCapsule.naturalWidth) {
          const size = r * 3.7;
          ctx.drawImage(assets.stage8FreightCapsule, -size / 2, -size / 2, size, size);
        } else {
          ctx.fillStyle = b.color;
          ctx.strokeStyle = "#ffffff";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          if (b.kind === "stage8Cargo") ctx.roundRect(-r * 1.5, -r * 0.72, r * 3, r * 1.44, 3);
          else {
            ctx.moveTo(r * 1.8, 0);
            ctx.lineTo(-r, -r * 0.75);
            ctx.lineTo(-r * 0.45, 0);
            ctx.lineTo(-r, r * 0.75);
            ctx.closePath();
          }
          ctx.fill();
          ctx.stroke();
        }
        ctx.restore();
        continue;
      }
      if (["stage7Eclipse", "stage7PawMine", "stage7PawPellet", "stage7Scratch", "stage7Ink"].includes(b.kind)) {
        const r = b.r || 7;
        const a = Math.atan2(b.vy, b.vx);
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.kind === "stage7PawMine" ? Math.sin(b.age * 3) * 0.12 : b.kind === "stage7Eclipse" ? b.age * 5.4 : a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 7 : 22;
        ctx.shadowColor = b.color;
        if (b.kind === "stage7Eclipse" && assets.stage7EclipseBlade.complete && assets.stage7EclipseBlade.naturalWidth) {
          const size = r * 4;
          ctx.drawImage(assets.stage7EclipseBlade, -size / 2, -size / 2, size, size);
        } else if (b.kind === "stage7PawMine" && assets.stage7CatPawMine.complete && assets.stage7CatPawMine.naturalWidth) {
          const size = r * 3.8;
          ctx.drawImage(assets.stage7CatPawMine, -size / 2, -size / 2, size, size);
        } else {
          ctx.fillStyle = b.color;
          ctx.strokeStyle = "#ffffff";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          if (b.kind === "stage7Scratch") ctx.roundRect(-r * 2.5, -r * 0.45, r * 5, r * 0.9, r * 0.4);
          else if (b.kind === "stage7PawPellet") ctx.arc(0, 0, r, 0, Math.PI * 2);
          else {
            ctx.moveTo(r * 1.8, 0);
            ctx.lineTo(-r * 0.9, -r * 0.65);
            ctx.lineTo(-r * 0.35, 0);
            ctx.lineTo(-r * 0.9, r * 0.65);
            ctx.closePath();
          }
          ctx.fill();
          ctx.stroke();
        }
        ctx.restore();
        continue;
      }      if (["stage8Flip", "stage8PolarityNode", "stage8Spark", "stage8Rail"].includes(b.kind)) {
        const r = b.r || 7;
        const a = Math.atan2(b.vy, b.vx);
        if (stage8LowFx && b.kind === "stage8Spark") {
          ctx.shadowBlur = 0;
          ctx.globalCompositeOperation = "source-over";
          ctx.fillStyle = b.color;
          ctx.beginPath();
          ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
          ctx.fill();
          continue;
        }
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.kind === "stage8Rail" && b.orientation === "vertical" ? Math.PI / 2 : a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 24;
        ctx.shadowColor = b.color;
        ctx.strokeStyle = "#ffffff";
        ctx.fillStyle = b.color;
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        if (b.kind === "stage8Rail") {
          ctx.roundRect(-r * 2.8, -r * 0.72, r * 5.6, r * 1.44, 3);
        } else if (b.kind === "stage8PolarityNode") {
          ctx.arc(0, 0, r * 1.25, 0, Math.PI * 2);
          ctx.moveTo(-r * 1.8, 0);
          ctx.lineTo(r * 1.8, 0);
          ctx.moveTo(0, -r * 1.8);
          ctx.lineTo(0, r * 1.8);
        } else if (b.kind === "stage8Flip") {
          ctx.moveTo(r * 2.1, 0);
          ctx.lineTo(-r * 0.9, -r);
          ctx.lineTo(-r * 0.25, 0);
          ctx.lineTo(-r * 0.9, r);
          ctx.closePath();
        } else {
          ctx.arc(0, 0, r, 0, Math.PI * 2);
        }
        ctx.fill();
        ctx.stroke();
        ctx.restore();
        continue;
      }
      if (["stage7Anchor", "stage7Drone", "stage7Blade", "stage7Shutter"].includes(b.kind)) {
        const r = b.r || 7;
        const a = Math.atan2(b.vy, b.vx);
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.kind === "stage7Blade" ? a : b.age * 3.2);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 7 : 24;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.kind === "stage7Shutter" ? "rgba(255,130,200,0.76)" : b.kind === "stage7Drone" ? "rgba(20,18,42,0.96)" : b.color;
        ctx.strokeStyle = "#ffffff";
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        if (b.kind === "stage7Blade") {
          ctx.moveTo(r * 2.5, 0);
          ctx.lineTo(-r * 1.1, -r * 0.7);
          ctx.lineTo(-r * 0.35, 0);
          ctx.lineTo(-r * 1.1, r * 0.7);
        } else if (b.kind === "stage7Shutter") {
          ctx.rect(-r * 2.1, -r * 0.75, r * 4.2, r * 1.5);
        } else {
          const sides = b.kind === "stage7Drone" ? 6 : 8;
          for (let i = 0; i < sides; i++) {
            const aa = i * Math.PI * 2 / sides;
            const rr = i % 2 ? r * 0.75 : r * 1.4;
            if (i === 0) ctx.moveTo(Math.cos(aa) * rr, Math.sin(aa) * rr);
            else ctx.lineTo(Math.cos(aa) * rr, Math.sin(aa) * rr);
          }
          ctx.closePath();
        }
        ctx.fill();
        ctx.stroke();
        ctx.restore();
        continue;
      }
      if (["stage6Shell", "stage6Cluster"].includes(b.kind)) {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 7;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.shadowBlur = lowBulletFx ? 7 : 20;
        ctx.shadowColor = b.color;
        ctx.fillStyle = "#24231d";
        ctx.strokeStyle = b.color;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(r * 2.2, 0);
        ctx.lineTo(r * 0.4, -r * 0.9);
        ctx.lineTo(-r * 1.4, -r * 0.65);
        ctx.lineTo(-r * 1.8, 0);
        ctx.lineTo(-r * 1.4, r * 0.65);
        ctx.lineTo(r * 0.4, r * 0.9);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(r * 0.65, 0, r * 0.32, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (["stage6Mine", "stage6Drone", "stage6Core"].includes(b.kind)) {
        const r = b.r || 10;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.age * (b.kind === "stage6Core" ? 1.8 : 3.6));
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 26;
        ctx.shadowColor = b.color;
        ctx.strokeStyle = b.color;
        ctx.lineWidth = 3;
        ctx.fillStyle = b.kind === "stage6Core" ? "rgba(255,75,180,0.62)" : "rgba(28,27,25,0.92)";
        ctx.beginPath();
        const sides = b.kind === "stage6Core" ? 10 : 6;
        for (let i = 0; i < sides; i++) {
          const a = i * Math.PI * 2 / sides;
          const rr = i % 2 ? r * 0.78 : r * 1.35;
          if (i === 0) ctx.moveTo(Math.cos(a) * rr, Math.sin(a) * rr); else ctx.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
        }
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(0, 0, r * 0.36, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (["stage6Ricochet", "stage6Shard", "stage6ShockShard"].includes(b.kind)) {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 5;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 5 : 16;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.moveTo(r * 2.2, 0);
        ctx.lineTo(0, -r * 0.9);
        ctx.lineTo(-r * 1.4, 0);
        ctx.lineTo(0, r * 0.9);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (["stage5JammerNode", "stage5CrownNode", "stage5RoadOrb"].includes(b.kind)) {
        const r = b.r || 8;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(b.age * 2.6);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 24;
        ctx.shadowColor = b.color;
        ctx.strokeStyle = b.color;
        ctx.lineWidth = 2.5;
        ctx.beginPath();
        for (let i = 0; i < 6; i++) {
          const a = i * Math.PI / 3;
          const x = Math.cos(a) * r * 1.45;
          const y = Math.sin(a) * r * 1.45;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.closePath();
        ctx.stroke();
        ctx.fillStyle = "rgba(255,255,255,0.82)";
        ctx.beginPath();
        ctx.arc(0, 0, r * 0.52, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (["stage5Arc", "stage5RoadShot", "stage5PulseDrop"].includes(b.kind)) {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 4.5;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 6 : 18;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.ellipse(0, 0, r * 2.1, r * 0.72, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.ellipse(r * 0.55, 0, r * 0.65, r * 0.28, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage4ScissorJet") {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 13;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 24;
        ctx.shadowColor = b.color;
        const trail = ctx.createLinearGradient(-r * 7, 0, -r, 0);
        trail.addColorStop(0, "rgba(255,255,255,0)");
        trail.addColorStop(1, b.color);
        ctx.strokeStyle = trail;
        ctx.lineWidth = 5;
        ctx.beginPath();
        ctx.moveTo(-r * 7, 0);
        ctx.lineTo(-r * 1.2, 0);
        ctx.stroke();
        ctx.fillStyle = "#162b38";
        ctx.strokeStyle = b.color;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(r * 2.1, 0);
        ctx.lineTo(r * 0.25, -r * 0.42);
        ctx.lineTo(-r * 1.1, -r * 1.18);
        ctx.lineTo(-r * 0.68, -r * 0.18);
        ctx.lineTo(-r * 1.5, 0);
        ctx.lineTo(-r * 0.68, r * 0.18);
        ctx.lineTo(-r * 1.1, r * 1.18);
        ctx.lineTo(r * 0.25, r * 0.42);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.ellipse(r * 0.42, 0, r * 0.42, r * 0.22, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage4WingShot") {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 5.5;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 6 : 18;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.moveTo(r * 2.3, 0);
        ctx.lineTo(-r, -r * 0.82);
        ctx.lineTo(-r * 0.35, 0);
        ctx.lineTo(-r, r * 0.82);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage4SiegeTurret") {
        const r = b.r || 18;
        const recoil = b.recoil || 0;
        const aim = Math.atan2(player.y - b.y, player.x - b.x);
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 26;
        ctx.shadowColor = b.color;
        ctx.rotate(b.age * (b.turretIndex % 2 ? 0.7 : -0.7));
        ctx.fillStyle = "rgba(12,24,34,0.96)";
        ctx.strokeStyle = b.color;
        ctx.lineWidth = 3;
        ctx.beginPath();
        for (let i = 0; i < 8; i++) {
          const a0 = Math.PI * 2 * i / 8;
          const px = Math.cos(a0) * r;
          const py = Math.sin(a0) * r;
          if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
        }
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.rotate(-b.age * (b.turretIndex % 2 ? 0.7 : -0.7));
        ctx.rotate(aim);
        ctx.fillStyle = b.color;
        ctx.fillRect(2 - recoil * 7, -4, r * 1.45, 8);
        ctx.fillStyle = "#ffffff";
        ctx.fillRect(r * 0.82 - recoil * 7, -2, r * 0.78, 4);
        ctx.rotate(-aim);
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(0, 0, r * 0.34, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage4SiegeShot") {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 6;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 7 : 22;
        ctx.shadowColor = b.color;
        ctx.strokeStyle = b.color;
        ctx.lineWidth = r * 0.7;
        ctx.globalAlpha = 0.42;
        ctx.beginPath();
        ctx.moveTo(-r * 5.5, 0);
        ctx.lineTo(-r, 0);
        ctx.stroke();
        ctx.globalAlpha = 1;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.moveTo(r * 2.2, 0);
        ctx.lineTo(0, -r);
        ctx.lineTo(-r * 1.25, 0);
        ctx.lineTo(0, r);
        ctx.closePath();
        ctx.fill();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(r * 0.28, 0, r * 0.38, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage4Missile") {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 7;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 6 : 22;
        ctx.shadowColor = b.color;
        const body = ctx.createLinearGradient(-r * 2, 0, r * 2.2, 0);
        body.addColorStop(0, "#183a48");
        body.addColorStop(0.48, b.color);
        body.addColorStop(1, "#ffffff");
        ctx.fillStyle = body;
        ctx.beginPath();
        ctx.moveTo(r * 2.2, 0);
        ctx.lineTo(-r * 1.3, -r * 0.72);
        ctx.lineTo(-r * 0.7, 0);
        ctx.lineTo(-r * 1.3, r * 0.72);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.76)";
        ctx.lineWidth = 1.2;
        ctx.stroke();
        ctx.restore();
        if (!lowBulletFx && b.hp < b.maxHp) {
          ctx.strokeStyle = "rgba(255,255,255,0.65)";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.arc(b.x, b.y, r + 5, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
          ctx.stroke();
        }
        continue;
      }
      if (b.kind === "stage4Flak") {
        const r = b.r || 9;
        const pulse = 1 + Math.sin(b.age * 14) * 0.08;
        ctx.save();
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : 26;
        ctx.shadowColor = b.color;
        const shell = ctx.createRadialGradient(b.x - r * 0.28, b.y - r * 0.32, 1, b.x, b.y, r * 1.45);
        shell.addColorStop(0, "#ffffff");
        shell.addColorStop(0.34, b.color);
        shell.addColorStop(1, "rgba(30, 40, 52, 0.06)");
        ctx.fillStyle = shell;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r * 1.2 * pulse, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.76)";
        ctx.lineWidth = 1.4;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r * (1.55 + Math.sin(b.age * 8) * 0.12), 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
        continue;
      }
      if (b.kind === "star") {
        const a = Math.atan2(b.vy, b.vx) + b.age * 1.8;
        const r = b.r;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 0 : 18;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        for (let i = 0; i < 10; i++) {
          const rr = i % 2 ? r * 0.48 : r * 1.35;
          const aa = -Math.PI / 2 + i * Math.PI / 5;
          const px = Math.cos(aa) * rr;
          const py = Math.sin(aa) * rr;
          if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
        }
        ctx.closePath();
        ctx.fill();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(0, 0, r * 0.42, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        if (!lowBulletFx && b.hp < b.maxHp) {
          ctx.strokeStyle = "rgba(255, 255, 255, 0.62)";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.arc(b.x, b.y, r + 5, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
          ctx.stroke();
        }
        continue;
      }
      if (b.kind === "stage3ShadowDrone") {
        const armed = !!b.dashLocked || b.age >= (b.armTime || 1.05);
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 12;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : (armed ? 26 : 16);
        ctx.shadowColor = armed ? "#ffb15f" : "#8fdcff";
        ctx.fillStyle = armed ? "#10141d" : "#182232";
        ctx.beginPath();
        ctx.moveTo(r * 1.7, 0);
        ctx.lineTo(-r * 0.9, -r * 0.95);
        ctx.lineTo(-r * 0.55, 0);
        ctx.lineTo(-r * 0.9, r * 0.95);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = armed ? "rgba(255, 177, 95, 0.9)" : "rgba(143, 220, 255, 0.88)";
        ctx.lineWidth = 1.8;
        ctx.stroke();
        ctx.fillStyle = "#ffffff";
        ctx.beginPath();
        ctx.arc(r * 0.18, 0, r * 0.32, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        continue;
      }
      if (b.kind === "stage3WingShard") {
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 5.6;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 0 : 18;
        ctx.shadowColor = b.color;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.moveTo(r * 2.2, 0);
        ctx.lineTo(-r * 1.2, -r * 0.55);
        ctx.lineTo(-r * 0.35, 0);
        ctx.lineTo(-r * 1.2, r * 0.55);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.72)";
        ctx.lineWidth = 1.2;
        ctx.stroke();
        ctx.restore();
        continue;
      }      if (b.kind === "stage3FlowerBit") {
        const armed = !!b.dashLocked || b.age >= (b.spinTime || 0.72);
        const a = Math.atan2(b.vy, b.vx);
        const r = b.r || 13;
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 8 : (armed ? 26 : 14);
        ctx.shadowColor = armed ? "#fff0a8" : "#ff9bd7";
        const petal = ctx.createLinearGradient(-r, 0, r * 1.6, 0);
        petal.addColorStop(0, armed ? "#ff76cf" : "#ffc6e8");
        petal.addColorStop(0.44, "#ffffff");
        petal.addColorStop(1, armed ? "#fff070" : "#ff9bd7");
        ctx.fillStyle = petal;
        ctx.beginPath();
        ctx.moveTo(r * 1.7, 0);
        ctx.bezierCurveTo(r * 0.35, -r * 0.95, -r * 1.35, -r * 0.62, -r * 1.45, 0);
        ctx.bezierCurveTo(-r * 1.35, r * 0.62, r * 0.35, r * 0.95, r * 1.7, 0);
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.82)";
        ctx.lineWidth = 1.4;
        ctx.stroke();
        ctx.restore();
        if (!lowBulletFx && b.hp < b.maxHp) {
          ctx.strokeStyle = "rgba(255, 255, 255, 0.62)";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.arc(b.x, b.y, r + 5, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
          ctx.stroke();
        }
        continue;
      }
      if (b.kind === "stage2EnergyOrb") {
        const r = b.r;
        const pulse = 1 + Math.sin(b.age * 12) * 0.08;
        ctx.save();
        ctx.globalCompositeOperation = "screen";
        ctx.shadowBlur = lowBulletFx ? 12 : 32;
        ctx.shadowColor = b.color;
        const g = ctx.createRadialGradient(b.x - r * 0.35, b.y - r * 0.45, 2, b.x, b.y, r * 1.65);
        g.addColorStop(0, "#ffffff");
        g.addColorStop(0.28, b.color);
        g.addColorStop(0.72, "rgba(255, 91, 232, 0.62)");
        g.addColorStop(1, "rgba(60, 3, 84, 0.05)");
        ctx.fillStyle = g;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r * 1.35 * pulse, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.82)";
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r * (0.88 + Math.sin(b.age * 18) * 0.06), 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
        if (!lowBulletFx && b.hp < b.maxHp) {
          ctx.strokeStyle = "rgba(255, 255, 255, 0.68)";
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.arc(b.x, b.y, r + 8, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
          ctx.stroke();
        }
        continue;
      }
      if (b.kind === "carrot") {
        const a = Math.atan2(b.vy, b.vx);
        const armed = b.age >= (b.armTime || 0.55);
        ctx.save();
        ctx.translate(b.x, b.y);
        ctx.rotate(a);
        ctx.shadowBlur = lowBulletFx ? 0 : (armed ? 22 : 10);
        ctx.shadowColor = armed ? "#ff9a36" : "#ffd080";
        const body = ctx.createLinearGradient(-12, 0, 15, 0);
        body.addColorStop(0, "#ffcf74");
        body.addColorStop(0.35, "#ff8e2f");
        body.addColorStop(1, "#ff5c1f");
        ctx.fillStyle = body;
        ctx.beginPath();
        ctx.moveTo(16, 0);
        ctx.lineTo(-10, -7);
        ctx.lineTo(-13, 0);
        ctx.lineTo(-10, 7);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = "rgba(255,255,255,0.72)";
        ctx.lineWidth = 1.3;
        ctx.beginPath();
        ctx.moveTo(-3, -3);
        ctx.lineTo(8, -1);
        ctx.stroke();
        ctx.fillStyle = armed ? "#6fff9a" : "#b8ffc8";
        ctx.beginPath();
        ctx.moveTo(-14, 0);
        ctx.lineTo(-23, -9);
        ctx.lineTo(-19, 1);
        ctx.lineTo(-24, 9);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
        if (!lowBulletFx && b.hp < b.maxHp) {
          ctx.shadowBlur = 0;
          ctx.strokeStyle = "rgba(255, 255, 255, 0.62)";
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          ctx.arc(b.x, b.y, b.r + 5, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
          ctx.stroke();
        }
        continue;
      }
      const r = b.r;
      if (lowBulletFx) {
        ctx.shadowBlur = 0;
        ctx.fillStyle = b.color;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
        ctx.fill();
        continue;
      }
      ctx.shadowBlur = 16;
      ctx.shadowColor = b.color;
      const g = ctx.createRadialGradient(b.x - r * 0.45, b.y - r * 0.55, r * 0.1, b.x, b.y, r * 1.35);
      g.addColorStop(0, "#ffffff");
      g.addColorStop(0.22, "#ffeef8");
      g.addColorStop(0.62, b.color);
      g.addColorStop(1, "rgba(40, 2, 44, 0.25)");
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 0.32;
      ctx.fillStyle = "#16051d";
      ctx.beginPath();
      ctx.arc(b.x + r * 0.35, b.y + r * 0.45, r * 0.55, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 1;
      if (!lowBulletFx && b.hp < b.maxHp) {
        ctx.shadowBlur = 0;
        ctx.strokeStyle = "rgba(255, 255, 255, 0.62)";
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(b.x, b.y, r + 3, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.max(0, b.hp / b.maxHp));
        ctx.stroke();
      }
    }
    ctx.restore();
  }
  function drawLasers() {
    ctx.save();
    ctx.globalCompositeOperation = "screen";
    for (const l of lasers) {
      const active = l.life < l.maxLife - l.warmup;
      const ex = l.x + Math.cos(l.angle) * l.length;
      const ey = l.y + Math.sin(l.angle) * l.length;
      const pulse = active ? 1 + Math.sin(l.life * 48) * 0.1 : 0.45;
      const isBossLaser = l.kind === "boss" || l.kind === "bossBurst";
      const glow = isBossLaser ? "#ff4fd8" : "#67f4ff";
      const outer = isBossLaser ? "rgba(255, 63, 216, 0.34)" : "rgba(103, 244, 255, 0.24)";
      const core = isBossLaser ? "rgba(255, 163, 241, 0.82)" : "rgba(196, 255, 255, 0.78)";
      ctx.lineCap = "round";
      ctx.shadowColor = glow;
      ctx.shadowBlur = active ? (isBossLaser ? 32 : 22) : 12;
      ctx.strokeStyle = active ? outer : (isBossLaser ? "rgba(255, 149, 232, 0.22)" : "rgba(130, 245, 255, 0.18)");
      ctx.lineWidth = active ? l.width * (isBossLaser ? 3.4 : 2.35) * pulse : 2.2;
      ctx.beginPath();
      ctx.moveTo(l.x, l.y);
      ctx.lineTo(ex, ey);
      ctx.stroke();
      ctx.strokeStyle = active ? core : (isBossLaser ? "rgba(255, 190, 242, 0.5)" : "rgba(205, 255, 255, 0.42)");
      ctx.lineWidth = active ? l.width * (isBossLaser ? 1.35 : 0.82) : 1.2;
      ctx.beginPath();
      ctx.moveTo(l.x, l.y);
      ctx.lineTo(ex, ey);
      ctx.stroke();
      if (active) {
        ctx.strokeStyle = isBossLaser ? "#fff0fb" : "#efffff";
        ctx.lineWidth = Math.max(1.4, l.width * (isBossLaser ? 0.34 : 0.24));
        ctx.beginPath();
        ctx.moveTo(l.x, l.y);
        ctx.lineTo(ex, ey);
        ctx.stroke();
      }
    }
    ctx.restore();
  }
  function drawPowerUps() {
    ctx.save();
    for (const p of powerUps) {
      const pulse = 1 + Math.sin(p.t * 9) * 0.12;
      const isHeal = p.type === "heal";
      ctx.shadowBlur = isHeal ? 24 : 18;
      ctx.shadowColor = isHeal ? "#ffef8a" : "#7dffcf";
      ctx.fillStyle = isHeal ? "rgba(255, 224, 82, 0.24)" : "rgba(21, 255, 189, 0.22)";
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r * (isHeal ? 1.9 : 1.6) * pulse, 0, Math.PI * 2);
      ctx.fill();
      const g = ctx.createRadialGradient(p.x - 4, p.y - 5, 1, p.x, p.y, p.r);
      if (isHeal) {
        g.addColorStop(0, "#ffffff");
        g.addColorStop(0.45, "#ffef8a");
        g.addColorStop(1, "#d99020");
      } else {
        g.addColorStop(0, "#ffffff");
        g.addColorStop(0.45, "#75ffd0");
        g.addColorStop(1, "#0c8e71");
      }
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r * pulse, 0, Math.PI * 2);
      ctx.fill();
      if (isHeal) {
        ctx.strokeStyle = "#fff8c8";
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(p.x - p.r * 0.55, p.y);
        ctx.lineTo(p.x + p.r * 0.55, p.y);
        ctx.moveTo(p.x, p.y - p.r * 0.55);
        ctx.lineTo(p.x, p.y + p.r * 0.55);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
      ctx.fillStyle = isHeal ? "#4c2500" : "#06241f";
      ctx.font = "800 13px Segoe UI, sans-serif";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(isHeal ? "H" : "P", p.x, p.y + 0.5);
    }
    ctx.textAlign = "left";
    ctx.textBaseline = "alphabetic";
    ctx.restore();
  }


  function destructionShipImage(kind, stage = currentStage) {
    if (stage === 9) return kind === "boss" ? assets.stage9BossShip : assets.stage9MidbossShip;
    if (stage === 8) return kind === "boss" ? assets.stage8BossShip : assets.stage8MidbossShip;
    if (stage === 7) return kind === "boss" ? assets.stage7BossShip : assets.stage7MidbossShip;
    if (stage === 6) return kind === "boss" ? assets.stage6BossShip : assets.stage6MidbossShip;
    if (stage === 5) return kind === "boss" ? assets.stage5BossShip : assets.stage5MidbossShip;
    if (stage === 4) return kind === "boss" ? assets.stage4BossShip : assets.stage4MidbossShip;
    if (stage === 3) return kind === "boss" ? assets.stage3BossShip : assets.stage3MidbossShip;
    if (stage === 2) return kind === "boss" ? assets.stage2BossShip : assets.stage2MidbossShip;
    return kind === "boss" ? assets.sprites : assets.rabbitMidboss;
  }

  function drawDestructionShipImage(img, kind, stage, size) {
    if (!img || !img.complete || !img.naturalWidth) return false;
    if (stage === 9 || stage === 8 || stage === 7) {
      drawStage7ShipImage(img, 0, 0, size);
      return true;
    }
    if (stage === 6) {
      drawStage6ShipImage(img, 0, 0, size);
      return true;
    }
    if (stage >= 2) {
      drawGeneratedStage2Ship(img, 0, 0, size);
      return true;
    }
    if (kind === "boss") {
      drawSprite(frames.boss, 0, 0, size, size);
      return true;
    }
    ctx.drawImage(img, -size / 2, -size / 2, size, size);
    return true;
  }

  function drawWreckPieceImage(piece) {
    const stage = piece.stage || 1;
    const img = destructionShipImage(piece.kind, stage);
    if (!img || !img.complete || !img.naturalWidth) return false;
    if (stage >= 2) {
      const cw = img.width * 0.42;
      const ch = img.height * 0.42;
      const sx = Math.min(img.width - cw, piece.clipX * img.width * 0.58);
      const sy = Math.min(img.height - ch, piece.clipY * img.height * 0.58);
      ctx.drawImage(img, sx, sy, cw, ch, -piece.size / 2, -piece.size / 2, piece.size, piece.size);
      return true;
    }
    if (piece.kind === "boss") {
      const src = frames.boss;
      const cw = src.w * 0.42;
      const ch = src.h * 0.42;
      const sx = src.x + Math.min(src.w - cw, piece.clipX * src.w * 0.58);
      const sy = src.y + Math.min(src.h - ch, piece.clipY * src.h * 0.52);
      ctx.drawImage(assets.sprites, sx, sy, cw, ch, -piece.size / 2, -piece.size / 2, piece.size, piece.size);
      return true;
    }
    const cw = img.width * 0.48;
    const ch = img.height * 0.48;
    const sx = Math.min(img.width - cw, piece.clipX * img.width * 0.52);
    const sy = Math.min(img.height - ch, piece.clipY * img.height * 0.52);
    ctx.drawImage(img, sx, sy, cw, ch, -piece.size / 2, -piece.size / 2, piece.size, piece.size);
    return true;
  }
  function drawDestructionCore() {
    if (!destructionScene) return;
    const p = Math.max(0, Math.min(1, destructionScene.t / destructionScene.duration));
    const isBoss = destructionScene.kind === "boss";
    const size = (destructionScene.stage === 9 ? (isBoss ? 312 : 238) : destructionScene.stage === 8 ? (isBoss ? 296 : 226) : destructionScene.stage === 7 ? (isBoss ? 282 : 218) : destructionScene.stage === 6 ? (isBoss ? 262 : 204) : destructionScene.stage === 5 ? (isBoss ? 246 : 186) : destructionScene.stage === 4 ? (isBoss ? 236 : 174) : destructionScene.stage === 3 ? (isBoss ? 228 : 168) : destructionScene.stage === 2 ? (isBoss ? 214 : 150) : isBoss ? 188 : 112) * (1.08 - p * 0.18 + Math.sin(destructionScene.t * 35) * 0.018);
    ctx.save();
    ctx.translate(destructionScene.x + (Math.random() - 0.5) * shake * 10, destructionScene.y + (Math.random() - 0.5) * shake * 10);
    ctx.rotate(Math.sin(destructionScene.t * 8) * (isBoss ? 0.05 : 0.08));
    ctx.globalAlpha = Math.max(0, 0.92 - p * 0.68);
    ctx.shadowBlur = isBoss ? 34 : 24;
    ctx.shadowColor = isBoss ? "#ff5be9" : "#8ff6ff";
    const shipImage = destructionShipImage(destructionScene.kind, destructionScene.stage);
    drawDestructionShipImage(shipImage, destructionScene.kind, destructionScene.stage, size);
    ctx.globalCompositeOperation = "lighter";
    ctx.globalAlpha = Math.max(0, 0.55 - p * 0.35);
    const hot = ctx.createRadialGradient(0, 0, 3, 0, 0, size * 0.62);
    hot.addColorStop(0, "rgba(255, 255, 255, 0.92)");
    hot.addColorStop(0.22, isBoss ? "rgba(255, 92, 229, 0.5)" : "rgba(143, 246, 255, 0.44)");
    hot.addColorStop(1, "rgba(255, 100, 40, 0)");
    ctx.fillStyle = hot;
    ctx.beginPath();
    ctx.arc(0, 0, size * 0.62, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
  function drawWreckPieces() {
    if (!wreckPieces.length) return;
    ctx.save();
    for (const p of wreckPieces) {
      const alpha = Math.max(0, Math.min(1, p.alpha));
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      ctx.globalAlpha = alpha * 0.92;
      ctx.shadowBlur = 18;
      ctx.shadowColor = p.kind === "boss" ? "#ff65df" : "#8ff6ff";
      drawWreckPieceImage(p);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.kind === "boss" ? "rgba(255, 91, 233, 0.28)" : "rgba(143, 246, 255, 0.24)";
      ctx.beginPath();
      ctx.arc(0, 0, p.size * 0.36, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
    ctx.restore();
  }
  function drawParticles() {
    ctx.save();
    for (const p of particles) {
      const alpha = Math.max(0, Math.min(1, p.life / (p.maxLife || 0.6)));
      ctx.globalAlpha = alpha;
      if (p.type === "bombFlash") {
        ctx.globalAlpha = alpha * 0.72;
        ctx.fillStyle = p.color;
        ctx.fillRect(0, 0, W, H);
      } else if (p.type === "bombRing") {
        const progress = 1 - alpha;
        ctx.globalCompositeOperation = "lighter";
        ctx.strokeStyle = p.color;
        ctx.lineWidth = 20 * alpha + 4;
        ctx.shadowBlur = 44;
        ctx.shadowColor = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * progress, 0, Math.PI * 2);
        ctx.stroke();
        ctx.globalAlpha = alpha * 0.18;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * progress * 0.72, 0, Math.PI * 2);
        ctx.fill();
        ctx.globalCompositeOperation = "source-over";
      } else if (p.type === "hitTick") {
        // No shadow or composite pass here: this is the cheap per-hit signal.
        ctx.fillStyle = p.color;
        const half = p.size * (0.55 + alpha * 0.45) * 0.5;
        ctx.fillRect(p.x - half, p.y - 1, half * 2, 2);
        ctx.fillRect(p.x - 1, p.y - half, 2, half * 2);
      } else if (p.type === "impact") {
        ctx.strokeStyle = p.color;
        ctx.lineWidth = 2;
        ctx.shadowBlur = 16;
        ctx.shadowColor = "#ffffff";
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * (1.15 - alpha), 0, Math.PI * 2);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(p.x - p.size, p.y);
        ctx.lineTo(p.x + p.size, p.y);
        ctx.moveTo(p.x, p.y - p.size);
        ctx.lineTo(p.x, p.y + p.size);
        ctx.stroke();
      } else if (p.type === "damageRing") {
        ctx.strokeStyle = p.color;
        ctx.lineWidth = 5 * alpha + 1;
        ctx.shadowBlur = 28;
        ctx.shadowColor = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * (1.15 - alpha), 0, Math.PI * 2);
        ctx.stroke();
      } else if (p.type === "floatingText") {
        ctx.fillStyle = p.color;
        ctx.shadowBlur = 18;
        ctx.shadowColor = p.color;
        ctx.font = `900 ${p.size || 18}px Segoe UI, sans-serif`;
        ctx.textAlign = "center";
        ctx.fillText(p.text || "", p.x, p.y - (1 - alpha) * 34);
        ctx.textAlign = "left";      } else if (p.type === "shard") {
        ctx.strokeStyle = p.color;
        ctx.lineWidth = p.size || 2;
        ctx.shadowBlur = 10;
        ctx.shadowColor = p.color;
        ctx.beginPath();
        ctx.moveTo(p.x, p.y);
        ctx.lineTo(p.x - p.vx * 0.025, p.y - p.vy * 0.025);
        ctx.stroke();
      } else {
        ctx.fillStyle = p.color;
        ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
      }
    }
    ctx.restore();
  }


  function drawApproachAlert() {
    if (!alertScene) return;
    const p = Math.min(1, alertScene.t / alertScene.duration);
    const blink = Math.sin(alertScene.t * Math.PI * 7) > 0;
    ctx.save();
    ctx.fillStyle = "rgba(2, 4, 12, 0.52)";
    ctx.fillRect(0, 0, W, H);
    ctx.globalCompositeOperation = "screen";
    const color = alertScene.kind === "boss" ? "#ff4fd8" : "#ffdf70";
    ctx.strokeStyle = blink ? "#ffffff" : color;
    ctx.lineWidth = alertScene.kind === "boss" ? 6 : 4;
    ctx.shadowBlur = 28;
    ctx.shadowColor = color;
    ctx.strokeRect(24, 56, W - 48, H - 112);
    ctx.beginPath();
    ctx.arc(W / 2, 188, 78 + Math.sin(alertScene.t * 8) * 8, 0, Math.PI * 2);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(W / 2 - 118, 188);
    ctx.lineTo(W / 2 + 118, 188);
    ctx.moveTo(W / 2, 70);
    ctx.lineTo(W / 2, 306);
    ctx.stroke();
    ctx.globalCompositeOperation = "source-over";
    ctx.textAlign = "center";
    ctx.fillStyle = blink ? "#ffffff" : color;
    ctx.font = "900 28px Segoe UI, sans-serif";
    ctx.fillText("WARNING", W / 2, 132);
    ctx.font = "900 18px Segoe UI, sans-serif";
    ctx.fillText(alertScene.title, W / 2, 356);
    ctx.fillStyle = "#9fe8ff";
    ctx.font = "800 15px Segoe UI, sans-serif";
    ctx.fillText(alertScene.sub, W / 2, 388);
    ctx.fillStyle = "rgba(255,255,255,0.76)";
    ctx.font = "700 13px Segoe UI, sans-serif";
    ctx.fillText(`SIGNAL LOCK ${Math.floor(p * 100)}%`, W / 2, 420);
    ctx.textAlign = "left";
    ctx.restore();
  }
  function drawDefeatChoice() {
    if (!defeatChoice) return;
    const isBoss = defeatChoice.kind === "boss";
    const name = isBoss ? pilotName : midPilotName;
    ctx.save();
    ctx.fillStyle = "rgba(1, 3, 10, 0.72)";
    ctx.fillRect(0, 0, W, H);
    ctx.strokeStyle = isBoss ? "rgba(255,79,216,0.9)" : "rgba(255,213,112,0.9)";
    ctx.lineWidth = 2;
    ctx.strokeRect(28, 118, W - 56, 314);
    const g = ctx.createLinearGradient(28, 118, W - 28, 432);
    g.addColorStop(0, "rgba(255, 213, 112, 0.18)");
    g.addColorStop(0.55, "rgba(255, 91, 220, 0.14)");
    g.addColorStop(1, "rgba(100, 240, 255, 0.08)");
    ctx.fillStyle = g;
    ctx.fillRect(28, 118, W - 56, 314);
    ctx.textAlign = "center";
    ctx.fillStyle = "#fff8d0";
    ctx.font = "900 25px Segoe UI, sans-serif";
    ctx.fillText("TARGET DISABLED", W / 2, 166);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 20px Segoe UI, sans-serif";
    ctx.fillText(name, W / 2, 204);
    ctx.fillStyle = "rgba(255,255,255,0.76)";
    ctx.font = "800 14px Segoe UI, sans-serif";
    ctx.fillText("対象の処遇を選択", W / 2, 232);
    for (const rect of defeatChoiceRects()) {
      const active = defeatChoice.selected === rect.action;
      const hover = pointer.x >= rect.x && pointer.x <= rect.x + rect.w && pointer.y >= rect.y && pointer.y <= rect.y + rect.h;
      ctx.save();
      ctx.shadowBlur = active || hover ? 22 : 0;
      ctx.shadowColor = rect.action === "capture" ? "#8ff6ff" : "#ff6a4a";
      ctx.fillStyle = rect.action === "capture" ? "rgba(50, 210, 255, 0.2)" : "rgba(255, 84, 54, 0.22)";
      ctx.strokeStyle = active || hover ? "#ffffff" : (rect.action === "capture" ? "#8ff6ff" : "#ff8b70");
      ctx.lineWidth = active || hover ? 4 : 2;
      ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
      ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
      ctx.fillStyle = "#ffffff";
      ctx.font = "900 24px Segoe UI, sans-serif";
      ctx.fillText(rect.label, rect.x + rect.w / 2, rect.y + 45);
      ctx.restore();
    }
    ctx.fillStyle = "rgba(255,255,255,0.72)";
    ctx.font = "700 12px Segoe UI, sans-serif";
    ctx.fillText("← → / Click で選択、Enterで決定", W / 2, H - 92);
    ctx.restore();
  }

  function drawKillCutin() {
    if (!killCutinScene) return;
    const isBoss = killCutinScene.kind === "boss";
    const img = isBoss ? assets.bossKilled : assets.midPilotKilled;
    ctx.save();
    ctx.fillStyle = "#020309";
    ctx.fillRect(0, 0, W, H);
    if (img && img.complete) {
      const scale = Math.max(W / img.width, H / img.height);
      const dw = img.width * scale;
      const dh = img.height * scale;
      ctx.filter = "contrast(1.08) saturate(1.08)";
      ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh);
      ctx.filter = "none";
    }
    const red = ctx.createLinearGradient(0, 0, W, H);
    red.addColorStop(0, "rgba(255, 55, 28, 0.12)");
    red.addColorStop(1, "rgba(0, 0, 0, 0.48)");
    ctx.fillStyle = red;
    ctx.fillRect(0, 0, W, H);
    const ready = killCutinScene.t >= (killCutinScene.minWait || 0.8);
    const panel = ctx.createLinearGradient(0, H - 176, W, H);
    panel.addColorStop(0, "rgba(4, 4, 8, 0.82)");
    panel.addColorStop(0.55, "rgba(45, 7, 14, 0.76)");
    panel.addColorStop(1, "rgba(0, 0, 0, 0.88)");
    ctx.fillStyle = panel;
    ctx.fillRect(0, H - 176, W, 176);
    ctx.strokeStyle = isBoss ? "rgba(255, 105, 82, 0.86)" : "rgba(255, 153, 202, 0.9)";
    ctx.lineWidth = 2;
    ctx.strokeRect(24, H - 158, W - 48, 128);
    ctx.textAlign = "center";
    ctx.fillStyle = "#ffdfcf";
    ctx.font = "900 20px Segoe UI, sans-serif";
    ctx.fillText("OFFICER TERMINATED", W / 2, H - 134);
    ctx.fillStyle = isBoss ? "#9fe8ff" : "#ff9fdf";
    ctx.font = "900 23px Segoe UI, sans-serif";
    ctx.fillText(killCutinScene.speaker || (isBoss ? pilotName : midPilotName), W / 2, H - 105);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 22px Segoe UI, sans-serif";
    wrapCenteredText(killCutinScene.line || "……", W / 2, H - 78, W - 86, 24, 2);
    ctx.fillStyle = ready ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,0.58)";
    ctx.font = "800 13px Segoe UI, sans-serif";
    ctx.fillText(ready ? "Click / Enter: 続行" : "処理記録を保存中...", W / 2, H - 16);
    ctx.restore();
  }
  function drawKilledPreview() {
    const target = killedPreviewTarget;
    if (!target) {
      drawInterrogationSelect();
      return;
    }
    const imageKey = (target.images && target.images.killed) || target.asset;
    const img = assets[imageKey] || assets[target.asset] || assets[target.fallbackAsset];
    const displayName = target.name
      .replace(new RegExp(`^${target.title}[ 　]+`), "")
      .replace(/^[^ 　]+[ 　]+/, "");
    ctx.save();
    ctx.fillStyle = "#020309";
    ctx.fillRect(0, 0, W, H);
    if (img && img.complete) {
      const scale = Math.max(W / img.width, H / img.height);
      const dw = Math.round(img.width * scale);
      const dh = Math.round(img.height * scale);
      ctx.filter = "contrast(1.08) saturate(1.08) brightness(0.94)";
      ctx.drawImage(img, Math.round((W - dw) / 2), Math.round((H - dh) / 2), dw, dh);
      ctx.filter = "none";
    }
    const veil = ctx.createLinearGradient(0, 0, W, H);
    veil.addColorStop(0, "rgba(255, 70, 42, 0.12)");
    veil.addColorStop(0.5, "rgba(0, 0, 0, 0.18)");
    veil.addColorStop(1, "rgba(0, 0, 0, 0.64)");
    ctx.fillStyle = veil;
    ctx.fillRect(0, 0, W, H);
    const panelY = H - 178;
    const panel = ctx.createLinearGradient(0, panelY, W, H);
    panel.addColorStop(0, "rgba(4, 4, 8, 0.84)");
    panel.addColorStop(0.58, "rgba(50, 8, 16, 0.78)");
    panel.addColorStop(1, "rgba(0, 0, 0, 0.9)");
    ctx.fillStyle = panel;
    ctx.fillRect(0, panelY, W, H - panelY);
    ctx.strokeStyle = target.accent || "rgba(255, 105, 82, 0.9)";
    ctx.lineWidth = 2;
    ctx.strokeRect(24, panelY + 18, W - 48, 124);
    ctx.textAlign = "center";
    ctx.fillStyle = "#ffdfcf";
    ctx.font = "900 18px Segoe UI, sans-serif";
    ctx.fillText("KILLED RECORD", W / 2, panelY + 50);
    ctx.fillStyle = target.accent || "#ff8b70";
    ctx.font = "900 15px Segoe UI, sans-serif";
    ctx.fillText(target.title, W / 2, panelY + 75);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 23px Segoe UI, sans-serif";
    ctx.fillText(displayName, W / 2, panelY + 103);
    ctx.fillStyle = "rgba(255,255,255,0.86)";
    ctx.font = "900 14px Segoe UI, sans-serif";
    ctx.fillText("殺害済みのため尋問できません", W / 2, panelY + 129);
    ctx.fillStyle = "rgba(255,255,255,0.82)";
    ctx.font = "800 13px Segoe UI, sans-serif";
    ctx.fillText("Click / Enter: クリア画面へ戻る", W / 2, H - 16);
    ctx.textAlign = "left";
    ctx.restore();
  }
  function drawInterrogationSelect() {
    ctx.save();
    ctx.fillStyle = "rgba(1, 6, 14, 0.82)";
    ctx.fillRect(0, 0, W, H);
    const scan = ctx.createLinearGradient(0, 0, W, H);
    scan.addColorStop(0, "rgba(95, 230, 255, 0.16)");
    scan.addColorStop(0.5, "rgba(255, 91, 220, 0.1)");
    scan.addColorStop(1, "rgba(0, 0, 0, 0)");
    ctx.fillStyle = scan;
    ctx.fillRect(0, 0, W, H);
    ctx.strokeStyle = "rgba(143, 246, 255, 0.22)";
    ctx.lineWidth = 1;
    for (let y = 58; y < H; y += 28) {
      ctx.beginPath();
      ctx.moveTo(0, y + Math.sin(y * 0.08) * 3);
      ctx.lineTo(W, y);
      ctx.stroke();
    }

    ctx.fillStyle = "#f7fdff";
    ctx.font = "900 28px Segoe UI, sans-serif";
    ctx.fillText("CAPTURED OFFICERS", 32, 58);
    ctx.fillStyle = "#9fe8ff";
    ctx.font = "800 13px Segoe UI, sans-serif";
    ctx.fillText("POST-BATTLE INTERROGATION TERMINAL", 34, 82);

    for (const { target, x, y, w, h } of interrogationCardRects()) {
      const img = assets[target.asset] || assets[target.fallbackAsset];
      const hover = pointer.x >= x && pointer.x <= x + w && pointer.y >= y && pointer.y <= y + h;
      ctx.save();
      if (hover) {
        ctx.shadowBlur = 34;
        ctx.shadowColor = "#ffffff";
        ctx.fillStyle = "rgba(255, 255, 255, 0.12)";
        ctx.fillRect(x - 8, y - 8, w + 16, h + 16);
      }
      ctx.fillStyle = hover ? "rgba(12, 24, 42, 0.94)" : "rgba(5, 12, 26, 0.88)";
      ctx.strokeStyle = hover ? "rgba(255, 255, 255, 0.96)" : (target.killed ? "rgba(255, 105, 82, 0.9)" : target.done ? "rgba(117, 255, 208, 0.86)" : target.accent);
      ctx.lineWidth = hover ? 4 : 2;
      ctx.fillRect(x, y, w, h);
      ctx.strokeRect(x, y, w, h);
      if (hover) {
        ctx.shadowBlur = 18;
        ctx.strokeStyle = target.accent;
        ctx.lineWidth = 1.5;
        ctx.strokeRect(x + 6, y + 6, w - 12, h - 12);
        ctx.shadowBlur = 0;
      }
      ctx.beginPath();
      ctx.rect(x + 8, y + 8, 126, h - 16);
      ctx.clip();
      if (img && img.complete) {
        const scale = Math.max(126 / img.width, (h - 16) / img.height);
        const dw = img.width * scale;
        const dh = img.height * scale;
        ctx.filter = "contrast(1.06) saturate(1.05)";
        ctx.drawImage(img, x + 8 + (126 - dw) / 2, y + 8 + ((h - 16) - dh) / 2, dw, dh);
        ctx.filter = "none";
      }
      ctx.restore();

      const textMaxW = w - (target.done || target.killed ? 286 : 174);
      const displayName = target.name
        .replace(new RegExp(`^${target.title}[ 　]+`), "")
        .replace(/^[^ 　]+[ 　]+/, "");
      ctx.fillStyle = "rgba(247,253,255,0.88)";
      ctx.font = "800 12px Segoe UI, sans-serif";
      ctx.fillText(target.title, x + 154, y + 34);
      ctx.fillStyle = target.accent;
      let nameFontSize = 18;
      ctx.font = `900 ${nameFontSize}px Segoe UI, sans-serif`;
      while (ctx.measureText(displayName).width > textMaxW && nameFontSize > 13) {
        nameFontSize -= 1;
        ctx.font = `900 ${nameFontSize}px Segoe UI, sans-serif`;
      }
      ctx.fillText(displayName, x + 152, y + 58);
      ctx.fillStyle = target.killed ? "#ff8b70" : target.done ? "#75ffd0" : "#ffd45f";
      ctx.font = "900 17px Segoe UI, sans-serif";
      ctx.fillText(target.killed ? "STATUS: KILLED" : target.done ? "STATUS: RECORDED" : "STATUS: READY", x + 154, y + 104);
      ctx.fillStyle = "rgba(255,255,255,0.7)";
      ctx.font = "700 12px Segoe UI, sans-serif";
      ctx.fillText(target.killed ? "殺害済み・尋問不可" : target.done ? "尋問ログ保存済み" : "クリックして尋問シーケンスへ", x + 154, y + 130);
      if (target.done || target.killed) {
        ctx.save();
        ctx.translate(x + w - 88, y + 48);
        ctx.rotate(-0.18);
        ctx.strokeStyle = target.killed ? "rgba(255,105,82,0.95)" : "rgba(117,255,208,0.95)";
        ctx.fillStyle = target.killed ? "rgba(255,105,82,0.16)" : "rgba(117,255,208,0.15)";
        ctx.lineWidth = 3;
        ctx.strokeRect(-50, -22, 100, 44);
        ctx.fillRect(-50, -22, 100, 44);
        ctx.fillStyle = target.killed ? "#ffd0c8" : "#bffff0";
        ctx.font = "900 20px Segoe UI, sans-serif";
        ctx.textAlign = "center";
        ctx.fillText(target.killed ? "殺害" : "尋問済み", 0, 7);
        ctx.restore();
      }
    }
    const allDone = allInterrogationTargetsResolved();
    const noticeActive = interrogationNotice && interrogationNotice.t > 0;
    const nextRect = nextStageButtonRect();
    const nextHover = pointer.x >= nextRect.x && pointer.x <= nextRect.x + nextRect.w && pointer.y >= nextRect.y && pointer.y <= nextRect.y + nextRect.h;
    ctx.textAlign = "center";
    ctx.fillStyle = noticeActive ? "#ff8b70" : allDone ? "#75ffd0" : "#ffffff";
    ctx.shadowBlur = noticeActive ? 26 : allDone ? 18 : 22;
    ctx.shadowColor = noticeActive ? "#ff4d36" : allDone ? "#75ffd0" : "#9fe8ff";
    ctx.font = noticeActive ? "900 20px Segoe UI, sans-serif" : "900 15px Segoe UI, sans-serif";
    ctx.fillText(noticeActive ? interrogationNotice.text : allDone ? "処遇記録完了。ネクストステージへ進めます。" : "対象人物をクリックして尋問を開始", W / 2, H - 94);
    ctx.shadowBlur = allDone || nextHover ? 24 : 0;
    ctx.shadowColor = allDone ? "#75ffd0" : "rgba(255,255,255,0)";
    ctx.fillStyle = allDone ? (nextHover ? "rgba(117,255,208,0.34)" : "rgba(117,255,208,0.22)") : "rgba(80,92,106,0.34)";
    ctx.strokeStyle = allDone ? (nextHover ? "#ffffff" : "#75ffd0") : "rgba(180,195,210,0.38)";
    ctx.lineWidth = allDone && nextHover ? 4 : 2;
    ctx.fillRect(nextRect.x, nextRect.y, nextRect.w, nextRect.h);
    ctx.strokeRect(nextRect.x, nextRect.y, nextRect.w, nextRect.h);
    ctx.shadowBlur = 0;
    ctx.fillStyle = allDone ? "#f7fff9" : "rgba(247,253,255,0.42)";
    ctx.font = "900 22px Segoe UI, sans-serif";
    ctx.fillText("ネクストステージ", W / 2, nextRect.y + 31);
    ctx.fillStyle = allDone ? "rgba(255,255,255,0.72)" : "rgba(255,255,255,0.42)";
    ctx.font = "800 11px Segoe UI, sans-serif";
    ctx.fillText(allDone ? "Click / Enter" : "尋問または殺害で解放", W / 2, nextRect.y + 60);
    ctx.textAlign = "left";
    ctx.restore();
  }

  function drawInterrogation() {
    const target = interrogationTarget || activeInterrogationTargets()[0];
    const turns = interrogationTurnsFor(target);
    const turn = turns[Math.min(interrogationStep, turns.length - 1)];
    const sceneAsset = turn.asset || target.asset;
    const img = turn.imageData ? interrogationEditorImage(turn.imageData) : (assets[sceneAsset] || assets[target.asset] || assets[target.fallbackAsset]);
    const activeLine = turn.text;
    const speakerName = Object.prototype.hasOwnProperty.call(turn, "speaker") ? turn.speaker : target.name;
    const speakerColor = speakerName === target.name ? "#ff8bd8" : (turn.accent || target.accent);
    const isSilentTurn = turn.speaker === "";
    const sceneStage = turn.stage || target.title;
    const effectName = interrogationEffect ? interrogationEffect.name : "none";
    const effectAge = interrogationEffect ? Math.min(1, (performance.now() - interrogationEffect.started) / 420) : 1;
    ctx.save();
    if (effectName === "shake" && effectAge < 1) {
      const force = (1 - effectAge) * 5;
      ctx.translate(Math.sin(performance.now() * 0.09) * force, Math.cos(performance.now() * 0.12) * force);
    }
    ctx.fillStyle = "#020712";
    ctx.fillRect(0, 0, W, H);
    const bg = ctx.createLinearGradient(0, 0, W, H);
    bg.addColorStop(0, "rgba(143, 246, 255, 0.11)");
    bg.addColorStop(0.42, "rgba(255, 91, 220, 0.08)");
    bg.addColorStop(1, "rgba(0, 0, 0, 0.42)");
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    const bottomBoxX = 28;
    const bottomBoxW = W - 56;
    const bottomBoxH = 100;
    const bottomBoxY = H - 14 - bottomBoxH;
    const photoX = 20;
    const photoY = 14;
    const photoW = W - 40;
    const photoH = bottomBoxY - photoY - 14;
    ctx.save();
    ctx.shadowBlur = 28;
    ctx.shadowColor = target.accent;
    ctx.fillStyle = "rgba(3, 9, 18, 0.94)";
    ctx.fillRect(photoX, photoY, photoW, photoH);
    ctx.strokeStyle = target.accent;
    ctx.lineWidth = 2;
    ctx.strokeRect(photoX, photoY, photoW, photoH);
    ctx.beginPath();
    ctx.rect(photoX + 8, photoY + 8, photoW - 16, photoH - 16);
    ctx.clip();
    if (img && img.complete) {
      const zoom = effectName === "zoom" && effectAge < 1 ? 1.10 - effectAge * 0.10 : 1;
      const scale = Math.max((photoW - 16) / img.width, (photoH - 16) / img.height) * zoom;
      // The interrogation canvas is displayed in a taller frame. Draw the source wider here so the on-screen image keeps its original aspect ratio.
      const interrogationCanvasStretch = 3.38 / 3;
      const dw = Math.round(img.width * scale * interrogationCanvasStretch);
      const dh = Math.round(img.height * scale);
      ctx.globalAlpha = effectName === "fade" && effectAge < 1 ? 0.35 + effectAge * 0.65 : 1;
      ctx.filter = "contrast(1.08) saturate(1.02) brightness(0.92)";
      const imageOffset = turn.imageOffset || {}; const offsetX = Math.max(-45, Math.min(45, Number(imageOffset.x) || 0)); const offsetY = Math.max(-45, Math.min(45, Number(imageOffset.y) || 0));
      ctx.drawImage(img, photoX + 8 + Math.round((photoW - 16 - dw) / 2) + Math.round((photoW - 16 - dw) * offsetX / 100), photoY + 8 + Math.round((photoH - 16 - dh) / 2) + Math.round((photoH - 16 - dh) * offsetY / 100), dw, dh);
      ctx.filter = "none";
      ctx.globalAlpha = 1;
    }
    ctx.restore();

    if (effectName === "flash" && effectAge < 1) {
      ctx.fillStyle = `rgba(225, 248, 255, ${(1 - effectAge) * 0.46})`;
      ctx.fillRect(0, 0, W, H);
    }
    if (effectName === "noise" && effectAge < 1) {
      ctx.save();
      ctx.globalAlpha = (1 - effectAge) * 0.32;
      ctx.fillStyle = "#d9f8ff";
      for (let y = photoY + 10; y < photoY + photoH - 10; y += 6) ctx.fillRect(photoX + 10, y, photoW - 20, 1);
      ctx.restore();
    }

    const interrogationBadgeX = photoX + 18;
    const interrogationBadgeY = photoY + 18;
    const interrogationBadgeW = 142;
    const interrogationBadgeH = 38;
    ctx.save();
    ctx.shadowBlur = 14;
    ctx.shadowColor = target.accent;
    ctx.fillStyle = "rgba(4, 9, 18, 0.88)";
    ctx.fillRect(interrogationBadgeX, interrogationBadgeY, interrogationBadgeW, interrogationBadgeH);
    ctx.strokeStyle = target.accent;
    ctx.lineWidth = 1.5;
    ctx.strokeRect(interrogationBadgeX, interrogationBadgeY, interrogationBadgeW, interrogationBadgeH);
    ctx.fillStyle = target.accent;
    ctx.fillRect(interrogationBadgeX, interrogationBadgeY, 5, interrogationBadgeH);
    ctx.shadowBlur = 0;
    ctx.fillStyle = "rgba(245, 251, 255, 0.72)";
    ctx.font = "800 8px Segoe UI, sans-serif";
    ctx.fillText("INTERROGATION", interrogationBadgeX + 13, interrogationBadgeY + 14);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 15px Segoe UI, sans-serif";
    ctx.fillText("\u5c0b\u554f\u4e2d", interrogationBadgeX + 13, interrogationBadgeY + 30);
    ctx.restore();
    ctx.fillStyle = "rgba(0, 0, 0, 0.48)";
    ctx.fillRect(photoX + 8, photoY + photoH - 52, photoW - 16, 44);
    ctx.fillStyle = "#ffffff";
    ctx.font = "900 15px Segoe UI, sans-serif";
    if (!isSilentTurn) ctx.fillText(target.name, photoX + 22, photoY + photoH - 25);
    ctx.fillStyle = target.accent;
    ctx.font = "900 12px Segoe UI, sans-serif";
    ctx.textAlign = "right";
    ctx.fillText(sceneStage, photoX + photoW - 22, photoY + photoH - 25);
    ctx.textAlign = "left";

    ctx.fillStyle = "rgba(4, 10, 20, 0.96)";
    ctx.fillRect(bottomBoxX, bottomBoxY, bottomBoxW, bottomBoxH);
    ctx.strokeStyle = speakerColor;
    ctx.lineWidth = 2;
    ctx.strokeRect(bottomBoxX, bottomBoxY, bottomBoxW, bottomBoxH);
    ctx.fillStyle = speakerColor;
    ctx.font = "900 14px Segoe UI, sans-serif";
    if (!isSilentTurn) ctx.fillText(speakerName, bottomBoxX + 18, bottomBoxY + 22);
    const dialogueLayouts = [
      { fontSize: 18, lineHeight: 20, maxLines: 2 },
      { fontSize: 15, lineHeight: 16, maxLines: 3 },
      { fontSize: 12, lineHeight: 12, maxLines: 4 },
      { fontSize: 10, lineHeight: 11, maxLines: 5 }
    ];
    let dialogueLayout = dialogueLayouts[dialogueLayouts.length - 1];
    for (const candidate of dialogueLayouts) {
      ctx.font = `800 ${candidate.fontSize}px Segoe UI, sans-serif`;
      if (wrappedLineCount(activeLine, bottomBoxW - 36) <= candidate.maxLines) {
        dialogueLayout = candidate;
        break;
      }
    }
    ctx.fillStyle = "#f7fdff";
    ctx.font = `800 ${dialogueLayout.fontSize}px Segoe UI, sans-serif`;
    wrapText(activeLine, bottomBoxX + 18, bottomBoxY + (isSilentTurn ? 34 : 44), bottomBoxW - 36, dialogueLayout.lineHeight);
    ctx.fillStyle = "rgba(255,255,255,0.78)";
    ctx.font = "900 12px Segoe UI, sans-serif";
    ctx.textAlign = "right";
    ctx.fillText(interrogationStep >= turns.length - 1 ? "Click / Enter: 保存して戻る" : "Click / Enter: 次へ", W - 44, bottomBoxY + bottomBoxH - 10);
    ctx.textAlign = "left";
    ctx.restore();
  }
  function drawDialog() {
    const dialog = activeDialog || dialogs.preBoss;
    const entry = dialog.lines[Math.min(dialogStep, dialog.lines.length - 1)];
    ctx.save();
    ctx.fillStyle = "rgba(2, 4, 12, 0.62)";
    ctx.fillRect(0, 0, W, H);
    const img = assets[dialog.image] || assets.pilot;
    if (img && img.complete) {
      const isFullCutin = ["midPilot", "midPilotCrisis", "midPilotDefeat", "pilot", "pilotCrisis", "pilotDefeat"].includes(dialog.image);
      ctx.globalAlpha = isFullCutin ? 1 : 0.98;
      if (isFullCutin) {
        const scale = Math.max(W / img.width, H / img.height);
        const dw = Math.round(img.width * scale);
        const dh = Math.round(img.height * scale);
        const dx = Math.round((W - dw) / 2);
        const dy = Math.round(Math.min(0, (H - dh) * 0.38));
        ctx.filter = "contrast(1.12) saturate(1.1) brightness(1.03)";
        ctx.drawImage(img, dx, dy, dw, dh);
        ctx.filter = "none";
        const shade = ctx.createLinearGradient(0, H * 0.72, 0, H);
        shade.addColorStop(0, "rgba(2, 4, 12, 0)");
        shade.addColorStop(1, "rgba(2, 4, 12, 0.14)");
        ctx.fillStyle = shade;
        ctx.fillRect(0, H * 0.72, W, H * 0.28);
      } else {
        const scale = Math.max(330 / img.width, 520 / img.height);
        const dw = img.width * scale;
        const dh = img.height * scale;
        ctx.drawImage(img, W - dw + 58, 48, dw, dh);
      }
    }
    ctx.globalAlpha = 1;
    const grd = ctx.createLinearGradient(0, H - 190, W, H - 60);
    grd.addColorStop(0, "rgba(4, 10, 20, 0.94)");
    grd.addColorStop(0.65, "rgba(26, 6, 30, 0.92)");
    grd.addColorStop(1, "rgba(255, 79, 216, 0.42)");
    ctx.fillStyle = grd;
    ctx.fillRect(18, H - 190, W - 36, 148);
    ctx.strokeStyle = "rgba(255, 151, 235, 0.8)";
    ctx.lineWidth = 2;
    ctx.strokeRect(18, H - 190, W - 36, 148);
    ctx.fillStyle = entry.speaker === "SYSTEM" ? "#ff6fe4" : "#9fe8ff";
    ctx.font = "800 20px Segoe UI, sans-serif";
    ctx.fillText(entry.speaker, 38, H - 150);
    ctx.fillStyle = "#f7fdff";
    ctx.font = "700 19px Segoe UI, sans-serif";
    wrapText(entry.text, 38, H - 112, W - 76, 30);
    ctx.fillStyle = "rgba(255, 255, 255, 0.72)";
    ctx.font = "700 13px Segoe UI, sans-serif";
    ctx.textAlign = "right";
    ctx.fillText("Enter / Space / Click", W - 36, H - 58);
    ctx.textAlign = "left";
    ctx.restore();
  }

  function wrapCenteredText(text, centerX, y, maxWidth, lineHeight, maxLines = 2) {
    const lines = [];
    let line = "";
    for (const ch of text) {
      const test = line + ch;
      if (ctx.measureText(test).width > maxWidth && line) {
        lines.push(line);
        line = ch;
        if (lines.length >= maxLines) break;
      } else {
        line = test;
      }
    }
    if (line && lines.length < maxLines) lines.push(line);
    if (lines.length === maxLines) {
      while (ctx.measureText(lines[maxLines - 1] + "…").width > maxWidth && lines[maxLines - 1].length > 1) {
        lines[maxLines - 1] = lines[maxLines - 1].slice(0, -1);
      }
      if (!text.endsWith(lines[maxLines - 1])) lines[maxLines - 1] += "…";
    }
    for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], centerX, y + i * lineHeight);
  }
  function wrappedLineCount(text, maxWidth) {
    let lines = 1;
    let line = "";
    for (const ch of String(text || "")) {
      if (ch === "\n") { lines += 1; line = ""; continue; }
      const test = line + ch;
      if (ctx.measureText(test).width > maxWidth && line) { lines += 1; line = ch; }
      else line = test;
    }
    return lines;
  }
  function wrapText(text, x, y, maxWidth, lineHeight) {
    let line = "";
    for (const ch of text) {
      const test = line + ch;
      if (ctx.measureText(test).width > maxWidth && line) {
        ctx.fillText(line, x, y);
        line = ch;
        y += lineHeight;
      } else {
        line = test;
      }
    }
    if (line) ctx.fillText(line, x, y);
  }
  function drawTopUi() {
    ctx.fillStyle = "rgba(3, 10, 18, 0.45)";
    ctx.fillRect(0, 0, W, 42);
    ctx.fillStyle = "#f7fdff";
    ctx.font = "800 14px Segoe UI, sans-serif";
    ctx.fillText(`SCORE ${Math.floor(score)}`, 14, 27);
    const hpRate = Math.max(0, player.hp / player.maxHp);
    ctx.textAlign = "center";
    ctx.fillText(`HP ${Math.ceil(Math.max(0, player.hp))}/${player.maxHp}`, W / 2 - 24, 18);
    ctx.fillStyle = "rgba(255,255,255,0.22)";
    ctx.fillRect(W / 2 - 82, 25, 132, 8);
    ctx.fillStyle = hpRate < 0.32 ? "#ff5f75" : hpRate < 0.62 ? "#ffd45f" : "#75ffd0";
    ctx.fillRect(W / 2 - 82, 25, 132 * hpRate, 8);
    ctx.strokeStyle = "rgba(255,255,255,0.62)";
    ctx.lineWidth = 1;
    ctx.strokeRect(W / 2 - 82, 25, 132, 8);
    ctx.fillStyle = "#f7fdff";
    ctx.textAlign = "right";
    ctx.fillText(`STK ${player.lives}  B${player.bombs}  PWR ${player.power}  P+${player.powerStock}/30`, W - 14, 27);
    ctx.textAlign = "left";
    if (!boss && state === "play") {
      const targetTime = midBossDefeated ? bossReadyTime : midBossApproachTime();
      const remain = Math.max(0, targetTime - time);
      ctx.fillStyle = "#9fe8ff";
      const label = midBoss ? "MID-BOSS" : midBossDefeated ? (remain > 0 ? `BOSS IN ${Math.ceil(remain)}` : "WARNING") : (remain > 0 ? `MID IN ${Math.ceil(remain)}` : "WARNING");
      ctx.font = "800 12px Segoe UI, sans-serif";
      ctx.fillText(label, 176, 39);
    }
  }

  function frame(now) {
    const dt = Math.min(0.033, (now - last) / 1000 || 0);
    last = now;
    const interrogationLayout = state === "interrogation";
    if (renderedInterrogationLayout !== interrogationLayout) {
      document.body.classList.toggle("interrogation-layout", interrogationLayout);
      renderedInterrogationLayout = interrogationLayout;
    }
    const renderStartedAt = performance.now();
    try {
      update(dt);
      draw();
    } catch (err) {
      console.error(err);
      flash = 0;
      shake = 0;
      state = state === "play" ? "pause" : state;
      showOverlay("System Error", "描画エラーを検出しました。再開してください。", "再開");
    }
    updateRenderQuality(performance.now() - renderStartedAt);
    requestAnimationFrame(frame);
  }

  function pointerMove(evt) {
    const rect = canvas.getBoundingClientRect();
    const p = evt.touches ? evt.touches[0] : evt;
    pointer.x = ((p.clientX - rect.left) / rect.width) * W;
    pointer.y = ((p.clientY - rect.top) / rect.height) * H;
    if (state !== "play") return;
    player.x = Math.max(24, Math.min(W - 24, pointer.x));
    player.y = Math.max(48, Math.min(H - 34, pointer.y));
  }
  window.addEventListener("keydown", (evt) => {
    if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " ", "Shift", "Enter"].includes(evt.key)) evt.preventDefault();
    if (state === "dialog" && (evt.key === " " || evt.key === "Enter" || evt.key.toLowerCase() === "z")) {
      advanceDialog();
      return;
    }
    if (state === "defeatChoice" && defeatChoice) {
      if (evt.key === "ArrowLeft" || evt.key === "ArrowRight") {
        defeatChoice.selected = defeatChoice.selected === "capture" ? "kill" : "capture";
        return;
      }
      if (evt.key === " " || evt.key === "Enter" || evt.key.toLowerCase() === "z") {
        chooseDefeatOutcome(defeatChoice.selected);
        return;
      }
    }
    if (state === "killCutin" && (evt.key === " " || evt.key === "Enter" || evt.key.toLowerCase() === "z")) {
      advanceKillCutin();
      return;
    }
    if (state === "killedPreview" && (evt.key === " " || evt.key === "Enter" || evt.key.toLowerCase() === "z")) {
      closeKilledPreview();
      return;
    }
    if (state === "interrogation" && (evt.key === " " || evt.key === "Enter" || evt.key.toLowerCase() === "z")) {
      advanceInterrogation();
      return;
    }
    if (state === "interrogationSelect" && evt.key === "Enter") {
      goNextStage();
      return;
    }
    keys.add(evt.key.length === 1 ? evt.key.toLowerCase() : evt.key);
    if (evt.key === " ") bomb();
  });
  window.addEventListener("keyup", (evt) => keys.delete(evt.key.length === 1 ? evt.key.toLowerCase() : evt.key));
  canvas.addEventListener("mousemove", pointerMove);
  canvas.addEventListener("mouseleave", () => { pointer.x = -999; pointer.y = -999; });
  canvas.addEventListener("touchmove", (evt) => {
    evt.preventDefault();
    pointerMove(evt);
  }, { passive: false });
  canvas.addEventListener("touchstart", (evt) => {
    evt.preventDefault();
    pointerMove(evt);
  }, { passive: false });
  canvas.addEventListener("pointerdown", (evt) => {
    pointerMove(evt);
    if (state === "dialog") advanceDialog();
    else if (state === "defeatChoice" && defeatChoice) {
      for (const rect of defeatChoiceRects()) {
        if (pointer.x >= rect.x && pointer.x <= rect.x + rect.w && pointer.y >= rect.y && pointer.y <= rect.y + rect.h) {
          chooseDefeatOutcome(rect.action);
          return;
        }
      }
    }
    else if (state === "killCutin") advanceKillCutin();
    else if (state === "killedPreview") closeKilledPreview();
    else if (state === "interrogation") advanceInterrogation();
    else if (state === "interrogationSelect") {
      const nextRect = nextStageButtonRect();
      if (pointer.x >= nextRect.x && pointer.x <= nextRect.x + nextRect.w && pointer.y >= nextRect.y && pointer.y <= nextRect.y + nextRect.h) {
        goNextStage();
        return;
      }
      for (const card of interrogationCardRects()) {
        if (pointer.x >= card.x && pointer.x <= card.x + card.w && pointer.y >= card.y && pointer.y <= card.y + card.h) {
          startInterrogation(card.target);
          break;
        }
      }
    } else if (state === "play") bomb();
  });
  startButton.addEventListener("click", () => {
    if (state === "over") continueGame();
    else reset();
  });
  demoExitButton.addEventListener("click", () => { window.location.replace("about:blank"); });
  demoReplayButton.addEventListener("click", () => { reset(); });
  demoHpButton.addEventListener("click", () => { window.location.assign("https://mukiamaguri.com/necrobarrage/#characters"); });
  if (devTools) {
    devTools.hidden = !DEV_TOOLS;
    devTools.addEventListener("click", (evt) => {
      const button = evt.target.closest("[data-dev-jump]");
      if (button) devJump(button.dataset.devJump);
    });
    const devStageSelect = document.getElementById("devStageSelect");
    if (devStageSelect) {
      for (const stage of Object.keys(stageRuntime).map(Number).sort((a, b) => a - b)) {
        const value = stage === 1 ? "stage" : `stage${stage}`;
        if (devStageSelect.querySelector(`option[value="${value}"]`)) continue;
        const option = document.createElement("option");
        option.value = value;
        option.textContent = `Stage ${stage}`;
        devStageSelect.appendChild(option);
      }
      devStageSelect.addEventListener("change", () => {
        const target = devStageSelect.value;
        if (!target) return;
        devJump(target);
        devStageSelect.value = "";
      });
    }
  }

  const imageBoot = Object.values(assets)
    .filter((img) => img && typeof img.decode === "function")
    .map((img) => img.decode().catch(() => {}));
  Promise.race([Promise.allSettled(imageBoot), new Promise((resolve) => setTimeout(resolve, 1200))]).then(() => {
    requestAnimationFrame(frame);
  });
})();
































































































































































































































































































