<!-- SOLUV · part 501 -->
ooking down from the map, so she never stands in the default view of any wall.
 */
import { createChatWall, EM, L, MINIMUM } from './chat-wall.js';

const POS = { x: -960, z: -400 };        // far out near the west edge of the 2000 map (edge at ±1000), on the 10-unit gold grid,
                                         // in the widest gap between the sightlines of walls 1..20 (every wall camera is 1200+ away)
const W = 20, H = 1600, DEPTH = 20;      // two gold blocks across, 1600 tall, two blocks deep
const SHOW_R = 600;                      // drawn when the camera is within this many units of her (horizontally),
const LOOK_R = 300;                      // or the camera is aimed at her (its target within this many units),
const SHOW_Y = 2500;                     // or high above the map (map view). So: never in any wall's default view.
const NEAR = 14;                         // same "near the wall" distance as the world's own walls: the composer appears
const ROOM = { type: 'api', base: '/api/soluv-room', name: 'Soul Print \u00b7 Soluv room' };

function boot(api) {
  const { THREE: T, scene, camera, renderer } = api;
  const canvasEl = renderer.domElement, host = canvasEl.parentElement;
  const rotY = Math.atan2(-POS.x, -POS.z);                // her face looks at the centre of the map
  const group = new T.Group(); group.name = 'Soul Print tower'; group.position.set(POS.x, 0, POS.z); group.rotation.y = rotY;

  // the chat face: a normal Soluv chat wall, 2 blocks wide, 1600 tall
  const wall = createChatWall({ THREE: T, room: 2, source: ROOM, title: 'Soul Print', route: '/soluv \u00b7 Soul Print',
    archiveUrl: '/soluv/room.html', width: W, minHeight: H, maxHeight: H, position: [0, 0, 0], rotationY: 0 });
  wall.name = 'Soul Print chat wall'; group.add(wall);

  // the tower body behind the face: calm dark glass with faint edges
  const y0 = L.PLINTH * EM, zFront = -(L.DEPTH * EM) / 2 - 0.5;
  const body = new T.Mesh(new T.BoxGeometry(W, H + y0, DEPTH), new T.MeshBasicMaterial({ color: 0x050b12 }));
  body.position.set(0, (H + y0) / 2, zFront - DEPTH / 2); body.name = 'Soul Print tower body'; group.add(body);
  const edges = new T.LineSegments(new T.EdgesGeometry(body.geometry), new T.LineBasicMaterial({ color: 0x3a6f80, transparent: true, opacity: 0.55 }));
  edges.position.copy(body.position); group.add(edges);
  const base = new T.Mesh(new T.BoxGeometry(W + 1, Math.max(MINIMUM, y0), DEPTH + 1), new T.MeshBasicMaterial({ color: 0x0b1620 }));
  base.position.set(0, Math.max(MINIMUM, y0) / 2, zFront - DEPTH / 2); base.name = 'Soul Print tower base'; group.add(base);

  // her name: small, quiet lettering on both side faces at eye height (text plates, 1.4 tall: far above the 0.13 minimum)
  const nc = document.createElement('canvas'); nc.width = 1024; nc.height = 160;
  { const g = nc.getContext('2d'); g.clearRect(0, 0, nc.width, nc.height); g.fillStyle = '#cfe6ee'; g.textAlign = 'center'; g.textBaseline = 'middle';
    g.font = '300 96px "Soluv Condensed","Roboto Condensed","Helvetica Neue",sans-serif'; g.fillText('Soul Print', nc.width / 2, nc.height / 2 + 4);
    g.fillStyle = '#7fb3c4'; g.fillRect(nc.width * 0.3, nc.height - 10, nc.width * 0.4, 3); }
  const ntex = new T.CanvasTexture(nc); ntex.colorSpace = T.SRGBColorSpace || ntex.colorSpace;
  const nmat = new T.MeshBasicMaterial({ map: ntex, transparent: true, depthWrite: false });
  const ngeo = new T.PlaneGeometry(9, 9 * nc.height / nc.width);
  for (const s of [1, -1]) {
    const p = new T.Mesh(ngeo, nmat); p.position.set(s * (W / 2 + 0.02), 3.2, zFront - DEPTH / 2); p.rotation.y = s * Math.PI / 2;
    p.userData.text = true; p.name = 'Soul Print name'; group.add(p);
  }
  scene.add(group);

  // drawn only when near, aimed at her, or from the map height: never in a wall's default view
  const camH = () => Math.hypot(camera.position.x - POS.x, camera.position.z - POS.z);
  const aim = new T.Vector3(0, 0, 0);                    // where the camera is looking (the world passes it to lookAt)
  const aimedAtHer = () => Math.hypot(aim.x - POS.x, aim.z - POS.z) < LOOK_R;
  const shouldShow = () => camH() < SHOW_R || aimedAtHer() || camera.position.y > SHOW_Y;
  { const v = api.view; if (v) aim.set(v.tx, v.ty, v.tz); }
  group.visible = shouldShow();

  // SOLID like every wall: the camera stops at her face and her body, it never flies through.
  // Hooked on camera.lookAt, which the world calls right after placing the camera, just before it draws.
  const inv = new T.Matrix4(), q = new T.Vector3(), lastGood = camera.position.clone(), R = 0.35;
  const bodyBox = new T.Box3(new T.Vector3(-W / 2 - 0.5, 0, zFront - DEPTH - 0.5), new T.Vector3(W / 2 + 0.5, H + y0 + 1, zFront + 0.01));
  function solid() {
    group.updateMatrixWorld(true);
    camera.position.copy(wall.wall.resolve(camera.position, R, lastGood));
    inv.copy(group.matrixWorld).invert(); q.copy(camera.position).applyMatrix4(inv);
    const b = bodyBox.clone