<!-- SOLID CITY · part 2798 -->
this.idx = []; this.n = 0; }
  Batch.prototype.add = function (geo, mtx, color) {
    var p = geo.attributes.position, nr = geo.attributes.normal, ix = geo.index;
    var col = new T.Color(color);
    var nm = new T.Matrix3().getNormalMatrix(mtx);
    var v = new T.Vector3(), nv = new T.Vector3();
    var i, base = this.n;
    for (i = 0; i < p.count; i++) {
      v.fromBufferAttribute(p, i).applyMatrix4(mtx);
      this.pos.push(v.x, v.y, v.z);
      nv.fromBufferAttribute(nr, i).applyMatrix3(nm).normalize();
      this.nor.push(nv.x, nv.y, nv.z);
      this.col.push(col.r, col.g, col.b);
    }
    if (ix) { for (i = 0; i < ix.count; i++) this.idx.push(base + ix.getX(i)); }
    else { for (i = 0; i < p.count; i++) this.idx.push(base + i); }
    this.n += p.count;
    if (geo.dispose) geo.dispose();
  };
  Batch.prototype.build = function (mat) {
    var g = new T.BufferGeometry();
    g.setAttribute('position', new T.Float32BufferAttribute(this.pos, 3));
    g.setAttribute('normal', new T.Float32BufferAttribute(this.nor, 3));
    g.setAttribute('color', new T.Float32BufferAttribute(this.col, 3));
    g.setIndex(this.idx);
    var mesh = new T.Mesh(g, mat);
    mesh.frustumCulled = false;
    return mesh;
  };
  var solid = new Batch(), glowB = new Batch(), glassB = new Batch();

  var _e = new T.Euler(), _m = new T.Matrix4(), _q = new T.Quaternion();
  function M4(x, y, z, ry, rx, rz) {
    _e.set(rx || 0, ry || 0, rz || 0, 'YXZ'); _q.setFromEuler(_e);
    return new T.Matrix4().compose(new T.Vector3(x, y, z), _q.clone(), new T.Vector3(1, 1, 1));
  }
  // batched primitives — all coordinates in each home's LOCAL frame, baked to world by the caller via a base matrix
  var BASE = new T.Matrix4();
  function put(batch, geo, x, y, z, ry, rx, rz, color) {
    var m = M4(x, y, z, ry || 0, rx || 0, rz || 0);
    if (BASE) m.premultiply(BASE);
    batch.add(geo, m, color);
  }
  function box(batch, w, h, d, x, y, z, color, ry, rx, rz) { put(batch, new T.BoxGeometry(w, h, d), x, y, z, ry, rx, rz, color); }
  function cyl(batch, rt, rb, h, seg, x, y, z, color, ry, rx, rz) { put(batch, new T.CylinderGeometry(rt, rb, h, seg), x, y, z, ry, rx, rz, color); }
  function cone(batch, r, h, seg, x, y, z, color, ry, rx, rz) { put(batch, new T.ConeGeometry(r, h, seg), x, y, z, ry, rx, rz, color); }
  function sph(batch, r, x, y, z, color, sy) {
    var g = new T.SphereGeometry(r, 9, 7); if (sy) g.scale(1, sy, 1);
    put(batch, g, x, y, z, 0, 0, 0, color);
  }
  function rock(batch, r, x, y, z, color) { put(batch, new T.DodecahedronGeometry(r, 0), x, y, z, R() * 3, R() * 2, 0, color); }
  // triangular gable prism: ridge along X, base half-depth hd, apex rise, thickness t (along X)
  function prismGeo(hd, rise, t) {
    var A = [-t / 2, 0, -hd], B = [t / 2, 0, -hd], Cp = [t / 2, 0, hd], D = [-t / 2, 0, hd], E = [-t / 2, rise, 0], F = [t / 2, rise, 0];
    // This prism is used ONLY for gable-end infill (thin), so build just the two end triangles + slopes shell:
    var pos = [], nor = [], idx = [];
    function tri(p1, p2, p3, n) {
      var b = pos.length / 3;
      pos.push(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2], p3[0], p3[1], p3[2]);
      for (var i = 0; i < 3; i++) nor.push(n[0], n[1], n[2]);
      idx.push(b, b + 1, b + 2);
    }
    var sl = Math.hypot(hd, rise), nz = rise / sl, ny = hd / sl;
    tri(A, B, F, [0, nz, -ny]); tri(A, F, E, [0, nz, -ny]);       // back slope
    tri(D, F, Cp, [0, nz, ny]); tri(D, E, F, [0, nz, ny]);        // front slope
    tri(A, E, D, [-1, 0, 0]);                                      // left gable
    tri(B, Cp, F, [1, 0, 0]);                                      // right gable
    tri(A, Cp, B, [0, -1, 0]); tri(A, D, Cp, [0, -1, 0]);          // bottom
    var g = new T.BufferGeometry();
    g.setAttribute('position', new T.Float32BufferAttribute(pos, 3));
    g.setAttribute('normal', new T.Float32BufferAttribute(nor, 3));
    g.setAttribute('uv', new T.Float32BufferAttribute(new Float32Array((pos.length / 3) * 2), 2));
    g.setIndex(idx);
    return g;
  }
  function gable(batch, w, hd, rise, y, color) {
    // full-width gable infill: prism spanning the wall width
    var g = prismGeo(hd, rise, w);
    // prism ridge runs along its thickness axis (X) — exactly what we want
    put(batch, g, 0, y, 0, 0, 0, 0, color);
  }

  /* ---------------- terrain helpers ---------------- */
  function slopeAt(x, z, e) {
    var dx = (groundAt(x + e, z) - groundAt(x - e, z)) / (2 * e);
    var dz = (groundAt(x, z + e) - groundAt(x, z - e)) / (2 * e);
    return Math.hypot(dx, dz);
  }
  // PLACEMENT LAW: a bench is gentle ground with NO ledge/cliff break in or near the lot.
  // (Ancestors' coast: homes live between the shore h~7 and the high benches h~420.)
  function benchAt(x, z, lotR) {
    var h = groundAt(x, z);
    if (h < 7 || h > 420) return -1;
    if (slopeAt(x, z, 7) > 0.36) return -1;
    if (slopeAt(x, z, 20) > 0.52) return -1;
    for (var i = 0; 