new shit is on people post suggestions in discord
// ==UserScript==
// @name survev.io script -(VoidBacon)
// @namespace VoidBacon
// @version 3.2
// @author John pork
// @match *://survev.io/*
// @description new shit is on people post suggestions in discord
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
/*John pork says have fun*/
(function () {
'use strict';
// ── App capture via console.log hook ──────────────────────────
const inject = document.createElement('script');
inject.textContent = `(function(){
const _log = console.log.bind(console);
console.log = function(...args) {
for (const a of args) {
if (a && typeof a === 'object' && 'audioManager' in a && 'pixi' in a && 'game' in a && 'inputBinds' in a) {
window.__Re = a;
}
}
return _log(...args);
};
})();`;
(document.head || document.documentElement).appendChild(inject);
inject.remove();
function findRe() {
return window.__Re || null;
}
// ── Config ────────────────────────────────────────────────────
const cfg = { Esp:true, names:true, healthBars:true, lockOn:false, magnet:false, xray:false, lootEsp:false, grenTimer:true };
const KEY_W=87, KEY_A=65, KEY_S=83, KEY_D=68;
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 };
let _xrayBarnKey = null, _xrayPoolKey = null;
// ── Grenade fuse times (from survev source, seconds) ─────────
// frag: 4.0s cookable
// mirv: 4.0s cookable, spawns 4 children at 1.0s each
// smoke: 2.5s NOT cookable
// strobe: 3.0s, pin_wheel: 3.0s, potato: 2.5s
const GREN_FUSE = {
'frag': 4.0,
'mirv': 4.0,
'bomb': 1.0, // mirv child bomblet
'smoke': 2.5,
'strobe': 3.0,
'pin': 3.0, // pin_wheel
'potato': 2.5,
};
function getGrenFuse(typeId) {
if (!typeId) return 4.0;
const k = typeId.toLowerCase();
for (const [name, fuse] of Object.entries(GREN_FUSE)) {
if (k.includes(name)) return fuse;
}
return 4.0;
}
// Cook state tracking: { cookStart: timestamp, fuse: seconds, cooking: bool }
let _grenCook = null;
// Thrown grenade tracking: Map of projectile object => { spawnTime, fuse, typeId }
const _thrownGrenades = new Map();
// ── Player pool helper ────────────────────────────────────────
function getPlayers(barn) {
const pp = barn?.playerPool;
if (!pp) return [];
for (const k of Object.keys(pp)) {
const v = pp[k];
if (Array.isArray(v)) return v;
}
return [];
}
// ── Loot pool helper ──────────────────────────────────────────
function getLoot(game) {
// game.MUqt confirmed from debug: has lootPool + closestLoot
const lootBarn = game.MUqt;
if (!lootBarn) return [];
const lp = lootBarn.lootPool;
if (!lp) return [];
// Try array keys inside pool
for (const k of Object.keys(lp)) {
const v = lp[k];
if (Array.isArray(v) && v.length > 0) return v;
}
return [];
}
// item.type is a plain string e.g. "bar", "ak-47", "frag_grenade", "helmet03"
function lootColor(item) {
const t = (typeof item.type === 'string' ? item.type : '').toLowerCase();
// Tier 3 — rarest drops
if (/awm|deagle|mk_20|sv-98|mosin|l86|nt-16|mirv|strobe|spas|usas|saiga|qbb|pkp|dp-28|m249|helmet03|vest03|pack03/.test(t)) return '#ff66ff';
// Tier 2 — good loot
if (/scar|m416|ak|akm|bar|garand|model_94|mp220|vector|p90|m79|frag|smoke|flare|helmet02|vest02|pack02/.test(t)) return '#ffcc44';
// Tier 1 — decent
if (/m9|glock|ump|mp5|cz|p30|mac|helmet01|vest01|pack01/.test(t)) return '#44aaff';
// Ammo / consumables — grey
if (/ammo|bandage|medkit|soda|pills|gauze|2xscope|4xscope|8xscope|15xscope/.test(t)) return '#aaaaaa';
return '#cccccc';
}
function lootLabel(item) {
// item.type is a plain string typeId like "ak-47", "9mm", "helmet02", "frag_grenade"
const raw = typeof item.type === 'string' ? item.type : '?';
return raw.replace(/_/g, ' ').toUpperCase();
}
// ── Building pool for xray ────────────────────────────────────
function findBuildingPool(game) {
// Scan all top-level game objects for a pool-like object containing buildings (have .ceiling)
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) continue;
// Direct array
if (Array.isArray(sv) && sv.length > 0) {
const first = sv.find(x => x && typeof x === 'object');
if (first && 'ceiling' in first) { _xrayBarnKey=k; _xrayPoolKey=sk; return sv; }
}
// Object with numeric keys (pool-like)
if (typeof sv === 'object' && !Array.isArray(sv)) {
const vals = Object.values(sv);
if (vals.length > 0 && vals[0] && typeof vals[0] === 'object' && 'ceiling' in vals[0]) {
_xrayBarnKey=k; _xrayPoolKey=sk; return vals;
}
}
}
}
return null;
}
function getBuildingPool(game) {
if (_xrayBarnKey && _xrayPoolKey) {
const sv = game[_xrayBarnKey]?.[_xrayPoolKey];
if (Array.isArray(sv)) return sv;
if (sv && typeof sv === 'object') return Object.values(sv);
}
return findBuildingPool(game) || [];
}
function applyXray(buildings) {
for (const b of buildings) {
if (!b || !b.active) continue;
// Null out ceiling sprite alpha
if (b.ceiling) {
b.ceiling.fadeAlpha = 0;
if (b.ceiling.sprite) b.ceiling.sprite.alpha = 0;
if (b.ceiling.container) b.ceiling.container.alpha = 0;
}
// Kill any ceiling-tagged images
for (const img of (b.imgs||[])) {
if (img && img.isCeiling && img.sprite) img.sprite.alpha = 0;
}
// Also try zoomRegions which sometimes hold ceilings
for (const zr of (b.zoomRegions||[])) {
if (zr?.ceiling) { zr.ceiling.fadeAlpha=0; if(zr.ceiling.sprite) zr.ceiling.sprite.alpha=0; }
}
}
}
function tickXray(game) {
if (!cfg.xray) return;
applyXray(getBuildingPool(game));
}
function restoreXray(game) {
for (const b of getBuildingPool(game)) {
if (!b) continue;
if (b.ceiling) {
b.ceiling.fadeAlpha = 1;
if (b.ceiling.sprite) b.ceiling.sprite.alpha = 1;
if (b.ceiling.container) b.ceiling.container.alpha = 1;
}
for (const img of (b.imgs||[])) {
if (img && img.isCeiling && img.sprite) img.sprite.alpha = img.sprite.imgAlpha ?? 1;
}
for (const zr of (b.zoomRegions||[])) {
if (zr?.ceiling) { zr.ceiling.fadeAlpha=1; if(zr.ceiling.sprite) zr.ceiling.sprite.alpha=1; }
}
}
}
// ── Magnet ────────────────────────────────────────────────────
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;
input.keys[KEY_W] = ny < -threshold; _magnetKeys.w = input.keys[KEY_W];
input.keys[KEY_S] = ny > threshold; _magnetKeys.s = input.keys[KEY_S];
input.keys[KEY_A] = nx < -threshold; _magnetKeys.a = input.keys[KEY_A];
input.keys[KEY_D] = nx > threshold; _magnetKeys.d = input.keys[KEY_D];
}
// ── Stats helpers ─────────────────────────────────────────────
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>';});
};
// ── Overlay ───────────────────────────────────────────────────
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);
// FPS / MS counter
_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;
}
// Confirmed minified names from live debug:
// barn = game.dnXkCm (playerPool, playerInfo, playerStatus, groupInfo)
// myId = game.fyl (active player ID)
const barn = game.dnXkCm;
const myId = game.fyl;
if (!barn || !myId) return;
tickXray(game);
// World scale from pixi stage children[0] (world container)
// children[0].x/y = camera offset, scale.x = zoom level
const worldContainer = Re.pixi?.stage?.children?.[0];
const worldScale = worldContainer?.scale?.x || 30;
const STUD = worldScale;
const players = getPlayers(barn);
const myInfo = barn.playerInfo?.[myId];
const myGroup = myInfo?.groupId;
const myTeam = myInfo?.teamId;
const me = players.find(p => p.__id === myId);
if (!me?.active) return;
// Screen position via PIXI toGlobal — converts world coords to screen pixels
let mpos;
try { mpos = me.container.toGlobal({x:0, y:0}); }
catch(e) { return; }
const msx = mpos.x, msy = mpos.y;
// Find nearest enemy for lock-on / magnet
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.playerInfo?.[p.__id];
const isMate = (myGroup>0 && info?.groupId>0 && info.groupId===myGroup) ||
(myTeam>0 && info?.teamId>0 && info.teamId===myTeam);
if (isMate) continue;
let spos;
try { spos = p.container.toGlobal({x:0, y:0}); } catch(e) { continue; }
const d = Math.hypot(spos.x-msx, spos.y-msy);
if (d < nearestDist) { nearestDist=d; nearestEnemy={p, sx:spos.x, sy:spos.y}; }
}
_nearest = nearestEnemy ? {sx:nearestEnemy.sx, sy:nearestEnemy.sy} : null;
// Lock-on
if (cfg.lockOn && _nearest && Re.input) {
Re.input.mousePos.x = _nearest.sx;
Re.input.mousePos.y = _nearest.sy;
}
// Magnet
if (cfg.magnet && Re.input && nearestEnemy) {
const distStuds = nearestDist / STUD;
if (distStuds <= 6) {
applyMagnet(Re.input, nearestEnemy.sx-msx, nearestEnemy.sy-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.sx, nearestEnemy.sy, 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); }
// ── Grenade timer ─────────────────────────────────────────
// jvj.projectilePool.Wsryz = array of ALL grenades (bullets use separate system)
// proj.pos = {x, y} in WORLD coords — convert: screenX = worldContainer.x + pos.x * STUD
if (cfg.grenTimer) {
const now = Date.now();
const weapItem = (me.eSBXZ?.item ?? '').toLowerCase();
const isHoldingGren = /grenade|mirv|smoke|strobe|pin_wheel|potato|throwable/.test(weapItem);
const throwState = me.throwableState ?? 0;
// Only frag and mirv are cookable — smoke/strobe/potato always use full fuse on throw
const isCookable = /frag|mirv/.test(weapItem) && !/bomb|child/.test(weapItem);
// Cook start: only for cookable grenades
if (isCookable && throwState > 0 && !_grenCook) {
_grenCook = { cookStart: now, fuse: getGrenFuse(weapItem), typeId: weapItem, remaining: null, thrownAt: null };
}
// Throw detected: stamp remaining time (cook carry-over)
if (_grenCook && _grenCook.remaining === null && (!isHoldingGren || throwState === 0)) {
const cooked = (now - _grenCook.cookStart) / 1000;
_grenCook.remaining = Math.max(0.15, _grenCook.fuse - cooked);
_grenCook.thrownAt = now;
}
// Expire carry-over window after 500ms (enough time for projectile to enter pool)
if (_grenCook?.thrownAt && (now - _grenCook.thrownAt) > 500) _grenCook = null;
// Cancel if no longer holding and never threw
if (!isHoldingGren && _grenCook && !_grenCook.thrownAt) _grenCook = null;
// Scan projectile pool for new grenades
let newThisFrame = 0;
const newProjs = [];
const projArr = game.jvj?.projectilePool?.Wsryz;
if (Array.isArray(projArr)) {
for (const proj of projArr) {
if (!proj?.active) continue;
if (!_thrownGrenades.has(proj)) { newThisFrame++; newProjs.push(proj); }
}
}
for (const proj of newProjs) {
let fuse, typeId;
if (newThisFrame >= 3) {
// 3+ spawning simultaneously = MIRV children = 1.8s each
fuse = 1.8; typeId = 'mirv_child';
} else if (_grenCook?.remaining != null) {
// Cookable grenade thrown after cooking — use remaining fuse
fuse = _grenCook.remaining; typeId = _grenCook.typeId;
_grenCook = null; // consumed
} else {
// Non-cookable (smoke=2.5s, strobe=3s etc) OR enemy nade — full fuse from type
fuse = getGrenFuse(isHoldingGren ? weapItem : '');
typeId = isHoldingGren ? weapItem : 'grenade';
}
_thrownGrenades.set(proj, { spawnTime: now, fuse, typeId });
}
// Expire old
for (const [proj, data] of _thrownGrenades) {
const age = (now - data.spawnTime) / 1000;
if (!proj.active || age > data.fuse + 0.6) _thrownGrenades.delete(proj);
}
// Draw cook timer around player (only for cookable grenades: frag/mirv)
if (isCookable && isHoldingGren && _grenCook && !_grenCook.thrownAt) {
const elapsed = (now - _grenCook.cookStart) / 1000;
const remaining = Math.max(0, _grenCook.fuse - elapsed);
const progress = Math.min(1, elapsed / _grenCook.fuse);
const urgent = progress > 0.75;
const col = urgent ? '#ff5050' : '#ffdc3c';
ctx.save();
const arcR = 24;
ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(msx, msy, arcR, -Math.PI/2, -Math.PI/2 + Math.PI*2);
ctx.strokeStyle = 'rgba(80,80,80,0.4)'; ctx.stroke();
ctx.beginPath(); ctx.arc(msx, msy, arcR, -Math.PI/2, -Math.PI/2 + Math.PI*2*progress);
ctx.strokeStyle = col; ctx.stroke();
const lx2 = msx, ly2 = msy + arcR + 12;
ctx.fillStyle = 'rgba(0,0,0,0.75)';
ctx.beginPath(); ctx.roundRect(lx2-44, ly2-10, 88, 20, 5); ctx.fill();
ctx.font = 'bold 11px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.strokeStyle = 'rgba(0,0,0,0.9)'; ctx.lineWidth = 3;
const cookLabel = `💣 ${remaining.toFixed(1)}s`;
ctx.strokeText(cookLabel, lx2, ly2); ctx.fillStyle = col; ctx.fillText(cookLabel, lx2, ly2);
if (urgent) {
const pulse = (now % 250) / 250;
ctx.globalAlpha = 0.5 * (1 - pulse);
ctx.beginPath(); ctx.arc(msx, msy, arcR + pulse*14, 0, Math.PI*2);
ctx.strokeStyle='#ff3333'; ctx.lineWidth=2; ctx.stroke();
}
ctx.restore();
}
// Draw thrown grenades
for (const [proj, data] of _thrownGrenades) {
if (!proj.active || !proj.pos) continue;
// Correct world→screen formula confirmed from debug:
// worldContainer.x=-3329, pos.x=173, scale=23.4 → sx=719 ✓ (add)
// worldContainer.y=12456, pos.y=520, scale=23.4 → sy=288 ✓ (SUBTRACT — Y axis inverted)
const px = worldContainer.x + proj.pos.x * STUD;
const py = worldContainer.y - proj.pos.y * STUD;
const age = (now - data.spawnTime) / 1000;
const remaining = Math.max(0, data.fuse - age);
const progress = Math.min(1, age / data.fuse);
const urgent = remaining < 1.0;
const col = urgent ? '#ff3333' : remaining < 2.0 ? '#ff9922' : '#ffee44';
// Blast radius: frag=16 studs, smoke=10, everything else=12
const blastStuds = data.typeId.includes('smoke') ? 10 :
data.typeId.includes('mirv') ? 8 : 16;
const blastPx = blastStuds * STUD;
ctx.save();
// Blast radius circle
ctx.beginPath(); ctx.arc(px, py, blastPx, 0, Math.PI*2);
ctx.fillStyle = col; ctx.globalAlpha = 0.05 + progress * 0.13; ctx.fill();
ctx.globalAlpha = 0.35 + progress * 0.5;
ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke();
// Countdown arc on the grenade itself
ctx.globalAlpha = 1;
const arcR = 12;
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(0,0,0,0.7)';
ctx.beginPath(); ctx.arc(px, py, arcR, -Math.PI/2, -Math.PI/2 + Math.PI*2); ctx.stroke();
ctx.strokeStyle = col;
ctx.beginPath(); ctx.arc(px, py, arcR, -Math.PI/2, -Math.PI/2 + Math.PI*2*progress); ctx.stroke();
// Countdown number ON the grenade
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.strokeStyle = 'rgba(0,0,0,1)'; ctx.lineWidth = 4;
ctx.strokeText(remaining.toFixed(1), px, py);
ctx.fillStyle = col; ctx.fillText(remaining.toFixed(1), px, py);
// Last second pulse
if (urgent) {
const pulse = (now % 200) / 200;
ctx.globalAlpha = 0.7 * (1 - pulse);
ctx.beginPath(); ctx.arc(px, py, blastPx * (0.6 + pulse*0.4), 0, Math.PI*2);
ctx.strokeStyle='#ff1111'; ctx.lineWidth=3; ctx.stroke();
}
ctx.restore();
}
}
for (const p of players) {
if (!p.active || p.dead || !p.container?.visible) continue;
let spos;
try { spos = p.container.toGlobal({x:0, y:0}); } catch(e) { continue; }
const sx = spos.x, sy = spos.y;
const isSelf = p.__id === myId;
const info = barn.playerInfo?.[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.p.__id;
const color = isSelf?'#5bc470':isMate?'#6aabff':'#ff4444';
// HP bar
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();
}
// ── Weapon tag: box to the right of player ───────────────
if (cfg.Esp) {
// Get weapon from bBXCN (confirmed: contains current weapon string)
// Scan all string values, skip outfit/helmet/backpack
let weapId = '';
try {
const loadout = p.bBXCN;
if (loadout) {
for (const v of Object.values(loadout)) {
if (typeof v !== 'string' || v.length < 2) continue;
const vl = v.toLowerCase();
if (/^outfit|^backpack|^helmet|^skin|^vest/.test(vl)) continue;
// Accept weapon-like strings
if (/fists|[0-9]|rifle|shotgun|sniper|pistol|gun|mp5|ump|ak|m4|scar|bar|dp|pkp|p90|mac|vector|awm|mosin|sv|nt|l86|m249|qbb|spas|saiga|usas|s686|m870|garand|deagle|glock|cz|m79|model|frag|grenade|mirv|smoke|strobe|flare/.test(vl)) {
weapId = v; break;
}
}
}
if (!weapId) weapId = p.weapTypeOld ?? '';
} catch(e) { weapId = p.weapTypeOld ?? ''; }
if (weapId) {
const BOX_W = 58, BOX_H = 34;
const bx = sx + 20, by = sy - BOX_H / 2;
ctx.save();
ctx.globalAlpha = Math.min(1, alpha + 0.15);
// Background
ctx.fillStyle = 'rgba(0,0,0,0.78)';
ctx.beginPath(); ctx.roundRect(bx, by, BOX_W, BOX_H, 4); ctx.fill();
const wl = weapId.toLowerCase();
const wCol = /spas|saiga|usas|s686|m870|mp220|shotgun/.test(wl) ? '#ff9944'
: /awm|mosin|sv-98|sv98|nt-16|l86|sniper/.test(wl) ? '#cc88ff'
: /mp5|ump|vector|p90|mac|smg/.test(wl) ? '#44ddff'
: /grenade|mirv|smoke|strobe|frag/.test(wl) ? '#ffee44'
: /fists|melee/.test(wl) ? '#888888'
: '#88dd88';
ctx.strokeStyle = wCol; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.roundRect(bx, by, BOX_W, BOX_H, 4); ctx.stroke();
// Try real game sprite — textures are in loadout atlas as gun-{id}-01.img
let drewSprite = false;
try {
window._hbBuildTexCache?.();
const tex = window._hbGetWeapTex?.(weapId);
if (tex) {
const src = tex?.baseTexture?.resource?.source;
const f = tex?._frame ?? tex?.frame ?? tex?.orig;
if (src && f && f.width > 2 && f.height > 2) {
const PAD = 2, TEXTROW = 11;
const aspect = f.width / f.height;
const ih = BOX_H - PAD*2 - TEXTROW;
const iw = Math.min(BOX_W - PAD*2, ih * aspect);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(src, f.x, f.y, f.width, f.height,
bx + (BOX_W-iw)/2, by + PAD, iw, ih);
drewSprite = true;
}
}
} catch(e) {}
// Vector fallback if no sprite found
if (!drewSprite) {
const cx2 = bx+BOX_W/2, cy2 = by+(BOX_H-10)/2;
ctx.save();
ctx.strokeStyle=wCol; ctx.fillStyle=wCol;
ctx.lineWidth=1.5; ctx.lineCap='round'; ctx.lineJoin='round';
const isShotgun=/spas|saiga|usas|s686|m870|mp220/.test(wl);
const isSniper=/awm|mosin|sv-98|sv98|nt-16|l86/.test(wl);
const isGren=/grenade|mirv|smoke|strobe|frag/.test(wl);
const isFists=/fists/.test(wl);
const isSMG=/mp5|ump|vector|p90|mac/.test(wl);
if (isFists) {
ctx.beginPath(); ctx.arc(cx2-5,cy2,4,0,Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.arc(cx2+5,cy2,4,0,Math.PI*2); ctx.stroke();
} else if (isGren) {
ctx.beginPath(); ctx.arc(cx2,cy2+1,6,0,Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx2,cy2-5); ctx.lineTo(cx2,cy2-8);
ctx.moveTo(cx2-3,cy2-8); ctx.lineTo(cx2+3,cy2-8); ctx.stroke();
} else if (isSniper) {
ctx.beginPath();
ctx.moveTo(bx+3,cy2); ctx.lineTo(bx+50,cy2);
ctx.moveTo(bx+42,cy2); ctx.lineTo(bx+50,cy2+5);
ctx.moveTo(bx+34,cy2); ctx.lineTo(bx+34,cy2-5);
ctx.moveTo(bx+30,cy2-5); ctx.lineTo(bx+38,cy2-5);
ctx.stroke();
} else if (isShotgun) {
ctx.beginPath();
ctx.moveTo(bx+3,cy2); ctx.lineTo(bx+36,cy2);
ctx.moveTo(bx+3,cy2-2); ctx.lineTo(bx+3,cy2+2);
ctx.moveTo(bx+28,cy2); ctx.lineTo(bx+36,cy2+6);
ctx.moveTo(bx+20,cy2); ctx.lineTo(bx+20,cy2+5);
ctx.stroke();
ctx.beginPath(); ctx.moveTo(bx+12,cy2+1); ctx.lineTo(bx+18,cy2+1); ctx.lineWidth=3; ctx.stroke();
} else if (isSMG) {
ctx.beginPath();
ctx.moveTo(bx+3,cy2); ctx.lineTo(bx+34,cy2);
ctx.moveTo(bx+3,cy2-2); ctx.lineTo(bx+3,cy2+2);
ctx.moveTo(bx+26,cy2-3); ctx.lineTo(bx+34,cy2-3); ctx.lineTo(bx+34,cy2+2);
ctx.moveTo(bx+26,cy2); ctx.lineTo(bx+26,cy2+7);
ctx.moveTo(bx+14,cy2); ctx.lineTo(bx+14,cy2+5); ctx.lineTo(bx+20,cy2+5); ctx.lineTo(bx+20,cy2);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(bx+3,cy2); ctx.lineTo(bx+46,cy2);
ctx.moveTo(bx+3,cy2-2); ctx.lineTo(bx+3,cy2+2);
ctx.moveTo(bx+38,cy2); ctx.lineTo(bx+46,cy2+6);
ctx.moveTo(bx+30,cy2); ctx.lineTo(bx+30,cy2+6);
ctx.moveTo(bx+20,cy2); ctx.lineTo(bx+20,cy2+5); ctx.lineTo(bx+26,cy2+5); ctx.lineTo(bx+26,cy2);
ctx.stroke();
}
ctx.restore();
}
// Weapon name at bottom of box
const shortName = weapId.replace(/_/g,' ').toUpperCase().slice(0, 9);
const fs = shortName.length > 7 ? 7 : shortName.length > 5 ? 8 : 9;
ctx.font = `bold ${fs}px monospace`;
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
ctx.strokeStyle = 'rgba(0,0,0,0.9)'; ctx.lineWidth = 2.5;
ctx.strokeText(shortName, bx+BOX_W/2, by+BOX_H-1);
ctx.fillStyle = wCol;
ctx.fillText(shortName, bx+BOX_W/2, by+BOX_H-1);
ctx.restore();
}
}
}
}
// ── Loot ESP ──────────────────────────────────────────────
if (cfg.lootEsp) {
const lootItems = getLoot(game);
for (const item of lootItems) {
if (!item?.active || !item.pos) continue;
// Same world→screen as grenades: X adds, Y subtracts (inverted axis confirmed)
const lx = worldContainer.x + item.pos.x * STUD;
const ly = worldContainer.y - item.pos.y * STUD;
// Skip offscreen
if (lx < -50 || lx > innerWidth+50 || ly < -50 || ly > innerHeight+50) continue;
const dist = Math.hypot(lx-msx, ly-msy);
const col = lootColor(item);
const label = lootLabel(item);
const studs = Math.round(dist / STUD);
const alpha = Math.max(0.5, 1 - dist/2000);
const count = item.count > 1 ? ` x${item.count}` : '';
ctx.save();
ctx.globalAlpha = alpha;
// Box
const BOX = 30;
ctx.shadowColor = col;
ctx.shadowBlur = 8;
ctx.strokeStyle = col;
ctx.lineWidth = 2;
ctx.strokeRect(lx - BOX/2, ly - BOX/2, BOX, BOX);
ctx.globalAlpha = alpha * 0.12;
ctx.fillStyle = col;
ctx.fillRect(lx - BOX/2, ly - BOX/2, BOX, BOX);
ctx.globalAlpha = alpha;
ctx.shadowBlur = 0;
// Item name label above box
ctx.font = 'bold 11px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.strokeStyle = 'rgba(0,0,0,0.95)';
ctx.lineWidth = 4;
const txt = `${label}${count}`;
ctx.strokeText(txt, lx, ly - BOX/2 - 3);
ctx.fillStyle = col;
ctx.shadowColor = col;
ctx.shadowBlur = 4;
ctx.fillText(txt, lx, ly - BOX/2 - 3);
ctx.shadowBlur = 0;
// Distance below box
ctx.font = 'bold 9px monospace';
ctx.textBaseline = 'top';
ctx.strokeStyle = 'rgba(0,0,0,0.8)';
ctx.lineWidth = 2;
ctx.strokeText(`${studs}su`, lx, ly + BOX/2 + 3);
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.shadowBlur = 0;
ctx.fillText(`${studs}su`, lx, ly + BOX/2 + 3);
ctx.restore();
}
}
})();
setStatus('active', '#5bc470');
}
// ── CSS ───────────────────────────────────────────────────────
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}
@keyframes _hbBounce{from{transform:translateX(0)}to{transform:translateX(4px)}}
@keyframes _hbPulse{0%,100%{box-shadow:0 0 8px #7289da88}50%{box-shadow:0 0 18px #7289dacc,0 0 30px #4752c466}}
#_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;display:flex;align-items:center;justify-content:space-between}
._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}
._hbCopyHint{font-size:8px;color:#b8b4ac;margin-top:3px;letter-spacing:.5px}
`;
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">v3.2</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: T key | Magnet: E key (auto-moves when ≤6 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 class="_hbToggle yellow" id="_hbt-lootEsp"><span class="_hbToggleLbl">Loot ESP</span><div class="_hbPip"></div></div>
</div>
<div class="_hbNote">Loot: purple=tier3 gold=tier2 blue=tier1</div>
</div>
<div>
<div class="_hbSectionTitle">Grenades</div>
<div class="_hbGrid">
<div class="_hbToggle on yellow" id="_hbt-grenTimer"><span class="_hbToggleLbl">Nade Timer</span><div class="_hbPip"></div></div>
</div>
<div class="_hbNote">Cook timer while holding | Countdown + danger ring on thrown nades<br>Frag/Mirv: 4s | Smoke/Strobe: 3s | Potato: 2.5s</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>
<span class="_hbFootRight" style="display:flex;align-items:center;gap:4px">
<span style="font-size:14px;animation:_hbBounce 0.6s infinite alternate">👉</span>
<a class="_hbDiscord" href="https://discord.gg/7WgfQc4k" target="_blank" style="font-size:12px;font-weight:700;letter-spacing:1.5px;padding:3px 8px;background:linear-gradient(135deg,#4752c4,#7289da);-webkit-background-clip:unset;-webkit-text-fill-color:unset;background-clip:unset;color:#fff;border-radius:5px;text-decoration:none;box-shadow:0 0 10px #7289da88;animation:_hbPulse 1.5s infinite">DISCORD</a>
<span style="font-size:14px;animation:_hbBounce 0.6s infinite alternate 0.3s">👈</span>
</span>
<span class="_hbVer">v3.2</span>
</div>
</div>`;
document.body.appendChild(panel);
// ── Side setup panel ──────────────────────────────────────────
const sidePanel = document.createElement('div'); sidePanel.id = '_hbSidePanel';
sidePanel.innerHTML = `
<div class="_hbSideHeader">
<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">✕</button>
</div>
<div class="_hbSideBody">
<div class="_hbSideStep"><b>Step 1</b> — Press <b>F12</b> → <b>Sources</b> tab → <b>Snippets</b></div>
<div class="_hbSideStep">
<b>Step 2</b> — Create <b>Snippet 1</b> (debugger pause):<br><br>
<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>
</div>
<div class="_hbSideStep">
<b>Step 3</b> — Create <b>Snippet 2</b>:<br><br>
<div class="_hbSideCode" id="_hbSideSnip2">
<pre>window.__Re = Re</pre>
<div class="_hbCopyHint">▸ click to copy</div>
</div>
</div>
<div class="_hbSideStep">
<b>Step 4</b> — On the <b>loading screen</b>:<br><br>
1. Run <b>Snippet 1</b> — game will pause in debugger<br>
2. Run <b>Snippet 2</b> in the console<br>
3. Click <b>Resume ▶</b> to unpause<br><br>
Done ✓
</div>
<div class="_hbSideStep warn">⚠ Do this <b>every time</b> you open survev.io. After 1 game it stays — no need to redo unless you refresh.</div>
</div>`;
document.body.appendChild(sidePanel);
document.getElementById('_hbSideClose').onclick = () => { sidePanel.classList.remove('_vis'); sidePanel._dismissed = true; };
document.getElementById('_hbSideSnip1').onclick = function() {
navigator.clipboard.writeText('debug(getEventListeners(window).resize[0].listener);\nwindow.dispatchEvent(new Event(\'resize\'));').then(() => {
const h = this.querySelector('._hbCopyHint');
if (h) { const o=h.textContent; h.textContent='✓ copied!'; h.style.color='#5bc470'; setTimeout(()=>{h.textContent=o;h.style.color='';},1500); }
}).catch(()=>{});
};
document.getElementById('_hbSideSnip2').onclick = function() {
navigator.clipboard.writeText('window.__Re = Re').then(() => {
const h = this.querySelector('._hbCopyHint');
if (h) { const o=h.textContent; h.textContent='✓ copied!'; h.style.color='#5bc470'; setTimeout(()=>{h.textContent=o;h.style.color='';},1500); }
}).catch(()=>{});
};
function positionSide() {
const r = panel.getBoundingClientRect();
const sw = 260, margin = 10;
sidePanel.style.left = (r.right + margin + sw > window.innerWidth)
? (r.left - margin - sw) + 'px' : (r.right + margin) + 'px';
sidePanel.style.top = r.top + 'px';
}
window._hbPosSide = positionSide;
// ── Build game texture lookup cache from PIXI spritesheets ────
// Called once after game loads to cache weapon texture refs
window._hbTexCache = null;
window._hbBuildTexCache = function() {
if (window._hbTexCache) return;
try {
window._hbTexCache = {};
const atlases = window.__Re?.resourceManager?.atlases;
if (!atlases) return;
// Weapons are in loadout + main atlases. Format: gun-ak47-01.img
for (const atlas of Object.values(atlases)) {
const sheets = atlas?.spritesheets;
if (!sheets) continue;
const arr = Array.isArray(sheets) ? sheets : Object.values(sheets);
for (const sheet of arr) {
const textures = sheet?.textures;
if (!textures) continue;
for (const [name, tex] of Object.entries(textures)) {
window._hbTexCache[name.toLowerCase()] = tex;
}
}
}
} catch(e) {}
};
// Map weapon id to loadout texture name pattern: gun-{id}-01.img
window._hbGetWeapTex = function(weapId) {
const cache = window._hbTexCache;
if (!cache) return null;
const id = weapId.toLowerCase().replace(/ /g,'-').replace(/_/g,'-');
// Try patterns in order of likelihood
const tries = [
`gun-${id}-01.img`,
`gun-${id}-02.img`,
`loot-weapon-${id}-01.img`,
`loot-${id}-01.img`,
`loot-melee-${id}.img`,
`loot-melee-${id}-01.img`,
`${id}-01.img`,
`${id}.img`,
];
for (const t of tries) {
const tex = cache[t];
if (tex) return tex;
}
// fuzzy: find any key containing the id
for (const [k, v] of Object.entries(cache)) {
if (k.includes(id) && (k.startsWith('gun-') || k.startsWith('loot-weapon') || k.startsWith('loot-melee'))) return v;
}
return null;
};
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',
'_hbt-lootEsp':'lootEsp','_hbt-grenTimer':'grenTimer'
})) 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 id = {Esp:'_hbt-Esp',names:'_hbt-names',healthBars:'_hbt-healthBars',lockOn:'_hbt-lockOn',magnet:'_hbt-magnet',xray:'_hbt-xray',lootEsp:'_hbt-lootEsp',grenTimer:'_hbt-grenTimer'}[key];
const el = document.getElementById(id);
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 && !sp._dismissed) { if(window._hbPosSide) window._hbPosSide(); sp.classList.add('_vis'); }
window._hbBuildTexCache?.();
}
function closePanel() {
const p=document.getElementById('_hbPanel'); if(p) p.style.display='none'; isPanelVisible=false;
const sp=document.getElementById('_hbSidePanel'); if(sp && !sp._dismissed) 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==='t'||e.key==='T') flipToggle('lockOn');
if (e.key==='e'||e.key==='E') flipToggle('magnet');
}, 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... __Re:${!!window.__Re}`);
if (tries>240) {
clearInterval(poll);
setStatus('needs setup','#c05050');
console.warn('[VoidBacon] Could not auto-capture. Run in console:\n window.__Re = <App object>');
}
}, 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});
});
})();