Survev.io script

!NOT WORKING RIGHT NOW GOING TO FIX IT SOON!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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});
  });
})();