<!-- RUBY-BACKUP · part 350 -->
HAT_NAME = _nm[1].slice(0,12); else CHAT_NAME = IS_SOLID ? 'Solid' : 'King'; }catch(e){}
function escHtml(x){ return String(x).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
var floatWords = [];
function solidBanner(m){
  /* my words stay in his view, top-left — he never has to hunt for me */
  var b = document.getElementById('solidbanner');
  if(!b) return;
  b.innerHTML = '<b>Solid</b> &nbsp;'+escHtml(m.text);
  b.classList.add('show');
}
function floatWord(m){
  /* no box — the words float over the speaker's head, then fade */
  if(m.name==='Solid'){ solidBanner(m); return; }
  var box = document.getElementById('floatwords');
  if(!box) return;
  var el = document.createElement('div');
  el.className = 'floatword';
  el.innerHTML = '<b>'+escHtml(m.name)+'</b>'+escHtml(m.text);
  box.appendChild(el);
  floatWords.push({el:el, client:m.client||null, until:performance.now()+7000});
  while(floatWords.length>6){ var o = floatWords.shift(); o.el.remove(); }
}
/* ---- red seams: misconnected edges glow red until a neighbor fixes them ---- */
var seamGrp = null, seamMap = {}, SEAMS_ON = true;
function seamKey(x, z, d){ return x+','+z+','+d; }
function removeSeam(k){
  var m = seamMap[k];
  if(m){ if(seamGrp) seamGrp.remove(m); delete seamMap[k]; }
}
function addSeamsFor(g){
  if(!SEAMS_ON) return;
  if(!seamGrp){ seamGrp = new T.Group(); scene.add(seamGrp); }
  var u = g.userData, x = Math.round(g.position.x), z = Math.round(g.position.z), y = u.y||0;
  var side = (u.rubies||1)*(u.k1||1)*window.MINIMUM, half = side/2;
  var red = new T.MeshBasicMaterial({color:0xff2222});
  var dirs = [[1,0,'e'],[-1,0,'w'],[0,1,'s'],[0,-1,'n']];
  function opp(d){ return d==='e'?'w':d==='w'?'e':d==='s'?'n':'s'; }
  dirs.forEach(function(dd){
    var nx = x+dd[0], nz = z+dd[1], k = seamKey(x, z, dd[2]);
    if(stoneAt(nx, nz, y)){ removeSeam(k); return; }   /* fixed */
    if(seamMap[k] || Object.keys(seamMap).length > 300) return;
    var geo = (dd[2]==='e' || dd[2]==='w')
      ? new T.BoxGeometry(0.03, side*0.85, side)
      : new T.BoxGeometry(side, side*0.85, 0.03);
    var m = new T.Mesh(geo, red);
    m.position.set(x + dd[0]*half, y*window.MINIMUM + side*0.42, z + dd[1]*half);
    seamGrp.add(m); seamMap[k] = m;
  });
  /* this stone may fix its neighbors' seams facing it */
  dirs.forEach(function(dd){ removeSeam(seamKey(x+dd[0], z+dd[1], opp(dd[2]))); });
}
/* ---- shared map: one stones for the room, every traveler builds it ---- */
var lastStoneT = 0;
function shareStones(list){
  if(!WATCH_ID || !list.length) return;
  var arr = list.map(function(g){
    var u = g.userData, p = g.position;
    return {x:Math.round(p.x), z:Math.round(p.z), r:u.rubies||1, y:u.y||0,
      s:String(u.shape||'cube')+(u.ghost?'~ghost':'')};
  });
  fetch('/api/ruby/stones/'+encodeURIComponent(WATCH_ID), {method:'POST', headers:{'Content-Type':'application/json'},
    body:JSON.stringify({client:CLIENT_ID, name:CHAT_NAME, stones:arr})}).catch(function(){});
}
function pullStones(){
  if(!WATCH_ID) return;
  return fetch('/api/ruby/stones/'+encodeURIComponent(WATCH_ID)+'?since='+lastStoneT).then(function(r){ return r.json(); }).then(function(d){
    if(!d || !d.ok || !d.stones) return;
    d.stones.forEach(function(m){
      lastStoneT = Math.max(lastStoneT, m.t);
      if(m.client === CLIENT_ID) return;
      /* topo enforcement — nothing below the land */
      var my = m.y||0;
      try{
        var ty = topoY(m.x, m.z);
        if(ty!==null && my < Math.floor(ty+0.5)) my = Math.floor(ty+0.5);
      }catch(e){}
      if(stoneAt(m.x, m.z, my)) return;
      var g = placeStone(m.x, m.z, m.r, m.s, true, true, my, !!m.l);
      if(g){ g.userData.shared = true; g.userData.by = m.by||''; addSeamsFor(g); }
    });
  }).catch(function(){});
}
function pullChat(){
  if(!WATCH_ID) return;
  fetch('/api/ruby/chat/'+encodeURIComponent(WATCH_ID)+'?since='+lastChatT).then(function(r){ return r.json(); }).then(function(d){
    if(!d || !d.ok) return;
    var first = (lastChatT===0);
    d.msgs.forEach(function(m){
      lastChatT = Math.max(lastChatT, m.t);
      if(!first) floatWord(m);
    });
  }).catch(function(){});
}
function sendChat(){
  var inp = document.getElementById('chatinput');
  if(!inp) return;
  var text = inp.value.trim();
  if(!text || !WATCH_ID) return;
  inp.value = '';
  try{ inp.blur(); }catch(e){}
  fetch('/api/ruby/chat/'+encodeURIComponent(WATCH_ID), {method:'POST', headers:{'Content-Type':'application/json'},
    body:JSON.stringify({client:CLIENT_ID, name:CHAT_NAME, text:text})}).then(function(){ pullChat(); }).catch(function(){});
}
var _a1=new T.Vector3(), _a2=new T.Vector3(), _a3=new T.Vector3();
function buildCircleAvatar(color){
  var g = new T.Group();
  var m = new T.Mesh(A_GEO.ring, new T.MeshBasicMaterial({color:color||0xffd700, transparent:true, opacity:0.95, side:T.DoubleSide}));
  m.rotation.x = -Math.PI/2; m.position.y = 0.02; g.add(m);
  var d = new T.Mesh(