<!-- SOLID CITY · part 3418 -->
on steer(a, target, speed, turn, dt) { desired.copy(target).sub(a.pos); const d = desired.length(); if (d > 0.01) desired.multiplyScalar(speed / d); a.vel.lerp(desired, 1 - Math.exp(-turn * dt)); return d; }
function updateAliens(dt) {
  if (!aliens.length) return; const P = S.pos, fwd = forward(fwdA), calm = S.invuln > 0 || S.phase !== 'battle';
  for (const a of aliens) {
    if (!a.alive) continue; a.t += dt; a.modeT -= dt; a.fireT -= dt;
    const d = a.pos.distanceTo(P);
    if (a.type === 'scout') {
      if (a.mode === 'attack') { steer(a, tv3.copy(P).addScaledVector(fwd, 60), a.vmax, 1.4, dt); if (d < 110) { a.mode = 'break'; a.modeT = 2.6; a.aim.set(Math.random() - 0.5, Math.random() * 0.6 - 0.3, Math.random() - 0.5).normalize().multiplyScalar(800).add(P); } }
      else { steer(a, a.aim.lengthSq() ? a.aim : tv3.copy(P).addScaledVector(fwd, 600), a.vmax * 1.1, 1.2, dt); if (a.modeT <= 0) { a.mode = 'attack'; a.aim.set(0, 0, 0); } }
      if (a.fireT <= 0 && d < 950 && !calm) { if (tv.copy(P).sub(a.pos).normalize().dot(tv2.copy(a.vel).normalize()) > 0.6) { alienFire(a, 0.04); a.fireT = 1.9 + Math.random() * 1.2; } else a.fireT = 0.4; }
    } else if (a.type === 'circler') {
      a.orbit += dt * (0.42 + 0.06 * a.side) * a.side; const rad = 330 + Math.sin(a.t * 0.7) * 80;
      tv3.set(Math.cos(a.orbit) * rad, Math.sin(a.t * 0.9 + a.orbit) * 90, Math.sin(a.orbit) * rad).add(P).addScaledVector(fwd, 150);
      steer(a, tv3, a.vmax + S.speed * 0.8, 1.6, dt);
      if (a.fireT <= 0 && d < 900 && !calm) { alienFire(a, 0.06); a.fireT = 2.3 + Math.random() * 1.4; }
    } else if (a.type === 'diver') {
      if (a.mode === 'hold') { steer(a, tv3.copy(P).addScaledVector(fwd, 700).add(tv2.set(Math.cos(a.t + a.side) * 200, 150, Math.sin(a.t) * 200)), S.speed + 60, 1.2, dt); if (a.modeT <= 0 && !calm) { a.mode = 'dive'; a.modeT = 4; sfx('zap'); } }
      else if (a.mode === 'dive') {
        steer(a, P, a.vmax + S.speed * 0.4, 2.2, dt);
        if (d < 12) { damage(22, a.pos); a.mode = 'away'; a.modeT = 2.5; a.aim.copy(a.vel).normalize().multiplyScalar(900).add(a.pos).add(tv2.set(0, 300, 0)); }
        else if (d < 80 || a.modeT <= 0) { a.mode = 'away'; a.modeT = 2.2; a.aim.copy(a.vel).normalize().multiplyScalar(900).add(a.pos).add(tv2.set(0, 300, 0)); }
      } else { steer(a, a.aim, a.vmax, 1.2, dt); if (a.modeT <= 0) { a.mode = 'hold'; a.modeT = 2.5 + Math.random() * 2; } }
      if (a.fireT <= 0 && d < 700 && a.mode !== 'dive' && !calm) { alienFire(a, 0.08); a.fireT = 3 + Math.random() * 2; }
    } else {
      tv3.copy(P).addScaledVector(fwd, 900).add(tv2.set(Math.sin(a.t * 0.3) * 300, 140 + Math.sin(a.t * 0.5) * 60, Math.cos(a.t * 0.3) * 300));
      if (d > 2600) tv3.copy(P); steer(a, tv3, Math.max(a.vmax, S.speed * 0.85), 0.5, dt);
      a.mesh.rotation.y += dt * 0.35;
      if (a.fireT <= 0 && d < 1500 && !calm) { for (let k = 0; k < 3; k++) alienFire(a, 0.05 + k * 0.03); a.fireT = 2.8; }
    }
    a.pos.addScaledVector(a.vel, dt); a.mesh.position.copy(a.pos);
    if (a.type !== 'mother') { a.mesh.lookAt(tv.copy(a.pos).add(a.vel)); if (a.type === 'circler') a.mesh.rotateZ(a.t * 2); }
    a.flash = Math.max(0, a.flash - dt); const e = a.flash > 0 ? 2.5 : 0; a.hull.emissive.setRGB(e, e, e);
  }
}

/* --- ship effects --- */
function updateShipFx(dt) {
  const ph = S.phase; let p = 0.25, b = 0;
  if (ph === 'count') p = 0.3 + S.phaseT * 0.15; else if (ph === 'climb') p = 1; else if (ph === 'battle' || ph === 'return') p = 0.55 + clamp(S.speed / BOOST_V, 0, 1) * 0.35; else if (ph === 'landing') p = 0.45; else if (ph === 'parked' || ph === 'pad' || ph === 'result') p = 0.12;
  if (S.boosting) { p = 1.3; b = 1; }
  const u = shipFx.flameMat.uniforms; u.uP.value = damp(u.uP.value, p, 8, dt); u.uB.value = damp(u.uB.value, b, 6, dt);
  const fl = u.uP.value;
  shipFx.flames.forEach((f) => { f.scale.set(0.9 + fl * 0.3, 0.9 + fl * 0.3, (1.2 + fl * 5.5) * (0.9 + Math.random() * 0.2)); });
  shipFx.glows.forEach((g) => { g.scale.setScalar(2.4 + fl * 4 + Math.random() * 0.6); g.material.color.setRGB(2.5 + b, 1.2 + b * 1.4, 0.5 + b * 2.5); });
  shipFx.heat.visible = S.heat > 0.02; shipFx.heat.material.uniforms.uA.value = S.heat * 0.55;
  shipFx.shield.material.opacity = Math.max(0, shipFx.shield.material.opacity - dt * 1.8) + (S.invuln > 0 && ship.visible ? 0.08 + 0.06 * Math.sin(S.t * 20) : 0);
}

/* --- camera: cinematic on the pad, chase in flight, high chase while parking --- */
const camWant = new V3(), camLook = new V3(), camF = new V3(), camH = new V3();
function updateCamera(dt) {
  const ph = S.phase, P = S.pos, f = forward(camF); let k = 5;
  if (ph === 'pad' || ph === 'loading' || ph === 'result' || ph === 'parked') {
    const a = S.t * 0.12 + 0.6, r = ph === 'pad' || ph === 'loading' ? 22 : 26;
    camWant.set(P.x + Math.sin(a) * r, P.y + 6 + Math.sin(S.t * 0.2) * 1.5, P.z + Math.cos(a) * r); camLook.copy(P).y += 1.4; k = 2;
  } else if (ph === 'count' || (ph === 'climb'