<!-- SOLID CITY · part 3415 -->
;
  const add = (type, p) => { const a = makeAlien(type); a.pos.copy(p); a.vel.copy(P).sub(p).normalize().multiplyScalar(a.vmax * 0.5); a.mesh.position.copy(a.pos); aliens.push(a); return a; };
  if (w === 1) {
    for (let i = 0; i < 6; i++) { const row = Math.floor((i + 1) / 2), side = i === 0 ? 0 : (i % 2 ? 1 : -1); add('scout', tv.copy(P).addScaledVector(f, 1500 + row * 70).addScaledVector(r, side * row * 55).add(tv2.set(0, 60 - row * 10, 0))).fireT = 3 + i * 0.5; }
    banner('WAVE 1 / 3', 'SWOOPERS · hold FIRE, aim close', 3);
  } else if (w === 2) {
    for (let i = 0; i < 6; i++) { const a = add('circler', tv.copy(P).addScaledVector(f, Math.cos(i * 1.047) * 700 + 400).addScaledVector(r, Math.sin(i * 1.047) * 700).add(tv2.set(0, i % 2 ? 80 : -60, 0))); a.orbit = i * 1.047; a.fireT = 2.5 + i * 0.6; }
    banner('WAVE 2 / 3', 'CIRCLERS · they orbit you · turn and burn', 3);
  } else {
    add('mother', tv.copy(P).addScaledVector(f, 1300).add(tv2.set(0, 120, 0))).fireT = 4;
    for (let i = 0; i < 4; i++) { const a = add('diver', tv.copy(P).addScaledVector(f, 1100).addScaledVector(r, (i - 1.5) * 160).add(tv2.set(0, 200, 0))); a.mode = 'hold'; a.modeT = 2 + i * 1.6; }
    banner('WAVE 3 / 3', 'MOTHERSHIP + DIVERS · take it down', 3.2); H['h-mother'].hidden = false;
  }
  sfx('alarm');
}
function allDown() { setPhase('return'); H['h-mother'].hidden = true; S.homeShown = true; banner('RETURN HOME', 'All aliens down · follow the arrow, dive through re-entry', 4.5); sfx('chime'); }
function startLanding() {
  setPhase('landing'); const f = forward(tv); S.landing = { hv: new V3(f.x, 0, f.z).multiplyScalar(S.speed), vy: f.y * S.speed };
  banner('LANDING MODE', IS_TOUCH ? 'Hover onto the glowing circle · stick steers' : 'Hover onto the glowing circle · WASD steers', 3.2); sfx('zap');
}
function parked() {
  setPhase('parked'); S.speed = 0; S.pos.y = SHIP_REST; S.pitch = 0; S.roll = 0; guide.visible = false;
  banner('PARKED', 'Welcome home, pilot', 2.2); sfx('chime');
  S.result = { timeMs: Math.round(S.runT * 1000), kills: S.kills, hits: S.hits, top: Math.round(S.topSpeed * KMH) };
  setTimeout(() => showResult(S.result), 1500);
}
function damage(dmg, from) {
  if (S.invuln > 0 || S.phase !== 'battle' || bot.god) return;
  S.shields = Math.max(0, S.shields - dmg); S.hits++; S.hurt = 1; S.shake = Math.max(S.shake, 0.9); S.slowT = 1.1; S.sinceHit = 0;
  shipFx.shield.material.opacity = 0.55; sfx('ouch'); if (from) hitSpark(from, COL.green);
  if (S.shields <= 0) {   // no harsh game over: knocked out, respawn at the last checkpoint
    explode(S.pos.clone(), 1.4, COL.red); sfx('bigboom'); ship.visible = false; setPhase('respawn'); S.respawns = (S.respawns || 0) + 1;
    banner('SHIELDS DOWN', 'Respawning at the last checkpoint…', 2.4);
  }
}

/* ======================= UPDATE ======================= */
const homeV = new V3(0, SHIP_REST, 0);
function update(dt) {
  S.t += dt; S.phaseT += dt; U.time.value += dt;
  readInput(dt);
  const ph = S.phase;
  if (ph === 'climb' || ph === 'battle' || ph === 'return' || ph === 'landing' || ph === 'respawn') S.runT += dt;
  if (ph === 'count') {
    const n = Math.ceil(3 - S.phaseT); if (n !== countN) { countN = n; H['h-count'].textContent = n > 0 ? String(n) : 'GO'; H['h-count'].classList.remove('pop'); void H['h-count'].offsetWidth; H['h-count'].classList.add('pop'); sfx(n > 0 ? 'beep' : 'go'); }
    S.shake = Math.max(S.shake, 0.1 + S.phaseT * 0.08);
    if (S.phaseT > 3.25) { H['h-count'].hidden = true; setPhase('climb'); S.vUp = 0; banner('BLAST OFF', 'Climb through the clouds into space', 2.5); }
  }
  if (ph === 'climb') updateClimb(dt);
  else if (ph === 'battle' || ph === 'return') updateFlight(dt);
  else if (ph === 'landing') updateLanding(dt);
  else if (ph === 'respawn' && S.phaseT > 2.4) { const c = S.checkpoint; S.pos.copy(c.pos); S.yaw = c.yaw; S.pitch = 0; S.speed = BASE_V; S.shields = 100; S.invuln = 3; ship.visible = true; setPhase('battle'); S.phaseT = 3; camState.init = false; banner('BACK IN THE FIGHT', 'Shields restored', 2); }
  const canBoost = ph === 'climb' || ph === 'battle' || ph === 'return';
  if (canBoost && input.boosting && S.boostMeter > (S.boosting ? 0 : 0.15)) { if (!S.boosting) sfx('boost'); S.boosting = true; S.boostMeter = Math.max(0, S.boostMeter - dt * 0.26); S.boostLock = 0.9; }
  else { S.boosting = false; S.boostLock -= dt; if (S.boostLock <= 0) S.boostMeter = Math.min(1, S.boostMeter + dt * 0.17); }
  S.sinceHit += dt; S.invuln = Math.max(0, S.invuln - dt); S.slowT = Math.max(0, S.slowT - dt);
  if (S.sinceHit > 4 && S.shields < 100 && ph === 'battle') S.shields = Math.min(100, S.shields + dt * 6);
  S.fireCd -= dt;
  if (input.firing && (ph === 'battle' || ph === 'return' || ph === 'climb') && ship.visible && S.fireCd <= 0) { fireLaser(); S.fireCd = 0.11; }
  updateLock(); updateAliens(dt); updateLasers(dt); updateOrbs(dt);
  if (ph === 'battle') {
    const alive = aliens.some((a) => a.alive);