<!-- NEW3 — TEST 3 · THE RIVER'S JOURNEY · part 443 -->
;
  g.fillStyle = '#77726c'; g.fillRect(0, 0, 16, 16);
  const tx = new THREE.CanvasTexture(cv); tx.wrapS = tx.wrapT = THREE.RepeatWrapping; tx.colorSpace = THREE.SRGBColorSpace;
  return tx;
}
function towersKit(THREE, group, R, rnd, buildings, counts, inWater) {
  const tex = towerPlate(THREE);
  const geos = [];
  const cols = Math.max(2, Math.floor(R.w / 64)), rows = Math.max(1, Math.floor(R.d / 64));
  for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
    if (rnd() < 0.18) continue;
    const x = R.x - R.w / 2 + (i + 0.5) * (R.w / cols) + (rnd() - 0.5) * 8, z = R.z - R.d / 2 + (j + 0.5) * (R.d / rows) + (rnd() - 0.5) * 8;
    if (inWater && inWater(x, z)) continue;
    const w = 20 + rnd() * 10, d = 20 + rnd() * 10, floors = 5 + Math.floor(rnd() * (R.floors || 14)), h = floors * 3.1;
    const geo = new THREE.BoxGeometry(w, h, d); const uv = geo.attributes.uv;
    for (let f = 0; f < 6; f++) for (let k = 0; k < 4; k++) { const ix = f * 4 + k; if (f === 2 || f === 3) uv.setXY(ix, 8 / 128, 1 - 8 / 256); else uv.setXY(ix, uv.getX(ix) * Math.max(1, Math.round(w / 12)), uv.getY(ix) * Math.max(1, Math.round(floors / 8))); }
    geo.applyMatrix4(M4(THREE, x, h / 2, z));
    geos.push(geo); buildings.push({ x, z, w, d, h }); counts.towers++;
  }
  if (geos.length) { const m = new THREE.Mesh(mergeGeos(THREE, geos), new THREE.MeshLambertMaterial({ map: tex })); m.name = 'TOWERS_' + R.name; group.add(m); }
}

// ---------- water geometry ----------
function ribbon(THREE, pts, width, y) {
  const pos = [], idx = [];
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i], pn = pts[Math.min(i + 1, pts.length - 1)], pp = pts[Math.max(i - 1, 0)];
    let dx = pn[0] - pp[0], dz = pn[1] - pp[1]; const l = Math.hypot(dx, dz) || 1; dx /= l; dz /= l;
    const nx = -dz * width / 2, nz = dx * width / 2;
    pos.push(p[0] - nx, y, p[1] - nz, p[0] + nx, y, p[1] + nz);
    if (i > 0) { const a = (i - 1) * 2; idx.push(a, a + 1, a + 2, a + 1, a + 3, a + 2); }
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
  g.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array((pos.length / 3) * 2), 2));
  g.setIndex(idx); g.computeVertexNormals();
  return g;
}
function distToSeg(px, pz, a, b) { const vx = b[0] - a[0], vz = b[1] - a[1], wx = px - a[0], wz = pz - a[1]; const c1 = vx * wx + vz * wz, c2 = vx * vx + vz * vz; const t = c2 ? Math.max(0, Math.min(1, c1 / c2)) : 0; const dx = wx - vx * t, dz = wz - vz * t; return Math.hypot(dx, dz); }

// ---------- the build ----------
export function buildWorld(THREE, MAP) {
  const mergeGeometries = (gs) => mergeGeos(THREE, gs);
  const group = new THREE.Group(); group.name = 'WORLD_' + MAP.id.toUpperCase();
  const buildings = [], decks = [];
  const counts = { fashion: 0, towers: 0, houses: 0, bridges: 0, regions: MAP.regions.length, trees: 0 };
  const bankGeos = [], waterGeos = [];
  const inRiver = (x, z) => { for (const r of MAP.water.rivers) { for (let i = 0; i < r.pts.length - 1; i++) if (distToSeg(x, z, r.pts[i], r.pts[i + 1]) < r.width / 2) return true; } return false; };
  const inLake = (x, z) => { for (const L of (MAP.water.lakes || [])) if (Math.hypot(x - L.x, z - L.z) < L.r) return true; return false; };
  const inWater = (x, z) => inRiver(x, z) || inLake(x, z);
  // water + banks
  for (const r of MAP.water.rivers) { bankGeos.push(ribbon(THREE, r.pts, r.width + 14, 0.25)); waterGeos.push(ribbon(THREE, r.pts, r.width, 0.6)); }
  for (const L of (MAP.water.lakes || [])) {
    const bg = new THREE.CircleGeometry(L.r + 7, 40); bg.rotateX(-Math.PI / 2); bg.translate(L.x, 0.25, L.z); bankGeos.push(bg);
    const wg = new THREE.CircleGeometry(L.r, 40); wg.rotateX(-Math.PI / 2); wg.translate(L.x, 0.6, L.z); waterGeos.push(wg);
  }
  if (bankGeos.length) { const m = new THREE.Mesh(mergeGeometries(bankGeos), new THREE.MeshLambertMaterial({ color: 0x6b6257 })); m.name = 'BANKS'; group.add(m); }
  if (waterGeos.length) { const m = new THREE.Mesh(mergeGeometries(waterGeos), new THREE.MeshLambertMaterial({ color: 0x2f6db3 })); m.name = 'WATER'; group.add(m); }
  // bridges + streets
  const streetGeos = [], deckGeos = [], pierGeos = [], railGeos = [];
  const deckProfileAt = (dk, x, z) => {
    const ux = x - dk.cx, uz = z - dk.cz; const u = ux * dk.dx + uz * dk.dz, v = -ux * dk.dz + uz * dk.dx;
    const half = dk.len / 2, RAMP = 15;
    if (Math.abs(v) > dk.wid / 2 + 0.6 || Math.abs(u) > half + RAMP) return 0;
    if (Math.abs(u) <= half) return 4.5;
    return 4.5 * (1 - (Math.abs(u) - half) / RAMP);
  };
  for (const s of MAP.streets) {
    const dx = s.b[0] - s.a[0], dz = s.b[1] - s.a[1], len = Math.hypot(dx, dz), ux = dx / len, uz = dz / len;
    const ang = Math.atan2(dx, dz);
    const sg = new THREE.BoxGeometry(s.w, 0.12, len); sg.applyMatrix4(new THREE.Matrix4().compose(new THREE.Vector3((s.a[0] + s.b[0]) / 2, 0.37, (s.a[1] +