<!-- SOLID CITY · part 908 -->
dM = new T.MeshLambertMaterial({ color: 0xd8aa48, emissive: 0x2a1a04 });
  var body = new T.Mesh(new T.ConeGeometry(1.6, 11, 8), hullM); body.rotation.x = Math.PI / 2; body.position.z = 1.5; g.add(body);
  var aft = new T.Mesh(new T.CylinderGeometry(1.6, 1.9, 3, 8), hullM); aft.rotation.x = Math.PI / 2; aft.position.z = -5.4; g.add(aft);
  var stripe = new T.Mesh(new T.BoxGeometry(0.5, 0.2, 8), goldM); stripe.position.set(0, 1.25, 0); g.add(stripe);
  var cockpit = new T.Mesh(new T.SphereGeometry(1.0, 12, 8, 0, Math.PI * 2, 0, Math.PI / 2), new T.MeshLambertMaterial({ color: 0x220a10, emissive: 0x5a0a14 })); cockpit.scale.set(0.8, 0.7, 1.6); cockpit.position.set(0, 0.9, 1.4); g.add(cockpit);
  var wing = new T.Mesh(new T.BoxGeometry(12, 0.25, 3.6), redM); wing.position.set(0, -0.2, -2.6); g.add(wing);
  [-1, 1].forEach(function (s) {
    var tipw = new T.Mesh(new T.BoxGeometry(0.3, 2.4, 2.2), goldM); tipw.position.set(6 * s, 0.8, -3.2); g.add(tipw);
    var eng = new T.Mesh(new T.CylinderGeometry(0.7, 0.8, 3.6, 10), hullM); eng.rotation.x = Math.PI / 2; eng.position.set(2.4 * s, -0.3, -5.2); g.add(eng);
    var fl = new T.Mesh(new T.ConeGeometry(0.62, 4, 10), new T.MeshBasicMaterial({ color: 0xff4a1a, transparent: true, opacity: 0.75, blending: T.AdditiveBlending, depthWrite: false })); fl.rotation.x = -Math.PI / 2; fl.position.set(2.4 * s, -0.3, -9); g.add(fl); fl.name = 'flame';
  });
  var fin = new T.Mesh(new T.BoxGeometry(0.25, 2.6, 3), redM); fin.position.set(0, 1.6, -5); g.add(fin);
  return g;
}

/* ---------------- SABLE NPC ---------------- */
function makeSable() {
  var g = makeAvatar(0xffd27a);
  g.scale.setScalar(1.35);
  var halo = new T.Mesh(new T.TorusGeometry(0.55, 0.05, 8, 32), new T.MeshBasicMaterial({ color: 0xff2a40 })); halo.rotation.x = Math.PI / 2; halo.position.y = 2.2; g.add(halo);
  var cloak = new T.Mesh(new T.ConeGeometry(0.62, 1.3, 8, 1, true), new T.MeshLambertMaterial({ color: 0x5a0410, side: T.DoubleSide })); cloak.position.y = 0.95; g.add(cloak);
  var tag = new T.Sprite(new T.SpriteMaterial({ map: labelTex('SABLE', 'TAP OR WALK UP TO TALK'), depthTest: true })); tag.scale.set(3.6, 0.9, 1); tag.position.y = 3.1; g.add(tag);
  g.userData.halo = halo;
  return g;
}

/* ================= game state ================= */
var Y0 = CITY_Y, PAD = { x: D.port.x, y: CITY_Y + 2, z: D.port.z };
var S = {
  mode: 'walk',   // walk | boarded | countdown | launch | fly | auto
  p: { x: 6, y: Y0, z: 62, face: Math.PI, vy: 0 },
  camYaw: 0, camPitch: 0.28, camDist: 7.5,
  ship: { x: PAD.x, y: PAD.y + 2.4, z: PAD.z, q: new T.Quaternion(), speed: 0, throttle: 0.5, boost: false },
  autoLevel: true, auto: null, t: 0, launchT: 0, warnT: 0, portalCool: 0, lastPortal: null
};
var avatar = makeAvatar(); scene.add(avatar);
var ship = makeShip(); scene.add(ship);
var sable = makeSable(); scene.add(sable);
var SABLE_POS = { x: -16, y: Y0, z: -24 };
var shipFlames = []; ship.traverse(function (o) { if (o.name === 'flame') shipFlames.push(o); });
ship.position.set(0, 0, 0);

var input = { sx: 0, sy: 0, keys: {}, dragX: 0, dragY: 0, wheel: 0 };
var V3 = T.Vector3, tv = new V3(), tv2 = new V3(), tq = new T.Quaternion(), tm = new T.Matrix4(), UP = new V3(0, 1, 0);
var AX = new V3(1, 0, 0), AZ = new V3(0, 0, 1);

/* ---------------- input: joystick ---------------- */
(function () {
  var st = $('sc-stick'), knob = st.querySelector('i'), id = null, cx = 0, cy = 0, R = 50;
  function set(x, y) { var d = Math.hypot(x, y); if (d > R) { x *= R / d; y *= R / d; } knob.style.transform = 'translate(' + x + 'px,' + y + 'px)'; input.sx = x / R; input.sy = y / R; }
  st.addEventListener('pointerdown', function (e) { id = e.pointerId; st.setPointerCapture(id); var r = st.getBoundingClientRect(); cx = r.left + r.width / 2; cy = r.top + r.height / 2; set(e.clientX - cx, e.clientY - cy); e.preventDefault(); });
  st.addEventListener('pointermove', function (e) { if (e.pointerId === id) set(e.clientX - cx, e.clientY - cy); });
  function end(e) { if (e.pointerId === id) { id = null; set(0, 0); } }
  st.addEventListener('pointerup', end); st.addEventListener('pointercancel', end);
})();
/* look / mouse-fly drag on the canvas, tap to select */
(function () {
  var id = null, lx = 0, ly = 0, sx = 0, sy = 0, moved = 0;
  canvas.addEventListener('pointerdown', function (e) { if (id !== null) return; id = e.pointerId; canvas.setPointerCapture(id); lx = sx = e.clientX; ly = sy = e.clientY; moved = 0; });
  canvas.addEventListener('pointermove', function (e) {
    if (e.pointerId !== id) return; var dx = e.clientX - lx, dy = e.clientY - ly; lx = e.clientX; ly = e.clientY; moved += Math.abs(dx) + Math.abs(dy);
    if (S.mode === 'fly') { input.dragX = clamp((e.clientX - sx) / 120, -1, 1); input.dragY = clamp((e.clientY - sy) / 120, -1, 1); }
    else { S.camYaw -= dx * 0.006; S.camPitch = clamp(S.camPitch + dy * 0.004, -0.35, 1.2); }
  });
  function end(e) { if (e.pointerId !== id) return; id = 