<!-- PLAYA 2.0'S WORLD · part 10 -->
 hz, hx) : rectAt(x, z, hx, hz); if (spotFree(ctx, I, r, kind === 'bridge')) chosen = { x, z, ry, r }; else console.warn('[new8 module] position', opts.position, 'overlaps the core layout; picking an open spot'); }
  const fits = kind === 'bridge' ? Math.max(hz, 0) <= I.road : Math.max(hx, hz) <= I.band.hw;
  if (!chosen && fits) for (let n = 0; n < cands.length; n++) { const o = cands[(pref + n) % cands.length]; if (spotFree(ctx, I, o.r, kind === 'bridge')) { chosen = o; break; } }
  if (!chosen) throw new Error('no open spot for ' + kind + ' footprint ' + hx + 'x' + hz);
  (ctx.__new8Claims ||= []).push(chosen.r); return chosen;
}
// place group: with ctx -> open spot + ctx.groundY + colliders; without ctx -> opts.position as before.
function place(scene, group, opts, foot) {
  const ctx = opts.ctx, s = opts.scale ?? 1; if (s < MIN) throw new Error('scale below 0.13'); group.scale.setScalar(s);
  if (ctx && ctx.layout) {
    const sp = pickSpot(ctx, opts, foot.kind, foot.hx * s, foot.hz * s, opts.spot ?? foot.pref ?? 0);
    const gy = typeof ctx.groundY === 'function' ? ctx.groundY(sp.x, sp.z) : 0;
    group.position.set(sp.x, gy, sp.z); group.rotation.y = sp.ry;
  } else {
    const p = opts.position || [0, 0, 0]; group.position.set(p[0], p[1], p[2]); group.rotation.y = opts.rotationY || 0;
  }
  scene.add(group);
  const cols = [];
  if (ctx && typeof ctx.addCollider === 'function') {
    scene.updateMatrixWorld(true); const m = group.matrixWorld, THREE_V = group.position.constructor;
    for (const [x0, x1, z0, z1, h] of foot.cols || []) {
      let a = Infinity, b = -Infinity, c = Infinity, d = -Infinity;
      for (const [x, z] of [[x0, z0], [x1, z0], [x0, z1], [x1, z1]]) { const v = new THREE_V(x, 0, z).applyMatrix4(m); a = Math.min(a, v.x); b = Math.max(b, v.x); c = Math.min(c, v.z); d = Math.max(d, v.z); }
      const top = group.position.y + h * s; ctx.addCollider(a, b, c, d, top); cols.push([a, b, c, d, top]);
    }
  }
  group.userData.colliders = cols; group.userData.footprint = foot.kind; return group;
}

// Twisting spire tower: stacked rotated slabs forming a helix, glowing edge fins, needle spire.
export function createTwistingSpire(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'TwistingSpire';
  const M = mats(THREE, opts), k = kit(THREE, g);
  const floors = opts.floors ?? 28, fh = 0.9, twist = opts.twist ?? Math.PI * 0.9;
  k.cyl(M.stone, 4.2, 4.6, 0.6, 8, 0, 0.3, 0);
  k.cyl(M.glow, 4.25, 4.25, 0.15, 8, 0, 0.62, 0);
  for (let i = 0; i < floors; i++) {
    const t = i / floors, w = 5.2 - t * 2.6, a = t * twist, y = 0.7 + i * fh + fh / 2;
    k.box(i % 4 === 3 ? M.red : M.black, w, fh * 0.86, w, 0, y, 0, 0, a, 0);
    k.box(M.glow, w + 0.14, 0.14, w + 0.14, 0, y + fh * 0.45, 0, 0, a, 0);
  }
  const top = 0.7 + floors * fh;
  k.cyl(M.red, 0.6, 1.3, 2.2, 8, 0, top + 1.1, 0);
  k.cyl(M.black, 0.14, 0.6, 7, 8, 0, top + 5.7, 0);
  k.cyl(M.glow, 0.2, 0.2, 0.6, 8, 0, top + 9.5, 0);
  k.build(); return place(scene, g, opts, { kind: 'land', hx: 4.6, hz: 4.6, pref: 16, cols: [[-4.6, 4.6, -4.6, 4.6, top + 9.8]] });
}

// Ringed obelisk: tapering four-sided obelisk with floating glowing rings and a red pyramidion.
export function createRingedObelisk(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'RingedObelisk';
  const M = mats(THREE, opts), k = kit(THREE, g);
  const h = opts.height ?? 22;
  k.box(M.stone, 7, 0.6, 7, 0, 0.3, 0); k.box(M.black, 5.2, 0.8, 5.2, 0, 1.0, 0);
  k.box(M.glow, 5.3, 0.14, 5.3, 0, 1.45, 0);
  k.cyl(M.black, 1.0 * Math.SQRT2, 1.8 * Math.SQRT2, h, 4, 0, 1.4 + h / 2, 0, 0, Math.PI / 4, 0);
  k.cyl(M.red, 0.14, 1.0 * Math.SQRT2, 2.4, 4, 0, 1.4 + h + 1.2, 0, 0, Math.PI / 4, 0);
  const rings = opts.rings ?? 5;
  for (let i = 0; i < rings; i++) {
    const y = 4 + i * (h - 5) / rings, R = 2.8 - i * 0.25;
    k.torus(i % 2 ? M.red : M.glow, R, 0.09, 0, y, 0, Math.PI / 2 + (i % 2 ? 0.12 : -0.12), 0, 0);
  }
  for (let s = 0; s < 4; s++) { const a = s * Math.PI / 2; k.box(M.glow, 0.14, h * 0.8, 0.14, Math.cos(a) * 1.55, 1.4 + h * 0.42, Math.sin(a) * 1.55, 0, 0, 0); }
  k.build(); return place(scene, g, opts, { kind: 'land', hx: 3.5, hz: 3.5, pref: 28, cols: [[-3.5, 3.5, -3.5, 3.5, h + 3.8]] });
}

// Lattice crown tower: cylindrical core with diagrid struts and a glowing crown.
export function createLatticeTower(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'LatticeTower';
  const M = mats(THREE, opts), k = kit(THREE, g);
  const h = opts.height ?? 26, R = 3, n = 10, levels = 8;
  k.cyl(M.stone, 4, 4.3, 0.6, 16, 0, 0.3, 0);
  k.cyl(M.graphite, R - 0.4, R - 0.2, h, 16, 0, 0.6 + h / 2, 0);
  for (let l = 0; l < levels; l++) {
    const y0 = 0.6 + l * h / levels, y1 = y0 + h / levels;
    for (let i = 0; i < n; i++) {
      const a0 = i / n * Math.PI * 2, a1 = (i + 1) / n * Math.PI * 2;
      k.rod(M.black, 0.1, [Math.cos(a0) * R, y0, Math.sin(a0) * R], [Math.cos(a1) * R, y1, Math.sin(