<!-- RUBY-BACKUP · part 560 -->
AnimationFrame(spin); }catch(e){} })();
    status('gear placed — turning slow, remembering.');
  }

  function placeWeb(x, z){
    try{
      if(typeof window.spiderDist === 'function') window.spiderDist(4);
      if(typeof window.__weaveAt === 'function'){
        var p = new THREE.Vector3(x, groundY(x,z)+2, z);
        var web = window.__weaveAt(p);
        status(web ? 'web woven from salvage.' : 'no points there to weave');
      } else status('spider not ready');
    }catch(e){ status('web failed'); }
  }

  function placeRope(x, z){
    /* rope needs two ends: thread between the two nearest pulleys/gears */
    status('tap a second point — rope threads between them');
    placeArmed = {kind:'ropeA', a:new THREE.Vector3(x, groundY(x,z)+3, z)};
  }

  function placeRopeB(x, z){
    try{
      var a = placeArmed.a;
      var b = new THREE.Vector3(x, groundY(x,z)+3, z);
      var geo = new THREE.BufferGeometry().setFromPoints([a, b]);
      var line = new THREE.Line(geo, new THREE.LineBasicMaterial({color:0xc9a227, transparent:true, opacity:0}));
      if(window.scene) window.scene.add(line);
      var t0 = Date.now();
      (function fade(){
        try{
          var t = Math.min(1, (Date.now()-t0)/600);
          line.material.opacity = 0.9*t;
          if(t<1) requestAnimationFrame(fade);
        }catch(e){}
      })();
      inventory.rope--; saveInv();
      status('rope threaded — salvaged line holds again.');
    }catch(e){}
    placeArmed = null;
  }

  function tryPlace(kind, sx, sy){
    if(!inventory[kind] || inventory[kind] < 1){ status('no '+kind+' in inventory'); placeArmed = null; return; }
    var pt = groundPoint(sx, sy);
    if(!pt){ status('tap the ground to place it'); return; }
    if(kind === 'pulley'){ placePulley(pt.x, pt.z); inventory.pulley--; }
    else if(kind === 'gear'){ placeGear(pt.x, pt.z); inventory.gear--; }
    else if(kind === 'web'){ placeWeb(pt.x, pt.z); inventory.web--; }
    else if(kind === 'rope'){ placeRope(pt.x, pt.z); return; /* decrements on second tap */ }
    saveInv();
    placeArmed = null;
  }

  /* ---------- tap hook ---------- */
  function hookTaps(){
    var tries = 0;
    var iv = setInterval(function(){
      try{
        tries++;
        if(typeof dDown === 'function' && !dDown.__salvageWrapped){
          var orig = dDown;
          var mine = function(id, sx, sy){
            try{
              if(window.__installArmed || window.__uninstallArmed)
                return orig.apply(this, arguments);
              /* rope second tap */
              if(placeArmed && placeArmed.kind === 'ropeA'){
                var pt = groundPoint(sx, sy);
                if(pt){ placeRopeB(pt.x, pt.z); return true; }
                return orig.apply(this, arguments);
              }
              /* placing a part */
              if(placeArmed && typeof placeArmed.kind === 'string'){
                tryPlace(placeArmed.kind, sx, sy);
                return true;
              }
              /* planting a salvage block */
              if(salvageArmed){
                salvageArmed = false;
                var pk = pickAt(sx, sy);
                if(pk && pk.obj){ plantSalvage(pk.obj); return true; }
                status('tap a block to plant the salvage block');
                return true;
              }
              /* collect mode: salvage block selected, awaiting machine tap */
              if(collectArmed){
                var ck = pickAt(sx, sy);
                /* tapping the salvage block again cancels */
                if(ck && ck.obj && isSalvageObj(ck.obj)){
                  collectArmed = null;
                  status('salvage standing by.');
                  return true;
                }
                /* tap anywhere = the machine to unbuild */
                var salv = collectArmed; collectArmed = null;
                disassembleMachine(salv);
                return true;
              }
              /* tapping a salvage block selects it for collection */
              var tk = pickAt(sx, sy);
              if(tk && tk.obj && isSalvageObj(tk.obj)){
                for(var i=0;i<salvageBlocks.length;i++){
                  var sb = salvageBlocks[i];
                  try{
                    var o = tk.obj, found = false;
                    while(o){ if(o === sb.obj){ found = true; break; } o = o.parent; }
                    if(found){ collectArmed = sb; break; }
                  }catch(e){}
                }
                if(collectArmed){
                  status('salvage ready — tap a machine to unbuild it into inventory.');
                  return true;
                }
              }
            }catch(e){}
            return orig.apply(this, arguments);
          };
          mine.__salvageWrapped = true;
          dDown = mine;
          clearInterval(iv);
        }
      }catch(e){}
      if(tries > 40) clearInterval(iv);
    }, 500);
  }

  /* ---------- runLine chain ---------- 