// ==UserScript==
// @name Evades.io - NipachuMod
// @namespace https://evades.io/
// @version 1.0.0
// @description HUD (H): Zoom+Reset, Anti-AFK, Tracers, Time Travel (-2.24s projected), Rainbow Aura (slider), Avoid (clearance < 25), Invites highlighter, LB/Chat/ChatH toggles, Region filter, Tryhard toggle
// @match https://evades.io/*
// @match https://*.evades.io/*
// @run-at document-end
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// ==/UserScript==
(() => {
'use strict';
// ---------- Styles ----------
GM_addStyle(`
@keyframes nmRainbow{0%{color:red;}16.666%{color:orange;}33.333%{color:yellow;}50%{color:green;}66.666%{color:blue;}83.333%{color:indigo;}100%{color:violet;}}
#e_hud{position:fixed;left:28px;top:28px;z-index:2147483647;background:rgba(18,18,18,.94);color:#fff;padding:12px;border-radius:10px;border:1px solid #585858;box-shadow:0 8px 22px rgba(0,0,0,.55);font:14px system-ui,Segoe UI,Roboto,Arial;user-select:none;min-width:560px;display:none}
#e_hud .hdr{cursor:move;display:flex;gap:10px;align-items:center;margin-bottom:10px}
#e_hud .hdr .title{font-weight:700;font-size:18px;animation:nmRainbow 4s linear infinite}
#e_hud .sub{font-size:11px;color:#aaa;margin-left:8px}
#e_hud .row{display:flex;gap:8px;align-items:center}
#e_hud .row.wrap{flex-wrap:wrap}
#e_hud .section{border-top:1px solid #2f2f2f;margin:10px 0}
.nm-btn{background:#030303;border:1px solid #2a2a2a;color:#fff;padding:6px 10px;border-radius:8px;cursor:pointer;transition:background .2s,transform .06s}
.nm-btn:hover{background:#121212}.nm-btn:active{transform:translateY(1px)}.nm-btn:disabled{opacity:.6;cursor:default}
#e_hud input[type="range"]{flex:1;height:6px;background:#4a5568;border-radius:999px;accent-color:#63b3ed}
#e_hud input[type="text"]{flex:1;min-width:180px;background:#0e0e0e;border:1px solid #2a2a2a;color:#fff;padding:6px 8px;border-radius:8px}
#nm_changelog{width:100%;max-height:220px;color:#fff;border:1px solid #585858;border-radius:5px;overflow:auto;background:rgba(0,0,0,.55);padding:10px;display:none}
#nm_changelog h3{margin:0 0 6px;font-weight:700;font-size:16px}
#nm_changelog .entry{font-size:13px;color:#ddd;margin:6px 0}
canvas#nm_tracer{position:fixed;left:0;top:0;pointer-events:none;z-index:2147483646}
/* Invite chips + leaderboard glow */
.nm-chip{display:inline-flex;align-items:center;gap:6px;background:#111;border:1px solid #3a3a3a;border-radius:999px;padding:2px 8px;margin:2px 4px 0 0}
.nm-chip .x{cursor:pointer;color:#bbb}
.nm-lb-glow{box-shadow:0 0 12px rgba(0,200,255,.8); outline:1px solid rgba(0,200,255,.7)}
`);
// ---------- Storage / defaults ----------
const STORE='evades_nipachumod', POSL='hud_left', POST='hud_top';
let hudLeft=num(GM_getValue(POSL,28),28), hudTop=num(GM_getValue(POST,28),28);
const PRESETS_LB=[0.8,1.0,1.2,1.4], PRESETS_CHAT=[0.8,1.0,1.2,1.4], PRESETS_CH=[150,200,260,320];
const defaults={
leaderboardScale:1.0, chatScale:1.0, chatHeight:200, hideUI:false, filterEnabled:false,
zoom:1.0, antiAfk:false, antiAfkSeconds:45,
tracersEnabled:true, tracerLine:2, tracerShowDist:true, tracerFont:'12px Arial',
tracerLabelOffsetX:4, tracerLabelOffsetY:-4, tracerFallbackRadius:20,
ttiEnabled:true,
auraEnabled:true, auraRadius:200,
showChangelog:true,
avoidEnabled:false,
invitesEnabled:true,
invites:[] // array of names (string)
};
let st=load(STORE,defaults);
// ---------- Game access helpers ----------
function getCamera(){
const q=document.querySelector('div.quests-launcher'); if(!q) return null;
const rk=Object.keys(q).find(k=>k.startsWith('__reactFiber$'));
return rk ? q[rk]?.memoizedProps?.children?._owner?.stateNode?.renderer?.camera : null;
}
function refs(){
const el=document.querySelector('div.quests-launcher'); if(!el) return null;
const fb=Object.keys(el).find(k=>k.startsWith('__reactFiber$'));
const sn=el[fb]?.memoizedProps?.children?._owner?.stateNode; if(!sn) return null;
const g=sn.gameState, cam=sn.renderer?.camera, me=g?.areaInfo?.self?.entity; if(!(g?.entities&&me&&cam)) return null;
return {g,cam,me};
}
function refsLight(){
const el=document.querySelector('div.quests-launcher'); if(!el) return null;
const fb=Object.keys(el).find(k=>k.startsWith('__reactFiber$'));
const sn=el[fb]?.memoizedProps?.children?._owner?.stateNode; if(!sn) return null;
const g=sn.gameState, me=g?.areaInfo?.self?.entity; if(!(g?.entities&&me)) return null; return {g,me};
}
// ---------- Zoom ----------
function setZoom(v){ const cam=getCamera(); if(cam) cam.scale=v; }
setInterval(()=>{ if(document.querySelector('canvas')) setZoom(clamp(st.zoom,0.1,2)); },500);
// ---------- Anti-AFK ----------
let antiTimer=null;
function startAntiAfk(){ stopAntiAfk(); antiTimer=setInterval(()=>{ tapShift(); wiggleCanvas(); }, Math.max(5, st.antiAfkSeconds|0)*1000); }
function stopAntiAfk(){ if(antiTimer){ clearInterval(antiTimer); antiTimer=null; } }
function applyAntiAfk(){ st.antiAfk?startAntiAfk():stopAntiAfk(); }
function tapShift(){ const ke=(type)=>new KeyboardEvent(type,{key:'Shift',code:'ShiftLeft',bubbles:true}); [window,document,document.body].forEach(t=>{t.dispatchEvent(ke('keydown'));t.dispatchEvent(ke('keyup'));}); }
function wiggleCanvas(){ const c=document.querySelector('canvas'); if(!c) return; const r=c.getBoundingClientRect(); const x=(r.left+r.width/2)|0, y=(r.top+r.height/2)|0; c.dispatchEvent(new MouseEvent('mousemove',{clientX:x+1,clientY:y,bubbles:true})); c.dispatchEvent(new MouseEvent('mousemove',{clientX:x,clientY:y,bubbles:true})); }
// ---------- Overlay canvas ----------
let dpr=window.devicePixelRatio||1, tcv=null, tctx=null, lastW=0,lastH=0;
function ensureCanvas(){
if(!tcv){ tcv=document.createElement('canvas'); tcv.id='nm_tracer'; document.documentElement.appendChild(tcv); tctx=tcv.getContext('2d'); }
const w=window.innerWidth,h=window.innerHeight;
if(w!==lastW||h!==lastH){ lastW=w; lastH=h; tcv.style.width=w+'px'; tcv.style.height=h+'px'; tcv.width=Math.floor(w*dpr); tcv.height=Math.floor(h*dpr); tctx.setTransform(dpr,0,0,dpr,0,0); }
}
function clearOverlay(){ if(!tcv||!tctx) return; tctx.clearRect(0,0,tcv.width/dpr,tcv.height/dpr); }
// ---------- Time Travel Indicator (projected) ----------
const TTI_MS=2240, SELF_MAX_MS=6000, SELF_BUF_CAP=1200;
const selfBuf=[]; let lastSelf=null;
function pushSelfSample(t,x,y){
if(lastSelf){
const jump=Math.hypot(x-lastSelf.x,y-lastSelf.y);
if(jump>1200) selfBuf.length=0; // ignore big teleports
}
selfBuf.push({t,x,y}); lastSelf={t,x,y};
const cut=t-SELF_MAX_MS;
while(selfBuf.length&&selfBuf[0].t<cut) selfBuf.shift();
if(selfBuf.length>SELF_BUF_CAP) selfBuf.splice(0,selfBuf.length-SELF_BUF_CAP);
}
function findIdxAt(time){ let lo=0,hi=selfBuf.length-1,ans=-1; while(lo<=hi){ const m=(lo+hi)>>1; if(selfBuf[m].t<=time){ ans=m; lo=m+1;} else hi=m-1;} return ans; }
function sampleSelfAt(time){
if(!selfBuf.length) return null;
if(time<=selfBuf[0].t) return {x:selfBuf[0].x,y:selfBuf[0].y};
if(time>=selfBuf[selfBuf.length-1].t) return {x:selfBuf[selfBuf.length-1].x,y:selfBuf[selfBuf.length-1].y};
const i=findIdxAt(time); if(i<0||i>=selfBuf.length-1) return null;
const A=selfBuf[i],B=selfBuf[i+1]; const dt=Math.max(1,B.t-A.t); const u=(time-A.t)/dt;
return {x:A.x+(B.x-A.x)*u,y:A.y+(B.y-A.y)*u};
}
function estimatePastSelf(now){
const n=selfBuf.length; if(n<4) return null;
const latest=selfBuf[n-1];
const refTime=now-180; // recent window for stable velocity
const ref=sampleSelfAt(refTime);
if(!ref) return sampleSelfAt(now-TTI_MS);
const dt=Math.max(1,latest.t-refTime);
const vx=(latest.x-ref.x)/dt, vy=(latest.y-ref.y)/dt;
const speed=Math.hypot(vx,vy);
if(!Number.isFinite(speed)||speed>5) return sampleSelfAt(now-TTI_MS);
return {x:latest.x-vx*TTI_MS, y:latest.y-vy*TTI_MS};
}
// ---------- Threat detection ----------
function isThreatLike(e, me){
if(!e) return false;
if(e.id === me.id) return false;
if(e.dead || e.removed) return false;
if(e.isItem || e.collectible) return false;
const r = typeof e.radius === 'number' ? e.radius : 0;
if(r <= 2) return false; // tiny dots
if (e.isEnemy || e.isHazard || e.hazard || e.isProjectile) return true;
if (typeof e.damage === 'number' && e.damage > 0) return true;
if (typeof e.damageRadius === 'number' && e.damageRadius > 0) return true;
return true; // treat unknown solids as threat
}
// ---------- Invite helpers ----------
function norm(s){ return (s||'').trim().toLowerCase(); }
function invitedSet(){ const set=new Set(); for(const n of st.invites) set.add(norm(n)); return set; }
function entityName(e){ return typeof e.name === 'string' ? e.name : (e.username || e.playerName || null); }
// best-effort glow in leaderboard DOM
function applyInviteDomHighlights(){
const lb=document.getElementById('leaderboard'); if(!lb) return;
const L = invitedSet();
for(const node of lb.children){
const t=(node.textContent||'').trim().toLowerCase();
node.classList.toggle('nm-lb-glow', L.size && [...L].some(n=>t.includes(n)));
}
}
setInterval(applyInviteDomHighlights, 1000);
// ---------- Rendering + main loop ----------
function draw(){
ensureCanvas();
const ctx=tctx; if(!ctx) return;
const R=refs();
const W=tcv.width/dpr,H=tcv.height/dpr;
ctx.clearRect(0,0,W,H);
if(!R) return;
const {g,cam,me}=R;
const cvs=document.querySelector('canvas'); if(!cvs) return;
const rect=cvs.getBoundingClientRect();
const offX=rect.left, offY=rect.top;
const scale=(typeof cam.originalGameScale==='number')?cam.originalGameScale:(cam.scale||1);
const left=cam.left, top=cam.top;
const cx=offX+rect.width/2, cy=offY+rect.height/2;
const now=performance.now();
// record your history
pushSelfSample(now, me.x, me.y);
ctx.save();
ctx.font=st.tracerFont;
// Aura (soft radial + rainbow ring)
if(st.auraEnabled){
const sx=offX+(me.x-left)*scale, sy=offY+(me.y-top)*scale, r=Math.max(4, st.auraRadius*scale);
const rg=ctx.createRadialGradient(sx,sy,0,sx,sy,r); rg.addColorStop(0,'rgba(255,255,255,0.06)'); rg.addColorStop(1,'rgba(255,255,255,0)');
ctx.fillStyle=rg; ctx.beginPath(); ctx.arc(sx,sy,r,0,Math.PI*2); ctx.fill();
if(ctx.createConicGradient){ const cg=ctx.createConicGradient(0,sx,sy);
cg.addColorStop(0/6,'#ff0000'); cg.addColorStop(1/6,'#ffa500'); cg.addColorStop(2/6,'#ffff00'); cg.addColorStop(3/6,'#00ff00'); cg.addColorStop(4/6,'#0000ff'); cg.addColorStop(5/6,'#4b0082'); cg.addColorStop(6/6,'#ff00ff'); ctx.strokeStyle=cg;
} else ctx.strokeStyle='#fff';
ctx.lineWidth=Math.max(2,r*0.02); ctx.beginPath(); ctx.arc(sx,sy,r,0,Math.PI*2); ctx.stroke();
}
// Tracers to threats
if(st.tracersEnabled){
for(const e of Object.values(g.entities)){
if(!isThreatLike(e,me)) continue;
const sx=offX+(e.x-left)*scale, sy=offY+(e.y-top)*scale; const rr=(typeof e.radius==='number'?e.radius:20)*scale;
ctx.lineWidth=st.tracerLine; ctx.strokeStyle=e.color||'#ff0';
const a=Math.atan2(sy-cy,sx-cx), ax=sx-Math.cos(a)*rr, ay=sy-Math.sin(a)*rr;
ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(ax,ay); ctx.stroke();
if(st.tracerShowDist){
ctx.fillStyle=e.color||'#ff0';
const d=Math.hypot(e.x-me.x,e.y-me.y);
ctx.fillText(Math.round(d), ax+st.tracerLabelOffsetX, ay+st.tracerLabelOffsetY);
}
}
}
// Invited players highlight
if(st.invitesEnabled && st.invites.length){
const L = invitedSet();
for(const e of Object.values(g.entities)){
const nm = entityName(e);
if(!nm) continue;
if(!L.has(norm(nm))) continue;
// world -> screen
const sx=offX+(e.x-left)*scale, sy=offY+(e.y-top)*scale;
const r=(Math.max(10, (e.radius||10))*scale);
const pulse = 0.5 + 0.5*Math.sin(now/220); // 0..1
const c1 = `rgba(0,200,255,${0.25+0.35*pulse})`;
const c2 = `rgba(255,0,200,${0.25+0.35*(1-pulse)})`;
// dual ring
ctx.lineWidth = 2 + 2*pulse;
ctx.strokeStyle = c1;
ctx.beginPath(); ctx.arc(sx,sy, r+10+6*pulse, 0, Math.PI*2); ctx.stroke();
ctx.strokeStyle = c2;
ctx.beginPath(); ctx.arc(sx,sy, r+18+6*(1-pulse), 0, Math.PI*2); ctx.stroke();
// label
ctx.fillStyle='rgba(0,0,0,0.65)';
ctx.fillRect(sx-30, sy-(r+34), 60, 18);
ctx.strokeStyle='rgba(0,200,255,0.9)';
ctx.strokeRect(sx-30, sy-(r+34), 60, 18);
ctx.fillStyle='#8ff';
ctx.textAlign='center';
ctx.textBaseline='middle';
ctx.font='12px monospace';
ctx.fillText('INVITE', sx, sy-(r+25));
}
}
// Time travel indicator (your -2.24s projected position)
if(st.ttiEnabled){
const ghost=estimatePastSelf(now);
if(ghost){
const px=offX+(ghost.x-left)*scale, py=offY+(ghost.y-top)*scale;
ctx.strokeStyle='rgba(80,200,255,0.9)'; ctx.fillStyle='rgba(80,200,255,0.25)';
ctx.beginPath(); ctx.arc(px,py,8,0,Math.PI*2); ctx.fill(); ctx.stroke();
const sx=offX+(me.x-left)*scale, sy=offY+(me.y-top)*scale;
ctx.setLineDash([6,6]); ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(sx,sy); ctx.stroke(); ctx.setLineDash([]);
}
}
ctx.restore();
}
function mainLoop(){
requestAnimationFrame(mainLoop);
if(!st.tracersEnabled && !st.ttiEnabled && !st.auraEnabled && !(st.invitesEnabled && st.invites.length)){ clearOverlay(); return; }
try{ draw(); }catch{}
try{ avoidanceTick(); }catch{}
}
requestAnimationFrame(mainLoop);
// ---------- Avoidance (clearance-based, nearest threat) ----------
const AVOID_CLEARANCE_THRESHOLD = 25; // how close is "too close" after subtracting radii
const held={w:false,a:false,s:false,d:false};
function sendKey(code,type){
const key=code.replace(/^Key/,'').toUpperCase();
const ev=new KeyboardEvent(type,{key,code,keyCode:key.charCodeAt(0),which:key.charCodeAt(0),bubbles:true});
const targets=[document.activeElement, document.querySelector('canvas'), document.body, document, window].filter(Boolean);
for(const t of targets) t.dispatchEvent(ev);
}
function pressKey(k,down){
if(held[k]===down && down){ sendKey({w:'KeyW',a:'KeyA',s:'KeyS',d:'KeyD'}[k],'keydown'); return; }
if(held[k]===down) return;
held[k]=down; const code={w:'KeyW',a:'KeyA',s:'KeyS',d:'KeyD'}[k]; if(!code) return;
sendKey(code, down?'keydown':'keyup');
}
function releaseAll(){ ['w','a','s','d'].forEach(k=>pressKey(k,false)); }
function uiActive(){ const el=document.activeElement; return el && /^(input|textarea)$/i.test(el.tagName); }
function refsLightSafe(){
try { return refsLight(); } catch { return null; }
}
function avoidanceTick(){
if(!st.avoidEnabled){ releaseAll(); return; }
if(uiActive()){ releaseAll(); return; }
const R=refsLightSafe(); if(!R){ releaseAll(); return; }
const {g,me}=R;
const myR = typeof me.radius==='number' ? me.radius : 8;
let nearest=null, bestClearance=Infinity, bestD=Infinity;
for(const e of Object.values(g.entities)){
if(!isThreatLike(e,me)) continue;
const ex=e.x, ey=e.y;
const d = Math.hypot(me.x-ex, me.y-ey);
const er = typeof e.radius==='number' ? e.radius : 0;
const clearance = d - (myR + er);
if (clearance <= AVOID_CLEARANCE_THRESHOLD) {
if (clearance < bestClearance || (clearance === bestClearance && d < bestD)) {
bestClearance = clearance; bestD = d; nearest = e;
}
}
}
if(!nearest){ releaseAll(); return; }
// Move away from the nearest threat
const dx = me.x - nearest.x;
const dy = me.y - nearest.y;
const m = Math.hypot(dx, dy) || 1;
const nx = dx / m;
const ny = dy / m;
const thr = 0.03;
if(Math.abs(nx)>thr){
if(nx>0){ pressKey('d',true); pressKey('a',false);} else { pressKey('a',true); pressKey('d',false); }
} else { pressKey('a',false); pressKey('d',false); }
if(Math.abs(ny)>thr){
if(ny>0){ pressKey('s',true); pressKey('w',false);} else { pressKey('w',true); pressKey('s',false); }
} else { pressKey('w',false); pressKey('s',false); }
}
// ---------- HUD ----------
let hud;
function buildHUD(){
hud=document.createElement('div'); hud.id='e_hud'; hud.style.left=hudLeft+'px'; hud.style.top=hudTop+'px';
hud.innerHTML=`
<div class="hdr"><span class="title">NipachuMod</span><span class="sub">Press H to toggle HUD</span></div>
<!-- Zoom -->
<div class="row" style="margin-bottom:6px">
<span class="sub">Zoom</span>
<button class="nm-btn" id="zDec">-</button>
<input id="zRange" type="range" min="0.1" max="2" step="0.01">
<button class="nm-btn" id="zInc">+</button>
<button class="nm-btn" id="zReset">Reset</button>
</div>
<div class="section"></div>
<!-- Anti-AFK -->
<div class="row"><button class="nm-btn" id="afkToggle"></button></div>
<div class="section"></div>
<!-- Tracers + TimeTravel -->
<div class="row wrap" style="margin-bottom:6px">
<button class="nm-btn" id="trToggle"></button>
<button class="nm-btn" id="trDist"></button>
<button class="nm-btn" id="ttiToggle"></button><span class="sub">(you -2.24s)</span>
</div>
<div class="section"></div>
<!-- Aura -->
<div class="row wrap" style="margin-bottom:6px">
<button class="nm-btn" id="auraToggle"></button>
<span class="sub">Aura</span>
<input id="auraRange" type="range" min="20" max="600" step="5" style="width:240px">
<span id="auraVal" class="sub"></span>
</div>
<div class="section"></div>
<!-- Avoid -->
<div class="row wrap" style="margin-bottom:6px">
<button class="nm-btn" id="avoidToggle"></button>
<span class="sub">Clearance threshold: 25</span>
</div>
<div class="section"></div>
<!-- Invites -->
<div class="row wrap" style="margin-bottom:6px">
<button class="nm-btn" id="invToggle"></button>
<input id="invInput" type="text" placeholder="Player name (case-insensitive)">
<button class="nm-btn" id="invAdd">+ Add</button>
<div id="invChips" class="row wrap" style="margin-top:6px"></div>
</div>
<div class="section"></div>
<!-- UI tweaks -->
<div class="row wrap" style="margin-bottom:6px">
<button class="nm-btn" id="lbCycle">LB 1.00x</button>
<button class="nm-btn" id="chatCycle">Chat 1.00x</button>
<button class="nm-btn" id="chCycle">ChatH 200</button>
<button class="nm-btn" id="filterRegion">Filter My Region</button>
<button class="nm-btn" id="tryhardBtn">Tryhard</button>
<button class="nm-btn" id="toggleChangelog">Changelog</button>
</div>
<div id="nm_changelog">
<h3>Changelog</h3>
<div class="entry">v1.0.0 - Invites highlighter (pulsing ring + label), Avoid uses clearance < 25 vs nearest threat, projected TimeTravel, Aura slider, Tryhard hides icons+leaderboard+chat.</div>
</div>
`;
document.documentElement.appendChild(hud);
drag(hud, hud.querySelector('.hdr'));
// Zoom controls
const zRange=gid('zRange');
const applyZoom=v=>{
const vv=clamp(parseFloat(v)||1,0.1,2);
st.zoom=vv; save(STORE,st); setZoom(vv);
};
zRange.value=(st.zoom||1).toFixed(2);
zRange.addEventListener('input',()=>applyZoom(zRange.value));
gid('zDec').onclick=()=>applyZoom((parseFloat(zRange.value)-0.01).toFixed(2));
gid('zInc').onclick=()=>applyZoom((parseFloat(zRange.value)+0.01).toFixed(2));
gid('zReset').onclick=()=>applyZoom(1.00);
// Anti-AFK
const afkBtn=gid('afkToggle');
const paintAfk=()=>{ afkBtn.textContent=st.antiAfk?'Anti-AFK: On':'Anti-AFK: Off'; };
paintAfk();
afkBtn.onclick=()=>{
st.antiAfk=!st.antiAfk; save(STORE,st); paintAfk(); applyAntiAfk();
};
applyAntiAfk();
// Tracers
const paintTr=()=>{
gid('trToggle').textContent=st.tracersEnabled?'Tracers: On':'Tracers: Off';
gid('trDist').textContent='Distance: ' + (st.tracerShowDist?'On':'Off');
};
paintTr();
gid('trToggle').onclick=()=>{
st.tracersEnabled=!st.tracersEnabled; save(STORE,st); paintTr();
if(!st.tracersEnabled && !st.ttiEnabled && !st.auraEnabled && !(st.invitesEnabled && st.invites.length)) clearOverlay();
};
gid('trDist').onclick=()=>{
st.tracerShowDist=!st.tracerShowDist; save(STORE,st); paintTr();
};
// Time travel
const ttiBtn=gid('ttiToggle');
const paintTTI=()=>{ ttiBtn.textContent=st.ttiEnabled?'TimeTravel: On':'TimeTravel: Off'; };
paintTTI();
ttiBtn.onclick=()=>{
st.ttiEnabled=!st.ttiEnabled; save(STORE,st); paintTTI();
if(!st.tracersEnabled && !st.ttiEnabled && !st.auraEnabled && !(st.invitesEnabled && st.invites.length)) clearOverlay();
};
// Aura
const auraRange=gid('auraRange'), auraVal=gid('auraVal');
const aurBtn=gid('auraToggle');
const paintAuraVals=()=>{ auraRange.value=st.auraRadius; auraVal.textContent=String(st.auraRadius); };
const paintAura=()=>{ aurBtn.textContent = st.auraEnabled ? 'Aura: On' : 'Aura: Off'; };
paintAuraVals(); paintAura();
aurBtn.onclick=()=>{
st.auraEnabled=!st.auraEnabled; save(STORE,st); paintAura();
if(!st.tracersEnabled && !st.ttiEnabled && !st.auraEnabled && !(st.invitesEnabled && st.invites.length)) clearOverlay();
};
auraRange.addEventListener('input',()=>{
st.auraRadius=Math.max(20, Math.min(600, parseInt(auraRange.value,10)||200));
paintAuraVals(); save(STORE,st);
});
// Avoid
const avT=gid('avoidToggle');
const paintAvoid=()=>{ avT.textContent=st.avoidEnabled?'Avoid: On':'Avoid: Off'; };
paintAvoid();
avT.onclick=()=>{
st.avoidEnabled=!st.avoidEnabled; save(STORE,st); paintAvoid();
if(!st.avoidEnabled) releaseAll();
};
// Invites
const invBtn = gid('invToggle');
const invInput = gid('invInput');
const invAdd = gid('invAdd');
const invChips = gid('invChips');
const paintInvToggle = () => { invBtn.textContent = st.invitesEnabled ? 'Invites: On' : 'Invites: Off'; };
const paintChips = () => {
invChips.innerHTML = '';
st.invites.forEach((name, idx) => {
const chip = document.createElement('div');
chip.className = 'nm-chip';
chip.innerHTML = `<span>${escapeHtml(name)}</span><span class="x" title="Remove">✖</span>`;
chip.querySelector('.x').onclick = () => {
st.invites.splice(idx,1); save(STORE,st); paintChips(); applyInviteDomHighlights();
};
invChips.appendChild(chip);
});
};
function addInvite(name){
name = (name||'').trim();
if(!name) return;
if(!st.invites.some(n=>norm(n)===norm(name))){
st.invites.push(name);
save(STORE,st);
paintChips();
applyInviteDomHighlights();
}
invInput.value='';
}
paintInvToggle();
paintChips();
invBtn.onclick = () => {
st.invitesEnabled = !st.invitesEnabled;
save(STORE,st);
paintInvToggle();
if(!st.tracersEnabled && !st.ttiEnabled && !st.auraEnabled && !(st.invitesEnabled && st.invites.length)) clearOverlay();
};
invAdd.onclick = () => addInvite(invInput.value);
invInput.addEventListener('keydown', e => { if(e.key==='Enter') addInvite(invInput.value); });
// LB/Chat/ChatH cycles
const lbBtn=gid('lbCycle'), chatBtn=gid('chatCycle'), chBtn=gid('chCycle');
const cycle=(arr,cur)=>arr[(Math.max(0,arr.findIndex(v=>Math.abs(v-cur)<1e-6))+1)%arr.length];
const paintLB=()=>{ lbBtn.textContent=`LB ${st.leaderboardScale.toFixed(2)}x`; };
const paintChat=()=>{ chatBtn.textContent=`Chat ${st.chatScale.toFixed(2)}x`; };
const paintCH=()=>{ chBtn.textContent=`ChatH ${st.chatHeight}`; };
const applyScales=()=>{
const lb=document.getElementById('leaderboard'), ch=document.getElementById('chat');
if(lb) lb.style.zoom=st.leaderboardScale===1?'':st.leaderboardScale;
if(ch) ch.style.zoom=st.chatScale===1?'':st.chatScale;
applyChatHeight(st.chatHeight);
};
lbBtn.onclick=()=>{ st.leaderboardScale=cycle(PRESETS_LB,st.leaderboardScale); save(STORE,st); paintLB(); applyScales(); };
chatBtn.onclick=()=>{ st.chatScale=cycle(PRESETS_CHAT,st.chatScale); save(STORE,st); paintChat(); applyScales(); };
chBtn.onclick=()=>{ st.chatHeight=cycle(PRESETS_CH,st.chatHeight); save(STORE,st); paintCH(); applyScales(); };
paintLB(); paintChat(); paintCH(); applyScales();
// Region filter toggle
gid('filterRegion').onclick = toggleRegionFilter;
// Tryhard
const tryBtn=gid('tryhardBtn');
const paintTry=()=>{ tryBtn.textContent=st.hideUI?'Tryhard: On':'Tryhard: Off'; };
tryBtn.onclick=()=>{ st.hideUI=!st.hideUI; save(STORE,st); applyUIVisibility(); paintTry(); };
paintTry();
// Changelog panel
const clBtn=gid('toggleChangelog'), clBox=gid('nm_changelog');
clBtn.onclick=()=>{ st.showChangelog=!st.showChangelog; save(STORE,st); clBox.style.display=st.showChangelog?'block':'none'; };
clBox.style.display=st.showChangelog?'block':'none';
applyUIVisibility();
if(st.filterEnabled) startRegionObserver();
}
// HUD toggle
window.addEventListener('keydown', e=>{
if((e.key||'').toLowerCase()==='h' && !e.repeat){
const t=document.activeElement && /^(input|textarea)$/i.test(document.activeElement.tagName); if(t) return;
if(!hud) buildHUD();
hud.style.display=(hud.style.display==='none'||!hud.style.display)?'block':'none';
}
}, true);
// ---------- UI/region-filter utilities ----------
let leaderboard=null, chatBox=null, regionMO=null;
const uiSelectors=['.settings-launcher','.quests-launcher','.mod-tools-launcher','#leaderboard','#chat'];
function refreshRefs(){ const lb=document.getElementById('leaderboard'); const ch=document.getElementById('chat'); if(lb) leaderboard=lb; if(ch) chatBox=ch; }
function applyChatHeight(h){ const win=document.getElementById('chat-window'); const input=document.getElementById('chat-input'); refreshRefs(); if(!chatBox||!win||!input) return; chatBox.style.height=h+'px'; win.style.height=(h-10)+'px'; input.style.top=h+'px'; }
function applyUIVisibility(){ uiSelectors.forEach(s=>{ document.querySelectorAll(s).forEach(el=>{ el.style.display=st.hideUI?'none':''; el.style.pointerEvents=st.hideUI?'none':''; }); }); }
function toggleRegionFilter(){ st.filterEnabled=!st.filterEnabled; save(STORE,st); if(st.filterEnabled) startRegionObserver(); else { stopRegionObserver(); showFullLB(); } }
function startRegionObserver(){ refreshRefs(); if(!leaderboard) return; if(regionMO) regionMO.disconnect(); regionMO=new MutationObserver(()=>filterLB()); regionMO.observe(leaderboard,{childList:true,subtree:true}); filterLB(); }
function stopRegionObserver(){ regionMO&®ionMO.disconnect(); regionMO=null; }
function myRegion(){ refreshRefs(); if(!leaderboard) return null; let cur=null,my=null; for(const ch of leaderboard.children){ if(ch.classList?.contains('leaderboard-title-break')) cur=ch.textContent.trim(); else if(ch.querySelector('b,strong')){ my=cur; break; } } return my; }
function filterLB(){ refreshRefs(); if(!leaderboard) return; const mr=myRegion(); if(!mr) return; let inReg=false; for(const c of leaderboard.children){ if(c.classList?.contains('leaderboard-title-break')){ const nm=c.textContent.trim(); inReg=(mr==='Ancient Abyss')?(nm==='Ancient Abyss'||nm==='Vast Void'):(nm.toLowerCase()===mr.toLowerCase()); c.style.display=inReg?'':'none'; } else c.style.display=inReg?'':'none'; } }
function showFullLB() {
refreshRefs();
if (!leaderboard) return;
const els = Array.from(leaderboard.children);
els.forEach(c => { c.style.display = ''; });
}
// ---------- tiny helpers ----------
function gid(id){ return document.getElementById(id); }
function drag(el,handle){ let go=false,ox=0,oy=0; handle.addEventListener('mousedown',e=>{ go=true; ox=e.clientX-el.offsetLeft; oy=e.clientY-el.offsetTop; e.preventDefault(); }); document.addEventListener('mouseup',()=>{ if(!go) return; go=false; hudLeft=el.offsetLeft; hudTop=el.offsetTop; GM_setValue(POSL,hudLeft); GM_setValue(POST,hudTop); }); document.addEventListener('mousemove',e=>{ if(!go) return; el.style.left=(e.clientX-ox)+'px'; el.style.top=(e.clientY-oy)+'px'; }); }
function clamp(v,lo,hi){ return Math.max(lo, Math.min(hi,v)); }
function num(v,d){ const n=Number(v); return Number.isFinite(n)?n:d; }
function load(k,def){ try{ const raw=GM_getValue(k,null); return raw?{...def,...JSON.parse(raw)}:{...def}; }catch{ return {...def}; } }
function save(k,obj){ try{ GM_setValue(k, JSON.stringify(obj)); }catch{} }
function escapeHtml(s){ return s.replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }
})();