<!-- SOLID CITY · part 3403 -->
ive(A); } catch (e) { console.warn('space arrive', e); }
  return true;
}
// the dock/button and chips do not need the 3D city; add them right away
function early() { try { addButton(); watchChips(); } catch (e) {} }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', early); else early();
if (!start()) {
  var done = false;
  addEventListener('sc-city-built', function () { if (!done && start()) done = true; });
  var k = 0, iv = setInterval(function () { if (done || ++k > 400) return clearInterval(iv); if (start()) { done = true; clearInterval(iv); } }, 300);
}
})();


===== FILE: public/city/space/src/game.js =====
/* SOLID CITY · SPACE MISSION  —  epistles.xyz/city/space/
 * The King, 2026-10-10: "Jump in, fly, shoot at aliens, hit them down, go home and park."  3D, not lame; faster than drones.
 *
 *   JUMP IN  the ship waits on the Hangar pad (same spot as the pad in /city, city coords -307, 410)
 *   FLY      blast-off, climb through the clouds into space (dusk sky -> stars, the planet curving below)
 *            base 400 km/h, BOOST 760 km/h (recharging meter), climb rockets ~1650 km/h, re-entry dive ~2300 km/h
 *   SHOOT    3 alien waves (swoopers, circlers, divers + mothership); FIRE with aim assist on touch screens
 *   GO HOME  RETURN HOME + guide arrow, re-entry through flames back to the island
 *   PARK     hover down onto the glowing Hangar circle; result (time + kills) -> /api/city/space/score
 *
 * Source of /city/space/space.min.js (bundled with three.js r186 by esbuild: see /city/space/BUILD.txt).
 * Everything runs in the browser. Test hooks: window.SPACE (state, stats(), test.*) plus a scripted test pilot.
 */
import * as THREE from 'three';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';

const $ = (id) => document.getElementById(id);
const QS = new URLSearchParams(location.search);
const IS_TOUCH = ('ontouchstart' in window) || navigator.maxTouchPoints > 0 || QS.has('touch');
const MOBILE = (IS_TOUCH && Math.min(screen.width, screen.height) < 900) || QS.has('mobile');
const progress = (p, label) => { try { if (window.__spaceLoad) window.__spaceLoad(p, label); } catch (e) { /* loader UI only */ } };
const tick = () => new Promise((r) => { let done = false; const go = () => { if (!done) { done = true; r(); } }; requestAnimationFrame(go); setTimeout(go, 60); });

/* ---------------- constants ---------------- */
const KMH = 3.6;
const BASE_V = 400 / KMH, BOOST_V = 760 / KMH, CLIMB_V = 1650 / KMH, REENTRY_V = 2300 / KMH, HOVER_V = 50;
const SEA = -22;                       // sea level; the city ground + Hangar pad are y = 0 (the city shows ALT 22 m there)
const PR = 60000;                      // planet radius (m): the curve shows from a few km up
const SPACE_ALT = 7000;                // altitude (m above sea) where the sky is black and the fleet waits
const CLOUD_LO = 1500, CLOUD_HI = 2400;
const CITY = { x: 307, z: -410 };      // the city origin (Forum Plaza) in mission coords; the Hangar pad is 0,0
const SHIP_REST = 1.15;
const TOTAL_ALIENS = 17;
const SUN = new THREE.Vector3(-0.55, 0.32, -0.77).normalize();   // the city's low golden sun over the western sea
const PAD_HEADING = Math.atan2(0.55, 0.77);                       // the ship faces the sunset on the pad
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const lerp = (a, b, t) => a + (b - a) * t;
const smooth = (t) => { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); };
const damp = (a, b, k, dt) => lerp(a, b, 1 - Math.exp(-k * dt));
const wrapA = (a) => { while (a > Math.PI) a -= Math.PI * 2; while (a < -Math.PI) a += Math.PI * 2; return a; };
const V3 = THREE.Vector3;
const altOf = (y) => y - SEA;
const curveDrop = (d2) => Math.sqrt(Math.max(PR * PR - d2, 0)) - PR;   // sphere surface offset at horizontal distance^2

/* ---------------- seeded noise (same family as the city) ---------------- */
function mulberry(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const rnd = mulberry(20261010);
const perm = new Uint8Array(512); { const p0 = []; for (let i = 0; i < 256; i++) p0[i] = i; for (let i = 255; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); const t = p0[i]; p0[i] = p0[j]; p0[j] = t; } for (let i = 0; i < 512; i++) perm[i] = p0[i & 255]; }
const G2 = [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [0, 1], [0, -1]];
const F2 = 0.5 * (Math.sqrt(3) - 1), Gk = (3 - Math.sqrt(3)) / 6;
function snoise(x, y) {
  const s = (x + y) * F2, i = Math.floor(x + s), j = Math.floor(y + s), t = (i + j) * Gk, x0 = x - (i - t), y0 = y - (j - t);
  const i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1, x1 = x0 - i1 + Gk, y1 = y0 - j1 + Gk, x2 = x0 - 1 + 2 * Gk, y2 = y0 - 1 + 2 * Gk;
  const ii = i & 255, jj = j & 255; let n = 0, g, q;
  q = 0.5 - x0 * x0 - y0 * y0; if (q > 0) { g = G2[perm[ii + 