Shell Shockers Aimbot + ESP

shellshock.io aimbot. Hold RMB to snap to nearest enemy (with optional lead/prediction). Press V to toggle red wireframe ESP boxes + tracers (see through walls). Press ` (backtick) to show/hide the settings menu.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Shell Shockers Aimbot + ESP
// @namespace    https://greasyfork.org/users/1603179-caringjellyemu
// @version      2.1
// @author       caringjellyemu
// @license      GPL-3.0-only
// @match        https://shellshock.io/*
// @grant        unsafeWindow
// @run-at       document-start
// @require      https://cdn.jsdelivr.net/npm/[email protected]/babylon.min.js
// @description  shellshock.io aimbot. Hold RMB to snap to nearest enemy (with optional lead/prediction). Press V to toggle red wireframe ESP boxes + tracers (see through walls). Press ` (backtick) to show/hide the settings menu.
// @supportURL   https://greasyfork.org/users/1603179-caringjellyemu
// ==/UserScript==

/*
 * Shell Shockers Aimbot + ESP
 * Copyright (c) 2026 caringjellyemu. All rights reserved where applicable.
 *
 * Licensed under the GNU General Public License v3.0 only (GPL-3.0-only).
 * You are free to use, study, modify, and redistribute this software under
 * the terms of that license, PROVIDED THAT:
 *   1. This copyright notice and the attribution below are preserved intact
 *      in all copies and derivative works.
 *   2. Any redistribution or derivative work remains licensed under
 *      GPL-3.0-only and discloses its complete corresponding source.
 *   3. The original author(s) are credited and not misrepresented as the
 *      author of an unmodified or trivially-modified copy.
 *
 * Removing or altering these notices, or republishing this work (in whole or
 * in part) under different authorship, violates the GPL-3.0 license and is
 * grounds for a takedown / attribution complaint.
 *
 * ── Attribution / lineage ──────────────────────────────────────────────────
 * This script is a derivative work. Portions originate from:
 *   • onlypuppy7 — "LibertyMutualV1" (GPL-3.0)
 *       https://github.com/onlypuppy7/LibertyMutualShellShockers/
 *   • StateFarm client — `predictPosition` lead/gravity algorithm (ported)
 * Original authorship of the modifications and additions herein:
 *   • caringjellyemu — 2026
 * ────────────────────────────────────────────────────────────────────────────
 */

(function () {
    'use strict';

    // ────────────────────────────────────────────────────────────────────────
    // STATE + SETTINGS
    // ────────────────────────────────────────────────────────────────────────
    let RMB = false;
    let H = {};
    const ss = {};

    // Per-frame snapshot. `hasLock` is true only on frames where RMB is held
    // and a valid enemy was picked — it drives the overlay's
    // "locked, idle / lead / waiting" state line. `me` is a cached reference
    // to the local player (used by the Nyx banner).
    const _aim = {
        hasLock: false,
        me: null,
    };
    const _pred = {
        enabled: false, active: false, airborne: false,
        speed: 0, t: 0, leadDist: 0, projSpeed: 0,
    };
    let nyxBannerEl = null;
    // Tuning constants — picked, not exposed in the menu.
    const SENSITIVITY           = 0.0025; // mouse-pixels → yaw radians
    const PROJECTILE_SPEED      = 80;     // fallback bullet speed when weapon lookup fails
    // Prediction now runs in the game's native PER-TICK units inside
    // predictPositionSF (gravity -0.012/tick², terminal 0.29/tick, +1-tick lead
    // bias), reading the game's own velocity vector — no per-second conversion
    // or mesh-derived velocity tracking is needed anymore.

    const SETTINGS_KEY = 'ssh_settings_v1';
    const DEFAULT_SETTINGS = {
        aimEnabled:  true,
        crosshairTarget: false, // ON = target the enemy nearest your crosshair; OFF = nearest by distance
        onlyVisible: false, // ON = never target enemies behind walls; OFF = prioritise visible, fall back to nearest
        espEnabled:  false,
        predEnabled: true,
        unlockSkins: true,
        itemEsp:     true,
        menuVisible: true,
    };
    const settings = Object.assign({}, DEFAULT_SETTINGS);
    try {
        const saved = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
        Object.assign(settings, saved);
        delete settings.noSpread;        // removed: old spread-zero flag
        delete settings.antiBloom;       // removed: anti-bloom feature
        delete settings.antiBloomManual; // removed: anti-bloom manual mode
    } catch (e) {}
    function saveSettings() {
        try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) {}
        // Mirror to the page's global so bundle-side patches can read these
        // flags without crossing the userscript sandbox.
        try { unsafeWindow.ssh_skinUnlock  = settings.unlockSkins; } catch (e) {}
    }
    try { unsafeWindow.ssh_skinUnlock = settings.unlockSkins; } catch (e) {}
    let refreshMenu = () => {}; // replaced by buildMenu() once DOM exists

    const log = (...a) =>
        console.log('%cShellhax', 'color:#000;background:#ff0;padding:2px 6px;border-radius:4px;font-weight:bold', ...a);
    log('started');

    // Scan an object's own properties for one whose value has `propName` set.
    // Used to discover the obfuscated `actor` key dynamically — much more
    // robust than relying on a regex that may not match every build.
    //
    // `skip` excludes known false-positive keys. `weapon` carries its own
    // `.mesh` (the gun model), and was previously matching first — making
    // the prediction read gun-tip position instead of the body actor, so
    // velocity tracked gun rotation rather than player movement.
    function findKeyWithProperty(obj, propName, skip) {
        for (const k in obj) {
            if (!obj.hasOwnProperty(k)) continue;
            if (skip && skip.indexOf(k) !== -1) continue;
            const v = obj[k];
            if (v && typeof v === 'object' && v.hasOwnProperty(propName)) return k;
        }
        return null;
    }

    // ────────────────────────────────────────────────────────────────────────
    // INPUT — RMB for aim, V for ESP, ` to toggle menu
    // ────────────────────────────────────────────────────────────────────────
    document.addEventListener('mousedown', e => { if (e.button === 2) RMB = true;  }, true);
    document.addEventListener('mouseup',   e => { if (e.button === 2) RMB = false; }, true);
    document.addEventListener('keydown', e => {
        const t = document.activeElement;
        if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA')) return;
        if (e.repeat) return;
        if (e.code === 'KeyV') {
            settings.espEnabled = !settings.espEnabled;
            saveSettings(); refreshMenu();
            log('ESP', settings.espEnabled ? 'ON' : 'OFF');
        } else if (e.code === 'Backquote') {
            settings.menuVisible = !settings.menuVisible;
            saveSettings(); refreshMenu();
        }
    }, true);

    // ────────────────────────────────────────────────────────────────────────
    // MENU — tiny on-screen panel, persists to localStorage.
    // ────────────────────────────────────────────────────────────────────────
    function buildMenu() {
        if (document.getElementById('ssh-menu')) return;
        if (!document.body) { setTimeout(buildMenu, 50); return; }

        // Collapsible-category styling: each header is clickable and toggles
        // its body open/closed. Everything else matches the original panel.
        const css = document.createElement('style');
        css.textContent = `
            #ssh-menu .ssh-cat-hdr{cursor:pointer;padding:4px 0;font-weight:bold;
                border-top:1px solid #333;display:flex;justify-content:space-between;align-items:center}
            #ssh-menu .ssh-cat-body{display:none;padding-left:6px}
            #ssh-menu .ssh-cat.open .ssh-cat-body{display:block}
            #ssh-menu .ssh-arr{transition:transform .2s;font-size:9px;opacity:0.6}
            #ssh-menu .ssh-cat.open .ssh-arr{transform:rotate(90deg)}
        `;
        document.head.appendChild(css);

        const wrap = document.createElement('div');
        wrap.id = 'ssh-menu';
        wrap.style.cssText = [
            'position:fixed', 'top:12px', 'left:12px', 'z-index:2147483647',
            'background:rgba(0,0,0,0.82)', 'color:#fff',
            'font:12px/1.4 -apple-system,system-ui,sans-serif',
            'padding:10px 12px', 'border-radius:6px', 'min-width:210px',
            'border:1px solid #000000', 'user-select:none',
            'box-shadow:0 2px 10px rgba(101, 0, 0, 0)',
        ].join(';');
        wrap.innerHTML = `
            <div style="font-weight:bold;color:#ff3b3b;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">
                <span>Shellhax</span>
                <span style="opacity:0.55;font-weight:normal;font-size:11px;">\` to hide</span>
            </div>
            <div class="ssh-cat open">
                <div class="ssh-cat-hdr">Combat <span class="ssh-arr">&#9654;</span></div>
                <div class="ssh-cat-body">
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="aimEnabled"> Aimbot <span style="opacity:0.55;">(hold RMB)</span></label>
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="crosshairTarget"> Target Crosshair <span style="opacity:0.55;">(else closest)</span></label>
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="onlyVisible"> Only Visible <span style="opacity:0.55;">(else prioritise)</span></label>
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="predEnabled"> Prediction</label>
                </div>
            </div>
            <div class="ssh-cat open">
                <div class="ssh-cat-hdr">ESP <span class="ssh-arr">&#9654;</span></div>
                <div class="ssh-cat-body">
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="espEnabled"> ESP <span style="opacity:0.55;">(V)</span></label>
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="itemEsp"> Item ESP <span style="opacity:0.55;">(ammo · grenades)</span></label>
                </div>
            </div>
            <div class="ssh-cat">
                <div class="ssh-cat-hdr">Skins <span class="ssh-arr">&#9654;</span></div>
                <div class="ssh-cat-body">
                    <label style="display:block;margin:3px 0;"><input type="checkbox" data-k="unlockSkins"> Unlock Skins</label>
                </div>
            </div>
            <div id="ssh-nyx" style="margin-top:6px;padding:6px 8px;border-radius:4px;background:rgba(180,80,255,0.18);border:1px solid rgba(180,80,255,0.55);color:#e9d5ff;font-size:11px;line-height:1.35;">
                Join the <b>Nyx</b> wave — start your name with <b>Nyx</b> so we can spot each other.
            </div>
        `;
        document.body.appendChild(wrap);

        nyxBannerEl = wrap.querySelector('#ssh-nyx');
        const inputs = wrap.querySelectorAll('[data-k]');

        // Collapsible categories: clicking a header toggles its body open/closed.
        wrap.querySelectorAll('.ssh-cat-hdr').forEach(hdr => {
            hdr.addEventListener('click', () => hdr.parentElement.classList.toggle('open'));
        });

        refreshMenu = () => {
            inputs.forEach(el => {
                const k = el.dataset.k;
                if (el.type === 'checkbox') el.checked = !!settings[k];
                else                        el.value   = settings[k];
            });
            wrap.style.display = settings.menuVisible ? '' : 'none';
        };
        refreshMenu();

        inputs.forEach(el => {
            el.addEventListener('input', () => {
                const k = el.dataset.k;
                if (el.type === 'checkbox') {
                    settings[k] = el.checked;
                } else {
                    const n = parseFloat(el.value);
                    settings[k] = Number.isFinite(n) ? n : DEFAULT_SETTINGS[k];
                }
                saveSettings(); refreshMenu();
            });
        });

        // Stop menu interactions from bleeding into the game.
        ['keydown','keyup','mousedown','mouseup','wheel','contextmenu'].forEach(ev =>
            wrap.addEventListener(ev, e => e.stopPropagation(), true));

        log('menu built');
    }
    if (document.body) buildMenu();
    else document.addEventListener('DOMContentLoaded', buildMenu);

    // ────────────────────────────────────────────────────────────────────────
    // YAW/PITCH MECHANISM (trimmed port of babylon.js's `yawpitch` helper)
    //
    // shellshock.io's camera state lives in WASM. You cannot move the camera
    // by mutating player.yaw / player.pitch directly — the WASM module is the
    // source of truth. The only thing that DOES affect the camera is the
    // game's `pointermove` listener (named "real" in the bundle), which
    // converts movementX/Y into WASM look deltas.
    //
    // Steps:
    //   1. Hook addEventListener early to capture that listener.
    //   2. To turn the camera N radians, synthesize a fake pointermove event
    //      with movementX = N/sensitivity and call the listener directly.
    //   3. After moving, read back the resulting yaw/pitch from the bundle's
    //      `unsafeWindow.get_yaw_pitch()` helper.
    // ────────────────────────────────────────────────────────────────────────
    let realPointerListener = null;
    const _origAEL = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function (type, listener, options) {
        try {
            if (type === 'pointermove' && listener && listener.name === 'real') {
                realPointerListener = listener;
                log('captured real pointermove listener');
            }
        } catch (e) { /* never break the page's own event hooks */ }
        return _origAEL.call(this, type, listener, options);
    };

    function getCurrentYawPitch() {
        try { return unsafeWindow.get_yaw_pitch(); } catch (e) { return null; }
    }

    function movePointer(mx, my) {
        mx = Math.round(mx); my = Math.round(my);
        if (mx === 0 && my === 0) return;
        if (!realPointerListener) return;
        realPointerListener({ movementX: mx, movementY: my, x: 1, isTrusted: true });
    }

    // Signed shortest-arc difference between two angles, in (-π, π].
    function radianDiff(a, b) {
        const TAU = 2 * Math.PI;
        a = ((a % TAU) + TAU) % TAU;
        b = ((b % TAU) + TAU) % TAU;
        let d = Math.abs(a - b);
        d = Math.min(d, TAU - d);
        return (((a - b + TAU) % TAU) > Math.PI) ? -d : d;
    }

    // Aim camera at the given yaw/pitch (in radians, get_yaw_pitch convention).
    function setToYawPitch(targetYaw, targetPitch) {
        const cur = getCurrentYawPitch();
        if (!cur) return;
        const dy = radianDiff(cur.yaw,   targetYaw);
        const dp = radianDiff(cur.pitch, targetPitch);
        movePointer(dy / SENSITIVITY, dp / SENSITIVITY);
    }

    // ────────────────────────────────────────────────────────────────────────
    // SHARED AIM MATH — yaw/pitch of a direction vector, in the game's
    // RotationYawPitchRoll basis (matches StateFarm's calculateYaw/calculatePitch).
    // Used by the aimbot to convert a (me → target) vector into the yaw/pitch the
    // camera should hold.
    // ────────────────────────────────────────────────────────────────────────
    const _mod = (a, n) => ((a % n) + n) % n;
    const _setPrecision = (v) => Math.round(v * 8192) / 8192; // game's shot precision
    const _calcYaw   = (pos) => _setPrecision(_mod(Math.atan2(pos.x, pos.z), 2 * Math.PI));
    const _calcPitch = (pos) => _setPrecision(-Math.atan2(pos.y, Math.hypot(pos.x, pos.z)) % 1.5);

    // Resolve the weapon's static config (bullet velocity, range, mesh name).
    // babylon2 reads it off `weapon.subClass`; older/other builds expose the
    // same data on `weapon.constructor` (where StateFarm reads it).
    function _weaponConfig(w) {
        return (w && w.subClass) || (w && w.constructor) || {};
    }

    // ────────────────────────────────────────────────────────────────────────
    // BUNDLE INTERCEPTION — same pattern as babylon.js (which works).
    // ────────────────────────────────────────────────────────────────────────
    const _origReplace = String.prototype.replace;
    String.prototype.sshReplace = function () { return _origReplace.apply(this, arguments); };

    const CB_NAME = 'ssh_' + Math.random().toString(36).slice(2, 10);

    const DEFAULTS = {
        x: 'Fh', y: 'Qh', z: 'Th',
        dz: 'dz', // per-tick velocity z (dx/dy are usually plain props; dz can be minified)
        yaw: 'Eh', pitch: '$h', coords: 'aa',
        playing: 'Gh',
        actor: 'eh', mesh: 'mesh',
        weapon: 'Ah',
        renderingGroupId: 'renderingGroupId',
        SCENE: 'eI', PLAYERS: 'rI',
        CULL: 'Xw',
        items: 'kr',
    };

    function extractKeys(js) {
        const out = Object.assign({}, DEFAULTS);
        try {
            let m;
            // WASM mouse-look bridge: `(zO.Kw=e.yaw,zO.Ew=e.pitch,zO.Qz=e.coords)`
            m = /\(([a-zA-Z_$0-9]+)\.([a-zA-Z_$0-9]+)=e\.yaw,\1\.([a-zA-Z_$0-9]+)=e\.pitch,\1\.([a-zA-Z_$0-9]+)=e\.coords\)/.exec(js);
            if (m) { out.yaw = m[2]; out.pitch = m[3]; out.coords = m[4]; }
            // Players-array iteration
            m = /for\s*\(\s*(?:var|let)\s+\w+=0;\w+<([a-zA-Z_$0-9]+);\w+\+\+\)\s*\{\s*(?:var|let)\s+\w+=([a-zA-Z_$0-9]+)\[\w+\];\s*\w+&&\w+\.([a-zA-Z_$0-9]+)&&/.exec(js);
            if (m) { out.PLAYERS = m[2]; out.playing = m[3]; }
            // Spectator-info builder pos fields
            m = /posX:[a-zA-Z_$0-9]+\.([a-zA-Z_$0-9]+),posY:[a-zA-Z_$0-9]+\.([a-zA-Z_$0-9]+),posZ:[a-zA-Z_$0-9]+\.([a-zA-Z_$0-9]+)/.exec(js);
            if (m) { out.x = m[1]; out.y = m[2]; out.z = m[3]; }
            // Actor + mesh
            m = /actorX:[a-zA-Z_$0-9]+\.([a-zA-Z_$0-9]+)\.([a-zA-Z_$0-9]+)\.position\.x/.exec(js);
            if (m) { out.actor = m[1]; out.mesh = m[2]; }
            // SCENE: first render() in the two-render() pattern
            m = /([a-zA-Z_$0-9]+)\.render\(\),[a-zA-Z_$0-9]+\.render\(\)\}\)\)/.exec(js);
            if (m) out.SCENE = m[1];
            // Items manager: the network-protocol handler calls
            // `<X>.spawnItem(s,p,m,v,y);break;` with exactly those 5 args.
            // Capture <X> as the items-manager instance.
            m = /([a-zA-Z_$0-9]+)\.spawnItem\([a-zA-Z_$0-9]+,[a-zA-Z_$0-9]+,[a-zA-Z_$0-9]+,[a-zA-Z_$0-9]+,[a-zA-Z_$0-9]+\);break;/.exec(js);
            if (m) out.items = m[1];
        } catch (e) { log('key extraction error:', e); }
        return out;
    }

    function patchBundle(js) {
        H = extractKeys(js);
        log('H map:', H);

        const argFields = Object.keys(H)
            .map(k => `${k}:(()=>{try{return ${H[k]}}catch(_){return null}})()`)
            .join(',');
        const find    = H.SCENE + '.render';
        const replace = `window["${CB_NAME}"]({${argFields}},true)||${H.SCENE}.render`;
        const before  = js;
        js = js.sshReplace(find, replace);
        if (before === js) log('WARNING: SCENE.render patch did not match');
        else               log('SCENE.render hook installed');

        // Cull inhibition: the bundle hides off-screen / occluded players via
        // `{if(<CULL>)`. Patch to `{if(true)` so all players keep rendering
        // even when behind walls — otherwise our ESP boxes (parented to the
        // player mesh) disappear with them.
        const cullBefore = js;
        js = js.sshReplace('{if(' + H.CULL + ')', '{if(true)');
        if (cullBefore === js) log('WARNING: cull-inhibition patch did not match (H.CULL=' + H.CULL + ')');
        else                   log('cull inhibition installed');

        // ── Item ESP hooks (ammo + grenade drops) ──
        // Anchor on the items-manager prototype methods. Both spawnItem
        // and collectItem have a stable shape:
        //   spawnItem(e,t,i,r,n){var a=this.pools[t].retrieve(e); ...}
        //   collectItem(e,t){var i=this.pools[e]; i.recycle(...); ...}
        // Param names are captured generically so renaming between builds
        // doesn't break the patch.
        // Anchor on the function body (`var a=this.pools[t].retrieve(e);`)
        // and accept both `<Cls>.prototype.spawnItem=function(...)` (old
        // bundle style) and `spawnItem(...){` (ES6 class syntax). The body
        // pattern is what disambiguates from call sites like
        // `kr.spawnItem(s,p,m,v,y);`.
        const spawnItemBefore = js;
        js = js.sshReplace(
            /((?:\.prototype\.spawnItem=function|\bspawnItem)\(([a-zA-Z_$0-9]+),([a-zA-Z_$0-9]+),([a-zA-Z_$0-9]+),([a-zA-Z_$0-9]+),([a-zA-Z_$0-9]+)\)\{var ([a-zA-Z_$0-9]+)=this\.pools\[\3\]\.retrieve\(\2\);)/,
            '$1window.ssh_onItemSpawn&&window.ssh_onItemSpawn($7,$3,$4,$5,$6);'
        );
        if (spawnItemBefore === js) log('WARNING: spawnItem pattern not found — item ESP disabled');
        else                        log('spawnItem hook installed');

        const collectItemBefore = js;
        js = js.sshReplace(
            /((?:\.prototype\.collectItem=function|\bcollectItem)\(([a-zA-Z_$0-9]+),([a-zA-Z_$0-9]+)\)\{var ([a-zA-Z_$0-9]+)=this\.pools\[\2\];)/,
            '$1window.ssh_onItemCollect&&window.ssh_onItemCollect($2,$4.objects[$3]);'
        );
        if (collectItemBefore === js) log('WARNING: collectItem pattern not found');
        else                          log('collectItem hook installed');

        // ── Skin unlock ──
        // The bundle's "do I own this skin?" check is:
        //   `inventory[X].id===Y.id) return true; return false`
        // Patch the trailing comparison so it ALSO passes when our flag
        // `window.ssh_skinUnlock` is set — every skin then appears owned.
        // Gated client-side; the server may still validate at use, but the
        // shop/inventory UI will let you try them on.
        const skinBefore = js;
        js = js.sshReplace(
            /inventory\[[a-zA-Z$_]+\]\.id===[a-zA-Z$_]+\.id\)return!0;return!1/,
            (m) => m + '||window.ssh_skinUnlock'
        );
        if (skinBefore === js) log('WARNING: skin-unlock pattern not found in this bundle');
        else                   log('skin-unlock hook installed');

        return js;
    }

    const _origAppendChild = HTMLElement.prototype.appendChild;
    HTMLElement.prototype.appendChild = function (node) {
        if (node && node.tagName === 'SCRIPT' && node.innerHTML &&
            node.innerHTML.startsWith('(()=>{')) {
            log('intercepting bundle, ' + node.innerHTML.length + ' chars');
            node.innerHTML = patchBundle(node.innerHTML);
        }
        return _origAppendChild.call(this, node);
    };

    // ────────────────────────────────────────────────────────────────────────
    // ESP — wireframe box per enemy, parented to their mesh so it follows
    // smoothly. Created on first sight; visibility toggled per frame.
    // ────────────────────────────────────────────────────────────────────────
    let _espWarned = false;
    // Tracks every player we've created an ESP box for. Used by the sweep
    // in the per-frame callback to dispose orphan boxes when a player leaves
    // the match (no longer in ss.PLAYERS) or switches to our team.
    const _espPlayers = new Set();
    function disposeEspFor(P) {
        if (P._ssh_box)    { try { P._ssh_box.dispose();    } catch (e) {} P._ssh_box    = null; }
        if (P._ssh_tracer) { try { P._ssh_tracer.dispose(); } catch (e) {} P._ssh_tracer = null; }
        _espPlayers.delete(P);
    }

    // ESP color palette. Default enemies render in red; players whose name
    // starts with "Nyx" render in purple so the community is visible at a
    // glance across the map. Reused across boxes + tracers; one Color3
    // instance each so we don't allocate per-frame.
    const ESP_COLOR_RED = (typeof BABYLON !== 'undefined') ? new BABYLON.Color3(1.0, 0.2, 0.2) : null;
    const ESP_COLOR_NYX = (typeof BABYLON !== 'undefined') ? new BABYLON.Color3(0.7, 0.3, 1.0) : null;
    const ESP_COLOR_AMMO    = (typeof BABYLON !== 'undefined') ? new BABYLON.Color3(1.0, 0.95, 0.2) : null;
    const ESP_COLOR_GRENADE = (typeof BABYLON !== 'undefined') ? new BABYLON.Color3(1.0, 0.5, 0.0)  : null;
    const _isNyxName = (n) => typeof n === 'string' && n.toLowerCase().startsWith('nyx');

    // Item ESP — markers for ammo/grenade pickups. The bundle's items
    // manager pools meshes (Qo.constructors = [Yo, Ko] → type 0 = ammo,
    // type 1 = grenade), so we keep our marker keyed by the pooled mesh
    // and dispose it on collect.
    // Map (not WeakMap) so we can iterate it each frame to dispose markers
    // whose item went inactive without us seeing the collect event.
    const itemMarkers = new Map();
    function makeItemMarker(x, y, z, color) {
        if (typeof BABYLON === 'undefined' || !ss.SCENE) return null;
        const s = 0.35;
        const V = BABYLON.Vector3;
        const m = BABYLON.MeshBuilder.CreateLineSystem(
            'sshitem_' + Math.random().toString(36).slice(2, 8),
            { lines: [
                [new V(-s, 0, 0), new V(s, 0, 0)],
                [new V(0, -s, 0), new V(0, s, 0)],
                [new V(0, 0, -s), new V(0, 0, s)],
            ]},
            ss.SCENE
        );
        m.color = color;
        m.position.x = x; m.position.y = y; m.position.z = z;
        m[H.renderingGroupId] = 1;
        m.alwaysSelectAsActiveMesh = true;
        m.isPickable = false;
        pierceWalls(m);
        return m;
    }
    unsafeWindow.ssh_onItemSpawn = function (item, type, x, y, z) {
        try {
            if (!settings.itemEsp) return;
            if (!item || !item.mesh) return;
            // Clean up any leftover marker for this pooled mesh.
            const old = itemMarkers.get(item.mesh);
            if (old) { try { old.dispose(); } catch (e) {} itemMarkers.delete(item.mesh); }
            const color = (type === 0 ? ESP_COLOR_AMMO : ESP_COLOR_GRENADE)
                       || new BABYLON.Color3(1, 1, 1);
            const marker = makeItemMarker(x, y, z, color);
            if (marker) itemMarkers.set(item.mesh, marker);
        } catch (e) {}
    };
    unsafeWindow.ssh_onItemCollect = function (_type, item) {
        try {
            if (!item || !item.mesh) return;
            const marker = itemMarkers.get(item.mesh);
            if (marker) {
                try { marker.dispose(); } catch (e) {}
                itemMarkers.delete(item.mesh);
            }
        } catch (e) {}
    };

    // Force a mesh to ignore the depth buffer entirely while rendering. This
    // is what actually makes the box/tracer punch through walls — neither
    // renderingGroupId nor setRenderingAutoClearDepthStencil is reliable on
    // its own in this Babylon version.
    function pierceWalls(mesh) {
        if (!mesh || mesh._ssh_pierced) return;
        mesh._ssh_pierced = true;
        let saved = null;
        mesh.onBeforeRenderObservable.add(() => {
            try {
                const eng = ss.SCENE && ss.SCENE.getEngine && ss.SCENE.getEngine();
                if (!eng) return;
                saved = eng.getDepthFunction();
                eng.setDepthFunction(BABYLON.Engine.ALWAYS);
            } catch (e) {}
        });
        mesh.onAfterRenderObservable.add(() => {
            try {
                const eng = ss.SCENE && ss.SCENE.getEngine && ss.SCENE.getEngine();
                if (!eng || saved === null) return;
                eng.setDepthFunction(saved);
                saved = null;
            } catch (e) {}
        });
    }
    function ensureEspBox(P) {
        if (P._ssh_box) return P._ssh_box;
        if (typeof BABYLON === 'undefined') {
            if (!_espWarned) { _espWarned = true; log('ESP: BABYLON not loaded'); }
            return null;
        }
        if (!ss.SCENE) {
            if (!_espWarned) { _espWarned = true; log('ESP: no SCENE'); }
            return null;
        }
        // Build geometry in local space (centered around 0, 0, 0). Position is
        // set per-frame from network coords below — no parenting, so we don't
        // inherit the game's interpolation freeze when it thinks the enemy
        // is occluded.
        const w = 0.4, h = 0.65, d = 0.4;
        const V = BABYLON.Vector3;
        const v = [
            new V(-w/2, 0,   -d/2), new V(w/2, 0,   -d/2),
            new V( w/2, h,   -d/2), new V(-w/2, h,  -d/2),
            new V(-w/2, 0,    d/2), new V(w/2, 0,    d/2),
            new V( w/2, h,    d/2), new V(-w/2, h,   d/2),
        ];
        const lines = [];
        for (let i = 0; i < 4; i++) {
            lines.push([v[i],   v[(i+1)%4]]);
            lines.push([v[i+4], v[(i+1)%4+4]]);
            lines.push([v[i],   v[i+4]]);
        }
        const box = BABYLON.MeshBuilder.CreateLineSystem(
            'sshesp_' + Math.random().toString(36).slice(2, 8),
            { lines },
            ss.SCENE
        );
        box.color = ESP_COLOR_RED || new BABYLON.Color3(1.0, 0.2, 0.2);
        box[H.renderingGroupId] = 1;
        box.alwaysSelectAsActiveMesh = true;
        box.isPickable = false;
        pierceWalls(box);
        P._ssh_box = box;
        _espPlayers.add(P);
        log('ESP: built box for', P.name || P.nickname || '?');
        return box;
    }

    // Tracer line from each enemy to a point 5 units BEHIND the camera (so it
    // appears to converge at your eye and project outward to the enemy).
    function ensureEspTracer(P) {
        if (P._ssh_tracer) return P._ssh_tracer;
        if (typeof BABYLON === 'undefined' || !ss.SCENE) return null;
        const placeholder = [new BABYLON.Vector3(0,0,0), new BABYLON.Vector3(0,0,0)];
        const tr = BABYLON.MeshBuilder.CreateLines(
            'sshtracer_' + Math.random().toString(36).slice(2, 8),
            { points: placeholder, updatable: true },
            ss.SCENE
        );
        tr.color = ESP_COLOR_RED || new BABYLON.Color3(1.0, 0.2, 0.2);
        tr.isPickable = false;
        tr.alwaysSelectAsActiveMesh = true;
        tr.doNotSyncBoundingInfo = true;
        tr[H.renderingGroupId] = 1;
        pierceWalls(tr);
        P._ssh_tracer = tr;
        return tr;
    }
    function updateEspTracer(P, crosshairs) {
        const tr = ensureEspTracer(P);
        if (!tr) return null;
        // Use network coords (always live) rather than mesh.position (freezes
        // when the game thinks the enemy is occluded).
        const from = new BABYLON.Vector3(P[H.x], P[H.y] + 0.4, P[H.z]);
        BABYLON.MeshBuilder.CreateLines(undefined, {
            points: [from, crosshairs.clone()],
            instance: tr,
            updatable: true,
        });
        return tr;
    }


    // Per-weapon bullet speed (Crackshot ≈150, EggK-47 ≈80…). Big difference
    // in long-range lead — fixed-time lead under/overshoots across weapons.
    function getProjectileSpeed(me) {
        try {
            const w = me && me[H.weapon];
            if (w && w.subClass && typeof w.subClass.velocity === 'number' && w.subClass.velocity > 0) {
                return w.subClass.velocity;
            }
        } catch (e) {}
        return PROJECTILE_SPEED;
    }

    // Best-effort floor height under (x, z): cast a ray straight down from just
    // above the target and return the first solid hit's y. "other script" uses
    // the game's map-only collider (Collider.grenadeCollidesWithCell) for this;
    // that filtered collider isn't exposed to us, so we pick the nearest solid
    // below via the scene — excluding our own ESP geometry. Returns null when
    // the scene/ray isn't available (then the caller skips the clamp).
    function getGroundY(x, z, fromY) {
        if (!ss.SCENE || typeof BABYLON === 'undefined') return null;
        try {
            const origin = new BABYLON.Vector3(x, fromY + 0.5, z);
            const ray = new BABYLON.Ray(origin, new BABYLON.Vector3(0, -1, 0), 60);
            const pick = ss.SCENE.pickWithRay(ray, (mesh) => {
                if (!mesh || mesh.isPickable === false) return false;
                if (mesh._ssh_box || mesh._ssh_tracer || mesh._ssh_pierced) return false;
                return true;
            });
            return (pick && pick.hit && pick.pickedPoint) ? pick.pickedPoint.y : null;
        } catch (e) { return null; }
    }

    // Lead prediction — faithful port of StateFarm's `predictPosition`.
    //
    // Works in the game's PER-TICK units (gravity -0.012/tick², terminal fall
    // 0.29/tick, +1-tick lead bias). Reads the game's native velocity vector
    // (player.dx/.dy/.dz) and the network position (H.x/y/z) rather than
    // deriving velocity from mesh deltas — this is what StateFarm does and is
    // what the user asked to replicate. Returns the predicted WORLD position to
    // aim at; the y is gravity-integrated and clamped to the floor when the
    // target is airborne, left at the network y when grounded.
    function predictPositionSF(player, me) {
        const base = { x: player[H.x], y: player[H.y], z: player[H.z] };
        try {
            const meMesh = me[H.actor] && me[H.actor][H.mesh];
            const pMesh  = player[H.actor] && player[H.actor][H.mesh];
            if (!meMesh || !pMesh) return base;
            const mp = meMesh.position, tp = pMesh.position;

            // Native per-tick velocity. dx/dy are usually plain props; dz can be
            // minified, so fall back to the discovered H.dz key, then 0.
            const vx = (typeof player.dx === 'number') ? player.dx : 0;
            const vy = (typeof player.dy === 'number') ? player.dy : 0;
            const vz = (typeof player.dz === 'number') ? player.dz
                     : (typeof player[H.dz] === 'number') ? player[H.dz] : 0;

            const cfg = _weaponConfig(me[H.weapon]);
            const bulletSpeed = (cfg && typeof cfg.velocity === 'number' && cfg.velocity > 0)
                ? cfg.velocity : PROJECTILE_SPEED;

            // Mesh-based 3D distance (StateFarm distancePlayers, yMultiplier 1).
            const dist = Math.hypot(tp.x - mp.x, tp.y - mp.y, tp.z - mp.z);
            const timeDiff = dist / bulletSpeed + 1; // ticks (+1-tick lead bias)

            const nx = base.x + vx * timeDiff;
            const nz = base.z + vz * timeDiff;
            let   ny = base.y;

            // Terminal-velocity model: StateFarm caps (vx, 0.29, vz) to the
            // terminal speed (0.29/tick) and takes -y as the terminal fall — so
            // the faster you move horizontally, the slower you fall. Replicated
            // here since the game's Math.capVector3 isn't exposed to us.
            const TERMINAL = 0.29;
            const cmag = Math.hypot(vx, TERMINAL, vz);
            const cappedY = (cmag > TERMINAL) ? TERMINAL * (TERMINAL / cmag) : TERMINAL;
            const terminalVelocity = -cappedY;

            // Only predict vertical motion when airborne (onGround === 0). When
            // grounded, y stays at the network y (it won't change).
            if (player.onGround == 0) {
                const g = -0.012; // per-tick²
                const timeAccel = Math.min(timeDiff, (terminalVelocity - vy) / g);
                const predictedY = vy * timeAccel + timeAccel * timeAccel * g / 2 + ny +
                    terminalVelocity * Math.max(timeDiff - timeAccel, 0);
                const groundY = getGroundY(nx, nz, Math.max(ny, predictedY));
                ny = Math.max(groundY != null ? groundY : 0, predictedY) - 0.072;
                _pred.airborne = true;
            } else {
                _pred.airborne = false;
            }

            _pred.active = true;
            _pred.speed = Math.hypot(vx, vy, vz);
            _pred.t = timeDiff;
            _pred.leadDist = Math.hypot(nx - base.x, nz - base.z);
            return { x: nx, y: ny, z: nz };
        } catch (e) { return base; }
    }

    // Line-of-sight test — faithful in spirit to StateFarm's getLineOfSight:
    // cast a ray from our eye toward the target and report whether the MAP
    // blocks it. StateFarm uses the game's map-only collider; that filtered
    // collider isn't exposed to us, so we pick against the scene and skip
    // (a) our own ESP/marker geometry and (b) skinned character meshes — walls
    // and floors aren't skinned, so excluding skinned meshes leaves map
    // geometry as the only occluder. Returns true when nothing solid is closer
    // than the target (or when the scene/ray isn't available to test).
    function hasLineOfSight(me, player) {
        if (!ss.SCENE || typeof BABYLON === 'undefined') return true;
        try {
            const meMesh = me[H.actor] && me[H.actor][H.mesh];
            const pMesh  = player[H.actor] && player[H.actor][H.mesh];
            if (!meMesh || !pMesh) return false;
            const eye = new BABYLON.Vector3(
                meMesh.position.x, meMesh.position.y + 0.3, meMesh.position.z);
            const to = pMesh.position;
            const dx = to.x - eye.x, dy = to.y - eye.y, dz = to.z - eye.z;
            const dist = Math.hypot(dx, dy, dz);
            if (dist <= 0.001) return true;
            const ux = dx / dist, uy = dy / dist, uz = dz / dist;
            // Skip the first ~1.2 units so our own (rigid, non-skinned) gun
            // viewmodel / body right at the origin can't register as a wall.
            const skip = Math.min(1.2, dist * 0.5);
            const len = dist - skip;
            const ray = new BABYLON.Ray(
                new BABYLON.Vector3(eye.x + ux * skip, eye.y + uy * skip, eye.z + uz * skip),
                new BABYLON.Vector3(ux, uy, uz), len);
            const pick = ss.SCENE.pickWithRay(ray, (mesh) => {
                if (!mesh || mesh.isPickable === false) return false;
                if (mesh._ssh_box || mesh._ssh_tracer || mesh._ssh_pierced) return false;
                if (mesh.skeleton) return false; // character model, not the map
                return true;
            });
            if (!pick || !pick.hit) return true;
            // Visible if the first solid hit is essentially at/past the target.
            return pick.distance >= len - 1.0;
        } catch (e) { return true; }
    }

    // Nyx-tag banner: visible until the local player's in-game name starts
    // with "Nyx" (case-insensitive). The banner lives inside the menu and is
    // hidden once the player adopts the tag — no menu spam after they
    // comply. Hidden also when the menu itself is hidden via backtick.
    function updateNyxBanner() {
        if (!nyxBannerEl) return;
        const name = _aim.me && typeof _aim.me.name === 'string' ? _aim.me.name : '';
        const hasTag = name.toLowerCase().startsWith('nyx');
        nyxBannerEl.style.display = hasTag ? 'none' : '';
    }

    // ────────────────────────────────────────────────────────────────────────
    // PER-FRAME CALLBACK — ESP refresh + aim
    // ────────────────────────────────────────────────────────────────────────
    unsafeWindow[CB_NAME] = function (vars) {
        try {
            Object.assign(ss, vars);
            if (!ss.PLAYERS) return false;

            // Find local player (the one with the .ws WebSocket attached).
            let me = null;
            for (const P of ss.PLAYERS) {
                if (P && P.hasOwnProperty('ws')) { me = P; break; }
            }
            if (!me) return false;

            // Discover the obfuscated `actor` key dynamically — it's whichever
            // key on the player object has a `.mesh` child, excluding the
            // weapon key (whose `.mesh` is the gun model, NOT the body —
            // picking it up made our velocity tracker derive from gun-tip
            // motion and sent the lead in the gun's facing direction).
            // Always run the scan — if H.actor was previously set to the
            // weapon key, the guard `me[H.actor].mesh` would otherwise pass
            // and the bad key would stick.
            const actorKey = findKeyWithProperty(me, H.mesh, [H.weapon]);
            if (actorKey && actorKey !== H.actor) {
                log('actor key resolved:', H.actor, '→', actorKey);
                H.actor = actorKey;
            }

            // One-time scene tweak: clear the depth buffer between rendering
            // group 0 (world) and group 1 (our boxes/tracers).
            if (ss.SCENE && !ss.SCENE._ssh_depthCleared && typeof BABYLON !== 'undefined') {
                try {
                    ss.SCENE.setRenderingAutoClearDepthStencil(1, true, true, true);
                    ss.SCENE._ssh_depthCleared = true;
                    log('depth-clear enabled for renderingGroupId=1 (see through walls)');
                } catch (e) { log('depth-clear setup failed:', e && e.message); }
            }

            // Crosshair convergence point: 5 units in front of the camera, so
            // tracer lines from each enemy visually fan out from the center of
            // your view.
            let crosshairs = null;
            const cur = getCurrentYawPitch();
            if (cur && typeof BABYLON !== 'undefined' && me[H.actor] && me[H.actor][H.mesh]) {
                crosshairs = new BABYLON.Vector3();
                crosshairs.copyFrom(me[H.actor][H.mesh].position);
                crosshairs.y += 0.4;
                const yaw = cur.yaw;
                const pitch = -cur.pitch;
                const off = -5;
                crosshairs.x += Math.sin(yaw) * Math.cos(pitch) * off;
                crosshairs.y += Math.sin(pitch) * off;
                crosshairs.z += Math.cos(yaw) * Math.cos(pitch) * off;
            }

            // ── ESP cleanup sweep ──
            // The main loop below only visits players who are STILL valid
            // enemies. Anyone who left the match (gone from ss.PLAYERS) or
            // switched onto our team would otherwise keep an orphan box at
            // their last position forever. Build the set of who should
            // currently have ESP, then dispose everything else we're tracking.
            const _shouldHaveEsp = new Set();
            for (const P of ss.PLAYERS) {
                if (!P || P === me) continue;
                if (me.team !== 0 && P.team === me.team) continue;
                _shouldHaveEsp.add(P);
            }
            for (const P of _espPlayers) {
                if (!_shouldHaveEsp.has(P)) disposeEspFor(P);
            }

            // ── ESP boxes + tracers ──
            for (const P of ss.PLAYERS) {
                if (!P || P === me) continue;
                if (me.team !== 0 && P.team === me.team) continue;
                if (!P[H.playing]) {
                    if (P._ssh_box)    P._ssh_box.visibility    = 0;
                    if (P._ssh_tracer) P._ssh_tracer.visibility = 0;
                    continue;
                }

                // Recolor based on whether this enemy is a Nyx user. Cache
                // the last-applied state on the player so we only touch the
                // mesh color when it actually changes (cheap most frames).
                const nyx = _isNyxName(P.name);
                const desiredColor = nyx ? ESP_COLOR_NYX : ESP_COLOR_RED;

                const box = ensureEspBox(P);
                if (box) {
                    box.position.x = P[H.x];
                    box.position.y = P[H.y];
                    box.position.z = P[H.z];
                    box.visibility = settings.espEnabled ? 1 : 0;
                    if (P._ssh_nyxColor !== nyx) {
                        box.color = desiredColor;
                        P._ssh_nyxColor = nyx;
                    }
                }
                if (crosshairs) {
                    const tr = updateEspTracer(P, crosshairs);
                    if (tr) {
                        tr.visibility = settings.espEnabled ? 1 : 0;
                        if (tr._ssh_nyxColor !== nyx) {
                            tr.color = desiredColor;
                            tr._ssh_nyxColor = nyx;
                        }
                    }
                }
            }

            // Reset per-frame state read by the overlay.
            _aim.hasLock = false;
            _aim.me = me;
            // Clear aim-smoothing memory when RMB is up so the next press
            // snaps onto target instantly instead of easing in from a stale
            // cached point.
            if (!RMB) _aim.smoothTarget = null;
            _pred.enabled = !!settings.predEnabled;
            _pred.active = false;
            _pred.airborne = false;
            _pred.speed = 0; _pred.t = 0; _pred.leadDist = 0;
            _pred.projSpeed = getProjectileSpeed(me);

            // ── Aim: RMB held + aimbot enabled → snap to the best enemy ──
            // Target mode: "Target Crosshair" ON picks the enemy nearest your
            // crosshair (smallest angle); OFF picks the nearest by (y-weighted)
            // 3D distance. Either way the visibility filter applies: prefer
            // enemies with line-of-sight, and with "Only Visible" ON never
            // target one behind a wall (otherwise fall back to the best overall).
            if (RMB && settings.aimEnabled) {
                const meMesh = me[H.actor] && me[H.actor][H.mesh];
                if (meMesh && meMesh.position) {
                    const meP = meMesh.position;

                    // Camera-forward unit vector — only needed for crosshair mode.
                    let fwdX = 0, fwdY = 0, fwdZ = 0, haveFwd = false;
                    if (settings.crosshairTarget) {
                        const cur = getCurrentYawPitch();
                        if (cur) {
                            const cp = Math.cos(cur.pitch);
                            fwdX = -Math.sin(cur.yaw) * cp;
                            fwdY =  Math.sin(cur.pitch);
                            fwdZ = -Math.cos(cur.yaw) * cp;
                            haveFwd = true;
                        }
                    }

                    let bestVis = null, bestVisRank = Infinity, bestVisMesh = null;
                    let bestAny = null, bestAnyRank = Infinity, bestAnyMesh = null;

                    for (const P of ss.PLAYERS) {
                        if (!P || P === me) continue;
                        if (!P[H.playing]) continue;
                        if (me.team !== 0 && P.team === me.team) continue;
                        const pMesh = P[H.actor] && P[H.actor][H.mesh];
                        if (!pMesh || !pMesh.position) continue;
                        const pp = pMesh.position;
                        const ex = pp.x - meP.x, ey = pp.y - meP.y, ez = pp.z - meP.z;
                        // Rank, lower = better. Crosshair mode → angular distance
                        // from the crosshair (1 - dot, 0 = dead-on). Else the
                        // (y-weighted ×2) 3D distance, which prefers an enemy on
                        // your level over one above/below at the same range.
                        let rank;
                        if (haveFwd) {
                            const elen = Math.hypot(ex, ey, ez) || 1;
                            rank = 1 - (fwdX * ex + fwdY * ey + fwdZ * ez) / elen;
                        } else {
                            rank = Math.hypot(ex, ey * 2, ez);
                            if (rank <= 0) continue; // self / exact overlap
                        }
                        if (rank < bestAnyRank) { bestAnyRank = rank; bestAny = P; bestAnyMesh = pp; }
                        if (rank < bestVisRank && hasLineOfSight(me, P)) {
                            bestVisRank = rank; bestVis = P; bestVisMesh = pp;
                        }
                    }

                    // Prefer the best visible; fall back to the best overall only
                    // when Only Visible is off.
                    let best = bestVis, bestMeshPos = bestVisMesh;
                    if (!best && !settings.onlyVisible) { best = bestAny; bestMeshPos = bestAnyMesh; }

                    if (best && bestMeshPos) {
                        // Aim point: prediction ON → StateFarm's network-velocity
                        // lead solve; OFF → the target's current mesh position.
                        let aimX, aimY, aimZ;
                        if (settings.predEnabled) {
                            const pred = predictPositionSF(best, me);
                            aimX = pred.x; aimY = pred.y; aimZ = pred.z;
                        } else {
                            aimX = bestMeshPos.x; aimY = bestMeshPos.y; aimZ = bestMeshPos.z;
                            _pred.active = false;
                        }

                        // Aim-point smoothing — absorbs the ~20 Hz network-coord
                        // stepping so the predicted point (and the camera) don't
                        // jitter. Reset on target switch so the first snap is
                        // instant. This filters the target/prediction only, never
                        // the aim motion itself (see memory).
                        const AIM_SMOOTH = 0.35;
                        if (_aim.smoothTarget !== best) {
                            _aim.smoothTarget = best;
                            _aim.smoothX = aimX; _aim.smoothY = aimY; _aim.smoothZ = aimZ;
                        } else {
                            _aim.smoothX += (aimX - _aim.smoothX) * AIM_SMOOTH;
                            _aim.smoothY += (aimY - _aim.smoothY) * AIM_SMOOTH;
                            _aim.smoothZ += (aimZ - _aim.smoothZ) * AIM_SMOOTH;
                            aimX = _aim.smoothX; aimY = _aim.smoothY; aimZ = _aim.smoothZ;
                        }

                        // Direction to target — StateFarm's getDirectionVectorFacingTarget
                        // with offsetY -0.05; yaw/pitch via the shared calc helpers
                        // (atan2(x,z) basis, pitch clamped %1.5).
                        const dirV = {
                            x: -(aimX - meP.x),
                            y: -(aimY - meP.y - 0.05),
                            z: -(aimZ - meP.z),
                        };
                        const targetYaw   = _calcYaw(dirV);
                        const targetPitch = _calcPitch(dirV);

                        _aim.hasLock = true;

                        // A single big synthetic movement gets clamped by the game's
                        // mouse-input pipeline; several smaller ones converge cleanly.
                        for (let i = 0; i < 5; i++) setToYawPitch(targetYaw, targetPitch);
                    }
                }
            }

            // Item ESP sweep — runs every frame so markers track items that
            // existed before our spawnItem hook installed (pre-join state),
            // and cleans up markers whose item became inactive via any
            // code path the collectItem hook didn't catch.
            if (settings.itemEsp && ss.items && ss.items.pools && typeof BABYLON !== 'undefined') {
                const seen = new Set();
                for (let t = 0; t < ss.items.pools.length; t++) {
                    const pool = ss.items.pools[t];
                    if (!pool || typeof pool.forEachActive !== 'function') continue;
                    const poolColor = (t === 0 ? ESP_COLOR_AMMO : ESP_COLOR_GRENADE);
                    pool.forEachActive((it) => {
                        if (!it || !it.mesh) return;
                        seen.add(it.mesh);
                        if (!itemMarkers.has(it.mesh)) {
                            const pos = it.mesh.position;
                            const m = makeItemMarker(pos.x, pos.y, pos.z, poolColor);
                            if (m) itemMarkers.set(it.mesh, m);
                        }
                    });
                }
                for (const [meshKey, marker] of itemMarkers) {
                    if (!seen.has(meshKey)) {
                        try { marker.dispose(); } catch (e) {}
                        itemMarkers.delete(meshKey);
                    }
                }
            } else if (!settings.itemEsp && itemMarkers.size > 0) {
                for (const marker of itemMarkers.values()) {
                    try { marker.dispose(); } catch (e) {}
                }
                itemMarkers.clear();
            }

            updateNyxBanner();
            return false;
        } catch (e) {
            log('per-frame error:', e && e.message);
            return false;
        }
    };

    // ────────────────────────────────────────────────────────────────────────
    // NOTES
    // ────────────────────────────────────────────────────────────────────────
    // • Hold RMB → aim at the best enemy. "Target Crosshair" picks the one
    //   nearest your crosshair; otherwise the nearest by distance. "Only
    //   Visible" never targets through walls; off, it falls back to the best
    //   enemy overall when none have line-of-sight.
    // • Press V → toggle red wireframe ESP on enemies.
    // • Press ` → show/hide the settings menu (top-left).
    // • Prediction is a faithful port of StateFarm's predictPosition: it reads
    //   the game's native per-tick velocity (player.dx/dy/dz) + network position
    //   and does a single-pass lead (t = dist/bulletSpeed + 1 tick) with a
    //   gravity + terminal-velocity vertical drop model and a ground clamp.
    // • If aim under/overshoots, your in-game mouse sensitivity differs from
    //   the menu's "Sensitivity" value (default 0.0025); tweak it.
})();