<!-- PLAYA 2.0'S WORLD · part 7 -->
ge) / 2 }; // open ring between outer blocks (+lamps) and the walk limit
  return { P, H, LIM, RZ0, RZ1, BR, N, blocks, band, road: (P - 2 * H) / 2 - 0.6 };
}
const hit = (a, b, m = 0) => a.x0 < b.x1 + m && a.x1 > b.x0 - m && a.z0 < b.z1 + m && a.z1 > b.z0 - m;
const rectAt = (x, z, hx, hz) => ({ x0: x - hx, x1: x + hx, z0: z - hz, z1: z + hz });
function spotFree(ctx, I, r, water) {
  if (I.blocks.some(b => hit(r, b, 0.5))) return false;
  if (!water && r.z1 > I.RZ0 - 2 && r.z0 < I.RZ1 + 2) return false;
  if (r.x0 < -I.LIM || r.x1 > I.LIM || r.z0 < -I.LIM || r.z1 > I.LIM) return false;
  return !(ctx.__new8Claims || []).some(c => hit(r, c, 1));
}
// kind 'land': half-extents hx,hz must fit the open outer ring (south side skipped: bus route A runs there).
// kind 'bridge': spans the river along a road line without an existing core bridge (rotated to run N-S).
function pickSpot(ctx, opts, kind, hx, hz, pref = 0) {
  const I = layoutInfo(ctx.layout); let cands = [];
  if (kind === 'bridge') {
    const zc = (I.RZ0 + I.RZ1) / 2;
    for (let k = -I.N; k < I.N; k++) { const x = (k + 0.5) * I.P; if (I.BR.some(b => Math.abs(b - x) < 12)) continue; cands.push({ x, z: zc, ry: Math.PI / 2, r: rectAt(x, zc, hz, hx) }); }
  } else {
    const c = I.band.c;
    for (let t = -I.N * I.P; t <= I.N * I.P; t += I.P / 2) cands.push({ x: t, z: -c }, { x: c, z: t }, { x: -c, z: t });
    cands = cands.map(o => ({ ...o, ry: 0, r: rectAt(o.x, o.z, hx, hz) }));
  }
  let chosen = null;
  if (opts.position) { const [x, , z] = opts.position, ry = opts.rotationY || 0, sw = Math.abs(Math.sin(ry)) > 0.5; const r = sw ? rectAt(x, z, 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;
}

export function createCentralPlaza(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'CentralPlaza';
  const M = mats(THREE, opts), k = kit(THREE, g);
  // with ctx the plaza goes to an open spot in the outer ring (never the Grand Plaza at 0,0), so it is sized to fit
  const fitR = opts.ctx && opts.ctx.layout ? layoutInfo(opts.ctx.layout).band.hw - 0.5 : Infinity;
  const R = Math.min(opts.radius ?? 18, fitR), f = Math.min(1, R / 18), q = v => Math.max(0.14, v * f), T = 0.14;
  const water = new THREE.MeshStandardMaterial({ color: 0x1a0306, emissive: 0x5a0010, emissiveIntensity: 0.6, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.85 });
  k.cyl(M.stone, R, R + 0.3, 0.3, 48, 0, 0.15, 0);
  for (let i = 0; i < 3; i++) k.torus(M.glow, R * (0.45 + i * 0.2), 0.07, 0, 0.32, 0);
  for (let i = 0; i < 16; i++) { const a = i / 16 * Math.PI * 2; k.box(i % 2 ? M.black : M.red, R * 0.5, T, Math.max(T, 0.3 * f), Math.cos(a) * R * 0.7, 0.33, Math.sin(a) * R * 0.7, 0, -a, 0); }
  // fountain basin
  const B = 6.2 * f;
  k.cyl(M.black, 6 * f, B, 0.7, 40, 0, 0.65, 0);
  k.torus(M.red, 6.05 * f, Math.max(0.075, 0.15 * f), 0, 1.0, 0);
  k.add(water, new THREE.CylinderGeometry(k.d(5.6 * f * 2) / 2, 5.6 * f, k.d(0.14), 40), 0, 0.95, 0);
  // mo