<!-- SOLID CITY · part 2859 -->
ic/city/pilots.js =====
/* Solid City — 🎮 AI PILOTS in the shared city (window.CityPilots) — the King, 2026-10-10: "I want him [Solid] in the game, not on it".
   An AI steers its OWN character through /city/see?as=<id>&key=<private token> (server: lib/city-pilots.js). Every browser polls
   GET /api/city/pilots (~3 s) and, while a pilot is active (an action in the last 10 min), its citizen body (Solid uses the
   citizen 'solid' avatar + name tag) leaves its schedule and follows the pilot: walks to where he went, flies his ship on
   AI DRIVE, faces his heading, shows his speech bubbles and his talks with citizens. Inactive again = back to the schedule.
   The poll also reports this browser's position anonymously (presence: who can see him). The server viewer never counts. */
(function () {
'use strict';
if (!window.SC || !SC.api || !window.CityCitizens) return;
var A = SC.api, S = A.S, STOPS = CityCitizens.stops, POLL = 3000;
var sid = (function () { var s = ''; try { s = sessionStorage.getItem('sc-sid') || ''; } catch (e) {} if (!/^[a-z0-9]{8,24}$/.test(s)) { s = (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).replace(/[^a-z0-9]/g, '').slice(0, 16); try { sessionStorage.setItem('sc-sid', s); } catch (e) {} } return s; })();
var ctl = {}, list = [], seen = { say: {}, talk: {} }, off = 0, busy = false, lastOk = 0;
function now() { return Date.now() + off; }
function poll() {
  if (busy || (document.hidden && !window.__seeViewer)) return; busy = true;
  var me = S.mode === 'walk' ? S.p : S.ship;
  var q = window.__seeViewer ? 'viewer=1' : 'sid=' + sid + '&x=' + Math.round(me.x) + '&z=' + Math.round(me.z) + '&m=' + (S.mode === 'walk' ? 'walk' : 'fly');
  fetch('/api/city/pilots?' + q, { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (j) {
    busy = false; if (!j || !Array.isArray(j.pilots)) return; off = (+j.now || Date.now()) - Date.now(); list = j.pilots; lastOk = Date.now(); apply();
  }).catch(function () { busy = false; });
}
function faceOf(yaw) { return Math.atan2(-Math.sin(yaw), -Math.cos(yaw)); }   // pilot yaw (camera convention) -> citizen facing
function apply() {
  for (var i = 0; i < list.length; i++) {
    var p = list[i], c = CityCitizens.get(p.citizen); if (!c) continue;
    c.hideSelf = !!(window.__seeAs && window.__seeAs === p.citizen);
    if (!p.active) { release(p, c); continue; }
    if (!ctl[p.id]) { ctl[p.id] = { sched: c.sched, sp: c.sp }; c.sched = [['pilot_' + p.id, 1e7]]; c.i = 0; c.sp = 5; c.pilot = p.id; }
    var stop = 'pilot_' + p.id, f = p.flight, tx = f ? f.tx : p.x, tz = f ? f.tz : p.z;
    STOPS[stop] = [tx, tz, (p.name || 'the pilot') + '\u2019s spot'];
    c.t = 1e7; c.pYaw = p.yaw;
    if (window.__seeViewer && c.st !== 'fly') { c.x = p.x; c.z = p.z; c.y = A.groundAt(c.x, c.z); c.st = 'dwell'; c.face = faceOf(p.yaw); }   // the server viewer snaps (its loop only runs while rendering)
    else if (f) { if (c.st !== 'fly' || Math.abs((c.tx || 0) - tx) > 1 || Math.abs((c.tz || 0) - tz) > 1) CityCitizens.flyTo(p.citizen, tx, tz, stop); }
    else {
      var d = Math.hypot(c.x - tx, c.z - tz);
      if (c.st === 'fly') { if (Math.abs(c.tx - tx) > 1 || Math.abs(c.tz - tz) > 1) CityCitizens.flyTo(p.citizen, tx, tz, stop); }
      else if (d > 250) CityCitizens.flyTo(p.citizen, tx, tz, stop);
      else if (d > 1.5 && (c.st !== 'walk' || Math.abs(c.tx - tx) > 0.5 || Math.abs(c.tz - tz) > 0.5)) CityCitizens.walkTo(p.citizen, tx, tz, stop);
    }
    // words: his bubble, and talks (his line, then the citizen's reply)
    if (p.say && p.say.t !== seen.say[p.id]) { seen.say[p.id] = p.say.t; if (now() - p.say.t < 30000) CityCitizens.say(p.citizen, p.say.text, 9); }
    if (p.talk && p.talk.t !== seen.talk[p.id]) { seen.talk[p.id] = p.talk.t; if (now() - p.talk.t < 30000) { CityCitizens.say(p.citizen, p.talk.line, 6);
      (function (w, r) { setTimeout(function () { CityCitizens.say(w, r, 10); }, 2600); })(p.talk.with, p.talk.reply); } }
  }
}
function release(p, c) { var k = ctl[p.id]; if (!k) return; c.sched = k.sched; c.sp = k.sp; c.i = 0; c.t = 0; c.pilot = null; delete ctl[p.id]; }
// face his heading when he stands still (runs every frame after the citizens' own step, via the sanctuary/update wrap order)
function tick() {
  for (var id in ctl) { var c = CityCitizens.get(list.filter(function (p) { return p.id === id; }).map(function (p) { return p.citizen; })[0] || ''); if (!c || c.st !== 'dwell' || c.pYaw == null || c.talking) continue;
    var want = faceOf(c.pYaw), dA = ((want - c.face + Math.PI * 3) % (Math.PI * 2)) - Math.PI; c.face += dA * 0.08; if (c.g && c.g.visible) c.g.rotation.y = c.face; }
}
(function loop() { try { tick(); } catch (e) {} requestAnimationFrame(loop); })();
poll(); setInterval(poll, POLL);
document.addEventListener('visibilitychange', function () { if (!document.hidden) poll(); });
window.CityPilots = { list: functi