<!-- SOLID CITY · part 2164 -->
0)); ii <= Math.min(N - 1, ci(x1)); ii++) g[jj * N + ii] = 1; }
  A.solids.forEach(function (s) { if (s.y0 < Y + 2.2 && s.y1 > Y + 0.3) mark(s.x0 - 1.1, s.z0 - 1.1, s.x1 + 1.1, s.z1 + 1.1); });
  mark(SB.x - 1.8, SB.z - 1.8, SB.x + 1.8, SB.z + 1.8);
  A.portals.forEach(function (p) { mark(p.x - 3.2, p.z - 3.2, p.x + 3.2, p.z + 3.2); });   // never stroll through a gate by accident
  grid = g;
}
function blocked(i, j) { return i < 0 || j < 0 || i >= N || j >= N || grid[j * N + i] === 1; }
function freeNear(i, j) { if (!blocked(i, j)) return [i, j]; for (var r = 1; r < 20; r++) for (var dj = -r; dj <= r; dj++) for (var di = -r; di <= r; di++) { if (Math.max(Math.abs(di), Math.abs(dj)) !== r) continue; if (!blocked(i + di, j + dj)) return [i + di, j + dj]; } return null; }
function los(a, b) { var dx = b.x - a.x, dz = b.z - a.z, l = Math.hypot(dx, dz), n = Math.ceil(l / 1); for (var k = 1; k < n; k++) { var t = k / n; if (blocked(ci(a.x + dx * t), ci(a.z + dz * t))) return false; } return true; }
function findPath(from, to) {
  if (!grid) buildGrid();
  var s = freeNear(ci(from.x), ci(from.z)), e = freeNear(ci(to.x), ci(to.z)); if (!s || !e) return null;
  var NN = N * N, gS = new Float32Array(NN).fill(Infinity), came = new Int32Array(NN).fill(-1), closed = new Uint8Array(NN);
  var heap = [], si = s[1] * N + s[0], ei = e[1] * N + e[0];
  function hh(k) { var dx = Math.abs(k % N - e[0]), dz = Math.abs((k / N | 0) - e[1]); return (dx + dz + (1.4142 - 2) * Math.min(dx, dz)); }
  function push(k, f) { heap.push([f, k]); var c = heap.length - 1; while (c > 0) { var p = (c - 1) >> 1; if (heap[p][0] <= heap[c][0]) break; var t = heap[p]; heap[p] = heap[c]; heap[c] = t; c = p; } }
  function pop() { var top = heap[0], last = heap.pop(); if (heap.length) { heap[0] = last; var c = 0; for (;;) { var l = 2 * c + 1, r = l + 1, m = c; if (l < heap.length && heap[l][0] < heap[m][0]) m = l; if (r < heap.length && heap[r][0] < heap[m][0]) m = r; if (m === c) break; var t = heap[m]; heap[m] = heap[c]; heap[c] = t; c = m; } } return top; }
  gS[si] = 0; push(si, hh(si)); var it = 0;
  while (heap.length && it++ < 900000) {
    var k = pop()[1]; if (closed[k]) continue; closed[k] = 1; if (k === ei) break;
    var x = k % N, z = k / N | 0;
    for (var dz = -1; dz <= 1; dz++) for (var dx = -1; dx <= 1; dx++) {
      if (!dx && !dz) continue; var nx = x + dx, nz = z + dz; if (blocked(nx, nz)) continue;
      if (dx && dz && (blocked(x + dx, z) || blocked(x, z + dz))) continue;
      var nk = nz * N + nx, ng = gS[k] + (dx && dz ? 1.4142 : 1);
      if (ng < gS[nk]) { gS[nk] = ng; came[nk] = k; push(nk, ng + hh(nk)); }
    }
  }
  if (came[ei] < 0 && ei !== si) return null;
  var cells = []; for (var c = ei; c >= 0; c = came[c]) { cells.push({ x: cc(c % N), z: cc(c / N | 0) }); if (c === si) break; }
  cells.reverse(); cells[0] = { x: from.x, z: from.z };
  if (!blocked(ci(to.x), ci(to.z))) cells[cells.length - 1] = { x: to.x, z: to.z };
  var out = [cells[0]], a = 0;   // string-pulling
  while (a < cells.length - 1) { var b = cells.length - 1; while (b > a + 1 && !los(cells[a], cells[b])) b--; out.push(cells[b]); a = b; }
  return out;
}

/* ---------------- history (RETURN) ---------------- */
function pushHist() { if (flying()) { H.fhist.push({ x: S.ship.x, y: S.ship.y, z: S.ship.z }); if (H.fhist.length > 20) H.fhist.shift(); } else if (S.mode === 'walk') { H.hist.push({ x: S.p.x, z: S.p.z, yaw: S.camYaw }); if (H.hist.length > 20) H.hist.shift(); } }
function goReturn() {
  if (flying()) {
    var f = H.fhist.pop(); if (!f) return false;
    if (S.mode === 'auto') { S.mode = 'fly'; S.auto = null; } H.flyDrive = null;
    S.ship.x = f.x; S.ship.y = f.y; S.ship.z = f.z; A.resetShipCam(); setMode('FLIGHT'); return true;
  }
  if (S.mode !== 'walk') return false;
  var p = H.hist.pop(); if (!p) return false;
  stopAI(true); S.p.x = S.p.lx = p.x; S.p.z = S.p.lz = p.z; S.p.y = A.groundAt(p.x, p.z); S.camYaw = p.yaw; return true;
}

/* ---------------- AI walk / AI flight ---------------- */
function startWalk(p, mode, reason, onArrive) {
  if (S.mode !== 'walk') return false;
  stopAI(true); hideCard();
  var path = null; if (p.route) try { path = p.route({ x: S.p.x, z: S.p.z }); } catch (e) { path = null; }   // e.g. a campus: join the University road and follow it
  if (!path) path = findPath(S.p, p.stand || p);
  if (!path) { narrate('Cannot reach <b>' + esc(p.name) + '</b> from here: no clear way through the solids.'); return false; }
  pushHist();
  var len = 0; for (var i = 1; i < path.length; i++) len += Math.hypot(path[i].x - path[i - 1].x, path[i].z - path[i - 1].z);
  H.nav = { p: p, path: path, i: 1, sp: 2, t: 0, px: S.p.x, pz: S.p.z, stuck: 0, onArrive: onArrive, len: len };
  S.ai = aiStep; showPath(path); setMode(mode || 'AI DRIVE');
  narrate('Driving to <b>' + esc(p.name) + '</b>. ' + fmtDist(len) + ' on foot, about ' + Math.max(1, Math.round(len / 13)) + ' s. ' + (reason || '') + ' <