<!-- SOLID CITY · part 3417 -->
Y(P.x, P.z);
  const ty = hd < 10 ? SHIP_REST : Math.max(gy + 14, Math.min(P.y, hd * 0.32 + 14));
  const wantVy = hd < 10 ? clamp((ty - P.y) * 1.4, -9, 0) : clamp((ty - P.y) * 1.1, -32, 18);
  L.vy = damp(L.vy, wantVy, 2.2, dt);
  P.x += L.hv.x * dt; P.z += L.hv.z * dt; P.y += L.vy * dt; if (P.y < gy + 1.2 && hd > 16) P.y = gy + 1.2;
  S.speed = Math.hypot(L.hv.x, L.hv.z, L.vy);
  S.pitch = damp(S.pitch, clamp(-Math.hypot(L.hv.x, L.hv.z) / 400, -0.15, 0), 3, dt); S.roll = damp(S.roll, -input.x * 0.3, 4, dt);
  if (hd < 10 && P.y < SHIP_REST + 0.35 && Math.hypot(L.hv.x, L.hv.z) < 7) { P.y = SHIP_REST; parked(); return; }
  if (P.y - gy < 40 && Math.random() < 0.6) { tv4.set(P.x + (Math.random() - 0.5) * 3, Math.max(gy, 0) + 0.5, P.z + (Math.random() - 0.5) * 3); tv.set((Math.random() - 0.5) * 30, 2, (Math.random() - 0.5) * 30); emit(sparks, tv4, tv, COL.smoke, 1.5, 6, 0.4); }
}

/* --- weapons --- */
const AIM_CONE = IS_TOUCH ? 0.3 : 0.12;
function updateLock() {
  S.lock = null; if (S.phase !== 'battle') return; forward(tv3); let best = AIM_CONE;
  for (const a of aliens) { if (!a.alive) continue; tv.copy(a.pos).sub(S.pos); const d = tv.length(); if (d > 1700) continue; const ang = Math.acos(clamp(tv.dot(tv3) / d, -1, 1)) - Math.atan(a.r / d) * 0.8; if (ang < best) { best = ang; S.lock = a; } }
}
function fireLaser() {
  const l = lasers.find((x) => !x.on); if (!l) return; S.gun ^= 1; S.shots++;
  forward(tv3); const right = tv2.set(1, 0, 0).applyQuaternion(shipQ);
  l.pos.copy(S.pos).addScaledVector(right, S.gun ? 2.4 : -2.4).addScaledVector(tv3, -0.6).add(tv.set(0, -0.12, 0).applyQuaternion(shipQ));
  l.prev.copy(l.pos); l.dir.copy(tv3);
  if (S.lock) { const a = S.lock, tt = a.pos.distanceTo(l.pos) / (1500 + S.speed); l.dir.copy(tv.copy(a.pos).addScaledVector(a.vel, tt).sub(l.pos).normalize()); }   // aim assist + lead
  l.v = 1500 + S.speed; l.life = 1.15; l.on = true; sfx('laser');
  flash(tv.copy(l.pos).addScaledVector(tv3, 1.5), 3, false, COL.muzzle);
}
const mtxL = new THREE.Matrix4(), qL = new THREE.Quaternion(), sL = new V3(1, 1, 16), zAxis = new V3(0, 0, 1), segA = new V3(), segB = new V3();
function segHit(p0, p1, c, r) { segA.copy(p1).sub(p0); const L2 = segA.lengthSq(); const t = L2 > 0 ? clamp(segB.copy(c).sub(p0).dot(segA) / L2, 0, 1) : 0; segB.copy(p0).addScaledVector(segA, t); return segB.distanceToSquared(c) < r * r; }
function updateLasers(dt) {
  let n = 0;
  for (const l of lasers) {
    if (!l.on) continue; l.prev.copy(l.pos); l.pos.addScaledVector(l.dir, l.v * dt); l.life -= dt;
    if (l.life <= 0) { l.on = false; continue; }
    for (const a of aliens) { if (a.alive && segHit(l.prev, l.pos, a.pos, a.r + (IS_TOUCH ? 3 : 1.5))) { l.on = false; hitAlien(a, l.pos); break; } }
    if (!l.on) continue;
    qL.setFromUnitVectors(zAxis, l.dir); mtxL.compose(tv.copy(l.pos).addScaledVector(l.dir, -8), qL, sL);
    laserMesh.setMatrixAt(n, mtxL); laserCore.setMatrixAt(n, mtxL); n++;
  }
  laserMesh.count = laserCore.count = n; laserMesh.instanceMatrix.needsUpdate = laserCore.instanceMatrix.needsUpdate = true;
}
function hitAlien(a, at) {
  a.hp--; a.flash = 0.09; hitSpark(at, a.type === 'circler' ? COL.magenta : COL.green); sfx('hit');
  if (a.type === 'scout' && a.mode === 'attack' && a.hp > 0) { a.mode = 'break'; a.modeT = 1.6; a.aim.set(0, 0, 0); }
  if (a.hp <= 0) {
    a.alive = false; a.mesh.visible = false; S.kills++; const big = a.type === 'mother';
    explode(a.pos.clone(), big ? 3.2 : 1, a.type === 'circler' ? COL.magenta : a.type === 'diver' ? COL.red : COL.green); sfx(big ? 'bigboom' : 'boom');
    S.shake = Math.max(S.shake, big ? 1.2 : 0.35);
    if (big) for (let k = 0; k < 5; k++) setTimeout(() => explode(a.pos.clone().add(new V3((Math.random() - 0.5) * 50, (Math.random() - 0.5) * 16, (Math.random() - 0.5) * 50)), 1.3, COL.fire), 160 + k * 170);
  }
}
function alienFire(a, spread) {
  const o = orbs.find((x) => !x.on); if (!o) return;
  const tt = a.pos.distanceTo(S.pos) / 260;
  tv.copy(S.pos).addScaledVector(forward(tv3), S.speed * tt * 0.7).sub(a.pos).normalize();
  if (spread) tv.add(tv2.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(spread)).normalize();
  o.pos.copy(a.pos); o.prev.copy(a.pos); o.vel.copy(tv).multiplyScalar(260); o.life = 5; o.on = true;
}
function updateOrbs(dt) {
  for (let i = 0; i < ORB_N; i++) {
    const o = orbs[i];
    if (o.on) { o.prev.copy(o.pos); o.pos.addScaledVector(o.vel, dt); o.life -= dt; if (o.life <= 0) o.on = false; else if (ship.visible && S.phase === 'battle' && segHit(o.prev, o.pos, S.pos, IS_TOUCH ? 4.6 : 5.2)) { o.on = false; damage(10, o.pos); } }
    if (o.on) { orbPos[i * 3] = o.pos.x; orbPos[i * 3 + 1] = o.pos.y; orbPos[i * 3 + 2] = o.pos.z; } else orbPos[i * 3 + 1] = -1e6;
  }
  orbGeo.attributes.position.needsUpdate = true;
}

/* --- alien attack patterns: swoop / circle / dive / mothership broadside --- */
const desired = new V3(), fwdA = new V3();
functi