Greasy Fork is available in English.

Survev.io script

!NOT WORKING RIGHT NOW GOING TO FIX IT SOON!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Survev.io script
// @namespace    VoidBacon
// @description  !NOT WORKING RIGHT NOW GOING TO FIX IT SOON!
// @version      2.7
// @author       John pork
// @match        *://survev.io/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

/*John pork says have fun*/
(function () {
  'use strict';

  const inject = document.createElement('script');
  inject.textContent = `(function(){
    window.__hbCandidates=[];
    const _o=Object.defineProperty;
    Object.defineProperty=function(obj,prop,desc){
      try{
        if(prop==='game'&&desc&&'value'in desc&&
           (desc.value===null||desc.value===undefined||desc.value===false)&&
           obj&&typeof obj==='object'&&obj!==window&&obj!==document)
          window.__hbCandidates.push(obj);
      }catch(e){}
      return _o.call(Object,obj,prop,desc);
    };
  })();`;
  (document.head || document.documentElement).appendChild(inject);
  inject.remove();

  function findRe() {
    if (window.__Re?.game !== undefined) return window.__Re;
    for (const c of (window.__hbCandidates || []))
      try { if (c && 'audioManager' in c && 'inputBinds' in c && 'pixi' in c) return c; } catch(e) {}
    return null;
  }

  function findMapInfo(game) {
    for (const k of Object.keys(game)) {
      const v = game[k];
      if (!v || typeof v !== 'object') continue;
      for (const sk of Object.keys(v)) {
        const sv = v[sk];
        if (sv && typeof sv.ZBG === 'function') {
          const pool = sv.ZBG();
          if (pool.length > 0 && pool[0]?.ceiling !== undefined)
            return { mapKey: k, buildPoolKey: sk };
        }
      }
    }
    return null;
  }

  const cfg = { Esp:true, names:true, healthBars:true, lockOn:false, magnet:false, xray:false };
  const KEY_W=87, KEY_A=65, KEY_S=83, KEY_D=68;
  let _zoomFn=null, _mapInfo=null;
  let _nearest=null;
  let isPanelVisible=false, panelX=null, panelY=null;
  let _fps=0, _fpsFrames=0, _fpsLast=performance.now();
  let _ms=0, _msLast=performance.now();
  let _magnetKeys={ w:false, a:false, s:false, d:false };

  function tickXray(game) {
    if (!cfg.xray) return;
    if (!_mapInfo) _mapInfo = findMapInfo(game);
    if (!_mapInfo) return;
    const pool = game[_mapInfo.mapKey]?.[_mapInfo.buildPoolKey]?.ZBG?.() || [];
    for (const b of pool) {
      if (!b.active||!b.ceiling) continue;
      b.ceiling.fadeAlpha = 0;
      for (const img of (b.imgs||[])) if (img.isCeiling&&img.sprite) img.sprite.alpha=0;
    }
  }
  function restoreXray(game) {
    if (!_mapInfo) return;
    const pool = game[_mapInfo.mapKey]?.[_mapInfo.buildPoolKey]?.ZBG?.() || [];
    for (const b of pool) {
      if (!b.ceiling) continue;
      b.ceiling.fadeAlpha=1;
      for (const img of (b.imgs||[])) if (img.isCeiling&&img.sprite) img.sprite.alpha=img.sprite.imgAlpha??1;
    }
  }

  function getZoom(game) {
    if (_zoomFn) { try { return _zoomFn(); } catch(e) { _zoomFn=null; } }
    for (const k of Object.keys(game)) {
      const v = game[k];
      if (!v||typeof v!=='object'||typeof v.screenWidth!=='number'||v.screenWidth<100) continue;
      for (const fk of Object.keys(v)) {
        if (typeof v[fk]!=='function') continue;
        try { const z=v[fk](); if(z>10&&z<500){_zoomFn=v[fk].bind(v);return z;} } catch(e) {}
      }
    }
    return 30;
  }

  function clearMagnetKeys(input) {
    if (_magnetKeys.w) { input.keys[KEY_W]=false; _magnetKeys.w=false; }
    if (_magnetKeys.a) { input.keys[KEY_A]=false; _magnetKeys.a=false; }
    if (_magnetKeys.s) { input.keys[KEY_S]=false; _magnetKeys.s=false; }
    if (_magnetKeys.d) { input.keys[KEY_D]=false; _magnetKeys.d=false; }
  }

  function applyMagnet(input, dx, dy) {
    const threshold = 0.3;
    const len = Math.sqrt(dx*dx + dy*dy);
    if (len === 0) { clearMagnetKeys(input); return; }
    const nx = dx/len, ny = dy/len;
    const wantW = ny < -threshold; const wantS = ny > threshold;
    const wantA = nx < -threshold; const wantD = nx > threshold;
    input.keys[KEY_W]=wantW; _magnetKeys.w=wantW;
    input.keys[KEY_S]=wantS; _magnetKeys.s=wantS;
    input.keys[KEY_A]=wantA; _magnetKeys.a=wantA;
    input.keys[KEY_D]=wantD; _magnetKeys.d=wantD;
  }

  const modeMap = {1:'Solo',2:'Duo',3:'Trio',4:'Squad'};
  function timeAgo(d) {
    const s=Math.floor((Date.now()-new Date(d))/1000);
    return s<60?`${s}s`:s<3600?`${Math.floor(s/60)}m`:s<86400?`${Math.floor(s/3600)}h`:`${Math.floor(s/86400)}d`;
  }

  function fetchStats(slug) {
    if (!slug) return;
    const box=document.getElementById('_hbStatsBox'), hist=document.getElementById('_hbHistoryBox');
    if(box) box.innerHTML='<span style="color:#b8b4ac;font-size:10px">loading...</span>';
    if(hist) hist.innerHTML='<span style="color:#b8b4ac;font-size:10px">loading...</span>';
    fetch('https://api.survev.io/api/user_stats',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug,interval:'alltime',mapIdFilter:'-1'})})
    .then(r=>r.json()).then(d=>{
      if(!box) return;
      if(!d||d.banned){box.innerHTML='<span style="color:#e87a7a;font-size:10px">not found</span>';return;}
      const kpg=d.games>0?(d.kills/d.games).toFixed(2):'0.00';
      const wPct=d.games>0?((d.wins/d.games)*100).toFixed(1):'0.0';
      box.innerHTML=`<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:2px;text-align:center">${[['WINS',d.wins??0],['KILLS',d.kills??0],['GAMES',d.games??0],['K/G',kpg],['WIN%',wPct+'%']].map(([l,v])=>`<div style="padding:4px 0"><div style="font-size:15px;font-weight:700;color:#2a2a2a">${v}</div><div style="font-size:8px;color:#bbb;letter-spacing:1px;margin-top:1px">${l}</div></div>`).join('')}</div>`;
    }).catch(()=>{if(box) box.innerHTML='<span style="color:#e87a7a;font-size:10px">error</span>';});
    fetch('https://api.survev.io/api/match_history',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug,offset:0,count:10,teamModeFilter:7})})
    .then(r=>r.json()).then(matches=>{
      if(!hist) return;
      if(!Array.isArray(matches)||!matches.length){hist.innerHTML='<span style="color:#b8b4ac;font-size:10px">no matches</span>';return;}
      hist.innerHTML=`<div style="border-radius:7px;overflow:hidden;border:1px solid #e4e0d8">
        <div style="display:grid;grid-template-columns:40px 40px 1fr 50px 28px;gap:4px;padding:5px 10px;background:#edeae4">${['RANK','MODE','KILLS','DMG','AGO'].map(h=>`<span style="font-size:8px;color:#b0aca4;letter-spacing:1px">${h}</span>`).join('')}</div>
        ${matches.map(m=>{
          const rCol=m.rank===1?'#5bc470':m.rank<=3?'#e8c060':'#aaa';
          const mCol=m.team_count===1?'#c8a8ff':m.team_count===2?'#80c8ff':m.team_count===3?'#80e8a0':'#ffb060';
          return `<div style="display:grid;grid-template-columns:40px 40px 1fr 50px 28px;gap:4px;align-items:center;padding:7px 10px;border-bottom:1px solid #f0ede8;cursor:pointer;transition:background .1s" onmouseover="this.style.background='#f8f6f2'" onmouseout="this.style.background=''" onclick="window._hbShowMatch('${m.guid}','${slug}')">
            <span style="font-size:9px;font-weight:700;color:${rCol}">${m.rank===1?'🥇':'#'+m.rank}</span>
            <span style="font-size:9px;font-weight:600;color:${mCol}">${modeMap[m.team_count]||'?'}</span>
            <span style="font-size:9px;color:#666">${m.kills??0}k / ${m.team_kills??0}tk</span>
            <span style="font-size:9px;color:#888">${Math.round((m.damage_dealt??0)/1000*10)/10}k</span>
            <span style="font-size:9px;color:#bbb;text-align:right">${timeAgo(m.end_time)}</span>
          </div>`;
        }).join('')}</div>`;
    }).catch(()=>{if(hist) hist.innerHTML='<span style="color:#e87a7a;font-size:10px">error</span>';});
  }

  window._hbShowMatch = function(guid, slug) {
    const detail=document.getElementById('_hbMatchDetail');
    if(!detail) return;
    detail.style.display='block';
    detail.innerHTML='<div style="color:#b8b4ac;font-size:10px;padding:8px">loading...</div>';
    fetch('https://api.survev.io/api/match_data',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({gameId:guid})})
    .then(r=>r.json()).then(players=>{
      if(!Array.isArray(players)||!players.length){detail.innerHTML='<span style="color:#e87a7a;font-size:10px;padding:8px">no data</span>';return;}
      const rows=players.map(p=>{
        const isMe=p.slug===slug;
        const t=Math.round(p.time_alive??0);
        const rCol=p.rank===1?'#5bc470':p.rank<=3?'#e8c060':'#aaa';
        return `<div style="display:grid;grid-template-columns:28px 1fr 24px 44px 44px 38px;gap:3px;align-items:center;padding:5px 10px;border-bottom:1px solid #f0ede8;${isMe?'background:#edf8f0':''}">
          <span style="font-size:9px;font-weight:700;color:${rCol}">#${p.rank}</span>
          <span style="font-size:9px;font-weight:${isMe?700:400};color:${isMe?'#2a8a44':'#444'};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.slug||p.username||'?'}</span>
          <span style="font-size:9px;color:#888">${p.kills??0}</span>
          <span style="font-size:9px;color:#888">${Math.round(p.damage_dealt??0)}</span>
          <span style="font-size:9px;color:#aaa">${Math.round(p.damage_taken??0)}</span>
          <span style="font-size:9px;color:#bbb">${Math.floor(t/60)}:${String(t%60).padStart(2,'0')}</span>
        </div>`;
      }).join('');
      detail.innerHTML=`
        <div style="display:flex;justify-content:space-between;align-items:center;padding:7px 10px;background:#edeae4;border-bottom:1px solid #e4e0d8">
          <span style="font-size:9px;color:#888;letter-spacing:.5px">MATCH DETAIL</span>
          <span style="cursor:pointer;color:#aaa;padding:2px 6px" onclick="document.getElementById('_hbMatchDetail').style.display='none'">✕</span>
        </div>
        <div style="display:grid;grid-template-columns:28px 1fr 24px 44px 44px 38px;gap:3px;padding:5px 10px;background:#f4f2ee">
          ${['#','PLAYER','K','DEALT','TAKEN','TIME'].map(h=>`<span style="font-size:8px;color:#b0aca4;letter-spacing:.5px">${h}</span>`).join('')}
        </div>
        <div style="max-height:200px;overflow-y:auto">${rows}</div>`;
    }).catch(()=>{detail.innerHTML='<span style="color:#e87a7a;font-size:10px;padding:8px">error</span>';});
  };

  function startOverlay(Re) {
    if (window._hbRunning) return;
    window._hbRunning = true;
    const canvas = document.createElement('canvas');
    canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9998;';
    document.body.appendChild(canvas);
    window._hbCanvas = canvas;
    const ctx = canvas.getContext('2d');
    const resize = () => { canvas.width=innerWidth; canvas.height=innerHeight; };
    resize();
    window.addEventListener('resize', resize);

    (function draw() {
      window._hbRaf = requestAnimationFrame(draw);
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      _fpsFrames++;
      const now = performance.now();
      const delta = now - _msLast; _msLast = now;
      _ms = Math.round(_ms * 0.85 + delta * 0.15);
      if (now - _fpsLast >= 1000) {
        _fps=_fpsFrames; _fpsFrames=0; _fpsLast=now;
        const fpsEl=document.getElementById('_hbFps');
        if(fpsEl){fpsEl.textContent=`${_fps} fps`;fpsEl.style.color=_fps>=55?'#5bc470':_fps>=30?'#e8c060':'#e87a7a';}
      }
      const msEl=document.getElementById('_hbMs');
      if(msEl){msEl.textContent=`${_ms} ms`;msEl.style.color=_ms<=16?'#5bc470':_ms<=33?'#e8c060':'#e87a7a';}

      const killEl=document.querySelector('.js-ui-player-kills');
      const killDisp=document.getElementById('_hbKills');
      if(killEl&&killDisp) killDisp.textContent=killEl.textContent||'0';

      const game=Re.game;
      if (!game?.initialized) { if (cfg.magnet && Re.input) clearMagnetKeys(Re.input); return; }
      const barn=game.kszNK, myId=game.FCmIQL;
      if (!barn||!myId) return;
      tickXray(game);
      const zoom=getZoom(game), STUD=zoom*16;
      const players=barn.playerPool.ZBG();
      const myInfo=barn.getPlayerInfo(myId);
      const myTeam=myInfo?.teamId, myGroup=myInfo?.groupId;
      const me=players.find(p=>p.__id===myId);
      if (!me?.active) return;
      const msx=me.container.x, msy=me.container.y;

      let nearestEnemy=null, nearestDist=Infinity;
      for (const p of players) {
        if (!p.active||p.__id===myId||p.dead||!p.container.visible) continue;
        const info=barn.getPlayerInfo(p.__id);
        const isMate=(myGroup>0&&info?.groupId>0&&info.groupId===myGroup)||(myTeam>0&&info?.teamId>0&&info.teamId===myTeam);
        if (isMate) continue;
        const d=Math.hypot(p.container.x-msx,p.container.y-msy);
        if (d<nearestDist){nearestDist=d;nearestEnemy=p;}
      }
      _nearest=nearestEnemy?{sx:nearestEnemy.container.x,sy:nearestEnemy.container.y}:null;

      if (cfg.lockOn && _nearest && Re.input) { Re.input.mousePos.x=_nearest.sx; Re.input.mousePos.y=_nearest.sy; }

      if (cfg.magnet && Re.input && nearestEnemy) {
        const distStuds=nearestDist/STUD;
        if (distStuds<=3) {
          applyMagnet(Re.input, nearestEnemy.container.x-msx, nearestEnemy.container.y-msy);
          const pulse=(Date.now()%600)/600;
          ctx.save(); ctx.globalAlpha=0.6+Math.sin(pulse*Math.PI*2)*0.4;
          ctx.beginPath(); ctx.arc(nearestEnemy.container.x,nearestEnemy.container.y,8+Math.sin(pulse*Math.PI*2)*3,0,Math.PI*2);
          ctx.strokeStyle='#ffff00'; ctx.lineWidth=2; ctx.stroke(); ctx.restore();
        } else { clearMagnetKeys(Re.input); }
      } else if (!cfg.magnet && Re.input) { clearMagnetKeys(Re.input); }

      for (const p of players) {
        if (!p.active||p.dead||!p.container.visible) continue;
        const sx=p.container.x, sy=p.container.y;
        const isSelf=p.__id===myId;
        const info=barn.getPlayerInfo(p.__id);
        const isMate=!isSelf&&((myGroup>0&&info?.groupId>0&&info.groupId===myGroup)||(myTeam>0&&info?.teamId>0&&info.teamId===myTeam));
        const isLocked=cfg.lockOn&&nearestEnemy&&p.__id===nearestEnemy.__id;
        const color=isSelf?'#5bc470':isMate?'#6aabff':'#ff4444';

        if (cfg.healthBars && !isSelf) {
          const status=barn.playerStatus?.[p.__id];
          const hp=typeof status?.health==='number'?status.health:null;
          if (hp !== null) {
            const BAR_W=80,BAR_H=8,BAR_X=sx-BAR_W/2,BAR_Y=sy-30;
            const filled=Math.max(0,Math.min(1,hp/100))*BAR_W;
            const hpCol=hp>60?'#5bc470':hp>30?'#e8c060':'#e87a7a';
            ctx.globalAlpha=0.75; ctx.fillStyle='rgba(0,0,0,0.55)';
            ctx.beginPath(); ctx.roundRect(BAR_X-1,BAR_Y-1,BAR_W+2,BAR_H+2,3); ctx.fill();
            ctx.globalAlpha=0.4; ctx.fillStyle='#333';
            ctx.beginPath(); ctx.roundRect(BAR_X,BAR_Y,BAR_W,BAR_H,2); ctx.fill();
            ctx.globalAlpha=0.95; ctx.fillStyle=hpCol;
            if(filled>0){ctx.beginPath();ctx.roundRect(BAR_X,BAR_Y,filled,BAR_H,2);ctx.fill();}
            ctx.globalAlpha=1; ctx.font='bold 9px monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
            ctx.strokeStyle='rgba(0,0,0,0.9)'; ctx.lineWidth=2.5;
            ctx.strokeText(`${Math.round(hp)}`,BAR_X+BAR_W/2,BAR_Y+BAR_H/2);
            ctx.fillStyle='#fff'; ctx.fillText(`${Math.round(hp)}`,BAR_X+BAR_W/2,BAR_Y+BAR_H/2);
          } else if (p.downed) {
            ctx.save(); ctx.globalAlpha=0.8; ctx.font='bold 10px monospace'; ctx.textAlign='center'; ctx.textBaseline='bottom';
            ctx.strokeStyle='rgba(0,0,0,0.8)'; ctx.lineWidth=3; ctx.strokeText('↓DOWNED',sx,sy-24);
            ctx.fillStyle='#e8c060'; ctx.fillText('↓DOWNED',sx,sy-24); ctx.restore();
          }
        }

        if (!isSelf) {
          const dist=Math.hypot(sx-msx,sy-msy);
          const alpha=Math.max(0.75,1-dist/2400);
          const studs=Math.round(dist/STUD);
          if (cfg.Esp) {
            ctx.save(); ctx.beginPath(); ctx.moveTo(msx,msy); ctx.lineTo(sx,sy);
            ctx.setLineDash(isMate?[6,4]:isLocked?[3,3]:[]);
            ctx.strokeStyle=isLocked?'#ff4444':color; ctx.lineWidth=isMate?2:2.5;
            ctx.globalAlpha=alpha; ctx.stroke(); ctx.setLineDash([]); ctx.restore();
          }
          if (cfg.names) {
            const name=info?.nameTruncated||info?.name||`#${p.__id}`;
            const label=isLocked?`${name}  ${studs}  ◎`:`${name}  ${studs}`;
            const lx=(msx+sx)/2, ly=(msy+sy)/2;
            ctx.save(); ctx.globalAlpha=Math.min(1,alpha+0.2);
            ctx.font='bold 11px monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
            ctx.strokeStyle='rgba(0,0,0,0.8)'; ctx.lineWidth=3; ctx.strokeText(label,lx,ly);
            ctx.fillStyle=isLocked?'#ff4444':color; ctx.fillText(label,lx,ly); ctx.restore();
          }
        }
      }
    })();
    setStatus('active','#5bc470');
  }

  const CSS = `
    @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap');
    #_hbPanel *,#_hbPanel *::before,#_hbPanel *::after{box-sizing:border-box;margin:0;padding:0}
    #_hbPanel{position:fixed;width:340px;background:#f4f3f0;border:1px solid #dbd9d3;border-radius:12px;z-index:10000;font-family:'DM Mono','Courier New',monospace;color:#2a2a2a;box-shadow:0 0 0 1px rgba(255,255,255,.7) inset,0 8px 32px rgba(0,0,0,.14);display:none;flex-direction:column;overflow:hidden;animation:_hbIn .18s cubic-bezier(.34,1.4,.64,1)}
    @keyframes _hbIn{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}
    #_hbHeader{display:flex;align-items:center;justify-content:space-between;padding:11px 16px;background:linear-gradient(to bottom,#f0ede8,#ebe8e2);border-bottom:1px solid #dbd9d3;cursor:grab;border-radius:12px 12px 0 0;user-select:none}
    #_hbHeader:active{cursor:grabbing}
    ._hbLogo{display:flex;align-items:center;gap:8px}
    ._hbDot{width:8px;height:8px;border-radius:50%;background:#ccc;box-shadow:0 0 10px #ccc4;transition:all .3s}
    @keyframes _hbShimmer{0%{background-position:200% center}100%{background-position:-200% center}}
    ._hbLogoText{font-size:10px;font-weight:500;letter-spacing:3px;text-transform:uppercase;background:linear-gradient(90deg,#111 0%,#888 40%,#fff 50%,#888 60%,#111 100%);background-size:300% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:_hbShimmer 4s linear infinite}
    ._hbDiscord{font-size:9px;font-weight:500;letter-spacing:1px;text-decoration:none;text-transform:uppercase;background:linear-gradient(90deg,#4752c4 0%,#7289da 40%,#b8c0ff 50%,#7289da 60%,#4752c4 100%);background-size:300% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:_hbShimmer 4s linear infinite;animation-delay:.8s}
    ._hbBadge{font-size:9px;color:#aaa;background:#e4e0d8;padding:2px 7px;border-radius:20px;letter-spacing:1px;border:1px solid #dbd9d3}
    ._hbHeaderRight{display:flex;align-items:center;gap:6px}
    ._hbHotkey{font-size:9px;color:#bbb;background:#ece9e2;padding:3px 8px;border-radius:20px;border:1px solid #dbd9d3}
    ._hbHotkey span{color:#2a2a2a;font-weight:600;background:#e0ddd6;padding:1px 5px;border-radius:3px}
    #_hbClose{background:none;border:none;color:#bbb;font-size:13px;cursor:pointer;padding:4px 6px;border-radius:6px;transition:all .15s}
    #_hbClose:hover{color:#d04040;background:#fae0e0}
    #_hbBody{padding:14px 16px;background:#f4f3f0;display:flex;flex-direction:column;gap:12px;overflow-y:auto;max-height:calc(100vh - 100px)}
    ._hbSectionTitle{font-size:9px;letter-spacing:2.5px;color:#b8b4ac;text-transform:uppercase;margin-bottom:7px;padding-bottom:5px;border-bottom:1px solid #e4e0d8}
    ._hbGrid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
    ._hbToggle{display:flex;align-items:center;justify-content:space-between;padding:9px 11px;background:#fff;border:1px solid #e8e4de;border-radius:7px;cursor:pointer;transition:all .15s;user-select:none}
    ._hbToggle:hover{background:#faf9f6;border-color:#d4d0c8}
    ._hbToggle.on{background:#edf8f0;border-color:#b0dfc0}
    ._hbToggleLbl{font-size:10px;font-weight:500;letter-spacing:.5px;color:#999;text-transform:uppercase}
    ._hbToggle.on ._hbToggleLbl{color:#2a8a44}
    ._hbPip{width:7px;height:7px;border-radius:50%;background:#ddd;flex-shrink:0;transition:all .2s}
    ._hbToggle.on ._hbPip{background:#5bc470;box-shadow:0 0 7px #5bc47066}
    ._hbToggle.red.on{background:#fdf2f2;border-color:#f0bfbf}
    ._hbToggle.red.on ._hbToggleLbl{color:#c04040}
    ._hbToggle.red.on ._hbPip{background:#e87a7a;box-shadow:0 0 7px #e87a7a66}
    ._hbToggle.blue.on{background:#f0f4ff;border-color:#a0c0f0}
    ._hbToggle.blue.on ._hbToggleLbl{color:#3060c0}
    ._hbToggle.blue.on ._hbPip{background:#6aabff;box-shadow:0 0 7px #6aabff66}
    ._hbToggle.yellow.on{background:#fffbf0;border-color:#f0d890}
    ._hbToggle.yellow.on ._hbToggleLbl{color:#a07010}
    ._hbToggle.yellow.on ._hbPip{background:#e8c060;box-shadow:0 0 7px #e8c06066}
    ._hbInfoRow{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#fff;border:1px solid #e8e4de;border-radius:7px}
    ._hbInfoLabel{font-size:9px;color:#aaa;letter-spacing:1.5px;text-transform:uppercase}
    ._hbInfoVal{font-size:14px;font-weight:700;color:#2a2a2a}
    ._hbCard{background:#fff;border:1px solid #e8e4de;border-radius:7px;padding:10px 12px}
    ._hbSlugRow{display:flex;gap:6px;margin-bottom:10px}
    ._hbSlugRow input{flex:1;background:#f4f3f0;border:1px solid #e0ddd6;border-radius:5px;padding:5px 8px;font:11px 'DM Mono',monospace;color:#2a2a2a;outline:none}
    ._hbSlugRow input:focus{border-color:#b0b8c8}
    ._hbSlugRow button{background:#2a2a2a;color:#fff;border:none;border-radius:5px;padding:5px 10px;font:10px 'DM Mono',monospace;cursor:pointer;letter-spacing:.5px;transition:all .15s}
    ._hbSlugRow button:hover{background:#444}
    ._hbNote{font-size:9px;color:#c0bab2;font-style:italic;margin-top:5px;line-height:1.4}
    #_hbMatchDetail{display:none;border-radius:7px;overflow:hidden;border:1px solid #e4e0d8;background:#fff;margin-top:8px}
    #_hbStatusBar{padding:7px 16px;background:linear-gradient(to top,#ebe8e2,#edeae4);border-top:1px solid #dbd9d3;display:flex;align-items:center;justify-content:space-between;border-radius:0 0 12px 12px;flex-shrink:0}
    #_hbStatusText{font-size:9px;color:#c8c4bc;letter-spacing:.5px;transition:color .3s}
    ._hbFootRight{display:flex;align-items:center;gap:10px}
    #_hbFps{font-size:9px;color:#5bc470;letter-spacing:.5px}
    #_hbMs{font-size:9px;color:#5bc470;letter-spacing:.5px}
    ._hbVer{font-size:9px;color:#c8c4bc;letter-spacing:1px}

    /* ── Setup accordion inside body ── */
    ._hbSetupToggle{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:#fff;border:1px solid #e8e4de;border-radius:7px;cursor:pointer;transition:all .15s;user-select:none}
    ._hbSetupToggle:hover{background:#faf9f6;border-color:#d4d0c8}
    ._hbSetupToggle.open{background:#f0f4ff;border-color:#a0c0f0;border-radius:7px 7px 0 0}
    ._hbSetupToggleLabel{font-size:10px;font-weight:500;letter-spacing:.5px;color:#999;text-transform:uppercase}
    ._hbSetupToggle.open ._hbSetupToggleLabel{color:#3060c0}
    ._hbSetupArrow{font-size:10px;color:#bbb;transition:transform .2s}
    ._hbSetupToggle.open ._hbSetupArrow{transform:rotate(180deg);color:#6aabff}
    ._hbSetupContent{display:none;background:#fff;border:1px solid #e8e4de;border-top:none;border-radius:0 0 7px 7px;padding:10px 12px;flex-direction:column;gap:8px}
    ._hbSetupContent.open{display:flex}
    ._hbSetupStep{font-size:9px;color:#555;line-height:1.7;padding:7px 9px;background:#f8f7f5;border-radius:5px;border-left:3px solid #dbd9d3}
    ._hbSetupStep b{color:#2a2a2a}
    ._hbSetupStep.warn{background:#fffbf0;border-left-color:#e8c060;color:#a07010}
    ._hbCodeBlock{background:#f0ede8;border:1px solid #e4e0d8;border-radius:5px;padding:7px 9px;margin-top:5px;cursor:pointer;transition:background .15s}
    ._hbCodeBlock:hover{background:#e8e4de}
    ._hbCodeBlock pre{font-size:9px;color:#2a2a2a;font-family:'DM Mono',monospace;white-space:pre-wrap;word-break:break-all;line-height:1.5;margin:0}
    ._hbCopyHint{font-size:8px;color:#b8b4ac;margin-top:3px;letter-spacing:.5px}

    /* ── Side setup panel ── */
    #_hbSidePanel{position:fixed;width:260px;background:#f4f3f0;border:1px solid #dbd9d3;border-radius:12px;z-index:10000;font-family:'DM Mono','Courier New',monospace;color:#2a2a2a;box-shadow:0 8px 32px rgba(0,0,0,.14);display:none;flex-direction:column;overflow:hidden}
    #_hbSidePanel._vis{display:flex;animation:_hbIn .18s cubic-bezier(.34,1.4,.64,1)}
    ._hbSideHeader{padding:10px 14px;background:linear-gradient(to bottom,#f0ede8,#ebe8e2);border-bottom:1px solid #dbd9d3;border-radius:12px 12px 0 0}
    ._hbSideTitle{font-size:9px;font-weight:500;letter-spacing:2.5px;color:#2a2a2a;text-transform:uppercase}
    ._hbSideBody{padding:12px 14px;display:flex;flex-direction:column;gap:8px;overflow-y:auto;max-height:80vh}
    ._hbSideStep{font-size:9px;color:#555;line-height:1.7;padding:8px 10px;background:#fff;border:1px solid #e8e4de;border-radius:7px}
    ._hbSideStep b{color:#2a2a2a;font-weight:700}
    ._hbSideStep.warn{background:#fffbf0;border-color:#f0d890;color:#a07010}
    ._hbSideCode{background:#f0ede8;border:1px solid #e4e0d8;border-radius:5px;padding:7px 9px;margin-top:6px;cursor:pointer;transition:background .15s}
    ._hbSideCode:hover{background:#e8e4de}
    ._hbSideCode pre{font-size:9px;color:#2a2a2a;font-family:'DM Mono',monospace;white-space:pre-wrap;word-break:break-all;line-height:1.5;margin:0}
    ._hbSideCode ._hbCopyHint{margin-top:4px}
  `;

  const SNIPPET_1 = `// Run this, then when paused type in console:
// window.__Re = Re  then hit Enter, then Resume ▶
// OR just use Snippet 2 if this doesn't pause

debug(getEventListeners(window).resize[0].listener);
window.dispatchEvent(new Event('resize'));`;

  const SNIPPET_2 = `window.__Re = Re`;

  function copyText(text, el) {
    navigator.clipboard.writeText(text).then(()=>{
      const hint=el.querySelector('._hbCopyHint');
      if(hint){const o=hint.textContent;hint.textContent='✓ copied!';hint.style.color='#5bc470';setTimeout(()=>{hint.textContent=o;hint.style.color='';},1500);}
    }).catch(()=>{});
  }

  function buildUI() {
    if (document.getElementById('_hbPanel')) return;
    const style=document.createElement('style'); style.textContent=CSS; document.head.appendChild(style);
    const panel=document.createElement('div'); panel.id='_hbPanel';
    panel.innerHTML=`
      <div id="_hbHeader">
        <div class="_hbLogo">
          <div class="_hbDot" id="_hbDot"></div>
          <span class="_hbLogoText">VoidBacon</span>
          <span class="_hbBadge">v2.7</span>
        </div>
        <div class="_hbHeaderRight">
          <div class="_hbHotkey"><span>ESC</span></div>
          <button id="_hbClose">✕</button>
        </div>
      </div>
      <div id="_hbBody">

        <div>
          <div class="_hbSectionTitle">Visuals</div>
          <div class="_hbGrid">
            <div class="_hbToggle on"  id="_hbt-Esp"><span class="_hbToggleLbl">Esp</span><div class="_hbPip"></div></div>
            <div class="_hbToggle on"  id="_hbt-names"><span class="_hbToggleLbl">Names</span><div class="_hbPip"></div></div>
            <div class="_hbToggle on"  id="_hbt-healthBars"><span class="_hbToggleLbl">HP Bars</span><div class="_hbPip"></div></div>
          </div>
          <div class="_hbNote">⚠ HP bars = teammates only (server never sends enemy HP)</div>
        </div>
        <div>
          <div class="_hbSectionTitle">Combat</div>
          <div class="_hbGrid">
            <div class="_hbToggle red"    id="_hbt-lockOn"><span class="_hbToggleLbl">Lock-On</span><div class="_hbPip"></div></div>
            <div class="_hbToggle yellow" id="_hbt-magnet"><span class="_hbToggleLbl">Magnet</span><div class="_hbPip"></div></div>
          </div>
          <div class="_hbNote">Lock-On: Q key &nbsp;|&nbsp; Magnet: E key (auto-moves when ≤3 studs)</div>
        </div>
        <div>
          <div class="_hbSectionTitle">World</div>
          <div class="_hbGrid">
            <div class="_hbToggle blue" id="_hbt-xray"><span class="_hbToggleLbl">X-Ray</span><div class="_hbPip"></div></div>
          </div>
        </div>
        <div>
          <div class="_hbSectionTitle">This Game</div>
          <div class="_hbInfoRow">
            <span class="_hbInfoLabel">Kills</span>
            <span class="_hbInfoVal" id="_hbKills">0</span>
          </div>
        </div>
        <div>
          <div class="_hbSectionTitle">Player Stats</div>
          <div class="_hbCard">
            <div class="_hbSlugRow">
              <input id="_hbSlugInput" type="text" placeholder="slug e.g. fr" />
              <button id="_hbStatsFetch">GO</button>
            </div>
            <div id="_hbStatsBox" style="text-align:center;padding:4px 0;font-size:10px;color:#aaa">enter slug above</div>
          </div>
        </div>
        <div>
          <div class="_hbSectionTitle">Recent Matches <span style="font-size:8px;color:#c0bab2;font-weight:normal;letter-spacing:0">— click row for details</span></div>
          <div id="_hbHistoryBox" style="font-size:10px;color:#aaa;text-align:center;padding:4px 0">search player above</div>
          <div id="_hbMatchDetail"></div>
        </div>
      </div>
      <div id="_hbStatusBar">
        <span id="_hbStatusText">waiting...</span>
        <div class="_hbFootRight">
          <span id="_hbFps">-- fps</span>
          <span id="_hbMs">-- ms</span>
          <a class="_hbDiscord" href="https://discord.gg/7WgfQc4k" target="_blank">discord</a>
          <span class="_hbVer">v2.7</span>
        </div>
      </div>`;
    document.body.appendChild(panel);

    // ── Side setup panel ──────────────────────────────────────────
    const sidePanel=document.createElement('div'); sidePanel.id='_hbSidePanel';
    sidePanel.innerHTML=`
      <div class="_hbSideHeader" style="display:flex;align-items:center;justify-content:space-between">
        <div class="_hbSideTitle">⚙ Setup Guide</div>
        <button id="_hbSideClose" style="background:none;border:none;color:#bbb;font-size:13px;cursor:pointer;padding:2px 6px;border-radius:5px;transition:color .15s" onmouseover="this.style.color='#d04040'" onmouseout="this.style.color='#bbb'">✕</button>
      </div>
      <div class="_hbSideBody">

        <div class="_hbSideStep">
          <b>Step 1</b> — Press <b>F12</b> to open DevTools → go to <b>Sources</b> tab → click <b>Snippets</b>.
        </div>

        <div class="_hbSideStep">
          <b>Step 2</b> — Create these 2 snippets:<br><br>
          <b>Snippet 1:</b>
          <div class="_hbSideCode" id="_hbSideSnip1">
            <pre>debug(getEventListeners(window).resize[0].listener);
window.dispatchEvent(new Event('resize'));</pre>
            <div class="_hbCopyHint">▸ click to copy</div>
          </div>
          <br><b>Snippet 2:</b>
          <div class="_hbSideCode" id="_hbSideSnip2">
            <pre>window.__Re = Re</pre>
            <div class="_hbCopyHint">▸ click to copy</div>
          </div>
        </div>

        <div class="_hbSideStep">
          <b>Step 3</b> — On the survev.io <b>loading screen</b>:<br><br>
          1. Run <b>Snippet 1</b><br>
          2. Run <b>Snippet 2</b><br>
          3. Turn <b>OFF Snippet 1</b> so the game doesn't freeze<br><br>
          Done ✓ it should be working now
        </div>

        <div class="_hbSideStep warn">
          ⚠ Do this <b>every time</b> you open survev.io. After 1 game it stays active — no need to redo unless you refresh or close the tab.
        </div>

      </div>`;
    document.body.appendChild(sidePanel);

    document.getElementById('_hbSideClose').onclick=()=>sidePanel.remove();
    document.getElementById('_hbSideSnip2').onclick=function(){copyText(`window.__Re = Re`,this);};

    function positionSide(){
      const r=panel.getBoundingClientRect();
      const sw=260, margin=10;
      // flip to left if not enough room on right
      if(r.right+margin+sw > window.innerWidth){
        sidePanel.style.left=(r.left-margin-sw)+'px';
      } else {
        sidePanel.style.left=(r.right+margin)+'px';
      }
      sidePanel.style.top=r.top+'px';
    }
    window._hbPosSide=positionSide;
    // snippet copy handlers are on the side panel only (set after sidePanel is built)

    document.getElementById('_hbClose').onclick=e=>{e.stopPropagation();closePanel();};

    for (const [id,key] of Object.entries({
      '_hbt-Esp':'Esp','_hbt-names':'names','_hbt-healthBars':'healthBars',
      '_hbt-lockOn':'lockOn','_hbt-magnet':'magnet','_hbt-xray':'xray'
    })) document.getElementById(id).onclick=()=>flipToggle(key);

    const slugInput=document.getElementById('_hbSlugInput');
    document.getElementById('_hbStatsFetch').onclick=()=>fetchStats(slugInput.value.trim());
    slugInput.addEventListener('keydown',e=>{if(e.key==='Enter') fetchStats(slugInput.value.trim());});

    setTimeout(()=>{
      const nameEl=document.getElementById('account-player-name');
      if(nameEl&&!slugInput.value){
        const slug=nameEl.textContent.trim().toLowerCase().replace(/\s+/g,'-');
        const skip=['log-in','create-account','guest','sign-in'];
        if(slug&&!skip.some(s=>slug.includes(s))){slugInput.value=slug;fetchStats(slug);}
      }
    },2000);

    let drag=false,ox=0,oy=0;
    document.getElementById('_hbHeader').onmousedown=e=>{
      if(e.target.id==='_hbClose') return;
      drag=true;const r=panel.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;
    };
    document.addEventListener('mousemove',e=>{if(!drag) return;panelX=e.clientX-ox;panelY=e.clientY-oy;panel.style.left=panelX+'px';panel.style.top=panelY+'px';panel.style.right='auto';if(window._hbPosSide)window._hbPosSide();});
    document.addEventListener('mouseup',()=>drag=false);
  }

  function flipToggle(key) {
    cfg[key]=!cfg[key];
    const el=document.getElementById({Esp:'_hbt-Esp',names:'_hbt-names',healthBars:'_hbt-healthBars',lockOn:'_hbt-lockOn',magnet:'_hbt-magnet',xray:'_hbt-xray'}[key]);
    if(el) el.classList.toggle('on',cfg[key]);
    if(key==='xray'&&!cfg.xray){const game=findRe()?.game;if(game) restoreXray(game);}
    if(key==='magnet'&&!cfg.magnet){const Re=findRe();if(Re?.input) clearMagnetKeys(Re.input);}
    setStatus(`${key} ${cfg[key]?'on':'off'}`);
  }

  function openPanel() {
    const panel=document.getElementById('_hbPanel'); if(!panel) return;
    if(panelX===null){panelX=Math.round((innerWidth-340)/2);panelY=Math.round((innerHeight-600)/2);}
    panel.style.left=panelX+'px';panel.style.top=panelY+'px';panel.style.right='auto';
    panel.style.display='flex';isPanelVisible=true;
    const sp=document.getElementById('_hbSidePanel');
    if(sp){if(window._hbPosSide)window._hbPosSide();sp.classList.add('_vis');}
  }
  function closePanel(){
    const p=document.getElementById('_hbPanel');if(p) p.style.display='none';isPanelVisible=false;
    const sp=document.getElementById('_hbSidePanel');if(sp)sp.classList.remove('_vis');
  }

  let _st=null;
  function setStatus(msg,color='#c8c4bc') {
    const el=document.getElementById('_hbStatusText'),dot=document.getElementById('_hbDot');
    if(!el) return;
    el.style.color=color;el.textContent=msg;
    if(dot){dot.style.background=color;dot.style.boxShadow=`0 0 10px ${color}44`;}
    clearTimeout(_st);
    if(color!=='#5bc470'&&color!=='#c8c4bc') _st=setTimeout(()=>setStatus('ready'),3000);
  }

  document.addEventListener('keydown',e=>{
    if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
    if(e.key==='Escape'){e.preventDefault();isPanelVisible?closePanel():openPanel();}
    if(e.key==='q'||e.key==='Q') flipToggle('lockOn');
    if(e.key==='e'||e.key==='E') flipToggle('magnet');
    if(e.key==='x'||e.key==='X') flipToggle('xray');
  },true);

  function waitForGame() {
    buildUI();openPanel();
    let tries=0;
    const poll=setInterval(()=>{
      tries++;
      const Re=findRe();
      if(Re?.game?.initialized){clearInterval(poll);startOverlay(Re);return;}
      if(Re?.game){setStatus('in lobby','#b8a060');return;}
      if(tries%10===0) console.log(`[VoidBacon] waiting... candidates: ${window.__hbCandidates?.length}`);
      if(tries>240){clearInterval(poll);setStatus('needs setup','#c05050');
        console.warn('[VoidBacon] Auto-capture failed. Open SETUP GUIDE in the panel for instructions.');}
    },500);
  }

  const obs=new MutationObserver((_,o)=>{if(document.getElementById('ui-game')){o.disconnect();waitForGame();}});
  window.addEventListener('DOMContentLoaded',()=>{
    if(document.getElementById('ui-game')) waitForGame();
    else obs.observe(document.body,{childList:true,subtree:true});
  });
})();