<!-- SOLID CITY · part 2039 -->
 — 🔨 THE FORGE: in-world building (2026-10-09)
 * THE CREATOR v1 (/playa) brought into the city. Shapes, materials, bend and the recipe format come from
 * forge-core.js (shared with the server). Walk around, a ghost preview floats in front of you, snapped to
 * the grid and 15° turns; PLACE drops it (stacking on what is under it). Tap a piece to select it; the
 * right-hand column moves / rotates / scales / colours it, 🧱 has the cheap Creator clay tools.
 * Every placed piece is a solid AABB in SC.api.solids, so walking, flight, WATCH and the Hyper A* grid
 * avoid it, and you can stand on low pieces (steps / slabs). No building on the pad, its flight lane,
 * the monument, the gates or the towers; 60 m height cap.
 * Pieces live in CREATIONS (groups that move / rotate / save as one). Local creations autosave to
 * localStorage; ☰ has the Library (stamp copies, import from THE CREATOR) and PUBLISH (logged in):
 * the King's builds go public at once, everyone else's wait for his approval (server: lib/city-builds.js).
 */
(function () {
'use strict';
if (!window.SC || !SC.api || !window.ForgeCore) return;
var A = SC.api, T = A.T, S = A.S, C = ForgeCore, L = C.LIMITS;
var $ = function (id) { return document.getElementById(id); };
function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function clone(o) { return JSON.parse(JSON.stringify(o)); }
function gnd(x, z) { return Math.max(0, A.groundAt(x, z)); }
var LS_BENCH = 'sc.forge.bench.v1', LS_LIB = 'sc.forge.lib.v1', LS_CRE_SCENE = 'playa-creator-v1-scene', LS_CRE_LIB = 'playa-creator-v1-library';

/* ---------------- geometry + materials (Creator shapeParts, cached) ---------------- */
function partsGeo(t, res) {
  var r = res || 1;
  if (t === 'cube') return [[new T.BoxGeometry(4, 4, 4, 2 * r, 4 * r, 2 * r), 2]];
  if (t === 'hourglass') return [[new T.ConeGeometry(2, 3, 4 * r, 4 * r), 4.5, true], [new T.ConeGeometry(2, 3, 4 * r, 4 * r), 1.5]];
  if (t === 'pylon') return [[new T.CylinderGeometry(1, 1.5, 8, 8 * r, 6 * r), 4]];
  if (t === 'tetra') return [[new T.TetrahedronGeometry(3, r - 1), 1]];
  if (t === 'pyramid') return [[new T.ConeGeometry(3, 2, 4 * r, 3 * r), 1], [new T.ConeGeometry(2, 1.5, 4 * r, 3 * r), 2.75]];
  if (t === 'sphere') return [[new T.SphereGeometry(2.5, 16 * r, 12 * r), 2.5]];
  if (t === 'cylinder') return [[new T.CylinderGeometry(2, 2, 5, 16 * r, 4 * r), 2.5]];
  if (t === 'cone') return [[new T.ConeGeometry(2.5, 5, 16 * r, 4 * r), 2.5]];
  if (t === 'torus') return [[new T.TorusGeometry(2.5, 0.8, 10 * r, 24 * r), 3.3]];
  if (t === 'plane') return [[new T.BoxGeometry(6, 0.2, 6, 4 * r, 1, 4 * r), 0.1]];
  return [[new T.IcosahedronGeometry(2.5, r), 2.5]];
}
var geoCache = {}, matCache = {};
function pieceGeos(d) {
  var flat = d.mats[0] && d.mats[0].f ? 1 : 0, key = d.t + '|' + d.res + '|' + d.bend + '|' + flat;
  if (geoCache[key]) return geoCache[key];
  var geos = partsGeo(d.t, d.res).map(function (p) { var g = p[0]; if (p[2]) g.rotateX(Math.PI); g.translate(0, p[1], 0); return g; });
  if (d.bend) { // Creator bendGeometry: x += k * h^2 * H * 0.35 (h = height fraction above the base)
    var lo = Infinity, hi = -Infinity;
    geos.forEach(function (g) { g.computeBoundingBox(); lo = Math.min(lo, g.boundingBox.min.y); hi = Math.max(hi, g.boundingBox.max.y); });
    var H = Math.max(0.001, hi - lo);
    geos.forEach(function (g) { var pos = g.attributes.position; for (var i = 0; i < pos.count; i++) { var h = (pos.getY(i) - lo) / H; pos.setX(i, pos.getX(i) + d.bend * h * h * H * 0.35); } pos.needsUpdate = true; g.computeVertexNormals(); });
  }
  if (flat) geos = geos.map(function (g) { var n = g.index ? g.toNonIndexed() : g; n.computeVertexNormals(); return n; });
  geos.forEach(function (g) { g.computeBoundingSphere(); g.computeBoundingBox(); });
  return (geoCache[key] = geos);
}
function matOf(m, ghost) {
  var key = [m.c, m.o, m.e, m.ei, m.w, ghost ? 1 : 0].join('|');
  if (matCache[key]) return matCache[key];
  var mt = new T.MeshLambertMaterial({ color: m.c, emissive: m.ei > 0 ? m.e : 0, wireframe: !!m.w });
  if (m.ei > 0) mt.emissiveIntensity = m.ei;
  var op = ghost ? Math.min(0.55, m.o) : m.o;
  if (op < 1) { mt.transparent = true; mt.opacity = op; mt.depthWrite = false; }
  return (matCache[key] = A.curve(mt));
}
function makeMesh(d, ghost) {
  var g = new T.Group(), geos = pieceGeos(d);
  geos.forEach(function (geo, i) { var m = new T.Mesh(geo, matOf(d.mats[i] || d.mats[0], ghost)); g.add(m); });
  g.position.fromArray(d.p); g.quaternion.fromArray(d.q); g.scale.fromArray(d.s);
  return g;
}
function newPiece(t, color, finish) {
  var d = C.sanitizePiece({ t: t, p: [0, 0, 0], q: [0, 0, 0, 1], s: [1, 1, 1] });
  if (color != null) d.mats.forEach(function (m) { m.c = color; });
  applyFinish(d, finish || 'matte');
  return d;