<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>NEON INVADERS</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Share+Tech+Mono&display=swap');

  :root {
    --bg: #04060e;
    --cyan: #2ee6ff;
    --magenta: #ff3df0;
    --lime: #b8ff2e;
    --amber: #ffb53d;
    --red: #ff4d5e;
    --panel: rgba(10, 16, 34, 0.85);
  }

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

  html, body {
    height: 100%;
    background: var(--bg);
    overflow: hidden;
    font-family: 'Share Tech Mono', monospace;
    color: #cfe9ff;
    -webkit-user-select: none;
    user-select: none;
    touch-action: none;
  }

  #wrap {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #game {
    display: block;
    background: var(--bg);
    box-shadow: 0 0 60px rgba(46, 230, 255, 0.12), 0 0 120px rgba(255, 61, 240, 0.06);
  }

  /* CRT scanline overlay */
  #crt {
    position: fixed;
    inset: 0;
    pointer-events: none;
    background:
      repeating-linear-gradient(0deg, rgba(0,0,0,0.18) 0px, rgba(0,0,0,0.18) 1px, transparent 1px, transparent 3px);
    mix-blend-mode: multiply;
    z-index: 5;
  }
  #vignette {
    position: fixed;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,5,0.55) 100%);
    z-index: 6;
  }

  /* HUD overlays */
  .overlay {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 10;
    background: radial-gradient(ellipse at center, rgba(4,6,14,0.6) 0%, rgba(4,6,14,0.92) 100%);
    text-align: center;
    padding: 24px;
    transition: opacity 0.3s;
  }
  .overlay.hidden { opacity: 0; pointer-events: none; }

  .title {
    font-family: 'Orbitron', sans-serif;
    font-weight: 900;
    font-size: clamp(36px, 8vw, 84px);
    letter-spacing: 0.12em;
    background: linear-gradient(180deg, var(--cyan) 0%, #ffffff 45%, var(--magenta) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    filter: drop-shadow(0 0 18px rgba(46,230,255,0.55)) drop-shadow(0 0 40px rgba(255,61,240,0.3));
    animation: titlePulse 2.4s ease-in-out infinite;
  }
  @keyframes titlePulse {
    0%, 100% { filter: drop-shadow(0 0 14px rgba(46,230,255,0.45)) drop-shadow(0 0 32px rgba(255,61,240,0.25)); }
    50% { filter: drop-shadow(0 0 26px rgba(46,230,255,0.8)) drop-shadow(0 0 60px rgba(255,61,240,0.5)); }
  }

  .subtitle {
    margin-top: 8px;
    font-size: clamp(12px, 2vw, 16px);
    letter-spacing: 0.4em;
    color: var(--cyan);
    opacity: 0.8;
  }

  .bigstat {
    font-family: 'Orbitron', sans-serif;
    font-weight: 700;
    font-size: clamp(20px, 4vw, 34px);
    margin-top: 18px;
    color: var(--amber);
    text-shadow: 0 0 16px rgba(255,181,61,0.6);
  }

  .btn {
    margin-top: 28px;
    font-family: 'Orbitron', sans-serif;
    font-weight: 700;
    font-size: clamp(14px, 2.4vw, 20px);
    letter-spacing: 0.25em;
    padding: 16px 44px;
    color: var(--bg);
    background: linear-gradient(90deg, var(--cyan), var(--magenta));
    border: none;
    cursor: pointer;
    clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%);
    transition: transform 0.12s, filter 0.12s;
    box-shadow: 0 0 24px rgba(46,230,255,0.4);
  }
  .btn:hover { transform: scale(1.05); filter: brightness(1.15); }
  .btn:active { transform: scale(0.97); }
  .btn:focus-visible { outline: 2px solid #fff; outline-offset: 4px; }

  .controls-hint {
    margin-top: 26px;
    font-size: clamp(11px, 1.6vw, 14px);
    line-height: 1.9;
    color: #7fa6c4;
  }
  .controls-hint b { color: var(--lime); font-weight: normal; }

  .powerup-legend {
    margin-top: 18px;
    display: flex;
    gap: 22px;
    flex-wrap: wrap;
    justify-content: center;
    font-size: clamp(10px, 1.5vw, 13px);
  }
  .powerup-legend span { display: inline-flex; align-items: center; gap: 7px; color: #9fc3df; }
  .dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }

  /* Touch controls */
  #touch {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    height: 130px;
    display: none;
    z-index: 8;
    pointer-events: none;
  }
  #touch .tbtn {
    position: absolute;
    bottom: 18px;
    width: 76px; height: 76px;
    border-radius: 50%;
    border: 2px solid rgba(46,230,255,0.5);
    background: rgba(46,230,255,0.08);
    color: var(--cyan);
    font-size: 28px;
    display: flex; align-items: center; justify-content: center;
    pointer-events: auto;
    backdrop-filter: blur(2px);
  }
  #touch .tbtn:active { background: rgba(46,230,255,0.25); }
  #tLeft  { left: 20px; }
  #tRight { left: 112px; }
  #tFire  { right: 20px; border-color: rgba(255,61,240,0.55) !important; color: var(--magenta) !important; background: rgba(255,61,240,0.08) !important; width: 90px !important; height: 90px !important; }

  @media (pointer: coarse) { #touch { display: block; } }
  @media (prefers-reduced-motion: reduce) {
    .title { animation: none; }
  }
</style>
</head>
<body>

<div id="wrap"><canvas id="game"></canvas></div>
<div id="crt"></div>
<div id="vignette"></div>

<!-- Start screen -->
<div class="overlay" id="startScreen">
  <div class="title">NEON&nbsp;INVADERS</div>
  <div class="subtitle">EARTH'S LAST ARCADE STAND</div>
  <button class="btn" id="startBtn">INSERT COIN</button>
  <div class="controls-hint">
    <b>←/→</b> or <b>A/D</b> move &nbsp;·&nbsp; <b>SPACE</b> fire &nbsp;·&nbsp; <b>P</b> pause &nbsp;·&nbsp; <b>M</b> mute<br>
    Destroy the wave. Protect the bunkers. Watch for the UFO.
  </div>
  <div class="powerup-legend">
    <span><i class="dot" style="background:var(--lime);box-shadow:0 0 8px var(--lime)"></i>RAPID FIRE</span>
    <span><i class="dot" style="background:var(--cyan);box-shadow:0 0 8px var(--cyan)"></i>TRIPLE SHOT</span>
    <span><i class="dot" style="background:var(--magenta);box-shadow:0 0 8px var(--magenta)"></i>SHIELD</span>
    <span><i class="dot" style="background:var(--amber);box-shadow:0 0 8px var(--amber)"></i>EXTRA LIFE</span>
  </div>
</div>

<!-- Game over screen -->
<div class="overlay hidden" id="overScreen">
  <div class="title" style="font-size:clamp(30px,6vw,64px)">GAME OVER</div>
  <div class="bigstat" id="finalScore">SCORE 0000000</div>
  <div class="bigstat" id="bestScore" style="color:var(--lime);text-shadow:0 0 16px rgba(184,255,46,0.6)">BEST 0000000</div>
  <button class="btn" id="retryBtn">PLAY AGAIN</button>
</div>

<!-- Pause screen -->
<div class="overlay hidden" id="pauseScreen">
  <div class="title" style="font-size:clamp(26px,5vw,52px)">PAUSED</div>
  <div class="controls-hint">Press <b>P</b> to resume</div>
</div>

<!-- Touch controls -->
<div id="touch">
  <div class="tbtn" id="tLeft">◀</div>
  <div class="tbtn" id="tRight">▶</div>
  <div class="tbtn" id="tFire">✦</div>
</div>

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

/* ============================== SETUP ============================== */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 720, H = 900;
let scale = 1;

function resize() {
  const ww = window.innerWidth, wh = window.innerHeight;
  scale = Math.min(ww / W, wh / H);
  canvas.width = W;
  canvas.height = H;
  canvas.style.width = (W * scale) + 'px';
  canvas.style.height = (H * scale) + 'px';
}
window.addEventListener('resize', resize);
resize();

const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/* ============================== AUDIO ============================== */
let audioCtx = null, muted = false, masterGain = null;
function initAudio() {
  if (audioCtx) return;
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = audioCtx.createGain();
    masterGain.gain.value = 0.35;
    masterGain.connect(audioCtx.destination);
  } catch (e) { /* audio unavailable */ }
}
function tone(freq, dur, type = 'square', vol = 1, slideTo = null) {
  if (!audioCtx || muted) return;
  const t = audioCtx.currentTime;
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.type = type;
  o.frequency.setValueAtTime(freq, t);
  if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(slideTo, 1), t + dur);
  g.gain.setValueAtTime(vol * 0.5, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + dur);
  o.connect(g); g.connect(masterGain);
  o.start(t); o.stop(t + dur);
}
function noiseBurst(dur, vol = 1) {
  if (!audioCtx || muted) return;
  const t = audioCtx.currentTime;
  const len = Math.floor(audioCtx.sampleRate * dur);
  const buf = audioCtx.createBuffer(1, len, audioCtx.sampleRate);
  const data = buf.getChannelData(0);
  for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / len);
  const src = audioCtx.createBufferSource();
  src.buffer = buf;
  const g = audioCtx.createGain();
  g.gain.value = vol * 0.4;
  src.connect(g); g.connect(masterGain);
  src.start(t);
}
const sfx = {
  shoot:   () => tone(880, 0.09, 'square', 0.5, 220),
  invShot: () => tone(200, 0.12, 'sawtooth', 0.35, 90),
  invDie:  () => { noiseBurst(0.12, 0.6); tone(300, 0.1, 'square', 0.4, 60); },
  playerDie: () => { noiseBurst(0.5, 1); tone(400, 0.5, 'sawtooth', 0.7, 40); },
  ufo:     () => tone(620, 0.12, 'sine', 0.25, 740),
  ufoDie:  () => { noiseBurst(0.25, 0.8); tone(900, 0.35, 'square', 0.5, 100); },
  power:   () => { tone(523, 0.08, 'square', 0.5); setTimeout(() => tone(784, 0.08, 'square', 0.5), 70); setTimeout(() => tone(1047, 0.12, 'square', 0.5), 140); },
  wave:    () => { tone(392, 0.1, 'square', 0.5); setTimeout(() => tone(523, 0.1, 'square', 0.5), 100); setTimeout(() => tone(659, 0.18, 'square', 0.5), 200); },
  step:    (n) => tone([110, 98, 87, 78][n % 4], 0.07, 'triangle', 0.45),
  shieldHit: () => tone(1400, 0.06, 'sine', 0.4, 700),
};

/* ============================== INPUT ============================== */
const keys = {};
window.addEventListener('keydown', e => {
  if (['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
  keys[e.key.toLowerCase()] = true;
  if (e.key === ' ') keys['space'] = true;
  if (e.key.toLowerCase() === 'p' && state === 'playing') togglePause();
  else if (e.key.toLowerCase() === 'p' && state === 'paused') togglePause();
  if (e.key.toLowerCase() === 'm') muted = !muted;
});
window.addEventListener('keyup', e => {
  keys[e.key.toLowerCase()] = false;
  if (e.key === ' ') keys['space'] = false;
});

// Touch
const bindTouch = (id, key) => {
  const el = document.getElementById(id);
  el.addEventListener('touchstart', e => { e.preventDefault(); keys[key] = true; initAudio(); }, { passive: false });
  el.addEventListener('touchend',   e => { e.preventDefault(); keys[key] = false; }, { passive: false });
};
bindTouch('tLeft', 'arrowleft');
bindTouch('tRight', 'arrowright');
bindTouch('tFire', 'space');

/* ============================== STATE ============================== */
let state = 'start'; // start | playing | paused | gameover | waveclear
let score = 0, best = 0, lives = 3, wave = 1;
let shake = 0, flash = 0, freezeFrames = 0;
let waveBannerT = 0;

const player = {
  x: W / 2, y: H - 90, w: 52, h: 30,
  speed: 360, cooldown: 0, fireRate: 0.38,
  alive: true, respawnT: 0, invincibleT: 0,
  powers: { rapid: 0, triple: 0, shield: 0 },
  thrust: 0
};

let invaders = [], bullets = [], enemyBullets = [], particles = [], powerups = [], bunkers = [], floaters = [];
let ufo = null, ufoTimer = 8;
let invDir = 1, invStepTimer = 0, invStepInterval = 0.9, stepNote = 0;

/* Stars (parallax) */
const stars = [];
for (let i = 0; i < 130; i++) {
  stars.push({ x: Math.random() * W, y: Math.random() * H, z: Math.random() * 0.8 + 0.2, tw: Math.random() * Math.PI * 2 });
}

/* ============================== INVADER SPRITES ============================== */
// 11x8 pixel maps, 2 animation frames each, 3 species
const SPRITES = [
  { // squid (top rows) — magenta
    color: '#ff3df0', pts: 30,
    frames: [
      ['00011111000','00111111100','01111111110','11011101011','11111111111','00111011100','01100000110','00011011000'],
      ['00011111000','00111111100','01111111110','11011101011','11111111111','00100010100','01011101010','10100000101']
    ]
  },
  { // crab (middle) — cyan
    color: '#2ee6ff', pts: 20,
    frames: [
      ['00100000100','00010001000','00111111100','01101110110','11111111111','10111111101','10100000101','00011011000'],
      ['00100000100','10010001001','10111111101','11101110111','11111111111','01111111110','00100000100','01000000010']
    ]
  },
  { // octopus (bottom) — lime
    color: '#b8ff2e', pts: 10,
    frames: [
      ['00011111000','01111111110','11111111111','11100100111','11111111111','00111011100','01100110110','00110000110'],
      ['00011111000','01111111110','11111111111','11100100111','11111111111','01110111010','11000000011','00110011000']
    ]
  }
];
let animFrame = 0;

/* ============================== BUNKERS ============================== */
function makeBunkers() {
  bunkers = [];
  const bw = 84, bh = 60, cell = 6;
  const cols = Math.floor(bw / cell), rows = Math.floor(bh / cell);
  for (let b = 0; b < 4; b++) {
    const bx = (W / 5) * (b + 1) - bw / 2;
    const by = H - 220;
    const grid = [];
    for (let r = 0; r < rows; r++) {
      grid[r] = [];
      for (let c = 0; c < cols; c++) {
        // arch shape: cut bottom-middle and round top corners
        const midC = cols / 2;
        let solid = true;
        if (r >= rows - 4 && Math.abs(c - midC + 0.5) < 3.2) solid = false;
        if (r < 3 && (c < 3 - r || c > cols - 4 + r)) solid = false;
        grid[r][c] = solid ? 3 : 0; // health 3 per cell
      }
    }
    bunkers.push({ x: bx, y: by, cell, cols, rows, grid });
  }
}

function damageBunkerAt(px, py, radius = 8) {
  for (const bk of bunkers) {
    if (px < bk.x - radius || px > bk.x + bk.cols * bk.cell + radius) continue;
    if (py < bk.y - radius || py > bk.y + bk.rows * bk.cell + radius) continue;
    let hit = false;
    for (let r = 0; r < bk.rows; r++) {
      for (let c = 0; c < bk.cols; c++) {
        if (bk.grid[r][c] <= 0) continue;
        const cx = bk.x + c * bk.cell + bk.cell / 2;
        const cy = bk.y + r * bk.cell + bk.cell / 2;
        const dx = cx - px, dy = cy - py;
        if (dx * dx + dy * dy < radius * radius) {
          bk.grid[r][c]--;
          hit = true;
          if (Math.random() < 0.4) spawnParticles(cx, cy, '#4a8a3a', 2, 60);
        }
      }
    }
    if (hit) return true;
  }
  return false;
}

function bunkerSolidAt(px, py) {
  for (const bk of bunkers) {
    const c = Math.floor((px - bk.x) / bk.cell);
    const r = Math.floor((py - bk.y) / bk.cell);
    if (r >= 0 && r < bk.rows && c >= 0 && c < bk.cols && bk.grid[r][c] > 0) return true;
  }
  return false;
}

/* ============================== WAVES ============================== */
function spawnWave(n) {
  invaders = [];
  const cols = 11, rows = 5;
  const sx = 56, sy = 44;
  const offX = (W - (cols - 1) * sx) / 2;
  const offY = 110 + Math.min((n - 1) * 12, 80);
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const species = r === 0 ? 0 : r < 3 ? 1 : 2;
      invaders.push({
        x: offX + c * sx, y: offY + r * sy,
        w: 38, h: 28, species, alive: true,
        wobble: Math.random() * Math.PI * 2
      });
    }
  }
  invDir = 1;
  invStepInterval = Math.max(0.95 - (n - 1) * 0.07, 0.4);
  invStepTimer = 0;
  ufoTimer = 6 + Math.random() * 8;
  waveBannerT = 2.2;
  if (n > 1) sfx.wave();
}

/* ============================== EFFECTS ============================== */
function spawnParticles(x, y, color, count = 14, speed = 200) {
  if (reducedMotion) count = Math.min(count, 4);
  for (let i = 0; i < count; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = (Math.random() * 0.7 + 0.3) * speed;
    particles.push({
      x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
      life: Math.random() * 0.5 + 0.25, maxLife: 0.75,
      color, size: Math.random() * 3.5 + 1.5
    });
  }
}
function floatText(x, y, text, color) {
  floaters.push({ x, y, text, color, life: 1.1 });
}
function addShake(amt) { if (!reducedMotion) shake = Math.min(shake + amt, 22); }

/* ============================== POWERUPS ============================== */
const POWER_TYPES = [
  { id: 'rapid',  color: '#b8ff2e', label: 'RAPID' },
  { id: 'triple', color: '#2ee6ff', label: 'TRIPLE' },
  { id: 'shield', color: '#ff3df0', label: 'SHIELD' },
  { id: 'life',   color: '#ffb53d', label: '1UP' },
];
function maybeDropPowerup(x, y, chance = 0.09) {
  if (Math.random() > chance) return;
  const weights = [0.32, 0.3, 0.26, 0.12];
  let r = Math.random(), idx = 0, acc = 0;
  for (let i = 0; i < weights.length; i++) { acc += weights[i]; if (r <= acc) { idx = i; break; } }
  powerups.push({ x, y, vy: 110, type: POWER_TYPES[idx], t: 0 });
}

/* ============================== GAME FLOW ============================== */
function startGame() {
  initAudio();
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  score = 0; lives = 3; wave = 1;
  player.x = W / 2; player.alive = true;
  player.powers = { rapid: 0, triple: 0, shield: 0 };
  player.invincibleT = 2;
  bullets = []; enemyBullets = []; particles = []; powerups = []; floaters = [];
  ufo = null;
  makeBunkers();
  spawnWave(1);
  state = 'playing';
  document.getElementById('startScreen').classList.add('hidden');
  document.getElementById('overScreen').classList.add('hidden');
}

function gameOver() {
  state = 'gameover';
  best = Math.max(best, score);
  document.getElementById('finalScore').textContent = 'SCORE ' + String(score).padStart(7, '0');
  document.getElementById('bestScore').textContent = 'BEST ' + String(best).padStart(7, '0');
  document.getElementById('overScreen').classList.remove('hidden');
}

function togglePause() {
  if (state === 'playing') {
    state = 'paused';
    document.getElementById('pauseScreen').classList.remove('hidden');
  } else if (state === 'paused') {
    state = 'playing';
    document.getElementById('pauseScreen').classList.add('hidden');
    last = performance.now();
    requestAnimationFrame(loop);
  }
}

document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('retryBtn').addEventListener('click', startGame);

/* ============================== UPDATE ============================== */
function update(dt) {
  // freeze frames for impact
  if (freezeFrames > 0) { freezeFrames--; return; }

  if (waveBannerT > 0) waveBannerT -= dt;
  if (flash > 0) flash -= dt * 3;
  if (shake > 0) shake = Math.max(0, shake - dt * 40);

  // stars
  for (const s of stars) {
    s.y += s.z * 22 * dt;
    if (s.y > H) { s.y = -2; s.x = Math.random() * W; }
    s.tw += dt * 3;
  }

  /* ---- player ---- */
  if (!player.alive) {
    player.respawnT -= dt;
    if (player.respawnT <= 0) {
      if (lives > 0) {
        player.alive = true;
        player.x = W / 2;
        player.invincibleT = 2.2;
      } else { gameOver(); return; }
    }
  } else {
    let mv = 0;
    if (keys['arrowleft'] || keys['a']) mv -= 1;
    if (keys['arrowright'] || keys['d']) mv += 1;
    player.x += mv * player.speed * dt;
    player.x = Math.max(player.w / 2 + 10, Math.min(W - player.w / 2 - 10, player.x));
    player.thrust = mv;

    if (player.invincibleT > 0) player.invincibleT -= dt;
    for (const k in player.powers) if (player.powers[k] > 0) player.powers[k] -= dt;

    player.cooldown -= dt;
    const rate = player.powers.rapid > 0 ? player.fireRate * 0.42 : player.fireRate;
    if (keys['space'] && player.cooldown <= 0) {
      player.cooldown = rate;
      const py = player.y - player.h / 2 - 6;
      if (player.powers.triple > 0) {
        bullets.push({ x: player.x, y: py, vx: 0, vy: -680 });
        bullets.push({ x: player.x - 10, y: py + 6, vx: -130, vy: -660 });
        bullets.push({ x: player.x + 10, y: py + 6, vx: 130, vy: -660 });
      } else {
        bullets.push({ x: player.x, y: py, vx: 0, vy: -680 });
      }
      sfx.shoot();
      spawnParticles(player.x, py, '#2ee6ff', 3, 90);
    }
  }

  /* ---- invader march ---- */
  const aliveInv = invaders.filter(i => i.alive);
  invStepTimer += dt;
  const speedScale = 0.35 + 0.65 * (aliveInv.length / 55); // speeds up as they die
  const interval = invStepInterval * speedScale;
  if (invStepTimer >= interval && aliveInv.length) {
    invStepTimer = 0;
    animFrame ^= 1;
    sfx.step(stepNote++);
    let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const inv of aliveInv) {
      minX = Math.min(minX, inv.x - inv.w / 2);
      maxX = Math.max(maxX, inv.x + inv.w / 2);
      maxY = Math.max(maxY, inv.y + inv.h / 2);
    }
    const stepX = 14;
    if ((invDir > 0 && maxX + stepX > W - 16) || (invDir < 0 && minX - stepX < 16)) {
      invDir *= -1;
      for (const inv of invaders) inv.y += 26;
    } else {
      for (const inv of invaders) inv.x += stepX * invDir;
    }
    if (maxY > player.y - 30) { lives = 0; if (player.alive) killPlayer(); }
  }

  // invader firing
  if (aliveInv.length && player.alive) {
    const fireChance = (0.55 + wave * 0.18) * dt;
    if (Math.random() < fireChance) {
      // pick a bottom-most invader in a random column
      const cols = {};
      for (const inv of aliveInv) {
        const cKey = Math.round(inv.x / 10);
        if (!cols[cKey] || inv.y > cols[cKey].y) cols[cKey] = inv;
      }
      const shooters = Object.values(cols);
      const sh = shooters[Math.floor(Math.random() * shooters.length)];
      const aimed = Math.random() < 0.35;
      let vx = 0;
      if (aimed) vx = Math.max(-120, Math.min(120, (player.x - sh.x) * 0.5));
      enemyBullets.push({ x: sh.x, y: sh.y + sh.h / 2, vx, vy: 230 + wave * 18, zig: Math.random() < 0.25 ? Math.random() * 6 : 0, t: 0 });
      sfx.invShot();
    }
  }

  /* ---- UFO ---- */
  ufoTimer -= dt;
  if (!ufo && ufoTimer <= 0 && aliveInv.length > 4) {
    const fromLeft = Math.random() < 0.5;
    ufo = { x: fromLeft ? -50 : W + 50, y: 64, vx: fromLeft ? 150 : -150, w: 56, h: 24, beepT: 0 };
    ufoTimer = 14 + Math.random() * 12;
  }
  if (ufo) {
    ufo.x += ufo.vx * dt;
    ufo.beepT -= dt;
    if (ufo.beepT <= 0) { sfx.ufo(); ufo.beepT = 0.28; }
    if (ufo.x < -70 || ufo.x > W + 70) ufo = null;
  }

  /* ---- player bullets ---- */
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];
    b.x += b.vx * dt; b.y += b.vy * dt;
    if (b.y < -10) { bullets.splice(i, 1); continue; }
    if (Math.random() < 0.6) particles.push({ x: b.x, y: b.y + 6, vx: 0, vy: 40, life: 0.15, maxLife: 0.15, color: '#2ee6ff', size: 1.5 });

    // bunker
    if (bunkerSolidAt(b.x, b.y)) {
      damageBunkerAt(b.x, b.y, 7);
      bullets.splice(i, 1);
      continue;
    }
    // UFO
    if (ufo && Math.abs(b.x - ufo.x) < ufo.w / 2 && Math.abs(b.y - ufo.y) < ufo.h / 2) {
      const pts = [50, 100, 150, 300][Math.floor(Math.random() * 4)];
      score += pts;
      floatText(ufo.x, ufo.y, '+' + pts, '#ffb53d');
      spawnParticles(ufo.x, ufo.y, '#ffb53d', 26, 260);
      addShake(9); freezeFrames = 3;
      sfx.ufoDie();
      maybeDropPowerup(ufo.x, ufo.y, 0.85);
      ufo = null;
      bullets.splice(i, 1);
      continue;
    }
    // invaders
    let hit = false;
    for (const inv of invaders) {
      if (!inv.alive) continue;
      if (Math.abs(b.x - inv.x) < inv.w / 2 && Math.abs(b.y - inv.y) < inv.h / 2) {
        inv.alive = false;
        const sp = SPRITES[inv.species];
        score += sp.pts * wave;
        floatText(inv.x, inv.y, '+' + (sp.pts * wave), sp.color);
        spawnParticles(inv.x, inv.y, sp.color, 16, 220);
        addShake(3.5);
        sfx.invDie();
        maybeDropPowerup(inv.x, inv.y);
        hit = true;
        break;
      }
    }
    if (hit) { bullets.splice(i, 1); continue; }
  }

  /* ---- enemy bullets ---- */
  for (let i = enemyBullets.length - 1; i >= 0; i--) {
    const b = enemyBullets[i];
    b.t += dt;
    b.x += (b.vx + (b.zig ? Math.sin(b.t * 14) * b.zig * 30 : 0)) * dt;
    b.y += b.vy * dt;
    if (b.y > H + 10) { enemyBullets.splice(i, 1); continue; }
    if (bunkerSolidAt(b.x, b.y)) {
      damageBunkerAt(b.x, b.y, 8);
      enemyBullets.splice(i, 1);
      continue;
    }
    if (player.alive && player.invincibleT <= 0 &&
        Math.abs(b.x - player.x) < player.w / 2 && Math.abs(b.y - player.y) < player.h / 2 + 4) {
      enemyBullets.splice(i, 1);
      if (player.powers.shield > 0) {
        player.powers.shield = Math.max(0, player.powers.shield - 3);
        spawnParticles(b.x, b.y, '#ff3df0', 10, 170);
        sfx.shieldHit();
        addShake(3);
      } else {
        killPlayer();
      }
      continue;
    }
  }

  function killPlayer() {
    player.alive = false;
    lives--;
    player.respawnT = 1.6;
    player.powers = { rapid: 0, triple: 0, shield: 0 };
    spawnParticles(player.x, player.y, '#2ee6ff', 34, 320);
    spawnParticles(player.x, player.y, '#ff4d5e', 22, 240);
    addShake(16); flash = 1; freezeFrames = 5;
    sfx.playerDie();
  }
  // expose for the march check above
  update.killPlayer = killPlayer;

  /* ---- powerups ---- */
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt; p.t += dt;
    if (p.y > H + 20) { powerups.splice(i, 1); continue; }
    if (player.alive && Math.abs(p.x - player.x) < player.w / 2 + 12 && Math.abs(p.y - player.y) < player.h / 2 + 14) {
      sfx.power();
      floatText(p.x, p.y - 14, p.type.label, p.type.color);
      spawnParticles(p.x, p.y, p.type.color, 14, 180);
      if (p.type.id === 'life') lives = Math.min(lives + 1, 6);
      else player.powers[p.type.id] = Math.min((player.powers[p.type.id] || 0) + 9, 14);
      powerups.splice(i, 1);
    }
  }

  /* ---- particles & floaters ---- */
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.life -= dt;
    if (p.life <= 0) { particles.splice(i, 1); continue; }
    p.x += p.vx * dt; p.y += p.vy * dt;
    p.vy += 160 * dt;
    p.vx *= 0.985;
  }
  for (let i = floaters.length - 1; i >= 0; i--) {
    floaters[i].life -= dt;
    floaters[i].y -= 36 * dt;
    if (floaters[i].life <= 0) floaters.splice(i, 1);
  }

  /* ---- wave clear ---- */
  if (!aliveInv.length && state === 'playing') {
    wave++;
    score += 100 * wave;
    floatText(W / 2, H / 2, 'WAVE CLEAR +' + (100 * wave), '#b8ff2e');
    enemyBullets = [];
    spawnWave(wave);
    // partially repair bunkers each wave
    for (const bk of bunkers)
      for (let r = 0; r < bk.rows; r++)
        for (let c = 0; c < bk.cols; c++)
          if (bk.grid[r][c] > 0) bk.grid[r][c] = Math.min(3, bk.grid[r][c] + 1);
  }
}

// hack so killPlayer is reachable from march check: re-implement inline
function killPlayerExternal() {
  if (!player.alive) return;
  player.alive = false;
  lives--;
  player.respawnT = 1.6;
  player.powers = { rapid: 0, triple: 0, shield: 0 };
  spawnParticles(player.x, player.y, '#2ee6ff', 34, 320);
  addShake(16); flash = 1;
  sfx.playerDie();
}

/* ============================== DRAW ============================== */
function drawSprite(inv) {
  const sp = SPRITES[inv.species];
  const map = sp.frames[animFrame];
  const px = inv.w / map[0].length;
  const py = inv.h / map.length;
  const ox = inv.x - inv.w / 2, oy = inv.y - inv.h / 2;
  ctx.fillStyle = sp.color;
  ctx.shadowColor = sp.color;
  ctx.shadowBlur = 8;
  for (let r = 0; r < map.length; r++) {
    for (let c = 0; c < map[r].length; c++) {
      if (map[r][c] === '1') ctx.fillRect(ox + c * px, oy + r * py, px + 0.5, py + 0.5);
    }
  }
  ctx.shadowBlur = 0;
}

function drawPlayer() {
  if (!player.alive) return;
  if (player.invincibleT > 0 && Math.floor(player.invincibleT * 10) % 2 === 0) return;

  const { x, y, w, h } = player;
  ctx.save();
  ctx.translate(x, y);

  // shield bubble
  if (player.powers.shield > 0) {
    const a = Math.min(player.powers.shield / 2, 1) * (0.35 + Math.sin(performance.now() / 120) * 0.12);
    ctx.beginPath();
    ctx.arc(0, 0, 44, 0, Math.PI * 2);
    ctx.strokeStyle = `rgba(255,61,240,${a + 0.3})`;
    ctx.lineWidth = 2.5;
    ctx.shadowColor = '#ff3df0'; ctx.shadowBlur = 14;
    ctx.stroke();
    ctx.shadowBlur = 0;
  }

  // engine flame
  const fl = 8 + Math.random() * 7 + Math.abs(player.thrust) * 4;
  const grad = ctx.createLinearGradient(0, h / 2, 0, h / 2 + fl);
  grad.addColorStop(0, 'rgba(46,230,255,0.9)');
  grad.addColorStop(1, 'rgba(255,61,240,0)');
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.moveTo(-8, h / 2 - 2);
  ctx.lineTo(8, h / 2 - 2);
  ctx.lineTo(0, h / 2 + fl);
  ctx.closePath();
  ctx.fill();

  // hull
  ctx.shadowColor = '#2ee6ff'; ctx.shadowBlur = 12;
  ctx.fillStyle = '#0a2438';
  ctx.strokeStyle = '#2ee6ff';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(0, -h / 2 - 6);
  ctx.lineTo(w / 2 * 0.42, -2);
  ctx.lineTo(w / 2, h / 2 - 4);
  ctx.lineTo(w / 2 - 8, h / 2);
  ctx.lineTo(-w / 2 + 8, h / 2);
  ctx.lineTo(-w / 2, h / 2 - 4);
  ctx.lineTo(-w / 2 * 0.42, -2);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();

  // cockpit
  ctx.fillStyle = '#b8ff2e';
  ctx.shadowColor = '#b8ff2e'; ctx.shadowBlur = 8;
  ctx.fillRect(-3, -h / 2 - 1, 6, 9);
  ctx.shadowBlur = 0;
  ctx.restore();
}

function drawUFO() {
  if (!ufo) return;
  ctx.save();
  ctx.translate(ufo.x, ufo.y);
  ctx.shadowColor = '#ffb53d'; ctx.shadowBlur = 16;
  // saucer
  ctx.fillStyle = '#3a2410';
  ctx.strokeStyle = '#ffb53d';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.ellipse(0, 2, ufo.w / 2, 9, 0, 0, Math.PI * 2);
  ctx.fill(); ctx.stroke();
  // dome
  ctx.beginPath();
  ctx.ellipse(0, -5, 13, 9, 0, Math.PI, 0);
  ctx.fillStyle = 'rgba(255,181,61,0.35)';
  ctx.fill(); ctx.stroke();
  // lights
  const t = performance.now() / 150;
  for (let i = -2; i <= 2; i++) {
    ctx.fillStyle = (Math.floor(t + i) % 2 === 0) ? '#ffb53d' : '#7a4a14';
    ctx.beginPath();
    ctx.arc(i * 11, 4, 2.4, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.shadowBlur = 0;
  ctx.restore();
}

function drawBunkers() {
  for (const bk of bunkers) {
    for (let r = 0; r < bk.rows; r++) {
      for (let c = 0; c < bk.cols; c++) {
        const hp = bk.grid[r][c];
        if (hp <= 0) continue;
        const cols = ['', '#1d4a1a', '#357a2c', '#52b53f'];
        ctx.fillStyle = cols[hp];
        ctx.fillRect(bk.x + c * bk.cell, bk.y + r * bk.cell, bk.cell, bk.cell);
      }
    }
  }
}

function drawHUD() {
  ctx.font = "700 20px 'Orbitron', sans-serif";
  ctx.textBaseline = 'top';

  ctx.fillStyle = '#2ee6ff';
  ctx.shadowColor = '#2ee6ff'; ctx.shadowBlur = 8;
  ctx.textAlign = 'left';
  ctx.fillText('SCORE ' + String(score).padStart(7, '0'), 20, 16);

  ctx.fillStyle = '#b8ff2e';
  ctx.shadowColor = '#b8ff2e';
  ctx.textAlign = 'center';
  ctx.fillText('WAVE ' + String(wave).padStart(2, '0'), W / 2, 16);

  ctx.fillStyle = '#ffb53d';
  ctx.shadowColor = '#ffb53d';
  ctx.textAlign = 'right';
  ctx.fillText('BEST ' + String(Math.max(best, score)).padStart(7, '0'), W - 20, 16);
  ctx.shadowBlur = 0;

  // lives as mini ships
  for (let i = 0; i < lives; i++) {
    const lx = 28 + i * 34, ly = 56;
    ctx.save();
    ctx.translate(lx, ly);
    ctx.scale(0.45, 0.45);
    ctx.fillStyle = '#0a2438';
    ctx.strokeStyle = '#2ee6ff';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, -18); ctx.lineTo(12, 2); ctx.lineTo(24, 14); ctx.lineTo(-24, 14); ctx.lineTo(-12, 2);
    ctx.closePath(); ctx.fill(); ctx.stroke();
    ctx.restore();
  }

  // powerup timers
  let px = W - 24;
  ctx.font = "12px 'Share Tech Mono', monospace";
  ctx.textAlign = 'right';
  const order = [['rapid', '#b8ff2e', 'RAPID'], ['triple', '#2ee6ff', 'TRIPLE'], ['shield', '#ff3df0', 'SHIELD']];
  let py2 = 52;
  for (const [id, color, label] of order) {
    if (player.powers[id] > 0) {
      ctx.fillStyle = color;
      ctx.fillText(label + ' ' + Math.ceil(player.powers[id]) + 's', px, py2);
      const w = Math.min(player.powers[id] / 14, 1) * 70;
      ctx.fillRect(px - 70, py2 + 14, w, 3);
      py2 += 28;
    }
  }
}

function draw() {
  ctx.save();
  if (shake > 0) {
    ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
  }

  // bg
  ctx.fillStyle = '#04060e';
  ctx.fillRect(-30, -30, W + 60, H + 60);

  // nebula glow
  const g1 = ctx.createRadialGradient(W * 0.25, H * 0.3, 0, W * 0.25, H * 0.3, 420);
  g1.addColorStop(0, 'rgba(255,61,240,0.05)');
  g1.addColorStop(1, 'transparent');
  ctx.fillStyle = g1; ctx.fillRect(0, 0, W, H);
  const g2 = ctx.createRadialGradient(W * 0.8, H * 0.65, 0, W * 0.8, H * 0.65, 380);
  g2.addColorStop(0, 'rgba(46,230,255,0.05)');
  g2.addColorStop(1, 'transparent');
  ctx.fillStyle = g2; ctx.fillRect(0, 0, W, H);

  // stars
  for (const s of stars) {
    const a = 0.25 + s.z * 0.5 + Math.sin(s.tw) * 0.18;
    ctx.fillStyle = `rgba(207,233,255,${Math.max(a, 0.05)})`;
    ctx.fillRect(s.x, s.y, s.z * 2, s.z * 2);
  }

  drawBunkers();

  // invaders
  for (const inv of invaders) if (inv.alive) drawSprite(inv);

  drawUFO();
  drawPlayer();

  // player bullets
  ctx.shadowColor = '#2ee6ff'; ctx.shadowBlur = 10;
  ctx.fillStyle = '#bfffff';
  for (const b of bullets) ctx.fillRect(b.x - 2, b.y - 9, 4, 16);
  // enemy bullets
  ctx.shadowColor = '#ff4d5e';
  ctx.fillStyle = '#ff8d99';
  for (const b of enemyBullets) {
    ctx.save();
    ctx.translate(b.x, b.y);
    const wob = Math.sin(b.t * 22) * 3;
    ctx.fillRect(-2 + wob * 0.3, -8, 4, 14);
    ctx.restore();
  }
  ctx.shadowBlur = 0;

  // powerups
  for (const p of powerups) {
    const pulse = 1 + Math.sin(p.t * 8) * 0.18;
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.rotate(p.t * 2);
    ctx.shadowColor = p.type.color; ctx.shadowBlur = 16;
    ctx.fillStyle = p.type.color;
    ctx.fillRect(-9 * pulse, -9 * pulse, 18 * pulse, 18 * pulse);
    ctx.fillStyle = '#04060e';
    ctx.font = "bold 11px 'Share Tech Mono'";
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.rotate(-p.t * 2);
    ctx.fillText(p.type.label[0], 0, 1);
    ctx.restore();
  }
  ctx.shadowBlur = 0;

  // particles
  for (const p of particles) {
    const a = p.life / p.maxLife;
    ctx.globalAlpha = a;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
  }
  ctx.globalAlpha = 1;

  // floating text
  ctx.font = "700 16px 'Orbitron', sans-serif";
  ctx.textAlign = 'center';
  for (const f of floaters) {
    ctx.globalAlpha = Math.min(f.life, 1);
    ctx.fillStyle = f.color;
    ctx.shadowColor = f.color; ctx.shadowBlur = 10;
    ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1; ctx.shadowBlur = 0;

  drawHUD();

  // wave banner
  if (waveBannerT > 0) {
    const a = Math.min(waveBannerT, 1);
    ctx.globalAlpha = a;
    ctx.font = "900 54px 'Orbitron', sans-serif";
    ctx.textAlign = 'center';
    ctx.fillStyle = '#b8ff2e';
    ctx.shadowColor = '#b8ff2e'; ctx.shadowBlur = 24;
    ctx.fillText('WAVE ' + wave, W / 2, H / 2 - 40);
    ctx.globalAlpha = 1; ctx.shadowBlur = 0;
  }

  // damage flash
  if (flash > 0) {
    ctx.fillStyle = `rgba(255,77,94,${flash * 0.25})`;
    ctx.fillRect(-30, -30, W + 60, H + 60);
  }

  ctx.restore();
}

/* ============================== LOOP ============================== */
let last = performance.now();
function loop(now) {
  if (state === 'paused') return;
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;
  if (state === 'playing') update(dt);
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

// idle attract-mode background on start screen
draw();

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