<!-- RUBY-BACKUP · part 148 -->
turn {ok:false, error:'procedure failed'};
  for(var i=0;i<ops.length;i++){ ops[i].x = snap(ops[i].x); ops[i].z = snap(ops[i].z); ops[i].proc = name; }
  return {ok:true, ops:ops};   /* stamps: the cursor stays where it stood */
}

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   MARK: MIRROR — flip. Every procedure carries its equal and opposite.
   EDIT: mirror axis. Currently across the cursor's x (west↔east).
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
function mirrorOps(ops, cx0){
  return ops.map(function(o){
    var c = {shape:o.shape, size:o.size, x:2*cx0-o.x, z:o.z, y:o.y||0, proc:o.proc, remove:!!o.remove};
    return c;
  });
}

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   MARK: LAW OF GROUND — what happens when a stone meets a stone
     own work (same code) → stepped over (never duplicated)
     a new project claims its site → every previous project inside the new
        footprint is rendered obsolete, at the stone: the old code deletes,
        atomic, whatever its mass. newness decides, not mass.
     hand work on a project → the old mass rule (a tap never nukes a project)
     the eternal → coexisted with (full build in the cliff; terrain never yields)
   Cells are 3D (x, z, y in shells) — the law of space. Same x,z at different
   heights coexist: that is how windows sit inside walls.
   EDIT: the mass measure, the displacement test. Mass = total rubies.
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
function procMassOf(ops){ var m=0; for(var i=0;i<ops.length;i++){ if(!ops[i].remove) m += (ops[i].size||1); } return m; }
function stoneMass(g){ var u=g.userData; return u.procMass || u.rubies || 1; }
function deleteProcedure(name){
  var n=0;
  for(var i=stones.children.length-1;i>=0;i--)
    if(stones.children[i].userData.proc===name){ stones.remove(stones.children[i]); n++; }
  return n;
}
/* oops: the undo stack. every obsoleted project is remembered — its stones,
   its code — so typing oops brings the last one back. */
var undoStack = [];
function loadUndo(){ try{ undoStack = JSON.parse(localStorage.getItem('ruby-undo-v1')||'[]'); }catch(e){ undoStack=[]; } }
function saveUndo(){ try{ localStorage.setItem('ruby-undo-v1', JSON.stringify(undoStack.slice(-10))); }catch(e){} }
function pushUndo(proc, killer){
  var st = [], mass = 0;
  for(var i=0;i<stones.children.length;i++){
    var s = stones.children[i];
    if(s.userData.proc===proc){
      var u = s.userData;
      st.push({x:s.position.x, z:s.position.z, r:u.rubies||1, s:u.shape||'cube', v:u.ghost?1:0, y:u.y||0, p:proc});
      mass += (u.rubies||1);
    }
  }
  if(!st.length) return;
  undoStack.push({victim:proc, killer:killer||null, mass:mass, stones:st});
  if(undoStack.length>10) undoStack.shift();
  saveUndo();
}
function doOops(){
  var rec = undoStack.pop();
  if(!rec){ codeStatus('nothing to undo'); return; }
  saveUndo();
  if(rec.victim==='spot'){
    /* a spot save undone — the spot is freed, save it again somewhere new */
    var ssk='ruby-spots-v1', spts={};
    try{ spts=JSON.parse(localStorage.getItem(ssk)||'{}'); }catch(e){}
    delete spts[rec.spot];
    try{ localStorage.setItem(ssk, JSON.stringify(spts)); }catch(e){}
    codeStatus('spot '+rec.spot+' unsaved');
    return;
  }
  if(rec.killer) deleteProcedure(rec.killer);
  var restored=0, skipped=0;
  for(var i=0;i<rec.stones.length;i++){
    var c = rec.stones[i];
    var g = placeStone(c.x, c.z, c.r, c.s+(c.v?'~ghost':''), true, true, c.y||0, !!c.l);
    if(g){ g.userData.proc = (c.hasOwnProperty('p') ? c.p : rec.victim); g.userData.procMass = c.r || rec.mass || 1; restored++; }
    else skipped++;
  }
  save(); updateAnchors();
  codeStatus(rec.victim+' restored'+(skipped?' · '+skipped+' stepped over':''));
}

/* save: name anything you build and it becomes a word. select with oops-my-bad,
   `save mygate` — then `mygate` rises wherever the cursor stands. `saves` lists. */
var SAVES = {};
var SAVE_RESERVED = {oops:1, cut:1, keep:1, swap:1, saves:1, send:1, take:1, newmap:1, lock:1, unlock:1, anchors:1, portal:1, unportal:1, portals:1};
function loadSaves(){ try{ SAVES = JSON.parse(localStorage.getItem('ruby-saves-v1')||'{}'); }catch(e){ SAVES={}; } }
function persistSaves(){ try{ localStorage.setItem('ruby-saves-v1', JSON.stringify(SAVES)); }catch(e){} }
function saveSelection(name){
  if(!selection.length){ codeStatus('nothing selected — oops-my-bad first'); return; }
  var a = selection[0];
  var st = [];
  for(var i=0;i<selection.length;i++){
    var s=selection[i], u=s.userData;
    st.push({dx:Math.round((s.position.x-a.position.x)*100)/100, dz:Math.round((s.position.z-a.position.z)*100)/100,
             y:u.y||0, r:u.rubies||1, s:u.shape||'cube', v:u.ghost?1:0});
  }
  SAVES[name] = {stones:st};
  persistSaves();
  codeStatus(name+' saved ('+st.length+' stones)');
}
function listSaves(){
  var ks = Object.keys(SAVES);
  if(!ks.length){ codeSt