EZSQUAD for EvoWorld.io

Open-source EvoWorld.io userscript with a configurable GUI, player and object ESP, visual tools, skin and pet customization, keybinds, configs, and optional Auto Hit. Support: https://t.me/evoezsquad

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         EZSQUAD for EvoWorld.io
// @name:ru      EZSQUAD для EvoWorld.io
// @namespace    https://t.me/evoezsquad
// @version      1.1.0
// @description  Open-source EvoWorld.io userscript with a configurable GUI, player and object ESP, visual tools, skin and pet customization, keybinds, configs, and optional Auto Hit. Support: https://t.me/evoezsquad
// @description:ru Открытый userscript для EvoWorld.io: настраиваемое GUI, ESP игроков и объектов, визуальные функции, Skin/Pet Changer, бинды, конфиги и опциональный Auto Hit. Поддержка: https://t.me/evoezsquad
// @author       EZSQUAD
// @license      MIT
// @match        https://evoworld.io/*
// @match        https://www.evoworld.io/*
// @run-at       document-idle
// @grant        none
// @sandbox      raw
// @noframes
// @homepageURL  https://t.me/evoezsquad
// @supportURL   https://t.me/evoezsquad
// @compatible   chrome
// @compatible   edge
// @compatible   firefox
// ==/UserScript==

;(function () {
    "use strict";

    if (window.EZSQUAD && window.EZSQUAD.active) {
        if (document.getElementById("ez-menu")) return;
        try { if (typeof window.EZSQUAD.stop === "function") window.EZSQUAD.stop(); } catch (e) {}
    }

    var storeKey = "ezsquad.cfg.24";
    var legacyStoreKey = "ezsquad.cfg.23";
    var placeKey = "ezsquad.pos.7";
    var keysPlaceKey = "ezsquad.keys.pos.7";
    var raf = 0;
    var menu = null;
    var layer = null;
    var ctx = null;
    var keyBox = null;
    var keysPanel = null;
    var fileBox = null;
    var soundFileBox = null;
    var bindMode = false;
    var dead = false;
    var fitDirty = true;
    var listAt = 0;
    var scanAt = 0;
    var scanReadAt = 0;
    var fitAt = 0;
    var worldAt = 0;
    var worldDirty = true;
    var players = [];
    var hides = [];
    var extras = [];
    var scans = [];
    var live = new Set();
    var bank = new Map();
    var scanBank = new Map();
    var cardBank = new Map();
    var envBase = new Map();
    var viewBase = null;
    var brightContext = null;
    var brightDraw = null;
    var brightWrap = null;
    var canvasFilter = null;
    var bindPick = null;
    var bindPop = null;
    var holdKeys = Object.create(null);
    var deathPanel = null;
    var deathList = null;
    var deathEntries = [];
    var deathUiAt = 0;
    var deathSocket = null;
    var deathHandler = null;
    var deathPosKey = "ezsquad.deaths.pos.10";
    var spectatorPanel = null;
    var spectatorList = null;
    var spectatorSocket = null;
    var spectatorHandler = null;
    var spectatorEntries = new Map();
    var spectatorRequestAt = 0;
    var spectatorUiAt = 0;
    var spectatorPosKey = "ezsquad.spectators.pos.1";
    var killPanel = null;
    var killList = null;
    var killEntries = [];
    var killUiAt = 0;
    var killSocket = null;
    var killHandler = null;
    var killPosKey = "ezsquad.kills.pos.1";
    var skillTracks = new Map();
    var projectileOwners = new Map();
    var projectileSkillSeen = new Map();
    var staticFoodSpawnCache = new Map();
    var soundUploadKind = "hit";
    var killSoundFileBox = null;
    var foodEvolution = "";
    var audio = null;
    var customSounds = [];
    var soundDbPromise = null;
    var soundBuffers = new Map();
    var soundLoadToken = 0;
    var soundsReady = false;
    var hitBank = new Map();
    var lastHitSound = 0;
    var privacyNames = new Map();
    var privacyDom = new Map();
    var cosmeticAt = 0;
    var catalogAt = 0;
    var catalogSign = "";
    var skinLoads = new Set();
    var spriteBounds = new WeakMap();
    var recentPlayers = new Map();
    var darknessDraw = null;
    var darknessWrap = null;
    var attackSocket = null;
    var attackHandler = null;
    var attackDrawOriginal = null;
    var attackDrawWrap = null;
    var ownAttackAt = 0;
    var ownAttackDirection = 1;
    var ownAttackTargets = new Set();
    var ownAttackAnimation = "";
    var localPet = null;
    var localPetOwner = null;
    var localPetMe = null;
    var cosmeticMe = null;
    var appliedSkinSpawn = "";
    var appliedPetSpawn = "";
    var hiddenPet = null;
    var hiddenPetOpacity = 1;
    var skinHold = null;
    var petAttackSocket = null;
    var petAttackHandler = null;
    var spriteImages = new WeakMap();
    var bladeRegions = new WeakMap();
    var attackRegions = new Map();
    var foodObjects = [];
    var foodSpawns = [];
    var foodSpawnState = new Map();
    var scanIconBase = null;
    var bossSync = null;
    var skillSync = null;
    var skillSyncAt = 0;
    var bossSyncSocket = null;
    var bossSyncHandler = null;
    var foodSeen = new Map();
    var foodGone = new Map();
    var foodSyncSocket = null;
    var foodSyncHandler = null;
    var removeObjectOriginal = null;
    var removeObjectWrap = null;
    var protectedDom = new Map();
    var protectedAt = 0;
    var lastKillerName = "";
    var skillClock = null;
    var skillClockHost = null;
    var skillClockHostPosition = "";
    var skillLocalEnd = 0;
    var skillLocalMe = 0;
    var skillUseOriginal = null;
    var skillUseWrap = null;
    var fightNext = 0;
    var fightTry = 0;
    var fightLock = null;
    var fightLockUntil = 0;
    var fightRelease = 0;
    var fightAttack = 0;

    var defaults = {
        esp: {
            enabled: false,
            tracerOrigin: "bottom"
        },
        settings: {
            bind: "Insert",
            bindName: "Insert",
            keyList: false
        },
        all: {
            on: false,
            box: false,
            boxColor: "#b58af0",
            fill: false,
            fillColor: "#7252a8",
            line: false,
            lineColor: "#b58af0",
            name: false,
            nameColor: "#ffffff",
            level: false,
            levelColor: "#d9c7ff",
            distance: false,
            distanceColor: "#c3c6cd",
            health: false,
            healthColor: "#50cf72",
            armor: false,
            armorColor: "#52a9ff",
            arrows: false,
            arrowColor: "#b58af0",
            effects: false,
            skill: false,
            hidden: false
        },
        danger: {
            on: false,
            box: false,
            boxColor: "#ef5365",
            fill: false,
            fillColor: "#99323e",
            line: false,
            lineColor: "#ef5365",
            name: false,
            nameColor: "#ffe0e4",
            level: false,
            levelColor: "#ff9da8",
            distance: false,
            distanceColor: "#ffc0c8",
            health: false,
            healthColor: "#ef5365",
            armor: false,
            armorColor: "#6db6ff",
            arrows: false,
            arrowColor: "#ef5365",
            effects: false,
            skill: false,
            hidden: false
        },
        edible: {
            on: false,
            box: false,
            boxColor: "#45d39b",
            fill: false,
            fillColor: "#287e60",
            line: false,
            lineColor: "#45d39b",
            name: false,
            nameColor: "#dcfff1",
            level: false,
            levelColor: "#83e8c0",
            distance: false,
            distanceColor: "#a8efd5",
            health: false,
            healthColor: "#45d39b",
            armor: false,
            armorColor: "#68b9ff",
            arrows: false,
            arrowColor: "#45d39b",
            effects: false,
            skill: false,
            hidden: false
        },
        mutual: {
            on: false,
            box: false,
            boxColor: "#f0a14a",
            fill: false,
            fillColor: "#9b652d",
            line: false,
            lineColor: "#f0a14a",
            name: false,
            nameColor: "#ffe3bd",
            level: false,
            levelColor: "#ffc77d",
            distance: false,
            distanceColor: "#ffd39a",
            health: false,
            healthColor: "#f0a14a",
            armor: false,
            armorColor: "#70b7ff",
            arrows: false,
            arrowColor: "#f0a14a",
            effects: false,
            skill: false,
            hidden: false
        },
        extra: {
            tracerOrigin: "bottom",
            hide: false,
            hideColor: "#c59af2",
            hideBox: true,
            hideTracer: false,
            hideName: true,
            hideDistance: true,
            hideImage: false,
            hideRange: 2500,

            portal: false,
            portalColor: "#ffbf69",
            portalBox: true,
            portalTracer: false,
            portalName: true,
            portalDistance: true,
            portalImage: false,
            portalRange: 5000,

            food: false,
            foodColor: "#77df83",
            foodBox: true,
            foodTracer: false,
            foodName: true,
            foodDistance: true,
            foodImage: false,
            foodRange: 1800,
            foodTimer: false,

            boss: false,
            bossColor: "#ff6a78",
            bossBox: true,
            bossTracer: true,
            bossName: true,
            bossDistance: true,
            bossImage: false,
            bossRange: 8000,

            others: false,
            othersColor: "#67d9ff",
            othersBox: true,
            othersTracer: false,
            othersName: true,
            othersDistance: true,
            othersImage: true,
            othersRange: 2500,
            othersGem: true,
            othersArmorPotion: true,
            othersWaterDrop: true,
            othersRoots: true,
            othersFireball: true,
            othersPlasmaBall: true,
            othersShuriken: true,
            othersSnowball: true,
            othersBomb: true,
            othersBearTrap: true
        },
        other: {
            fullBright: false,
            zoom: false,
            zoomValue: 1,
            cloudAlpha: 100,
            bushAlpha: 100,
            waterAlpha: 100,
            swampAlpha: 100,
            lavaAlpha: 100
        },
        misc: {
            petChanger: false,
            petId: 0,
            skinPick: {},
            hitbox: false,
            hitboxColor: "#f0a14a",
            privacy: false,
            privacyServer: true,
            privacyLeaderboard: true,
            privacyIdentity: true,
            privacyPlayers: true,
            deathLog: false,
            deathTime: 60,
            spectatorList: false,
            killLog: false,
            killTime: 60,
            hitSound: false,
            hitSoundType: "Click",
            hitVolume: 45,
            killSound: false,
            killSoundType: "Bell",
            killVolume: 45,
            autoHit: false,
            reaperTargets: false
        },
        binds: {
            fullBright: { key: "", name: "None", mode: "toggle" },
            zoom: { key: "", name: "None", mode: "toggle" },
            autoHit: { key: "", name: "None", mode: "toggle" },
            reapersOnly: { key: "", name: "None", mode: "toggle" }
        }
    };

    var drawRows = [
        ["on", "Enabled"],
        ["box", "Box", "boxColor"],
        ["fill", "Fill", "fillColor"],
        ["line", "Lines", "lineColor"],
        ["name", "Name", "nameColor"],
        ["level", "Level", "levelColor"],
        ["distance", "Distance", "distanceColor"],
        ["health", "Health bar", "healthColor"],
        ["armor", "Armor bar", "armorColor"],
        ["arrows", "Arrows", "arrowColor"],
        ["effects", "Effects"],
        ["hidden", "Hidden / Cached"]
    ];

    function clone(value) {
        return JSON.parse(JSON.stringify(value));
    }

    function clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    }

    function hudScale() {
        var scale = 1;
        try {
            if (window.game && Number.isFinite(Number(game.fontScale))) scale = Number(game.fontScale);
            if (cfg.other && cfg.other.zoom) {
                var zoom = clamp(Number(cfg.other.zoomValue) || 1, .15, 1);
                scale = Math.min(scale, .55 + zoom * .45);
            }
        } catch (e) {}
        return clamp(scale, .55, 1);
    }

    function hudPx(value, minimum) {
        return Math.max(minimum == null ? 0 : minimum, value * hudScale());
    }

    function merge(base, saved) {
        if (!saved || typeof saved !== "object") return base;
        if (base && typeof base === "object" && !Array.isArray(base) && !Object.keys(base).length) return clone(saved);
        Object.keys(base).forEach(function (name) {
            if (!Object.prototype.hasOwnProperty.call(saved, name)) return;
            var a = base[name];
            var b = saved[name];
            if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
                base[name] = merge(a, b);
            } else if (typeof a === typeof b) {
                base[name] = b;
            }
        });
        return base;
    }

    function loadCfg() {
        var data = clone(defaults);
        try {
            var saved = localStorage.getItem(storeKey);
            var old = false;
            if (!saved) {
                saved = localStorage.getItem(legacyStoreKey);
                old = !!saved;
            }
            data = merge(data, JSON.parse(saved || "null"));
            if (old) {
                data.binds.autoHit = clone(defaults.binds.autoHit);
                data.binds.reapersOnly = clone(defaults.binds.reapersOnly);
            }
        } catch (e) {}
        return data;
    }

    var cfg = loadCfg();
    cfg.settings.bind = fixKey(cfg.settings.bind);
    Object.keys(cfg.binds).forEach(function (name) { cfg.binds[name].key = fixKey(cfg.binds[name].key); });
    ["all", "danger", "edible", "mutual"].forEach(function (name) { if (cfg[name]) cfg[name].skill = false; });

    function saveCfg() {
        try { localStorage.setItem(storeKey, JSON.stringify(cfg)); } catch (e) {}
    }

    function read(path) {
        var bits = path.split(".");
        return cfg[bits[0]][bits[1]];
    }

    function write(path, value) {
        var bits = path.split(".");
        cfg[bits[0]][bits[1]] = value;
        saveCfg();
        if (bits[0] === "other" || bits[0] === "misc") worldDirty = true;
        if (bits[0] === "extra" || bits[0] === "danger" || bits[0] === "edible" || bits[0] === "mutual" || path.indexOf("misc.privacy") === 0) cardBank.clear();
        if (path === "misc.spectatorList") updateSpectatorPanel(Date.now());
        if (path === "misc.autoHit" && !value) {
            clearTimeout(fightAttack);
            clearTimeout(fightRelease);
            try { if (typeof gameServer !== "undefined" && gameServer && typeof socketMsgType !== "undefined") gameServer.emit(socketMsgType.SKILL, false); } catch (e) {}
        }
        updateKeys();
    }

    function keyCode(event) {
        var code = String(event && event.code || "");
        if (code) return code;
        var num = Number(event && (event.keyCode || event.which));
        if (num >= 65 && num <= 90) return "Key" + String.fromCharCode(num);
        if (num >= 48 && num <= 57) return "Digit" + String.fromCharCode(num);
        if (num >= 96 && num <= 105) return "Numpad" + String(num - 96);
        var map = { 13: "Enter", 27: "Escape", 32: "Space", 45: "Insert", 46: "Delete", 8: "Backspace", 9: "Tab", 16: "ShiftLeft", 17: "ControlLeft", 18: "AltLeft", 20: "CapsLock", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "ArrowLeft", 38: "ArrowUp", 39: "ArrowRight", 40: "ArrowDown" };
        return map[num] || String(event && event.key || "");
    }

    function fixKey(value) {
        value = String(value || "");
        if (/^[a-z]$/i.test(value)) return "Key" + value.toUpperCase();
        if (/^[0-9]$/.test(value)) return "Digit" + value;
        return value;
    }

    function sameKey(value, event) {
        var saved = fixKey(value);
        var code = keyCode(event);
        if (saved && code && saved === code) return true;
        var num = Number(event && (event.keyCode || event.which));
        if (/^Key[A-Z]$/.test(saved) && num >= 65 && num <= 90) return saved.slice(3).charCodeAt(0) === num;
        if (/^Digit[0-9]$/.test(saved) && num >= 48 && num <= 57) return Number(saved.slice(5)) === num - 48;
        return false;
    }

    function keyName(event) {
        var code = keyCode(event);
        if (code === "Space") return "Space";
        if (code.indexOf("Key") === 0) return code.slice(3);
        if (code.indexOf("Digit") === 0) return code.slice(5);
        if (code.indexOf("Numpad") === 0) return "Num " + code.slice(6);
        return event.key && event.key.length < 12 ? event.key : code;
    }

    var bindFeatures = {
        fullBright: { path: "other.fullBright", title: "FullBright" },
        zoom: { path: "other.zoom", title: "Zoom" },
        autoHit: { path: "misc.autoHit", title: "Auto Hit" },
        reapersOnly: { path: "misc.reaperTargets", title: "Reapers Only" }
    };

    function bindText(name) {
        var item = cfg.binds[name];
        return item && item.name ? item.name : "None";
    }

    function bindButton(name) {
        return name ? '<button class="ez-feature-bind" type="button" data-feature-bind="' + name + '">' + bindText(name) + '</button>' : '';
    }

    function checkRow(path, title, colorPath, bindName, extra) {
        var color = colorPath
            ? '<input class="ez-color" type="color" data-color="' + colorPath + '" value="' + read(colorPath) + '">'
            : '<span class="ez-space"></span>';
        return '<label class="ez-row"><span>' + title + '</span><span class="ez-tools">' + (extra || '') + color + bindButton(bindName) +
            '<input class="ez-check" type="checkbox" data-check="' + path + '"' + (read(path) ? " checked" : "") + '></span></label>';
    }

    function bindCheckRow(path, title, name) {
        return '<label class="ez-row"><span>' + title + '</span><span class="ez-tools">' +
            '<input class="ez-check" type="checkbox" data-check="' + path + '"' + (read(path) ? " checked" : "") + '>' +
            bindButton(name) + '</span></label>';
    }

    function selectRow(path, title, values) {
        var html = values.map(function (item) {
            return '<option value="' + item[0] + '"' + (String(read(path)) === String(item[0]) ? ' selected' : '') + '>' + item[1] + '</option>';
        }).join('');
        return '<label class="ez-row"><span>' + title + '</span><select class="ez-select" data-select="' + path + '">' + html + '</select></label>';
    }

    function cleanFileName(name) {
        return String(name || "Custom").replace(/\.[^.]+$/, "").trim() || "Custom";
    }

    function soundOptionsHtml(kind) {
        var path = kind === "kill" ? "killSoundType" : "hitSoundType";
        var fallback = kind === "kill" ? "Bell" : "Click";
        var selected = String(cfg.misc[path] || fallback);
        var list = [["Click", "Click"], ["Bubble", "Bubble"], ["Bell", "Bell"], ["Metal", "Metal"]];
        for (var i = 0; i < customSounds.length; i++) list.push(["custom:" + customSounds[i].id, customSounds[i].name]);
        return list.map(function (item) {
            return '<option value="' + escapeHtml(item[0]) + '"' + (selected === item[0] ? ' selected' : '') + '>' + escapeHtml(item[1]) + '</option>';
        }).join("");
    }

    function soundRow(kind, title) {
        var isKill = kind === "kill";
        var toggle = isKill ? "misc.killSound" : "misc.hitSound";
        var type = isKill ? "misc.killSoundType" : "misc.hitSoundType";
        var volume = isKill ? "misc.killVolume" : "misc.hitVolume";
        return checkRow(toggle, title) +
            '<label class="ez-row ez-sound-row"><span>Sound</span><span class="ez-sound-tools">' +
                '<select class="ez-select ez-' + kind + '-select" data-select="' + type + '">' + soundOptionsHtml(kind) + '</select>' +
                '<button type="button" class="ez-mini" data-sound-action="upload" data-sound-kind="' + kind + '" title="Upload sound">+</button>' +
                '<button type="button" class="ez-mini" data-sound-action="play" data-sound-kind="' + kind + '" title="Play sound">▶</button>' +
                '<button type="button" class="ez-mini" data-sound-action="remove" data-sound-kind="' + kind + '" title="Remove custom sound">×</button>' +
            '</span></label>' +
            '<input class="ez-sound-file" data-sound-file="' + kind + '" type="file" accept="audio/*,.mp3,.wav,.ogg,.oga,.m4a,.aac,.flac,.webm">' +
            rangeRow(volume, "Volume", 0, 100, 5, "%");
    }

    function hitSoundRow() {
        return soundRow("hit", "Hitsound");
    }

    function killSoundRow() {
        return soundRow("kill", "Killsound");
    }

    function rangeRow(path, title, min, max, step, suffix) {
        var value = read(path);
        return '<label class="ez-range-row"><span>' + title + '</span><span class="ez-range-tools">' +
            '<input type="range" min="' + min + '" max="' + max + '" step="' + step + '" value="' + value + '" data-range="' + path + '">' +
            '<b data-range-value="' + path + '">' + value + suffix + '</b></span></label>';
    }

    function rangeSuffix(path) {
        if (path === "other.zoomValue") return "x";
        if (path === "misc.deathTime" || path === "misc.killTime") return " s";
        if (/Range$/.test(path)) return " m";
        return "%";
    }

    function modeText(name) {
        var out = [];
        if (read("extra." + name + "Box")) out.push("Box");
        if (read("extra." + name + "Tracer")) out.push("Tracer");
        if (read("extra." + name + "Name")) out.push("Name");
        if (read("extra." + name + "Distance")) out.push("Distance");
        if (read("extra." + name + "Image")) out.push("Image");
        return out.length ? out.join(", ") : "None";
    }

    var otherKinds = [
        ["Gem", "Gem"],
        ["ArmorPotion", "Armor potion"],
        ["WaterDrop", "Water drop"],
        ["Roots", "Roots"],
        ["Fireball", "Fireball"],
        ["PlasmaBall", "Plasma ball"],
        ["Shuriken", "Shuriken"],
        ["Snowball", "Snowball"],
        ["Bomb", "Bomb"],
        ["BearTrap", "Bear trap"]
    ];

    function objectText() {
        var out = [];
        for (var i = 0; i < otherKinds.length; i++) {
            if (read("extra.others" + otherKinds[i][0])) out.push(otherKinds[i][1]);
        }
        if (!out.length) return "None";
        if (out.length === otherKinds.length) return "All objects";
        return out.join(", ");
    }

    function objectMenu() {
        return otherKinds.map(function (item) {
            var path = "extra.others" + item[0];
            return '<label><input type="checkbox" data-check="' + path + '"' + (read(path) ? " checked" : "") + '>' + item[1] + '</label>';
        }).join("");
    }

    function extraRow(name, title, color, range) {
        return '<div class="ez-extra-item">' +
            '<label class="ez-row"><span>' + title + '</span><span class="ez-tools">' +
                '<input class="ez-color" type="color" data-color="extra.' + name + 'Color" value="' + color + '">' +
                '<input class="ez-check" type="checkbox" data-check="extra.' + name + '"' + (read("extra." + name) ? " checked" : "") + '>' +
                '<button class="ez-mode-btn" type="button" data-mode-button="' + name + '">' + modeText(name) + '</button>' +
            '</span></label>' +
            '<div class="ez-mode-pop" data-mode-menu="' + name + '">' +
                '<label><input type="checkbox" data-check="extra.' + name + 'Box"' + (read("extra." + name + "Box") ? " checked" : "") + '>Box</label>' +
                '<label><input type="checkbox" data-check="extra.' + name + 'Tracer"' + (read("extra." + name + "Tracer") ? " checked" : "") + '>Tracer</label>' +
                '<label><input type="checkbox" data-check="extra.' + name + 'Name"' + (read("extra." + name + "Name") ? " checked" : "") + '>Name</label>' +
                '<label><input type="checkbox" data-check="extra.' + name + 'Distance"' + (read("extra." + name + "Distance") ? " checked" : "") + '>Distance</label>' +
                '<label><input type="checkbox" data-check="extra.' + name + 'Image"' + (read("extra." + name + "Image") ? " checked" : "") + '>Image</label>' +
            '</div>' +
            rangeRow("extra." + name + "Range", "Range", 50, range, 50, " m") +
        '</div>';
    }


    function othersRow() {
        return '<div class="ez-extra-item">' +
            '<label class="ez-row"><span>Others ESP</span><span class="ez-tools">' +
                '<input class="ez-color" type="color" data-color="extra.othersColor" value="' + cfg.extra.othersColor + '">' +
                '<input class="ez-check" type="checkbox" data-check="extra.others"' + (read("extra.others") ? " checked" : "") + '>' +
                '<button class="ez-object-btn" type="button" data-object-button="others">' + objectText() + '</button>' +
                '<button class="ez-mode-btn" type="button" data-mode-button="others">' + modeText("others") + '</button>' +
            '</span></label>' +
            '<div class="ez-object-pop" data-object-menu="others">' + objectMenu() + '</div>' +
            '<div class="ez-mode-pop" data-mode-menu="others">' +
                '<label><input type="checkbox" data-check="extra.othersBox"' + (read("extra.othersBox") ? " checked" : "") + '>Box</label>' +
                '<label><input type="checkbox" data-check="extra.othersTracer"' + (read("extra.othersTracer") ? " checked" : "") + '>Tracer</label>' +
                '<label><input type="checkbox" data-check="extra.othersName"' + (read("extra.othersName") ? " checked" : "") + '>Name</label>' +
                '<label><input type="checkbox" data-check="extra.othersDistance"' + (read("extra.othersDistance") ? " checked" : "") + '>Distance</label>' +
                '<label><input type="checkbox" data-check="extra.othersImage"' + (read("extra.othersImage") ? " checked" : "") + '>Image</label>' +
            '</div>' +
            rangeRow("extra.othersRange", "Range", 50, 5000, 50, " m") +
        '</div>';
    }


    function tracerOriginSelect(path) {
        var values = [["bottom", "Bottom"], ["top", "Top"], ["player", "Player"]];
        var value = String(read(path) || "bottom");
        return '<span class="ez-tracer-mode"><span>Tracer Mode</span><select class="ez-select ez-tracer-select" data-select="' + path + '">' + values.map(function (item) {
            return '<option value="' + item[0] + '"' + (value === item[0] ? ' selected' : '') + '>' + item[1] + '</option>';
        }).join("") + '</select></span>';
    }

    function playerEspHead() {
        return '<div class="ez-head-controls">' +
            '<label class="ez-head-toggle"><span>Player ESP</span><input class="ez-check" type="checkbox" data-check="esp.enabled"' + (read("esp.enabled") ? " checked" : "") + '></label>' +
            tracerOriginSelect("esp.tracerOrigin") +
        '</div>';
    }

    function playerPage(name) {
        return '<div class="ez-subpage' + (name === "all" ? " ez-active" : "") + '" data-scope="visual" data-subpage="' + name + '">' +
            drawRows.map(function (item) {
                return checkRow(name + "." + item[0], item[1], item[2] ? name + "." + item[2] : "");
            }).join("") + '</div>';
    }

    function espPanel() {
        return '<div class="ez-page ez-active" data-page="esp">' +
            '<div class="ez-work">' +
                '<section class="ez-box">' +
                    '<div class="ez-box-head"><b>Visuals</b>' + playerEspHead() + '</div>' +
                    '<div class="ez-subtabs" data-scope-tabs="visual">' +
                        '<button class="ez-subtab ez-active" type="button" data-scope="visual" data-subtab="all">Players</button>' +
                        '<button class="ez-subtab" type="button" data-scope="visual" data-subtab="danger">Dangerous</button>' +
                        '<button class="ez-subtab" type="button" data-scope="visual" data-subtab="edible">Edible</button>' +
                        '<button class="ez-subtab" type="button" data-scope="visual" data-subtab="mutual">Mutual</button>' +
                    '</div>' +
                    playerPage("all") + playerPage("danger") + playerPage("edible") + playerPage("mutual") +
                '</section>' +
                '<section class="ez-box">' +
                    '<div class="ez-subtabs ez-side-tabs" data-scope-tabs="side">' +
                        '<button class="ez-subtab ez-active" type="button" data-scope="side" data-subtab="extra">Extra ESP</button>' +
                        '<span class="ez-side-tracer" data-extra-tracer>' + tracerOriginSelect("extra.tracerOrigin") + '</span>' +
                        '<button class="ez-subtab" type="button" data-scope="side" data-subtab="other">Other</button>' +
                    '</div>' +
                    '<div class="ez-subpage ez-active ez-extra-page" data-scope="side" data-subpage="extra">' +
                        extraRow("hide", "Hide ESP", cfg.extra.hideColor, 5000) +
                        extraRow("portal", "Portal ESP", cfg.extra.portalColor, 12000) +
                        extraRow("food", "Food ESP", cfg.extra.foodColor, 5000) +
                        extraRow("boss", "Boss ESP", cfg.extra.bossColor, 12000) +
                        othersRow() +
                    '</div>' +
                    '<div class="ez-subpage" data-scope="side" data-subpage="other">' +
                        checkRow("other.fullBright", "FullBright", "", "fullBright") +
                        checkRow("other.zoom", "Zoom", "", "zoom") +
                        rangeRow("other.zoomValue", "Zoom value", 0.15, 1, 0.05, "x") +
                        '<div class="ez-divider"></div>' +
                        rangeRow("other.cloudAlpha", "Cloud", 0, 100, 5, "%") +
                        rangeRow("other.bushAlpha", "Bush", 0, 100, 5, "%") +
                        rangeRow("other.waterAlpha", "Water", 0, 100, 5, "%") +
                        rangeRow("other.swampAlpha", "Swamp", 0, 100, 5, "%") +
                        rangeRow("other.lavaAlpha", "Lava", 0, 100, 5, "%") +
                        '<div class="ez-divider"></div>' +
                    '</div>' +
                '</section>' +
            '</div>' +
        '</div>';
    }

    function privacyText() {
        var out = [];
        if (cfg.misc.privacyServer) out.push("Server");
        if (cfg.misc.privacyLeaderboard) out.push("Leaderboard");
        if (cfg.misc.privacyIdentity) out.push("Identity");
        if (cfg.misc.privacyPlayers) out.push("Players");
        return out.length ? out.join(", ") : "None";
    }

    function privacyMenu() {
        return [
            ["privacyServer", "Server"],
            ["privacyLeaderboard", "Leaderboard"],
            ["privacyIdentity", "Identity"],
            ["privacyPlayers", "Players"]
        ].map(function (item) {
            return '<label><input type="checkbox" data-check="misc.' + item[0] + '"' + (cfg.misc[item[0]] ? ' checked' : '') + '>' + item[1] + '</label>';
        }).join('');
    }

    function miscPanel() {
        var privacyButton = '<button class="ez-object-btn" type="button" data-privacy-button>' + privacyText() + '</button>';
        return '<div class="ez-page" data-page="misc">' +
            '<div class="ez-misc">' +
                '<section class="ez-box ez-skin-box">' +
                    '<div class="ez-box-head"><b>Skin Changer</b></div>' +
                    '<div class="ez-pet-wrap">' +
                        checkRow("misc.petChanger", "Pet Changer", "", "", '<button class="ez-pet-button" type="button" data-pet-button>None</button>') +
                        '<div class="ez-pet-pop" data-pet-menu><div class="ez-empty">Pet list is loading</div></div>' +
                    '</div>' +
                    '<div class="ez-divider"></div>' +
                    '<div class="ez-skin-list" data-skin-list><div class="ez-empty">Skin list is loading</div></div>' +
                '</section>' +
                '<section class="ez-box ez-utility-box">' +
                    '<div class="ez-box-head"><b>Utility</b></div>' +
                    checkRow("misc.hitbox", "Hitbox", "misc.hitboxColor") +
                    checkRow("extra.foodTimer", "Food Timer") +
                    checkRow("misc.spectatorList", "Spectator List") +
                    '<div class="ez-divider"></div>' +
                    checkRow("misc.privacy", "Protected", "", "", privacyButton) +
                    '<div class="ez-object-pop ez-privacy-pop" data-privacy-menu>' + privacyMenu() + '</div>' +
                    '<div class="ez-divider"></div>' +
                    bindCheckRow("misc.autoHit", "Auto Hit", "autoHit") +
                    bindCheckRow("misc.reaperTargets", "Reapers Only", "reapersOnly") +
                    '<div class="ez-divider"></div>' +
                    checkRow("misc.deathLog", "Death Log") +
                    rangeRow("misc.deathTime", "Remove after", 10, 180, 5, " s") +
                    checkRow("misc.killLog", "Kill Log") +
                    rangeRow("misc.killTime", "Remove after", 10, 180, 5, " s") +
                    '<div class="ez-divider"></div>' +
                    hitSoundRow() +
                    '<div class="ez-divider"></div>' +
                    killSoundRow() +
                '</section>' +
            '</div>' +
        '</div>';
    }

    function settingsPanel() {
        return '<div class="ez-page" data-page="settings">' +
            '<div class="ez-settings">' +
                '<section class="ez-group"><h3>Interface</h3>' +
                    '<label class="ez-row"><span>Menu key</span><button class="ez-bind" type="button">' + cfg.settings.bindName + '</button></label>' +
                    checkRow("settings.keyList", "Keybind list") +
                '</section>' +
                '<section class="ez-group"><h3>Keybinds</h3>' +
                    '<div class="ez-bind-line"><span>Menu</span><b data-bind-name>' + cfg.settings.bindName + '</b></div>' +
                    Object.keys(bindFeatures).map(function (name) { return '<div class="ez-bind-line"><span>' + bindFeatures[name].title + '</span><b data-bind-setting="' + name + '">' + bindText(name) + '</b></div>'; }).join('') +
                '</section>' +
                '<section class="ez-group"><h3>Config</h3>' +
                    '<div class="ez-actions">' +
                        '<button type="button" data-action="save">Save</button>' +
                        '<button type="button" data-action="load">Load</button>' +
                        '<button type="button" data-action="reset">Reset</button>' +
                    '</div>' +
                    '<input class="ez-file" type="file" accept="application/json,.json">' +
                '</section>' +
            '</div>' +
        '</div>';
    }

    function addStyle() {
        var style = document.createElement("style");
        style.id = "ez-style";
        style.textContent = [
            "#ez-menu{position:fixed;left:18px;top:58px;width:820px;max-width:calc(100vw - 16px);z-index:2147483646;background:#151619;border:1px solid #373940;border-radius:5px;color:#d7d9de;font:12px Arial,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.46);user-select:none}",
            "#ez-menu *{box-sizing:border-box}",
            "#ez-menu.ez-hide{display:none}",
            "#ez-menu .ez-head{height:34px;display:flex;align-items:center;padding:0 7px 0 11px;border-bottom:1px solid #303238;background:#1a1b1f;cursor:move}",
            "#ez-menu .ez-head b{font-size:12px;letter-spacing:.8px}",
            "#ez-menu .ez-close{margin-left:auto;width:23px;height:23px;border:0;border-radius:3px;background:transparent;color:#858991;font:17px/21px Arial;cursor:pointer}",
            "#ez-menu .ez-close:hover{background:#342126;color:#ff7b88}",
            "#ez-menu .ez-tabs{height:32px;display:flex;align-items:end;padding:0 8px;border-bottom:1px solid #303238;background:#17181c}",
            "#ez-menu .ez-tab{height:27px;min-width:74px;padding:0 13px;border:0;border-bottom:2px solid transparent;background:transparent;color:#858a92;font:11px Arial;cursor:pointer}",
            "#ez-menu .ez-tab:hover{color:#c8cad0}",
            "#ez-menu .ez-tab.ez-active{color:#e2e3e7;border-bottom-color:#8b6bd1}",
            "#ez-menu .ez-page{display:none}",
            "#ez-menu .ez-page.ez-active{display:block}",
            "#ez-menu .ez-work{display:grid;grid-template-columns:1.12fr .88fr;gap:8px;padding:8px}",
            "#ez-menu .ez-box,#ez-menu .ez-group{overflow:hidden;border:1px solid #2e3036;border-radius:4px;background:#18191d}",
            "#ez-menu .ez-box-head{height:32px;padding:0 8px;display:flex;align-items:center;border-bottom:1px solid #2e3036;background:#1e1f24}",
            "#ez-menu .ez-box-head>b{font-size:10px;color:#c1c3ca}",
            "#ez-menu .ez-head-controls{margin-left:auto;display:flex;align-items:center;gap:10px}",
            "#ez-menu .ez-head-toggle{display:flex;align-items:center;gap:6px;color:#bfc2c8;font-size:10px;white-space:nowrap;cursor:pointer}",
            "#ez-menu .ez-subtabs{height:31px;display:flex;padding:4px 5px 0;border-bottom:1px solid #2e3036;background:#1a1b20}",
            "#ez-menu .ez-side-tabs{align-items:center;gap:3px;padding-left:7px;padding-right:6px}",
            "#ez-menu .ez-side-tabs .ez-subtab{flex:0 0 auto;min-width:68px}",
            "#ez-menu .ez-side-tracer{margin-left:auto;display:flex;align-items:center}",
            "#ez-menu .ez-subtab{height:26px;flex:1;padding:0 8px;border:0;border-bottom:2px solid transparent;background:transparent;color:#858990;font:10px Arial;cursor:pointer}",
            "#ez-menu .ez-subtab:hover{color:#c7c9ce}",
            "#ez-menu .ez-subtab.ez-active{color:#e4e5e8;border-bottom-color:#8d70ca}",
            "#ez-menu .ez-subpage{display:none}",
            "#ez-menu .ez-subpage.ez-active{display:block}",
            "#ez-menu .ez-extra-page{max-height:390px;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin}",
            "#ez-menu .ez-extra-item{position:relative;border-bottom:1px solid #303238}",
            "#ez-menu .ez-extra-item:last-child{border-bottom:0}",
            "#ez-menu .ez-extra-item>.ez-range-row{height:29px;padding-left:18px;background:#16171b}",
            "#ez-menu .ez-extra-item>.ez-range-row>span:first-child{color:#858991;font-size:10px}",
            "#ez-menu .ez-mode-btn,#ez-menu .ez-object-btn{height:20px;padding:0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid #44474e;border-radius:3px;background:#22242a;color:#bfc2c8;font:9px Arial;cursor:pointer}",
            "#ez-menu .ez-mode-btn{max-width:86px}",
            "#ez-menu .ez-object-btn{max-width:96px}",
            "#ez-menu .ez-mode-btn:hover,#ez-menu .ez-mode-btn.ez-open,#ez-menu .ez-object-btn:hover,#ez-menu .ez-object-btn.ez-open{border-color:#7660a8;color:#e0d5f8}",
            "#ez-menu .ez-mode-pop,#ez-menu .ez-object-pop{display:none;gap:4px;padding:6px 8px;border-top:1px solid #292b30;background:#141519}",
            "#ez-menu .ez-mode-pop{grid-template-columns:repeat(5,1fr)}",
            "#ez-menu .ez-object-pop{grid-template-columns:repeat(3,1fr)}",
            "#ez-menu .ez-mode-pop.ez-open,#ez-menu .ez-object-pop.ez-open{display:grid}",
            "#ez-menu .ez-mode-pop label,#ez-menu .ez-object-pop label{height:22px;display:flex;align-items:center;gap:5px;color:#aeb1b8;font-size:9px;cursor:pointer}",
            "#ez-menu .ez-mode-pop input,#ez-menu .ez-object-pop input{accent-color:#8d70ca}",
            "#ez-menu .ez-row{height:28px;padding:0 8px 0 9px;display:flex;align-items:center;border-bottom:1px solid #25272c;background:#18191d}",
            "#ez-menu .ez-row:last-child{border-bottom:0}",
            "#ez-menu .ez-row:hover,#ez-menu .ez-range-row:hover{background:#1b1d21}",
            "#ez-menu .ez-tools{margin-left:auto;display:flex;align-items:center;gap:8px}",
            "#ez-menu .ez-space{width:18px}",
            "#ez-menu .ez-check{appearance:none;width:27px;height:14px;margin:0;border:1px solid #4a4d54;border-radius:10px;background:#292b30;cursor:pointer;position:relative}",
            "#ez-menu .ez-check:before{content:'';position:absolute;left:2px;top:2px;width:8px;height:8px;border-radius:50%;background:#858991;transition:.12s}",
            "#ez-menu .ez-check:checked{border-color:#765cae;background:#4b3b6b}",
            "#ez-menu .ez-check:checked:before{left:15px;background:#d5c3ff}",
            "#ez-menu .ez-color{appearance:none;width:18px;height:16px;padding:0;border:1px solid #454850;border-radius:3px;background:none;cursor:pointer}",
            "#ez-menu .ez-color::-webkit-color-swatch-wrapper{padding:2px}",
            "#ez-menu .ez-color::-webkit-color-swatch{border:0;border-radius:1px}",
            "#ez-menu .ez-range-row{height:34px;padding:0 8px 0 9px;display:flex;align-items:center;border-bottom:1px solid #25272c;background:#18191d}",
            "#ez-menu .ez-range-tools{margin-left:auto;display:flex;align-items:center;gap:7px}",
            "#ez-menu input[type=range]{width:112px;height:3px;accent-color:#8d70ca;cursor:pointer}",
            "#ez-menu .ez-range-tools b{min-width:34px;color:#b9a4e8;font-size:9px;text-align:right}",
            "#ez-menu .ez-divider{height:8px;border-top:1px solid #303238;border-bottom:1px solid #25272c;background:#141519}",
            "#ez-menu .ez-settings,#ez-menu .ez-misc{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:8px}",
            "#ez-menu .ez-misc>.ez-box{min-height:410px}",
            "#ez-menu .ez-skin-box{position:relative}",
            "#ez-menu .ez-skin-list{max-height:330px;overflow:auto;scrollbar-width:thin}",
            "#ez-menu .ez-skin-row{min-height:43px;padding:4px 8px;display:grid;grid-template-columns:1fr 58px 145px;align-items:center;gap:7px;border-bottom:1px solid #25272c}",
            "#ez-menu .ez-skin-row>.ez-skin-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#bfc1c7}",
            "#ez-menu .ez-skin-preview{height:34px;display:grid;grid-template-columns:28px 1fr;grid-template-rows:17px 13px;align-items:center;column-gap:4px}",
            "#ez-menu .ez-skin-preview img,#ez-menu .ez-skin-preview em{grid-row:1/3;width:28px;height:28px;object-fit:contain;border-radius:3px;background:#111216;font-style:normal;font-size:8px;display:flex;align-items:center;justify-content:center;color:#8c9098}",
            "#ez-menu .ez-skin-preview small{font-size:7px;line-height:10px;color:#777c85}",
            "#ez-menu .ez-skin-preview small.ez-owned{color:#65ce92}",
            "#ez-menu .ez-skin-row select{width:145px}",
            "#ez-menu .ez-select{height:21px;padding:0 5px;border:1px solid #44474e;border-radius:3px;background:#22242a;color:#c8cad0;font:9px Arial;outline:none}",
            "#ez-menu .ez-tracer-mode{display:flex;align-items:center;gap:4px;margin-right:2px;color:#8f939b;font-size:9px;white-space:nowrap}",
            "#ez-menu .ez-tracer-select{width:58px;max-width:58px;font-size:9px}",
            "#ez-menu .ez-sound-tools{display:flex;align-items:center;gap:4px}",
            "#ez-menu .ez-hit-select{width:116px}",
            "#ez-menu .ez-mini{width:21px;height:21px;padding:0;border:1px solid #41444b;border-radius:3px;background:#23252a;color:#bbbfc7;font:10px Arial;cursor:pointer}",
            "#ez-menu .ez-mini:hover{border-color:#686c75;color:#fff}",
            "#ez-menu .ez-sound-file{display:none}",
            "#ez-menu .ez-utility-box{max-height:410px;overflow-y:auto;overflow-x:hidden}",
            "#ez-menu .ez-empty{padding:14px;color:#767b83;font-size:10px;text-align:center}",
            "#ez-menu .ez-pet-wrap{position:relative}",
            "#ez-menu .ez-pet-button{width:72px;height:21px;padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid #44474e;border-radius:3px;background:#22242a;color:#c8cad0;font:9px Arial;cursor:pointer}",
            "#ez-menu .ez-pet-button img{width:18px;height:18px;object-fit:contain;vertical-align:middle;margin-right:4px}",
            "#ez-menu .ez-pet-pop{display:none;grid-template-columns:repeat(7,1fr);gap:4px;max-height:235px;padding:7px;overflow:auto;border-top:1px solid #292b30;background:#141519}",
            "#ez-menu .ez-pet-pop.ez-open{display:grid}",
            "#ez-menu .ez-pet-choice{min-height:50px;padding:2px;border:1px solid #34373e;border-radius:3px;background:#202228;color:#aeb1b8;font:8px Arial;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px}",
            "#ez-menu .ez-pet-choice:hover,#ez-menu .ez-pet-choice.ez-active{border-color:#8067b5;background:#282331}",
            "#ez-menu .ez-pet-choice img{width:28px;height:28px;object-fit:contain}",
            "#ez-menu .ez-pet-choice small{font-size:7px;color:#7f838b}",
            "#ez-menu .ez-pet-choice small.ez-owned{color:#65ce92}",
            "#ez-menu .ez-settings .ez-group:last-child{grid-column:1/-1}",
            "#ez-menu .ez-group h3{height:28px;margin:0;padding:0 9px;display:flex;align-items:center;border-bottom:1px solid #2e3036;background:#1e1f24;font-size:10px;color:#bec1c8}",
            "#ez-menu .ez-bind,#ez-menu .ez-feature-bind{min-width:58px;height:21px;padding:0 6px;border:1px solid #44474e;border-radius:3px;background:#23252a;color:#d4d6db;font:9px Arial;cursor:pointer}",
            "#ez-menu .ez-feature-bind{max-width:68px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
            "#ez-menu .ez-feature-bind.ez-wait{border-color:#8b6bd1;color:#d9c7ff}",
            "#ez-menu .ez-bind.ez-wait{border-color:#8b6bd1;color:#d9c7ff}",
            "#ez-menu .ez-bind-line{height:30px;padding:0 9px;display:flex;align-items:center}",
            "#ez-menu .ez-bind-line b{margin-left:auto;color:#cdb8ff;font-size:10px}",
            "#ez-menu .ez-bind-line.ez-on span:before{content:'';display:inline-block;width:6px;height:6px;margin-right:6px;border-radius:50%;background:#62d58c;box-shadow:0 0 6px rgba(98,213,140,.7)}",
            "#ez-menu .ez-bind-line:not(.ez-on) span:before{content:'';display:inline-block;width:6px;height:6px;margin-right:6px;border-radius:50%;background:#555961}",
            "#ez-menu .ez-actions{padding:9px;display:flex;gap:6px}",
            "#ez-menu .ez-actions button{height:25px;min-width:58px;padding:0 10px;border:1px solid #3b3e45;border-radius:3px;background:#202228;color:#d0d2d7;font:10px Arial;cursor:pointer}",
            "#ez-menu .ez-actions button:hover{border-color:#6f5a9f;background:#27242f}",
            "#ez-menu .ez-file{display:none}",
            "#ez-bind-pop{position:fixed;display:none;z-index:2147483647;min-width:104px;padding:4px;border:1px solid #3c3f46;border-radius:4px;background:#17181c;box-shadow:0 7px 20px rgba(0,0,0,.45);font:9px Arial}",
            "#ez-bind-pop.ez-open{display:block}",
            "#ez-bind-pop button{display:block;width:100%;height:24px;padding:0 7px;border:0;border-radius:2px;background:transparent;color:#c8cad0;text-align:left;font:9px Arial;cursor:pointer}",
            "#ez-bind-pop button:hover{background:#282a30}",
            "#ez-deaths{position:fixed;left:12px;bottom:72px;width:196px;z-index:2147483644;border:1px solid #34363c;border-radius:4px;background:rgba(18,19,22,.9);color:#c9cbd0;font:10px Arial;user-select:none}",
            "#ez-deaths.ez-hide{display:none}",
            "#ez-deaths>b{height:25px;padding:0 8px;display:flex;align-items:center;border-bottom:1px solid #303238;cursor:move}",
            "#ez-deaths .ez-death-row{height:39px;padding:4px 6px;display:flex;align-items:center;gap:7px;border-bottom:1px solid #292b30}",
            "#ez-deaths .ez-death-row:last-child{border-bottom:0}",
            "#ez-deaths .ez-death-row img,#ez-deaths .ez-death-avatar{width:30px;height:30px;border-radius:50%;object-fit:contain;background:#25272c}",
            "#ez-deaths .ez-death-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}",
            "#ez-deaths .ez-death-time{color:#b9a4e8;font-size:9px}",
            "#ez-kills{position:fixed;left:12px;bottom:260px;width:196px;z-index:2147483644;border:1px solid #34363c;border-radius:4px;background:rgba(18,19,22,.9);color:#c9cbd0;font:10px Arial;user-select:none}",
            "#ez-kills.ez-hide{display:none}",
            "#ez-kills>b{height:25px;padding:0 8px;display:flex;align-items:center;border-bottom:1px solid #303238;cursor:move}",
            "#ez-kills .ez-kill-row{height:39px;padding:4px 6px;display:flex;align-items:center;gap:7px;border-bottom:1px solid #292b30}",
            "#ez-kills .ez-kill-row:last-child{border-bottom:0}",
            "#ez-kills .ez-kill-row img,#ez-kills .ez-kill-avatar{width:30px;height:30px;border-radius:50%;object-fit:contain;background:#25272c}",
            "#ez-kills .ez-kill-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}",
            "#ez-kills .ez-kill-time{color:#7edca7;font-size:9px}",
            "#ez-spectators{position:fixed;left:220px;bottom:72px;width:230px;z-index:2147483644;border:1px solid #34363c;border-radius:4px;background:rgba(18,19,22,.92);color:#c9cbd0;font:10px Arial;user-select:none;overflow:hidden}",
            "#ez-spectators.ez-hide{display:none}",
            "#ez-spectators>b{height:25px;padding:0 8px;display:flex;align-items:center;border-bottom:1px solid #303238;cursor:move}",
            "#ez-spectators .ez-spec-row{height:39px;padding:4px 6px;display:flex;align-items:center;gap:7px;border-bottom:1px solid #292b30}",
            "#ez-spectators .ez-spec-row:last-child{border-bottom:0}",
            "#ez-spectators .ez-spec-row img,#ez-spectators .ez-spec-avatar{width:30px;height:30px;border-radius:50%;object-fit:contain;background:#25272c;flex:0 0 30px}",
            "#ez-spectators .ez-spec-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}",
            "#ez-spectators .ez-spec-time{color:#b9a4e8;white-space:nowrap;font-size:9px}",
            "body.ez-spec-active #spectatorsList{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important}",
            "#ez-layer{position:fixed;display:none;pointer-events:none;z-index:2147483000}",
            "#ez-skill-clock{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:20;display:none;min-width:30px;height:21px;padding:0 4px;align-items:center;justify-content:center;pointer-events:none;border:1px solid rgba(255,255,255,.32);border-radius:4px;background:rgba(0,0,0,.62);color:#fff;text-shadow:0 1px 2px #000;font:700 13px/1 Arial,sans-serif}",
            "#ez-keys{position:fixed;right:12px;top:72px;width:188px;z-index:2147483645;border:1px solid #34363c;border-radius:4px;background:rgba(18,19,22,.9);color:#c9cbd0;font:10px Arial,sans-serif;pointer-events:auto;user-select:none}",
            "#ez-keys.ez-hide{display:none}",
            "#ez-keys b{height:24px;padding:0 8px;display:flex;align-items:center;border-bottom:1px solid #303238;font-size:10px;cursor:move}",
            "#ez-keys div{height:25px;padding:0 8px;display:flex;align-items:center;pointer-events:none;border-bottom:1px solid rgba(48,50,56,.6)}",
            "#ez-keys div span:first-child:before{content:'';display:inline-block;width:6px;height:6px;margin-right:6px;border-radius:50%;background:#555961}",
            "#ez-keys div.ez-on span:first-child:before{background:#62d58c;box-shadow:0 0 6px rgba(98,213,140,.65)}",
            "#ez-keys span:last-child{margin-left:auto;color:#cdb8ff}",
            "@media(max-width:780px){#ez-menu{left:8px;top:8px;width:calc(100vw - 16px);max-height:calc(100vh - 16px);overflow:auto}#ez-menu .ez-work{grid-template-columns:1fr}}"
        ].join("");
        document.head.appendChild(style);
    }

    function syncMenu() {
        if (!menu) return;
        menu.querySelectorAll("[data-check]").forEach(function (input) {
            input.checked = !!read(input.getAttribute("data-check"));
        });
        menu.querySelectorAll("[data-color]").forEach(function (input) {
            input.value = read(input.getAttribute("data-color"));
        });
        menu.querySelectorAll("[data-range]").forEach(function (input) {
            input.value = read(input.getAttribute("data-range"));
        });
        menu.querySelectorAll("[data-range-value]").forEach(function (node) {
            var path = node.getAttribute("data-range-value");
            node.textContent = read(path) + rangeSuffix(path);
        });
        if (keyBox) keyBox.textContent = cfg.settings.bindName;
        menu.querySelectorAll("[data-bind-name]").forEach(function (node) {
            node.textContent = cfg.settings.bindName;
        });
        menu.querySelectorAll("[data-feature-bind]").forEach(function (node) {
            node.textContent = bindText(node.getAttribute("data-feature-bind"));
            node.classList.remove("ez-wait");
        });
        menu.querySelectorAll("[data-bind-setting]").forEach(function (node) {
            node.textContent = bindText(node.getAttribute("data-bind-setting"));
        });
        renderSoundOptions();
        menu.querySelectorAll("[data-select]").forEach(function (node) {
            node.value = read(node.getAttribute("data-select"));
        });
        var privacyButton = menu.querySelector("[data-privacy-button]");
        if (privacyButton) privacyButton.textContent = privacyText();
        menu.querySelectorAll("[data-mode-button]").forEach(function (button) {
            button.textContent = modeText(button.getAttribute("data-mode-button"));
        });
        menu.querySelectorAll("[data-object-button]").forEach(function (button) {
            button.textContent = objectText();
        });
        if (window.EZSQUAD) window.EZSQUAD.settings = cfg;
        updateKeys();
        updateDeathPanel();
        updateKillPanel();
        updateSpectatorPanel(Date.now());
        updatePetButton();
        cardBank.clear();
        worldDirty = true;
    }

    function updateKeys() {
        if (!keysPanel) return;
        keysPanel.classList.toggle("ez-hide", !cfg.settings.keyList);
        var holder = keysPanel.querySelector("[data-key-list]");
        if (!holder) return;
        var menuOpen = !!(menu && !menu.classList.contains("ez-hide"));
        var body = '<div class="' + (menuOpen ? 'ez-on' : '') + '"><span>Menu</span><span>' + cfg.settings.bindName + ' · ' + (menuOpen ? 'Open' : 'Closed') + '</span></div>';
        Object.keys(bindFeatures).forEach(function (name) {
            var item = cfg.binds[name];
            if (!item || !item.key) return;
            var active = !!read(bindFeatures[name].path);
            body += '<div class="' + (active ? 'ez-on' : '') + '"><span>' + bindFeatures[name].title + '</span><span>' + item.name + ' · ' + (item.mode === 'hold' ? 'Hold' : 'Toggle') + ' · ' + (active ? 'ON' : 'OFF') + '</span></div>';
        });
        if (holder.innerHTML !== body) holder.innerHTML = body;

        if (menu) {
            menu.querySelectorAll("[data-bind-setting]").forEach(function (node) {
                var name = node.getAttribute("data-bind-setting");
                var feature = bindFeatures[name];
                var line = node.closest(".ez-bind-line");
                if (line && feature) line.classList.toggle("ez-on", !!read(feature.path));
            });
        }
    }

    function placeData(node, x, y) {
        if (!node) return null;
        var width = Math.max(0, Number(node.offsetWidth) || 0);
        var height = Math.max(0, Number(node.offsetHeight) || 0);
        var maxX = Math.max(0, innerWidth - width);
        var maxY = Math.max(0, innerHeight - height);
        x = clamp(Number(x) || 0, 0, maxX);
        y = clamp(Number(y) || 0, 0, maxY);
        return {
            x: Math.round(x),
            y: Math.round(y),
            rx: maxX > 0 ? x / maxX : 0,
            ry: maxY > 0 ? y / maxY : 0,
            vw: innerWidth,
            vh: innerHeight,
            w: Math.round(width),
            h: Math.round(height)
        };
    }

    function readPlace(key) {
        if (!key) return null;
        try { return JSON.parse(localStorage.getItem(key) || "null"); } catch (e) { return null; }
    }

    function nodePlace(node, key) {
        if (!node) return null;
        var x = parseFloat(node.style.left);
        var y = parseFloat(node.style.top);
        if (!Number.isFinite(x) || !Number.isFinite(y)) {
            var rect = null;
            try { rect = node.getBoundingClientRect(); } catch (e) {}
            if (rect && (rect.width || rect.height || node.offsetWidth || node.offsetHeight)) {
                x = rect.left;
                y = rect.top;
            } else {
                var saved = readPlace(key);
                if (saved) return saved;
            }
        }
        return Number.isFinite(x) && Number.isFinite(y) ? placeData(node, x, y) : null;
    }

    function saveNodePlace(node, key) {
        if (!node || !key) return;
        var pos = nodePlace(node, null);
        if (!pos) return;
        try { localStorage.setItem(key, JSON.stringify(pos)); } catch (e) {}
    }

    function resolvedPlace(node, pos) {
        if (!node || !pos) return null;
        var width = Math.max(0, Number(node.offsetWidth) || Number(pos.w) || 0);
        var height = Math.max(0, Number(node.offsetHeight) || Number(pos.h) || 0);
        var maxX = Math.max(0, innerWidth - width);
        var maxY = Math.max(0, innerHeight - height);
        var rx = Number(pos.rx);
        var ry = Number(pos.ry);
        if (!Number.isFinite(rx) && Number.isFinite(Number(pos.x)) && Number.isFinite(Number(pos.vw))) {
            var oldMaxX = Math.max(1, Number(pos.vw) - (Number(pos.w) || width));
            rx = Number(pos.x) / oldMaxX;
        }
        if (!Number.isFinite(ry) && Number.isFinite(Number(pos.y)) && Number.isFinite(Number(pos.vh))) {
            var oldMaxY = Math.max(1, Number(pos.vh) - (Number(pos.h) || height));
            ry = Number(pos.y) / oldMaxY;
        }
        var x = Number.isFinite(rx) ? clamp(rx, 0, 1) * maxX : Number(pos.x);
        var y = Number.isFinite(ry) ? clamp(ry, 0, 1) * maxY : Number(pos.y);
        if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
        return placeData(node, x, y);
    }

    function activeValue(selector, attr, fallback) {
        if (!menu) return fallback;
        var node = menu.querySelector(selector + ".ez-active");
        return node ? node.getAttribute(attr) || fallback : fallback;
    }

    function layoutState() {
        return {
            viewport: { width: innerWidth, height: innerHeight },
            menu: nodePlace(menu, placeKey),
            keybinds: nodePlace(keysPanel, keysPlaceKey),
            deaths: nodePlace(deathPanel, deathPosKey),
            kills: nodePlace(killPanel, killPosKey),
            spectators: nodePlace(spectatorPanel, spectatorPosKey),
            tabs: {
                main: activeValue(".ez-tab", "data-tab", "esp"),
                players: activeValue('.ez-subtab[data-scope="visual"]', "data-subtab", "all"),
                side: activeValue('.ez-subtab[data-scope="side"]', "data-subtab", "extra")
            }
        };
    }

    function putNode(node, pos, key) {
        if (!node || !pos) return;
        var next = resolvedPlace(node, pos);
        if (!next) return;
        node.style.right = "auto";
        node.style.bottom = "auto";
        node.style.left = clamp(next.x, 4, Math.max(4, innerWidth - node.offsetWidth - 4)) + "px";
        node.style.top = clamp(next.y, 4, Math.max(4, innerHeight - node.offsetHeight - 4)) + "px";
        if (key) saveNodePlace(node, key);
    }

    function selectTab(scope, value) {
        if (!menu || !value) return;
        var buttons;
        var pages;
        var buttonAttr;
        var pageAttr;
        if (scope === "main") {
            buttons = menu.querySelectorAll(".ez-tab");
            pages = menu.querySelectorAll(".ez-page");
            buttonAttr = "data-tab";
            pageAttr = "data-page";
        } else {
            buttons = menu.querySelectorAll('.ez-subtab[data-scope="' + scope + '"]');
            pages = menu.querySelectorAll('.ez-subpage[data-scope="' + scope + '"]');
            buttonAttr = "data-subtab";
            pageAttr = "data-subpage";
        }
        buttons.forEach(function (node) { node.classList.toggle("ez-active", node.getAttribute(buttonAttr) === value); });
        pages.forEach(function (node) { node.classList.toggle("ez-active", node.getAttribute(pageAttr) === value); });
    }

    function applyLayout(layout) {
        if (!layout || typeof layout !== "object") return;
        var viewport = layout.viewport || null;
        function withViewport(pos) {
            if (!pos || !viewport || Number.isFinite(Number(pos.vw)) || Number.isFinite(Number(pos.rx))) return pos;
            var next = {};
            Object.keys(pos).forEach(function (key) { next[key] = pos[key]; });
            next.vw = Number(viewport.width);
            next.vh = Number(viewport.height);
            return next;
        }
        putNode(menu, withViewport(layout.menu), placeKey);
        putNode(keysPanel, withViewport(layout.keybinds), keysPlaceKey);
        putNode(deathPanel, withViewport(layout.deaths), deathPosKey);
        putNode(killPanel, withViewport(layout.kills), killPosKey);
        putNode(spectatorPanel, withViewport(layout.spectators), spectatorPosKey);
        if (layout.tabs) {
            selectTab("main", layout.tabs.main);
            selectTab("visual", layout.tabs.players);
            selectTab("side", layout.tabs.side);
        }
    }

    function pullSkins() {
        if (!cfg.misc.skinPick || typeof cfg.misc.skinPick !== "object") cfg.misc.skinPick = {};
        if (!menu) return;
        menu.querySelectorAll("[data-skin-select]").forEach(function (select) {
            var name = select.getAttribute("data-skin-select");
            if (name) cfg.misc.skinPick[name] = Number(select.value) || 0;
        });
    }

    function saveFile() {
        pullSkins();
        saveCfg();
        exportSounds().then(function (sounds) {
            try {
                var blob = new Blob([JSON.stringify({ version: 30, settings: clone(cfg), layout: layoutState(), sounds: sounds }, null, 2)], { type: "application/json" });
                var url = URL.createObjectURL(blob);
                var link = document.createElement("a");
                link.href = url;
                link.download = "ezsquad-config.json";
                document.body.appendChild(link);
                link.click();
                link.remove();
                setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e) {} }, 60000);
            } catch (e) {}
        });
    }

    function loadFile(file) {
        if (!file) return;
        var reader = new FileReader();
        reader.onload = function () {
            try {
                var value = JSON.parse(String(reader.result || ""));
                var source = value && value.settings ? value.settings : value;
                cfg = merge(clone(defaults), source);
                saveCfg();
                var done = value && value.sounds ? importSounds(value.sounds) : Promise.resolve();
                done.then(function () {
                    syncMenu();
                    renderSkinPanel(true);
                    if (value && value.layout) requestAnimationFrame(function () { applyLayout(value.layout); });
                });
            } catch (e) {}
            if (fileBox) fileBox.value = "";
        };
        reader.readAsText(file);
    }

    function resetCfg() {
        clearTimeout(fightAttack);
        clearTimeout(fightRelease);
        cfg = clone(defaults);
        saveCfg();
        syncMenu();
        renderSkinPanel(true);
    }

    function makeKeys() {
        keysPanel = document.createElement("div");
        keysPanel.id = "ez-keys";
        keysPanel.innerHTML = '<b>Keybinds</b><section data-key-list></section>';
        document.body.appendChild(keysPanel);

        putNode(keysPanel, readPlace(keysPlaceKey), null);

        var head = keysPanel.querySelector("b");
        var moving = false;
        var dx = 0;
        var dy = 0;
        head.addEventListener("pointerdown", function (event) {
            moving = true;
            dx = event.clientX - keysPanel.offsetLeft;
            dy = event.clientY - keysPanel.offsetTop;
            head.setPointerCapture(event.pointerId);
            event.preventDefault();
        });
        head.addEventListener("pointermove", function (event) {
            if (!moving) return;
            keysPanel.style.right = "auto";
            keysPanel.style.left = clamp(event.clientX - dx, 4, innerWidth - keysPanel.offsetWidth - 4) + "px";
            keysPanel.style.top = clamp(event.clientY - dy, 4, innerHeight - 24) + "px";
        });
        head.addEventListener("pointerup", function (event) {
            if (!moving) return;
            moving = false;
            try { head.releasePointerCapture(event.pointerId); } catch (e) {}
            saveNodePlace(keysPanel, keysPlaceKey);
        });
        updateKeys();
    }

    function makeMenu() {
        addStyle();
        var node = document.createElement("div");
        node.id = "ez-menu";
        node.innerHTML = '<div class="ez-head"><b>EZSQUAD | https://t.me/evoezsquad</b><button class="ez-close" type="button" title="Unload">×</button></div>' +
            '<div class="ez-tabs"><button class="ez-tab ez-active" type="button" data-tab="esp">Visuals</button><button class="ez-tab" type="button" data-tab="misc">Misc</button><button class="ez-tab" type="button" data-tab="settings">Settings</button></div>' +
            espPanel() + miscPanel() + settingsPanel();
        document.body.appendChild(node);

        keyBox = node.querySelector(".ez-bind");
        fileBox = node.querySelector(".ez-file");
        soundFileBox = node.querySelector('[data-sound-file="hit"]');
        killSoundFileBox = node.querySelector('[data-sound-file="kill"]');
        makeBindPop();
        makeDeathPanel();
        makeKillPanel();

        putNode(node, readPlace(placeKey), null);

        node.addEventListener("click", function (event) {
            var tab = event.target.getAttribute("data-tab");
            if (tab) {
                node.querySelectorAll(".ez-tab").forEach(function (button) {
                    button.classList.toggle("ez-active", button.getAttribute("data-tab") === tab);
                });
                node.querySelectorAll(".ez-page").forEach(function (page) {
                    page.classList.toggle("ez-active", page.getAttribute("data-page") === tab);
                });
                return;
            }

            var sub = event.target.getAttribute("data-subtab");
            if (sub) {
                var scope = event.target.getAttribute("data-scope");
                node.querySelectorAll('.ez-subtab[data-scope="' + scope + '"]').forEach(function (button) {
                    button.classList.toggle("ez-active", button.getAttribute("data-subtab") === sub);
                });
                node.querySelectorAll('.ez-subpage[data-scope="' + scope + '"]').forEach(function (page) {
                    page.classList.toggle("ez-active", page.getAttribute("data-subpage") === sub);
                });
                if (scope === "side") {
                    var extraTracer = node.querySelector("[data-extra-tracer]");
                    if (extraTracer) extraTracer.style.display = sub === "extra" ? "flex" : "none";
                }
                return;
            }

            var petButton = event.target.closest && event.target.closest("[data-pet-button]");
            if (petButton) {
                event.preventDefault();
                var petMenu = node.querySelector("[data-pet-menu]");
                if (petMenu) petMenu.classList.toggle("ez-open");
                return;
            }

            var petChoice = event.target.closest && event.target.closest("[data-pet-id]");
            if (petChoice) {
                choosePet(Number(petChoice.getAttribute("data-pet-id")) || 0);
                var petList = node.querySelector("[data-pet-menu]");
                if (petList) petList.classList.remove("ez-open");
                return;
            }

            if (event.target.hasAttribute && event.target.hasAttribute("data-privacy-button")) {
                event.preventDefault();
                event.stopPropagation();
                var privacyMenu = node.querySelector("[data-privacy-menu]");
                if (privacyMenu) privacyMenu.classList.toggle("ez-open");
                return;
            }

            var featureBind = event.target.getAttribute("data-feature-bind");
            if (featureBind) {
                event.preventDefault();
                event.stopPropagation();
                beginFeatureBind(featureBind, event.target);
                return;
            }

            var mode = event.target.getAttribute("data-mode-button");
            var objectMenuName = event.target.getAttribute("data-object-button");
            if (mode || objectMenuName) {
                event.preventDefault();
                event.stopPropagation();
                var selector = mode ? '[data-mode-menu="' + mode + '"]' : '[data-object-menu="' + objectMenuName + '"]';
                var box = node.querySelector(selector);
                var open = box && !box.classList.contains("ez-open");
                node.querySelectorAll(".ez-mode-pop.ez-open,.ez-object-pop.ez-open").forEach(function (item) { item.classList.remove("ez-open"); });
                node.querySelectorAll(".ez-mode-btn.ez-open,.ez-object-btn.ez-open").forEach(function (item) { item.classList.remove("ez-open"); });
                if (open) {
                    box.classList.add("ez-open");
                    event.target.classList.add("ez-open");
                }
                return;
            }

            var soundAction = event.target.getAttribute("data-sound-action");
            if (soundAction) {
                var soundKind = event.target.getAttribute("data-sound-kind") === "kill" ? "kill" : "hit";
                if (soundAction === "upload") {
                    soundUploadKind = soundKind;
                    var soundInput = soundKind === "kill" ? killSoundFileBox : soundFileBox;
                    if (soundInput) soundInput.click();
                    return;
                }
                if (soundAction === "play") { playSound(soundKind); return; }
                if (soundAction === "remove") {
                    var chosen = selectedSound(soundKind);
                    if (chosen) {
                        removeSoundRow(chosen.id).then(function () {
                            soundBuffers.delete(chosen.id);
                            if (cfg.misc.hitSoundType === "custom:" + chosen.id) cfg.misc.hitSoundType = "Click";
                            if (cfg.misc.killSoundType === "custom:" + chosen.id) cfg.misc.killSoundType = "Bell";
                            saveCfg();
                            refreshCustomSounds();
                        });
                    }
                    return;
                }
            }

            var action = event.target.getAttribute("data-action");
            if (action === "save") saveFile();
            else if (action === "load" && fileBox) fileBox.click();
            else if (action === "reset") resetCfg();
        });

        node.addEventListener("input", function (event) {
            var color = event.target.getAttribute("data-color");
            if (color) write(color, event.target.value);
            var range = event.target.getAttribute("data-range");
            if (range) {
                var value = Number(event.target.value);
                write(range, value);
                var out = node.querySelector('[data-range-value="' + range + '"]');
                if (out) out.textContent = value + rangeSuffix(range);
                if (range === "misc.deathTime") updateDeathPanel(Date.now());
                if (range === "misc.killTime") updateKillPanel(Date.now());
            }
        });

        node.addEventListener("change", function (event) {
            var select = event.target.getAttribute("data-select");
            if (select) write(select, event.target.value);
            var skinName = event.target.getAttribute("data-skin-select");
            if (skinName) chooseSkin(skinName, Number(event.target.value) || 0);
            var path = event.target.getAttribute("data-check");
            if (path) {
                write(path, event.target.checked);
                if (path.indexOf("extra.") === 0) {
                    var match = path.match(/^extra\.([A-Za-z]+)(Box|Tracer|Name|Distance|Image)$/);
                    if (match) {
                        var button = node.querySelector('[data-mode-button="' + match[1] + '"]');
                        if (button) button.textContent = modeText(match[1]);
                    }
                    if (/^extra\.others(Gem|ArmorPotion|WaterDrop|Roots|Fireball|PlasmaBall|Shuriken|Snowball|Bomb|BearTrap)$/.test(path)) {
                        var objectButton = node.querySelector('[data-object-button="others"]');
                        if (objectButton) objectButton.textContent = objectText();
                    }
                }
                if (path.indexOf("misc.privacy") === 0) {
                    var button = node.querySelector("[data-privacy-button]");
                    if (button) button.textContent = privacyText();
                }
                updateDeathPanel();
                updateKillPanel();
            }
            if (event.target === fileBox) loadFile(fileBox.files && fileBox.files[0]);
            if (event.target === soundFileBox || event.target === killSoundFileBox) {
                var kind = event.target === killSoundFileBox ? "kill" : "hit";
                uploadSound(event.target.files && event.target.files[0], kind);
                event.target.value = "";
            }
        });

        node.addEventListener("contextmenu", function (event) {
            var button = event.target.closest && event.target.closest("[data-feature-bind]");
            if (!button) return;
            event.preventDefault();
            openBindPop(button.getAttribute("data-feature-bind"), event.clientX, event.clientY);
        });

        node.addEventListener("pointerdown", ensureAudio, { passive: true });

        keyBox.addEventListener("click", function () {
            bindMode = true;
            keyBox.textContent = "Press key";
            keyBox.classList.add("ez-wait");
        });

        node.querySelector(".ez-close").addEventListener("click", stop);

        var head = node.querySelector(".ez-head");
        var moving = false;
        var dx = 0;
        var dy = 0;

        head.addEventListener("pointerdown", function (event) {
            if (event.target.closest("button,input")) return;
            moving = true;
            dx = event.clientX - node.offsetLeft;
            dy = event.clientY - node.offsetTop;
            head.setPointerCapture(event.pointerId);
        });

        head.addEventListener("pointermove", function (event) {
            if (!moving) return;
            node.style.left = clamp(event.clientX - dx, 4, innerWidth - node.offsetWidth - 4) + "px";
            node.style.top = clamp(event.clientY - dy, 4, innerHeight - 34) + "px";
        });

        head.addEventListener("pointerup", function (event) {
            if (!moving) return;
            moving = false;
            try { head.releasePointerCapture(event.pointerId); } catch (e) {}
            saveNodePlace(node, placeKey);
        });

        return node;
    }

    function beginFeatureBind(name, button) {
        if (!cfg.binds[name]) return;
        bindPick = { name: name, button: button };
        button.textContent = "Press key";
        button.classList.add("ez-wait");
    }

    function cancelFeatureBind() {
        if (!bindPick) return;
        if (bindPick.button) {
            bindPick.button.textContent = bindText(bindPick.name);
            bindPick.button.classList.remove("ez-wait");
        }
        bindPick = null;
    }

    function useFeatureKey(name, down, repeat) {
        var bind = cfg.binds[name];
        var feature = bindFeatures[name];
        if (!bind || !feature || !bind.key) return false;
        if (bind.mode === "hold") {
            if (down && !repeat) {
                holdKeys[name] = true;
                write(feature.path, true);
                syncMenu();
            } else if (!down && holdKeys[name]) {
                delete holdKeys[name];
                write(feature.path, false);
                syncMenu();
            }
        } else if (down && !repeat) {
            write(feature.path, !read(feature.path));
            syncMenu();
        }
        return true;
    }

    function onKey(event) {
        var code = keyCode(event);

        if (bindMode) {
            event.preventDefault();
            event.stopPropagation();
            if (code === "Escape") {
                bindMode = false;
                keyBox.textContent = cfg.settings.bindName;
                keyBox.classList.remove("ez-wait");
                return;
            }
            cfg.settings.bind = fixKey(code);
            cfg.settings.bindName = keyName(event);
            bindMode = false;
            keyBox.classList.remove("ez-wait");
            saveCfg();
            syncMenu();
            return;
        }

        if (sameKey(cfg.settings.bind, event) && !event.repeat) {
            event.preventDefault();
            event.stopPropagation();
            if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
            cancelFeatureBind();
            if (bindPop) bindPop.classList.remove("ez-open");
            if (document.activeElement && typeof document.activeElement.blur === "function") document.activeElement.blur();
            if (menu) menu.classList.toggle("ez-hide");
            syncScanIcon();
            updateKeys();
            return;
        }

        if (bindPick) {
            event.preventDefault();
            event.stopPropagation();
            if (code === "Escape") {
                cancelFeatureBind();
                return;
            }
            cfg.binds[bindPick.name].key = fixKey(code);
            cfg.binds[bindPick.name].name = keyName(event);
            saveCfg();
            cancelFeatureBind();
            syncMenu();
            return;
        }

        if (code === "Space" && !event.repeat) {
            var active = document.activeElement;
            var tag = active && active.tagName ? active.tagName.toLowerCase() : "";
            if (tag !== "input" && tag !== "textarea" && tag !== "select") noteSkillUse(performance.now());
        }

        Object.keys(bindFeatures).some(function (name) {
            var bind = cfg.binds[name];
            if (!bind || !sameKey(bind.key, event)) return false;
            event.preventDefault();
            event.stopPropagation();
            useFeatureKey(name, true, event.repeat);
            return true;
        });
    }

    function onKeyUp(event) {
        Object.keys(bindFeatures).some(function (name) {
            var bind = cfg.binds[name];
            if (!bind || !sameKey(bind.key, event) || bind.mode !== "hold") return false;
            event.preventDefault();
            useFeatureKey(name, false, false);
            return true;
        });
    }

    function makeBindPop() {
        bindPop = document.createElement("div");
        bindPop.id = "ez-bind-pop";
        bindPop.innerHTML = '<button type="button" data-bind-mode="toggle">Toggle</button><button type="button" data-bind-mode="hold">Hold</button><button type="button" data-bind-mode="none">Unload key</button>';
        document.body.appendChild(bindPop);
        bindPop.addEventListener("click", function (event) {
            var mode = event.target.getAttribute("data-bind-mode");
            var name = bindPop.getAttribute("data-feature");
            if (!mode || !name || !cfg.binds[name]) return;
            if (mode === "none") {
                cfg.binds[name].key = "";
                cfg.binds[name].name = "None";
                delete holdKeys[name];
            } else {
                cfg.binds[name].mode = mode;
            }
            saveCfg();
            bindPop.classList.remove("ez-open");
            syncMenu();
        });
        document.addEventListener("pointerdown", closeBindPop, true);
    }

    function openBindPop(name, x, y) {
        if (!bindPop || !cfg.binds[name]) return;
        bindPop.setAttribute("data-feature", name);
        bindPop.style.left = clamp(x, 4, innerWidth - 112) + "px";
        bindPop.style.top = clamp(y, 4, innerHeight - 82) + "px";
        bindPop.classList.add("ez-open");
    }

    function closeBindPop(event) {
        if (!bindPop || !bindPop.classList.contains("ez-open")) return;
        if (event && (bindPop.contains(event.target) || (event.target.closest && event.target.closest("[data-feature-bind]")))) return;
        bindPop.classList.remove("ez-open");
    }

    function makeDeathPanel() {
        deathPanel = document.createElement("div");
        deathPanel.id = "ez-deaths";
        deathPanel.innerHTML = '<b>Death log</b><div data-death-list></div>';
        document.body.appendChild(deathPanel);
        deathList = deathPanel.querySelector("[data-death-list]");
        putNode(deathPanel, readPlace(deathPosKey), null);
        var head = deathPanel.querySelector("b");
        var moving = false, dx = 0, dy = 0;
        head.addEventListener("pointerdown", function (event) {
            moving = true;
            dx = event.clientX - deathPanel.offsetLeft;
            dy = event.clientY - deathPanel.offsetTop;
            head.setPointerCapture(event.pointerId);
            event.preventDefault();
        });
        head.addEventListener("pointermove", function (event) {
            if (!moving) return;
            deathPanel.style.bottom = "auto";
            deathPanel.style.left = clamp(event.clientX - dx, 4, innerWidth - deathPanel.offsetWidth - 4) + "px";
            deathPanel.style.top = clamp(event.clientY - dy, 4, innerHeight - 25) + "px";
        });
        head.addEventListener("pointerup", function (event) {
            if (!moving) return;
            moving = false;
            try { head.releasePointerCapture(event.pointerId); } catch (e) {}
            saveNodePlace(deathPanel, deathPosKey);
        });
        updateDeathPanel();
    }

    function updateDeathPanel(now) {
        if (!deathPanel) return;
        deathPanel.classList.toggle("ez-hide", !cfg.misc.deathLog);
        if (!cfg.misc.deathLog || !deathList) return;
        now = Number(now) || Date.now();
        var life = clamp(Number(cfg.misc.deathTime) || 60, 10, 180) * 1000;
        deathEntries = deathEntries.filter(function (item) {
            var born = Number(item.at) || Math.max(0, Number(item.until) - life);
            item.at = born;
            return born + life > now;
        }).slice(-6);
        var html = deathEntries.map(function (item) {
            var left = Math.max(0, Math.ceil((item.at + life - now) / 1000));
            var time = left < 60 ? left + "s" : Math.floor(left / 60) + ":" + String(left % 60).padStart(2, "0");
            var avatar = item.image ? '<img src="' + item.image + '">' : '<div class="ez-death-avatar"></div>';
            var shownName = protectedPlayers() ? "Player" : item.name;
            return '<div class="ez-death-row">' + avatar + '<span class="ez-death-name">' + escapeHtml(shownName) + '</span><span class="ez-death-time">(' + time + ')</span></div>';
        }).join('');
        deathList.innerHTML = html || '<div class="ez-empty">No deaths</div>';
    }

    function watchTime(seconds) {
        seconds = Math.max(0, Math.floor(Number(seconds) || 0));
        if (seconds < 60) return seconds + "s";
        return Math.floor(seconds / 60) + ":" + String(seconds % 60).padStart(2, "0");
    }

    function spectatorMatch(item) {
        var id = item && item.id != null ? String(item.id) : "";
        var nick = String(item && item.nick || "").toLowerCase();
        for (var i = 0; i < players.length; i++) {
            var obj = players[i];
            if (!obj || obj.deleted) continue;
            if ((id && String(obj.userId != null ? obj.userId : obj.id) === id) || (nick && String(rawNick(obj)).toLowerCase() === nick)) {
                return { object: obj, scan: null };
            }
        }
        for (var j = 0; j < scans.length; j++) {
            var scan = scans[j];
            if (!scan) continue;
            if ((id && String(scan.id) === id) || (nick && String(scan.nick || "").toLowerCase() === nick)) {
                return { object: null, scan: scan };
            }
        }
        return { object: null, scan: null };
    }

    function spectatorImage(item) {
        var found = spectatorMatch(item);
        var sprite = found.object ? objectSprite(found.object, performance.now()) : (found.scan ? scanSprite(found.scan) : null);
        return sprite ? spriteUrl(sprite) : "";
    }

    function makeKillPanel() {
        killPanel = document.createElement("div");
        killPanel.id = "ez-kills";
        killPanel.innerHTML = '<b>Kill Log</b><div data-kill-list></div>';
        document.body.appendChild(killPanel);
        killList = killPanel.querySelector("[data-kill-list]");
        putNode(killPanel, readPlace(killPosKey), null);
        var head = killPanel.querySelector("b");
        var moving = false, dx = 0, dy = 0;
        head.addEventListener("pointerdown", function (event) {
            moving = true; dx = event.clientX - killPanel.offsetLeft; dy = event.clientY - killPanel.offsetTop;
            head.setPointerCapture(event.pointerId); event.preventDefault();
        });
        head.addEventListener("pointermove", function (event) {
            if (!moving) return;
            killPanel.style.bottom = "auto";
            killPanel.style.left = clamp(event.clientX - dx, 4, innerWidth - killPanel.offsetWidth - 4) + "px";
            killPanel.style.top = clamp(event.clientY - dy, 4, innerHeight - 25) + "px";
        });
        head.addEventListener("pointerup", function (event) {
            if (!moving) return; moving = false;
            try { head.releasePointerCapture(event.pointerId); } catch (e) {}
            saveNodePlace(killPanel, killPosKey);
        });
        updateKillPanel(Date.now());
    }

    function feedImage(user) {
        if (!user) return "";
        try {
            if (typeof getSystemFeedObjectIconSprite === "function") {
                var name = String(user.evolution || user.character || user.name || "");
                var id = getSystemFeedObjectIconSprite(name, Number(user.skin) || 0);
                var sprite = id && game.sprites ? game.sprites[id] : null;
                if (sprite) return spriteUrl(sprite);
            }
        } catch (e) {}
        try {
            var evolution = String(user.evolution || user.character || user.name || "");
            var def = game.objectsDef && game.objectsDef[evolution];
            if (def && def.animations) {
                var anim = def.animations[def.animation] || def.animations.idle_right || def.animations.flying_right || def.animations.none;
                var sprite = anim && anim.sprites && game.sprites[anim.sprites[0]];
                if (sprite) return spriteUrl(sprite);
            }
        } catch (e) {}
        return "";
    }

    function updateKillPanel(now) {
        if (!killPanel) return;
        killPanel.classList.toggle("ez-hide", !cfg.misc.killLog);
        if (!cfg.misc.killLog || !killList) return;
        now = Number(now) || Date.now();
        var life = clamp(Number(cfg.misc.killTime) || 60, 10, 180) * 1000;
        killEntries = killEntries.filter(function (item) { return item.at + life > now; }).slice(-6);
        killList.innerHTML = killEntries.map(function (item) {
            var left = Math.max(0, Math.ceil((item.at + life - now) / 1000));
            var time = watchTime(left);
            var avatar = item.image ? '<img src="' + item.image + '">' : '<div class="ez-kill-avatar"></div>';
            var shownName = protectedPlayers() ? "Player" : item.name;
            return '<div class="ez-kill-row">' + avatar + '<span class="ez-kill-name">' + escapeHtml(shownName) + '</span><span class="ez-kill-time">(' + time + ')</span></div>';
        }).join("") || '<div class="ez-empty">No kills</div>';
    }

    function onGameFeed(data) {
        if (!data || data.type !== "player-kill") return;
        var victim = data.victim || {};
        var name = String(victim.nick || victim.name || data.nick || victim.id || "Player");
        killEntries.push({ name: name, image: feedImage(victim), at: Date.now() });
        killEntries = killEntries.slice(-6);
        if (cfg.misc.killSound) playKill();
        updateKillPanel(Date.now());
    }

    function attachKillFeed() {
        try {
            if (typeof gameServer === "undefined" || !gameServer || killSocket === gameServer) return;
            if (killSocket && killHandler) {
                if (typeof killSocket.off === "function") killSocket.off("game:feed", killHandler);
                else if (typeof killSocket.removeListener === "function") killSocket.removeListener("game:feed", killHandler);
            }
            killSocket = gameServer;
            killHandler = onGameFeed;
            killSocket.on("game:feed", killHandler);
        } catch (e) {}
    }

    function makeSpectatorPanel() {
        spectatorPanel = document.createElement("div");
        spectatorPanel.id = "ez-spectators";
        spectatorPanel.innerHTML = '<b>Spectators</b><div data-spectator-list></div>';
        document.body.appendChild(spectatorPanel);
        spectatorList = spectatorPanel.querySelector("[data-spectator-list]");
        putNode(spectatorPanel, readPlace(spectatorPosKey), null);
        var head = spectatorPanel.querySelector("b");
        var moving = false, dx = 0, dy = 0;
        head.addEventListener("pointerdown", function (event) {
            moving = true;
            dx = event.clientX - spectatorPanel.offsetLeft;
            dy = event.clientY - spectatorPanel.offsetTop;
            head.setPointerCapture(event.pointerId);
            event.preventDefault();
        });
        head.addEventListener("pointermove", function (event) {
            if (!moving) return;
            spectatorPanel.style.bottom = "auto";
            spectatorPanel.style.left = clamp(event.clientX - dx, 4, innerWidth - spectatorPanel.offsetWidth - 4) + "px";
            spectatorPanel.style.top = clamp(event.clientY - dy, 4, innerHeight - 25) + "px";
        });
        head.addEventListener("pointerup", function (event) {
            if (!moving) return;
            moving = false;
            try { head.releasePointerCapture(event.pointerId); } catch (e) {}
            saveNodePlace(spectatorPanel, spectatorPosKey);
        });
        updateSpectatorPanel(Date.now());
    }

    function onSpectatorList(list) {
        if (!cfg.misc.spectatorList || !Array.isArray(list)) return;
        var now = Date.now();
        var next = new Map();
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            if (!item) continue;
            var key = String(item.socketId || item.id || item.nick || i);
            var old = spectatorEntries.get(key);
            next.set(key, {
                key: key,
                nick: String(item.nick || ""),
                id: item.id != null ? String(item.id) : "?",
                socketId: item.socketId || "",
                since: old ? old.since : now,
                image: old && old.image ? old.image : spectatorImage(item)
            });
        }
        spectatorEntries = next;
        updateSpectatorPanel(now);
        setTimeout(function () {
            var original = document.getElementById("spectatorsList");
            if (original) original.style.setProperty("display", "none", "important");
        }, 0);
    }

    function attachSpectators() {
        try {
            if (typeof gameServer === "undefined" || !gameServer || typeof socketMsgType === "undefined" || socketMsgType.SPECTATORSLIST == null) return;
            if (spectatorSocket === gameServer) return;
            if (spectatorSocket && spectatorHandler) {
                if (typeof spectatorSocket.off === "function") spectatorSocket.off(socketMsgType.SPECTATORSLIST, spectatorHandler);
                else if (typeof spectatorSocket.removeListener === "function") spectatorSocket.removeListener(socketMsgType.SPECTATORSLIST, spectatorHandler);
            }
            spectatorSocket = gameServer;
            spectatorHandler = onSpectatorList;
            spectatorSocket.on(socketMsgType.SPECTATORSLIST, spectatorHandler);
        } catch (e) {}
    }

    function requestSpectators(now) {
        if (!cfg.misc.spectatorList || now - spectatorRequestAt < 1200) return;
        spectatorRequestAt = now;
        try {
            if (typeof gameServer !== "undefined" && gameServer && window.game && game.me && !game.me.deleted) gameServer.emit("game:spectatorList");
        } catch (e) {}
    }

    function updateSpectatorPanel(now) {
        if (!spectatorPanel) return;
        var enabled = !!cfg.misc.spectatorList;
        spectatorPanel.classList.toggle("ez-hide", !enabled);
        document.body.classList.toggle("ez-spec-active", enabled);
        if (!enabled || !spectatorList) {
            if (!enabled) spectatorEntries.clear();
            return;
        }
        now = Number(now) || Date.now();
        var rows = [];
        spectatorEntries.forEach(function (item) {
            if (!item.image) item.image = spectatorImage(item);
            var avatar = item.image ? '<img src="' + item.image + '">' : '<div class="ez-spec-avatar"></div>';
            var shownName = protectedPlayers() ? "Player" : String(item.nick || "").trim();
            var idText = item.id && item.id !== "?" ? "#" + item.id : "";
            var labelText = shownName && shownName !== String(item.id) && shownName !== idText ? shownName + (idText ? " (" + idText + ")" : "") : (idText || "Unknown");
            rows.push('<div class="ez-spec-row">' + avatar + '<span class="ez-spec-text">' + escapeHtml(labelText) + '</span><span class="ez-spec-time">' + watchTime((now - item.since) / 1000) + '</span></div>');
        });
        spectatorList.innerHTML = rows.join("") || '<div class="ez-empty">No spectators</div>';
    }

    function escapeHtml(value) {
        return String(value == null ? "" : value).replace(/[&<>"']/g, function (char) {
            return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char];
        });
    }

    function makeLayer() {
        layer = document.createElement("canvas");
        layer.id = "ez-layer";
        document.body.appendChild(layer);
        ctx = layer.getContext("2d", { alpha: true });
    }

    function markFit() {
        fitDirty = true;
    }

    function fit(now) {
        if (!window.game || !game.canvas || !game.canvas.width || !game.canvas.height) {
            if (layer) layer.style.display = "none";
            return false;
        }
        var canvas = game.canvas;
        if (layer.width !== canvas.width) layer.width = canvas.width;
        if (layer.height !== canvas.height) layer.height = canvas.height;

        if (fitDirty || now - fitAt > 180) {
            var rect = canvas.getBoundingClientRect();
            if (!rect.width || !rect.height) {
                layer.style.display = "none";
                return false;
            }
            layer.style.left = rect.left + "px";
            layer.style.top = rect.top + "px";
            layer.style.width = rect.width + "px";
            layer.style.height = rect.height + "px";
            layer.style.display = "block";
            fitAt = now;
            fitDirty = false;
        }
        return true;
    }

    function playerKeys(obj) {
        var out = [];
        if (obj && obj.userId != null && obj.userId !== 0) out.push("u:" + obj.userId);
        if (obj && displayNick(obj)) out.push("n:" + String(displayNick(obj)).toLowerCase());
        return out;
    }

    function scanKeys(item) {
        var out = [];
        if (item && item.id != null) out.push("u:" + item.id);
        if (item && item.nick) out.push("n:" + String(item.nick).toLowerCase());
        return out;
    }

    var bossNames = {
        boss1: 1,
        blackWidow: 1,
        giantRat: 1,
        mummy: 1,
        yeti: 1,
        tyrannosaurus: 1,
        tyrannosaurus_easter: 1,
        mammoth: 1,
        zombie: 1,
        alienBigInsect: 1,
        alienEye: 1
    };

    function bossObject(obj) {
        if (!obj || !obj.name || !window.objectType) return false;
        try {
            var name = String(obj.name);
            var def = window.game && game.objectsDef ? game.objectsDef[name] : null;
            if (obj.type === objectType.BOSS || (def && def.type === objectType.BOSS)) return true;
            if (bossNames[name] || /^boss/i.test(name)) return true;
            if (obj.customData && (obj.customData.boss || obj.customData.isBoss)) return true;
        } catch (e) {}
        return false;
    }

    function extraKind(obj) {
        if (!obj || !obj.name || !window.objectType) return "";
        if (bossObject(obj)) return "boss";
        if (obj.type === objectType.HIDE) return "hide";
        if (obj.type === objectType.TELEPORT || /portal/i.test(obj.name)) return "portal";
        if (obj.type === objectType.FOOD) return "food";
        return "";
    }

    function otherKind(obj) {
        if (!obj || !obj.name) return "";
        if (obj.name === "gem" || obj.name === "gem_moveable") return "Gem";
        if (obj.name === "potion_armor") return "ArmorPotion";
        if (obj.name === "waterDrop") return "WaterDrop";
        if (obj.name === "roots") return "Roots";
        if (obj.name === "fireball") return "Fireball";
        if (obj.name === "plasmaball") return "PlasmaBall";
        if (obj.name === "shuriken") return "Shuriken";
        if (obj.name === "snowball") return "Snowball";
        if (obj.name === "bomb") return "Bomb";
        if (obj.name === "bear_trap") return "BearTrap";
        return "";
    }

    function canEat(obj) {
        if (!obj || !window.game || !game.me || !window.objectType || obj.type !== objectType.FOOD || typeof checkFoodChain !== "function") return false;
        try {
            var result = checkFoodChain(game.me, obj);
            return !!result && result.check === 1;
        } catch (e) {
            return false;
        }
    }

    function canEatName(name) {
        try {
            var def = game.objectsDef && game.objectsDef[name];
            if (!def || def.type !== objectType.FOOD || typeof checkFoodChain !== "function") return false;
            var result = checkFoodChain(game.me, def);
            return !!result && result.check === 1;
        } catch (e) { return false; }
    }

    function foodSpawn(obj) {
        if (!obj || !obj.position || !window.game || !window.objectType) return null;
        try {
            var def = game.objectsDef && game.objectsDef[obj.name];
            if (!def || def.type !== objectType.SPAWN || !def.spawning) return null;
            var target = game.objectsDef[def.spawning];
            if (!target || target.type !== objectType.FOOD || !canEatName(def.spawning)) return null;
            return {
                obj: obj,
                name: def.spawning,
                delay: Math.max(1000, Number(def.delay) || 30000),
                key: obj.id != null ? "id:" + obj.id : "pos:" + def.spawning + ":" + Math.round(obj.position.x) + ":" + Math.round(obj.position.y)
            };
        } catch (e) { return null; }
    }

    function timerSeconds(value) {
        if (value == null) return null;
        if (typeof value === "object") {
            var fields = ["left", "remaining", "timeLeft", "timer", "time", "b"];
            for (var i = 0; i < fields.length; i++) {
                if (value[fields[i]] != null) {
                    var parsed = timerSeconds(value[fields[i]]);
                    if (parsed != null) return parsed;
                }
            }
            return null;
        }
        var num = Number(value);
        if (!Number.isFinite(num) || num < 0) return null;
        if (num > 1e12) return Math.max(0, (num - Date.now()) / 1000);
        if (num > 1e9) return Math.max(0, num - Date.now() / 1000);
        if (num > 10000) return num / 1000;
        return num;
    }

    function serverSpawnLeft(spawn) {
        var raw = null;
        try { raw = typeof spawnTimerData !== "undefined" ? spawnTimerData : window.spawnTimerData; } catch (e) {}
        if (!raw) return null;
        var id = spawn.obj && spawn.obj.id;
        var item = null;
        try {
            if (!Array.isArray(raw) && id != null && raw[id] != null) item = raw[id];
            else if (Array.isArray(raw)) {
                var best = Infinity;
                var sx = Number(spawn.obj.position.x) || 0;
                var sy = Number(spawn.obj.position.y) || 0;
                for (var i = 0; i < raw.length; i++) {
                    var row = raw[i];
                    if (!row || typeof row !== "object") continue;
                    if (row.c === spawn.name && Number.isFinite(Number(row.a)) && Number.isFinite(Number(row.b))) {
                        var dx = Number(row.a) - sx;
                        var dy = Number(row.b) - sy;
                        var dist = dx * dx + dy * dy;
                        if (dist < best) { best = dist; item = row.d; }
                        continue;
                    }
                    if ((id != null && (row.id == id || row.spawnId == id)) || row.name === spawn.name || row.spawning === spawn.name) {
                        item = row;
                        if (id != null && (row.id == id || row.spawnId == id)) break;
                    }
                }
            }
        } catch (e) {}
        return timerSeconds(item);
    }

    function worldDistance(a, b) {
        if (!a || !b || !a.position || !b.position) return Infinity;
        var ax = a.position.x + (Number(a.width) || 0) / 2;
        var ay = a.position.y + (Number(a.height) || 0) / 2;
        var bx = b.position.x + (Number(b.width) || 0) / 2;
        var by = b.position.y + (Number(b.height) || 0) / 2;
        var dx = ax - bx, dy = ay - by;
        return Math.sqrt(dx * dx + dy * dy);
    }

    function syncFoodSpawns(now) {
        var used = new Set();
        for (var i = 0; i < foodSpawns.length; i++) {
            var spawn = foodSpawns[i];
            var state = foodSpawnState.get(spawn.key) || { bound: null, end: 0, estimated: false, had: false, seen: now, name: spawn.name };
            state.seen = now;
            state.name = spawn.name;
            var current = null;
            if (state.bound != null) {
                for (var p = 0; p < foodObjects.length; p++) {
                    if (foodObjects[p].id == state.bound && !foodObjects[p].deleted) { current = foodObjects[p]; break; }
                }
            }
            if (!current) {
                var best = Infinity;
                for (var f = 0; f < foodObjects.length; f++) {
                    var food = foodObjects[f];
                    if (!food || food.deleted || food.name !== spawn.name || used.has(food.id)) continue;
                    var dist = worldDistance(spawn.obj, food);
                    var limit = Math.max(90, Math.min(260, (Number(food.width) || 50) * 1.35));
                    if (dist < limit && dist < best) { best = dist; current = food; }
                }
            }
            if (current) {
                used.add(current.id);
                state.bound = current.id;
                state.end = 0;
                state.estimated = false;
                state.had = true;
            } else {
                var exact = serverSpawnLeft(spawn);
                if (exact != null) {
                    state.end = now + exact * 1000;
                    state.estimated = false;
                } else if (!state.end) {
                    state.end = now + spawn.delay;
                    state.estimated = !state.had;
                }
            }
            foodSpawnState.set(spawn.key, state);
        }
        foodSpawnState.forEach(function (state, key) {
            if (!state || now - state.seen > 15000) foodSpawnState.delete(key);
        });
    }

    function clearFoodGone(obj) {
        if (!obj || !obj.position) return;
        var bestKey = null;
        var best = Infinity;
        var limit = Math.max(28, Math.min(110, Math.max(Number(obj.width) || 0, Number(obj.height) || 0) * .75));
        foodGone.forEach(function (item, key) {
            if (!item || item.name !== obj.name) return;
            var dx = item.x - obj.position.x;
            var dy = item.y - obj.position.y;
            var distance = Math.sqrt(dx * dx + dy * dy);
            if (distance <= limit && distance < best) {
                best = distance;
                bestKey = key;
            }
        });
        if (bestKey != null) foodGone.delete(bestKey);
    }


    function ownPlayer() {
        try {
            if (!window.game || !game.me || game.me.deleted) return null;
            if ((typeof imDead !== "undefined" && imDead) || (typeof spectatorMode !== "undefined" && spectatorMode)) return null;
            if (window.user && user.id != null && game.me.userId != null && Number(game.me.userId) !== Number(user.id)) return null;
            return game.me;
        } catch (e) { return null; }
    }

    function updateList(now) {
        if (now - listAt < 100) return;
        listAt = now;
        var currentMe = ownPlayer();
        var currentEvolution = currentMe ? String(currentMe.name || "") : "";
        if (currentEvolution && currentEvolution !== foodEvolution) {
            foodEvolution = currentEvolution;
            foodGone.forEach(function (item, key) { if (!item || !canEatName(item.name)) foodGone.delete(key); });
            foodSpawnState.forEach(function (state, key) {
                var keep = false;
                for (var si = 0; si < foodSpawns.length; si++) if (foodSpawns[si] && foodSpawns[si].key === key && canEatName(foodSpawns[si].name)) { keep = true; break; }
                if (!keep && state && state.name && !canEatName(state.name)) foodSpawnState.delete(key);
            });
        }
        players.length = 0;
        hides.length = 0;
        extras.length = 0;
        foodObjects.length = 0;
        foodSpawns.length = 0;
        live.clear();

        if (!window.game || !game.gameObjects || !window.objectType) return;
        var list = game.gameObjects;
        for (var id in list) {
            if (!Object.prototype.hasOwnProperty.call(list, id)) continue;
            var obj = list[id];
            if (!obj || obj.deleted || !obj.position) continue;
            if (obj.type === objectType.FOOD) {
                foodObjects.push(obj);
                foodSeen.set(String(obj.id), {
                    name: obj.name,
                    x: Number(obj.position.x) || 0,
                    y: Number(obj.position.y) || 0,
                    at: now,
                    sprite: objectSprite(obj, now)
                });
                clearFoodGone(obj);
            }
            var spawn = foodSpawn(obj);
            if (spawn) foodSpawns.push(spawn);
            if (obj.type === objectType.PLAYER && obj !== game.me) {
                players.push(obj);
                if (obj.id != null && obj.userId != null && obj.userId !== 0) projectileOwners.set(String(obj.id), String(obj.userId));
                var recentName = String(rawNick(obj) || "").toLowerCase();
                if (recentName) recentPlayers.set(recentName, { obj: obj, sprite: objectSprite(obj, now), at: now });
                var keys = playerKeys(obj);
                for (var i = 0; i < keys.length; i++) live.add(keys[i]);
            } else if (obj.type === objectType.HIDE) {
                hides.push(obj);
            }
            var kind = extraKind(obj);
            if (kind && cfg.extra[kind]) {
                if (kind !== "food" || canEat(obj)) {
                    var range = Number(cfg.extra[kind + "Range"]) || 0;
                    if (!range || meters(obj) <= range + 300) extras.push({ obj: obj, kind: kind, subtype: "" });
                }
            }

            var subtype = otherKind(obj);
            if (subtype && cfg.extra.others && cfg.extra["others" + subtype]) {
                var otherRange = Number(cfg.extra.othersRange) || 0;
                if (!otherRange || meters(obj) <= otherRange + 300) extras.push({ obj: obj, kind: "others", subtype: subtype });
            }
        }
        foodSeen.forEach(function (value, key) { if (!value || now - value.at > 60000) foodSeen.delete(key); });
        projectileSkillSeen.forEach(function (at, key) { if (now - at > 30000) projectileSkillSeen.delete(key); });
        syncFoodSpawns(now);
        recentPlayers.forEach(function (item, name) {
            if (!item || now - item.at > 120000) recentPlayers.delete(name);
        });
    }

    function updateScan(now) {
        if (now - scanReadAt < 80) return;
        scanReadAt = now;
        var list = null;
        try {
            if (typeof scanPlayersArr !== "undefined" && Array.isArray(scanPlayersArr)) list = scanPlayersArr;
            else if (Array.isArray(window.scanPlayersArr)) list = window.scanPlayersArr;
        } catch (e) {}

        scans.length = 0;
        if (!list) return;
        var meId = window.game && game.me ? game.me.userId : null;
        var meNick = window.game && game.me && game.me.nick ? String(game.me.nick).toLowerCase() : "";
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            if (!item) continue;
            if (meId != null && item.id == meId) continue;
            if (meNick && item.nick && String(item.nick).toLowerCase() === meNick) continue;
            scans.push(item);
        }
    }

    function doScan(now, force) {
        if (!force && now - scanAt < 450) return false;
        if (!window.game || !game.me || game.me.deleted || game.me.inBattleRoyaleZone) return false;
        try {
            if (typeof gameServer === "undefined" || typeof socketMsgType === "undefined" || socketMsgType.SCANPLAYERS == null) return false;
            gameServer.emit(socketMsgType.SCANPLAYERS);
            scanAt = now;
            return true;
        } catch (e) {
            return false;
        }
    }

    function itemBank(obj) {
        var key = obj.id != null ? "o:" + obj.id : "n:" + String(displayNick(obj) || "player").toLowerCase();
        var item = bank.get(key);
        if (!item) {
            item = { rel: 0, relAt: 0, sprite: null, spriteAt: 0, spriteName: "", spriteSkin: null };
            bank.set(key, item);
        }
        return item;
    }

    function pair(target) {
        if (!window.game || !game.me || typeof checkFoodChain !== "function") return 0;
        var eat = false;
        var eaten = false;
        try { eat = checkFoodChain(game.me, target).check === 1; } catch (e) {}
        try { eaten = checkFoodChain(target, game.me).check === 1; } catch (e) {}
        if (eat && eaten) return 2;
        if (eaten) return -1;
        if (eat) return 1;
        return 0;
    }

    function relation(obj, now) {
        var item = itemBank(obj);
        if (now - item.relAt < 220) return item.rel;
        item.relAt = now;
        item.rel = pair(obj);
        return item.rel;
    }

    function scanTarget(item) {
        var def = null;
        try { def = game.objectsDef && game.objectsDef[item.evolution]; } catch (e) {}
        def = def || {};
        return {
            id: "scan:" + (item.id != null ? item.id : item.nick),
            name: item.evolution,
            type: window.objectType ? objectType.PLAYER : 1,
            eatsSouls: !!def.eatsSouls,
            hasSoul: !!def.hasSoul,
            immortal: false
        };
    }

    function scanRelation(item, now) {
        var key = String(item.id != null ? item.id : item.nick) + ":" + item.evolution;
        var data = scanBank.get(key);
        if (!data) {
            data = { rel: 0, at: 0, sprite: null };
            scanBank.set(key, data);
        }
        if (now - data.at > 600) {
            data.at = now;
            data.rel = pair(scanTarget(item));
        }
        return data.rel;
    }

    function objectSprite(obj, now) {
        var item = itemBank(obj);
        if (item.sprite && item.spriteName === obj.name && item.spriteSkin === obj.skin && now - item.spriteAt < 1400) return item.sprite;
        item.spriteAt = now;
        item.spriteName = obj.name;
        item.spriteSkin = obj.skin;

        try {
            if (typeof getObjectSprite === "function") {
                var ready = getObjectSprite(obj);
                if (ready && ready.width > 0 && ready.height > 0) return item.sprite = ready;
            }
        } catch (e) {}

        try {
            if (!obj.animations || !game.sprites) return item.sprite;
            var anim = obj.animations[obj.animation] || obj.animations.none || obj.animations.flying_right;
            if (!anim || !anim.sprites || !anim.sprites.length) return item.sprite;
            var name = anim.sprites[(obj.animationFrame || 0) % anim.sprites.length];
            if (obj.skin && game.objectsDef[obj.name] && game.objectsDef[obj.name].skinsLoaded[obj.skin]) name += "_" + obj.skin;
            var sprite = game.sprites[name];
            if (sprite && sprite.width > 0 && sprite.height > 0) item.sprite = sprite;
        } catch (e) {}
        return item.sprite;
    }

    function scanSprite(item) {
        try {
            if (!item || !item.evolution || !game.objectsDef || !game.sprites) return null;
            var key = String(item.id != null ? item.id : item.nick) + ":" + item.evolution + ":" + (Number(item.skin) || 0);
            var data = scanBank.get(key);
            if (data && data.sprite && data.sprite.width > 0 && data.sprite.height > 0) return data.sprite;
            var def = game.objectsDef[item.evolution];
            if (!def || !def.animations) return null;
            var anim = def.animations.flying_right || def.animations.none;
            if (!anim || !anim.sprites) return null;
            var list = Array.isArray(anim.sprites) ? anim.sprites : Object.keys(anim.sprites).map(function (name) { return anim.sprites[name]; });
            if (!list.length) return null;
            var name = list[0];
            var skin = Number(item.skin) || 0;
            if (skin && def.skinsLoaded && def.skinsLoaded[skin]) name += "_" + skin;
            var sprite = game.sprites[name];
            if (!data) {
                data = { rel: 0, at: 0, sprite: null };
                scanBank.set(key, data);
            }
            if (sprite && sprite.width > 0 && sprite.height > 0) data.sprite = sprite;
            return sprite && sprite.width > 0 && sprite.height > 0 ? sprite : null;
        } catch (e) {
            return null;
        }
    }

    function styleFor(rel) {
        if (rel === 2 && cfg.mutual.on) return cfg.mutual;
        if (rel < 0 && cfg.danger.on) return cfg.danger;
        if (rel > 0 && cfg.edible.on) return cfg.edible;
        return cfg.all.on ? cfg.all : null;
    }

    function hiddenStyle(rel) {
        if (rel === 2 && cfg.mutual.on && cfg.mutual.hidden) return cfg.mutual;
        if (rel < 0 && cfg.danger.on && cfg.danger.hidden) return cfg.danger;
        if (rel > 0 && cfg.edible.on && cfg.edible.hidden) return cfg.edible;
        return cfg.all.on && cfg.all.hidden ? cfg.all : null;
    }

    function relationColor(rel) {
        var style = hiddenStyle(rel);
        return style ? style.boxColor : null;
    }

    function middle(obj) {
        if (obj.positionCenter && Number.isFinite(obj.positionCenter.x) && Number.isFinite(obj.positionCenter.y)) return obj.positionCenter;
        return {
            x: obj.position.x + (Number(obj.width) || 0) / 2,
            y: obj.position.y + (Number(obj.height) || 0) / 2
        };
    }

    function meters(obj) {
        if (!window.game || !game.me || !game.me.position) return 0;
        var a = middle(game.me);
        var b = middle(obj);
        var dx = b.x - a.x;
        var dy = b.y - a.y;
        return Math.round(Math.sqrt(dx * dx + dy * dy));
    }

    function rgba(hex, alpha) {
        var value = String(hex || "#ffffff").replace("#", "");
        if (value.length === 3) value = value.replace(/./g, function (c) { return c + c; });
        var number = parseInt(value, 16);
        if (!Number.isFinite(number)) return "rgba(255,255,255," + alpha + ")";
        return "rgba(" + ((number >> 16) & 255) + "," + ((number >> 8) & 255) + "," + (number & 255) + "," + alpha + ")";
    }

    function roundPath(g, x, y, w, h, r) {
        r = Math.min(r, w / 2, h / 2);
        g.beginPath();
        g.moveTo(x + r, y);
        g.arcTo(x + w, y, x + w, y + h, r);
        g.arcTo(x + w, y + h, x, y + h, r);
        g.arcTo(x, y + h, x, y, r);
        g.arcTo(x, y, x + w, y, r);
        g.closePath();
    }

    function roundRect(x, y, w, h, r) {
        roundPath(ctx, x, y, w, h, r);
    }

    function label(value, x, y, color, size, align) {
        ctx.font = "600 " + size + "px Arial,sans-serif";
        ctx.textAlign = align || "center";
        ctx.textBaseline = "bottom";
        ctx.lineJoin = "round";
        ctx.lineWidth = Math.max(1, size * .24);
        ctx.strokeStyle = "rgba(0,0,0,.84)";
        ctx.strokeText(value, x, y);
        ctx.fillStyle = color;
        ctx.fillText(value, x, y);
    }

    function effects(obj) {
        var list = [];
        if (obj.fire) list.push({ text: "Burning", color: "#ff3b30", pulse: true });
        if (obj.poisoned) list.push({ text: "Poisoned", color: "#5dde71" });
        if (obj.infected) list.push({ text: "Infected", color: "#bd4b4b" });
        if (obj.boostLock) list.push({ text: "Boost lock", color: "#bd75e8" });
        if (obj.grabbed) list.push({ text: "Grabbed", color: "#b7b7b7" });
        if (obj.isProtected) list.push({ text: "Protected", color: "#78a9ff" });
        if (obj.inSafeZone) list.push({ text: "Safe zone", color: "#75c8ff" });
        if (obj.inHide) list.push({ text: "Hidden", color: "#d7d7d7" });
        if (obj.invisibleTime && window.game && obj.invisibleTime > game.time) list.push({ text: "Invisible", color: "#d9c5ff" });
        return list;
    }

    function drawEffects(obj, x, y, now) {
        var list = effects(obj);
        if (!list.length) return 0;
        var scale = hudScale();
        var fontSize = Math.max(5, 9 * scale);
        var rowHeight = Math.max(7, 13 * scale);
        var padX = Math.max(3, 5 * scale);
        var gap = Math.max(1, 3 * scale);
        var count = Math.min(4, list.length);
        var widths = [];
        var total = 0;
        ctx.font = "600 " + fontSize + "px Arial,sans-serif";
        for (var i = 0; i < count; i++) {
            var name = i === 3 && list.length > 4 ? "+" + (list.length - 3) : list[i].text;
            widths[i] = Math.ceil(ctx.measureText(name).width) + padX * 2;
            total += widths[i] + (i ? gap : 0);
        }
        var left = x - total / 2;
        for (i = 0; i < count; i++) {
            var effect = list[i];
            var textValue = i === 3 && list.length > 4 ? "+" + (list.length - 3) : effect.text;
            var alpha = effect.pulse ? 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(now / 80)) : 0.9;
            roundRect(left, y - rowHeight, widths[i], rowHeight, Math.max(1.5, 3 * scale));
            ctx.fillStyle = "rgba(12,13,16,.84)";
            ctx.fill();
            ctx.strokeStyle = rgba(effect.color, alpha);
            ctx.lineWidth = Math.max(.6, scale);
            ctx.stroke();
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.fillStyle = rgba(effect.color, Math.min(1, alpha + 0.1));
            ctx.fillText(textValue, left + widths[i] / 2, y - rowHeight / 2);
            left += widths[i] + gap;
        }
        return rowHeight + Math.max(1, 3 * scale);
    }

    function avatarOn(g, sprite, x, y, size, circle) {
        g.save();
        if (circle) {
            g.beginPath();
            g.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2);
            g.clip();
        }
        g.fillStyle = "rgba(255,255,255,.07)";
        g.fillRect(x, y, size, size);
        if (sprite && sprite.width > 0 && sprite.height > 0) {
            var scale = Math.min(size / sprite.width, size / sprite.height);
            var w = sprite.width * scale;
            var h = sprite.height * scale;
            try { g.drawImage(sprite, x + (size - w) / 2, y + (size - h) / 2, w, h); } catch (e) {}
        }
        g.restore();
    }

    function avatar(sprite, x, y, size) {
        avatarOn(ctx, sprite, x, y, size, true);
    }

    function trimText(g, value, width, font) {
        var name = String(value || "Player");
        g.font = font || "600 10px Arial,sans-serif";
        if (g.measureText(name).width <= width) return name;
        while (name.length > 2 && g.measureText(name + "…").width > width) name = name.slice(0, -1);
        return name + "…";
    }

    function skillOf(obj) {
        if (!obj) return null;
        try {
            var skill = obj.skill || (game.objectsDef && game.objectsDef[obj.name] && game.objectsDef[obj.name].skill);
            if (!skill || !(Number(skill.cooldown) > 0)) return null;
            return skill;
        } catch (e) { return null; }
    }

    function skillTitle(skill) {
        if (!skill) return "Skill";
        var value = String(skill.shortName || skill.fullName || skill.name || "Skill");
        value = value.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^skill[_ ]?/i, "");
        return value.length > 18 ? value.slice(0, 17) + "…" : value;
    }

    function trackedSkill(obj, now) {
        if (!obj) return null;
        var row = skillTracks.get(String(obj.id));
        if (!row) return null;
        if (row.end <= now) { skillTracks.delete(String(obj.id)); return null; }
        return { title: row.title, left: row.end - now, total: row.total };
    }

    function drawSkillTag(obj, style, x, y, now) {
        if (!style.skill) return 0;
        var row = trackedSkill(obj, now);
        if (!row) return 0;
        var text = row.title + " " + (row.left < 10000 ? (row.left / 1000).toFixed(1) : Math.ceil(row.left / 1000)) + "s";
        ctx.save();
        ctx.font = "700 10.5px Arial,sans-serif";
        var w = Math.max(64, ctx.measureText(text).width + 16);
        var h = 20;
        roundRect(x - w / 2, y, w, h, 4);
        ctx.fillStyle = "rgba(13,14,17,.93)"; ctx.fill();
        ctx.strokeStyle = rgba(style.boxColor || style.nameColor || "#b58af0", .98); ctx.lineWidth = 1.1; ctx.stroke();
        ctx.fillStyle = "#f4f4f6"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
        ctx.fillText(text, x, y + h / 2 - 1);
        var ratio = clamp(row.left / Math.max(1, row.total), 0, 1);
        ctx.fillStyle = style.boxColor || "#b58af0";
        ctx.fillRect(x - w / 2 + 2, y + h - 3, (w - 4) * ratio, 2);
        ctx.restore();
        return h + 3;
    }

    function startSkillTrack(obj, skill, now) {
        if (!obj || !skill || !(Number(skill.cooldown) > 0)) return;
        var total = Number(skill.cooldown) || 0;
        skillTracks.set(String(obj.id), { end: now + total, total: total, title: skillTitle(skill) });
    }

    function noteSkillAttack(data) {
        if (!data || !window.game || !game.gameObjects) return;
        var obj = game.gameObjects[data.objectId];
        if (!obj) return;
        if (game.me && String(data.objectId) === String(game.me.id)) return;
        var skill = skillOf(obj);
        if (!skill) return;
        var attackEvent = false;
        try {
            attackEvent = typeof skillEvent === "undefined" || skill.event === skillEvent.onAttack || skill.event === skillEvent.onObtainAttack;
        } catch (e) { attackEvent = true; }
        if (!attackEvent && String(skill.fullName || "").toLowerCase() !== "scythe attack") return;
        startSkillTrack(obj, skill, performance.now());
    }

    function projectileOwnerObjectId(obj) {
        if (!obj) return null;
        var raw = obj.customData;
        if (raw == null) raw = obj.ownerId != null ? obj.ownerId : (obj.owner != null ? obj.owner : obj.throwerId);
        if (raw && typeof raw === "object") {
            raw = raw.ownerId != null ? raw.ownerId : (raw.owner != null ? raw.owner : (raw.objectId != null ? raw.objectId : (raw.playerId != null ? raw.playerId : raw.id)));
        }
        if (Array.isArray(raw)) raw = raw.length ? raw[0] : null;
        return raw == null || raw === "" ? null : raw;
    }

    function noteProjectileSkill(obj, subtype, now) {
        if (!obj || !/^(Fireball|PlasmaBall)$/.test(subtype || "")) return;
        var key = String(obj.id != null ? obj.id : obj.name + ":" + Math.round(obj.position.x) + ":" + Math.round(obj.position.y));
        if (projectileSkillSeen.has(key)) return;
        var ownerId = projectileOwnerObjectId(obj);
        var owner = ownerId != null && game.gameObjects ? game.gameObjects[ownerId] : null;
        if (!owner || (game.me && owner === game.me)) return;
        var skill = skillOf(owner);
        if (!skill) skill = subtype === "PlasmaBall" && game.skills ? game.skills.flame : (game.skills ? game.skills.fireball : null);
        if (!skill) return;
        projectileSkillSeen.set(key, now);
        startSkillTrack(owner, skill, now);
    }

    function projectileOwner(obj) {
        var raw = projectileOwnerObjectId(obj);
        if (raw == null) return "";
        try {
            var owner = game.gameObjects && game.gameObjects[raw];
            if (owner) {
                var id = owner.userId != null ? owner.userId : (owner.accountId != null ? owner.accountId : null);
                if (id != null && id !== 0) {
                    projectileOwners.set(String(raw), String(id));
                    return "#" + String(id).replace(/^#/, "");
                }
            }
            if (projectileOwners.has(String(raw))) return "#" + String(projectileOwners.get(String(raw))).replace(/^#/, "");
        } catch (e) {}
        return "";
    }

    function drawArrow(obj, style, targetX, targetY, now) {
        if (!style.arrows) return;
        var scaleUi = hudScale();
        var cx = layer.width / 2;
        var cy = layer.height / 2;
        var dx = targetX - cx;
        var dy = targetY - cy;
        if (!dx && !dy) return;

        var angle = Math.atan2(dy, dx);
        var margin = Math.max(24, 50 * scaleUi);
        var edgeX = Math.max(30, layer.width / 2 - margin);
        var edgeY = Math.max(30, layer.height / 2 - margin);
        var sx = dx === 0 ? Infinity : edgeX / Math.abs(dx);
        var sy = dy === 0 ? Infinity : edgeY / Math.abs(dy);
        var edgeScale = Math.min(sx, sy);
        var x = cx + dx * edgeScale;
        var y = cy + dy * edgeScale;
        var pulse = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(now / 80));
        var color = obj.fire && style.effects ? rgba("#ff3b30", pulse) : style.arrowColor;
        var arrowLen = Math.max(5, 11 * scaleUi);
        var arrowWing = Math.max(4, 8.5 * scaleUi);

        ctx.save();
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.moveTo(x + Math.cos(angle) * arrowLen, y + Math.sin(angle) * arrowLen);
        ctx.lineTo(x + Math.cos(angle + 2.48) * arrowWing, y + Math.sin(angle + 2.48) * arrowWing);
        ctx.lineTo(x + Math.cos(angle - 2.48) * arrowWing, y + Math.sin(angle - 2.48) * arrowWing);
        ctx.closePath();
        ctx.fill();

        var ix = -Math.cos(angle);
        var iy = -Math.sin(angle);
        var cardW = Math.max(72, Math.round(124 * scaleUi));
        var cardH = Math.max(24, Math.round(38 * scaleUi));
        var cardX = clamp(x + ix * Math.max(38, 72 * scaleUi) - cardW / 2, 5, layer.width - cardW - 5);
        var cardY = clamp(y + iy * Math.max(31, 58 * scaleUi) - cardH / 2, 5, layer.height - cardH - 5);

        roundRect(cardX, cardY, cardW, cardH, Math.max(3, 5 * scaleUi));
        ctx.fillStyle = "rgba(13,14,17,.88)";
        ctx.fill();
        ctx.strokeStyle = color;
        ctx.lineWidth = Math.max(.7, scaleUi);
        if (obj.inHide) ctx.setLineDash([3 * scaleUi, 3 * scaleUi]);
        ctx.stroke();
        ctx.setLineDash([]);

        var av = Math.max(16, 28 * scaleUi);
        var gap = Math.max(4, 5 * scaleUi);
        avatar(objectSprite(obj, now), cardX + gap, cardY + (cardH - av) / 2, av);
        var textX = cardX + gap * 2 + av;
        ctx.textAlign = "left";
        ctx.textBaseline = "top";
        var nameSize = Math.max(5.5, 10 * scaleUi);
        ctx.font = "600 " + nameSize + "px Arial,sans-serif";
        ctx.fillStyle = "#f1f1f3";
        ctx.fillText(trimText(ctx, displayNick(obj), Math.max(20, cardW - (textX - cardX) - gap)), textX, cardY + Math.max(3, 6 * scaleUi));
        var distSize = Math.max(5, 9 * scaleUi);
        ctx.font = distSize + "px Arial,sans-serif";
        ctx.fillStyle = style.distanceColor;
        ctx.fillText("[" + meters(obj) + " m]", textX, cardY + cardH - distSize - Math.max(2, 5 * scaleUi));
        ctx.restore();
    }

    function bar(x, y, width, value, color) {
        var height = hudPx(3.5, 1.4);
        ctx.fillStyle = "rgba(0,0,0,.76)";
        ctx.fillRect(x, y, width, height);
        ctx.fillStyle = color;
        ctx.fillRect(x, y, width * clamp(value, 0, 100) / 100, height);
        ctx.strokeStyle = "rgba(0,0,0,.9)";
        ctx.lineWidth = 1;
        ctx.strokeRect(x, y, width, height);
        return height;
    }


    function tracerStart(origin) {
        var mode = String(origin || "bottom");
        if (mode === "top") return { x: layer.width / 2, y: 10 };
        if (mode === "player") {
            try {
                var me = ownPlayer();
                if (me && me.position) {
                    var pos = game.getRenderPosition(me.position.x, me.position.y);
                    var width = (Number(me.width) || 50) * game.scaleX * game.zoom;
                    var height = (Number(me.height) || 50) * game.scaleY * game.zoom;
                    if (pos && Number.isFinite(pos.x) && Number.isFinite(pos.y)) return { x: pos.x + width / 2, y: pos.y - height / 2 };
                }
            } catch (e) {}
        }
        return { x: layer.width / 2, y: layer.height - 10 };
    }

    function drawPlayer(obj, style, now) {
        if (!obj.position || !Number.isFinite(obj.position.x) || !Number.isFinite(obj.position.y)) return;
        var pos = game.getRenderPosition(obj.position.x, obj.position.y);
        var width = (Number(obj.width) || 50) * game.scaleX * game.zoom;
        var height = (Number(obj.height) || 50) * game.scaleY * game.zoom;
        var left = pos.x;
        var top = pos.y - height;
        var right = left + width;
        var bottom = pos.y;
        var midX = left + width / 2;
        var midY = top + height / 2;
        var pad = hudPx(2.5, .8);

        if (!Number.isFinite(left) || !Number.isFinite(top) || width <= 0 || height <= 0) return;
        var visible = right > 6 && left < layer.width - 6 && bottom > 6 && top < layer.height - 6;
        if (!visible) {
            drawArrow(obj, style, midX, midY, now);
            return;
        }

        ctx.save();
        var alpha = clamp(Number(obj.opacity) || 1, 0.3, 1);
        if (obj.inHide) alpha *= 0.8;
        ctx.globalAlpha = alpha;
        if (obj.inHide) ctx.setLineDash([4, 3]);

        if (style.fill) {
            ctx.fillStyle = rgba(style.fillColor, 0.15);
            ctx.fillRect(left - pad, top - pad, width + pad * 2, height + pad * 2);
        }

        if (style.box) {
            ctx.lineWidth = hudPx(1.35, .65);
            ctx.strokeStyle = style.boxColor;
            ctx.strokeRect(left - pad, top - pad, width + pad * 2, height + pad * 2);
        }

        if (style.effects && obj.fire) {
            var pulse = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(now / 80));
            ctx.setLineDash([]);
            ctx.lineWidth = hudPx(2, .9);
            ctx.strokeStyle = rgba("#ff3b30", pulse);
            ctx.strokeRect(left - pad - 2, top - pad - 2, width + pad * 2 + 4, height + pad * 2 + 4);
        }

        if (style.line) {
            var lineStart = tracerStart(cfg.esp && cfg.esp.tracerOrigin);
            ctx.beginPath();
            ctx.moveTo(lineStart.x, lineStart.y);
            ctx.lineTo(midX, bottom + pad);
            ctx.lineWidth = hudPx(1, .6);
            ctx.strokeStyle = rgba(style.lineColor, 0.88);
            ctx.stroke();
        }
        ctx.setLineDash([]);

        var barY = bottom + pad + 4;
        var barW = width + pad * 2;
        if (style.health) barY += bar(left - pad, barY, barW, Number.isFinite(obj.hp) ? obj.hp : 100, style.healthColor) + 2;
        if (style.armor && Number.isFinite(obj.armor) && obj.armor > 0) barY += bar(left - pad, barY, barW, obj.armor, style.armorColor) + 2;

        var size = hudPx(9.7, 5.2);
        var textY = top - pad - 2;
        if (style.effects) textY -= drawEffects(obj, midX, textY, now);
        if (style.distance) {
            label("[" + meters(obj) + " m]", midX, textY, style.distanceColor, size);
            textY -= size + 1;
        }
        var hideInfo = cfg.misc.privacy && cfg.misc.privacyPlayers;
        if (style.level && !hideInfo) {
            label("Lv. " + (Number.isFinite(obj.level) ? obj.level : 0), midX, textY, style.levelColor, size);
            textY -= size + 1;
        }
        if (style.name && !hideInfo) label(displayNick(obj), midX, textY, style.nameColor, size);
        ctx.restore();
    }

    function hideSpot(obj) {
        if (!obj || !obj.position) return null;
        var pos = game.getRenderPosition(obj.position.x, obj.position.y);
        var width = (Number(obj.width) || 50) * game.scaleX * game.zoom;
        var height = (Number(obj.height) || 50) * game.scaleY * game.zoom;
        var left = pos.x;
        var top = pos.y - height;
        var right = left + width;
        var bottom = pos.y;
        if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
        if (right < 8 || left > layer.width - 8 || bottom < 8 || top > layer.height - 8) return null;
        var dist = 0;
        if (game.me && game.me.position) {
            var a = middle(game.me);
            var b = middle(obj);
            var dx = b.x - a.x;
            var dy = b.y - a.y;
            dist = dx * dx + dy * dy;
        }
        return {
            x: clamp(left + width / 2, 58, layer.width - 58),
            y: clamp(top + height / 2, 18, layer.height - 18),
            left: left,
            top: top,
            width: width,
            height: height,
            dist: dist
        };
    }

    function missingPlayers() {
        var out = [];
        for (var i = 0; i < scans.length; i++) {
            var keys = scanKeys(scans[i]);
            var found = false;
            for (var k = 0; k < keys.length; k++) {
                if (live.has(keys[k])) {
                    found = true;
                    break;
                }
            }
            if (!found) out.push(scans[i]);
        }
        return out;
    }

    function guessCard(item, chance, color, now) {
        var sprite = scanSprite(item);
        var scale = 1;
        var cw = Math.max(72, Math.round(122 * scale));
        var ch = Math.max(18, Math.round(27 * scale));
        var key = String(item.id != null ? item.id : item.nick) + ":" + item.evolution + ":" + (item.skin || 0) + ":" + chance + ":" + color + ":" + (cfg.misc.privacy && cfg.misc.privacyPlayers ? 1 : 0) + ":" + cw;
        var ready = cardBank.get(key);
        if (ready && ready.sprite === sprite && now - ready.at < 1500) return ready.canvas;

        var canvas = document.createElement("canvas");
        canvas.width = cw;
        canvas.height = ch;
        var g = canvas.getContext("2d");
        roundPath(g, .5, .5, cw - 1, ch - 1, Math.max(2, 4 * scale));
        g.fillStyle = "rgba(13,14,17,.92)";
        g.fill();
        g.strokeStyle = rgba(color, .96);
        g.lineWidth = Math.max(.7, scale);
        g.setLineDash([Math.max(2, 3 * scale), Math.max(2, 3 * scale)]);
        g.stroke();
        g.setLineDash([]);
        var av = Math.max(12, 19 * scale);
        avatarOn(g, sprite, Math.max(2, 4 * scale), (ch - av) / 2, av, true);
        var tx = Math.max(18, 28 * scale);
        var nameSize = Math.max(5, 9 * scale);
        g.textAlign = "left";
        g.textBaseline = "top";
        g.font = "600 " + nameSize + "px Arial,sans-serif";
        g.fillStyle = "#f0f1f3";
        g.fillText(trimText(g, cfg.misc.privacy && cfg.misc.privacyPlayers ? "Player" : item.nick, cw - tx - 3, "600 " + nameSize + "px Arial,sans-serif"), tx, Math.max(2, 4 * scale));
        var chanceSize = Math.max(4.5, 8 * scale);
        g.font = chanceSize + "px Arial,sans-serif";
        g.fillStyle = rgba(color, 1);
        g.fillText(chance + "% here", tx, ch - chanceSize - Math.max(1, 3 * scale));
        cardBank.set(key, { canvas: canvas, sprite: sprite, at: now });
        return canvas;
    }

    function drawCached(now) {
        if (!hides.length || !(cfg.all.hidden || cfg.danger.hidden || cfg.edible.hidden || cfg.mutual.hidden)) return;
        var spots = [];
        var limit = Number(cfg.extra.hideRange) || 0;
        for (var i = 0; i < hides.length; i++) {
            if (limit > 0 && meters(hides[i]) > limit) continue;
            var spot = hideSpot(hides[i]);
            if (spot) spots.push(spot);
        }
        if (!spots.length) return;
        spots.sort(function (a, b) { return a.dist - b.dist; });
        if (spots.length > 4) spots.length = 4;

        var missing = missingPlayers().filter(function (item) {
            return !!hiddenStyle(scanRelation(item, now));
        });
        if (!missing.length) return;
        var firstStyle = hiddenStyle(scanRelation(missing[0], now));
        var frameColor = firstStyle ? firstStyle.boxColor : cfg.all.boxColor;
        var scale = 1;
        ctx.save();
        ctx.setLineDash([Math.max(2, 5 * scale), Math.max(2, 4 * scale)]);
        ctx.lineWidth = Math.max(.7, 1.2 * scale);
        ctx.strokeStyle = rgba(frameColor, .88);
        ctx.fillStyle = rgba(frameColor, .05);
        for (i = 0; i < spots.length; i++) {
            ctx.fillRect(spots[i].left, spots[i].top, spots[i].width, spots[i].height);
            ctx.strokeRect(spots[i].left, spots[i].top, spots[i].width, spots[i].height);
        }
        ctx.setLineDash([]);

        var chance = Math.max(1, Math.round(100 / spots.length));
        var sample = guessCard(missing[0], chance, frameColor, now);
        var rowH = sample.height + Math.max(1, Math.round(2 * scale));
        var fitCount = Math.max(1, Math.min(missing.length, Math.floor((layer.height - 12) / rowH), 12));
        for (i = 0; i < spots.length; i++) {
            var start = clamp(spots[i].y - fitCount * rowH / 2, 4, layer.height - fitCount * rowH - 4);
            for (var p = 0; p < fitCount; p++) {
                var rel = scanRelation(missing[p], now);
                var cardStyle = hiddenStyle(rel);
                if (!cardStyle) continue;
                var color = cardStyle.boxColor;
                var card = guessCard(missing[p], chance, color, now);
                var left = clamp(spots[i].x - card.width / 2, 4, layer.width - card.width - 4);
                ctx.drawImage(card, left, start + p * rowH);
            }
            if (missing.length > fitCount) label("+" + (missing.length - fitCount), spots[i].x, start + fitCount * rowH + Math.max(5, 10 * scale), frameColor, Math.max(5, 9 * scale));
        }
        ctx.restore();
    }

    function objectTitle(obj, fallback) {
        try {
            var def = game.objectsDef && game.objectsDef[obj.name];
            if (def && def.fullName) return def.fullName;
        } catch (e) {}
        if (fallback === "Food" && obj && obj.name) return String(obj.name).replace(/_/g, " ");
        return fallback;
    }

    function extraStyle(kind, obj, subtype) {
        if (!cfg.extra[kind]) return null;
        var titles = {
            hide: "Hide",
            portal: "Portal",
            food: "Food",
            boss: "Boss"
        };
        var objectTitles = {
            Gem: "Gem",
            ArmorPotion: "Armor potion",
            WaterDrop: "Water drop",
            Roots: "Roots",
            Fireball: "Fireball",
            PlasmaBall: "Plasma ball",
            Shuriken: "Shuriken",
            Snowball: "Snowball",
            Bomb: "Bomb",
            BearTrap: "Bear trap"
        };
        var base = kind === "others" ? "others" : kind;
        var fallback = kind === "others" ? (objectTitles[subtype] || "Object") : (titles[kind] || kind);
        return {
            kind: kind,
            subtype: subtype || "",
            color: cfg.extra[base + "Color"],
            title: objectTitle(obj, fallback),
            image: !!cfg.extra[base + "Image"],
            box: !!cfg.extra[base + "Box"],
            tracer: !!cfg.extra[base + "Tracer"],
            name: !!cfg.extra[base + "Name"],
            distance: !!cfg.extra[base + "Distance"],
            range: Number(cfg.extra[base + "Range"]) || 0
        };
    }

    function drawExtra(obj, info, now) {
        if (!obj || !obj.position) return;
        var dist = meters(obj);
        if (info.range > 0 && dist > info.range) return;
        var pos = game.getRenderPosition(obj.position.x, obj.position.y);
        var width = (Number(obj.width) || 45) * game.scaleX * game.zoom;
        var height = (Number(obj.height) || 45) * game.scaleY * game.zoom;
        var left = pos.x;
        var top = pos.y - height;
        if (left + width < 4 || left > layer.width - 4 || top + height < 4 || top > layer.height - 4) return;
        var x = left + width / 2;
        var bottom = top + height;
        var scaleUi = hudScale();
        var y = top - Math.max(1, 3 * scaleUi);
        ctx.save();
        ctx.strokeStyle = info.color;
        ctx.fillStyle = rgba(info.color, .07);
        ctx.lineWidth = Math.max(.65, 1.2 * scaleUi);
        if (info.box) {
            var boxPad = Math.max(.7, 2 * scaleUi);
            ctx.fillRect(left - boxPad, top - boxPad, width + boxPad * 2, height + boxPad * 2);
            ctx.strokeRect(left - boxPad, top - boxPad, width + boxPad * 2, height + boxPad * 2);
        }
        if (info.tracer) {
            var extraLineStart = tracerStart(cfg.extra && cfg.extra.tracerOrigin);
            ctx.beginPath();
            ctx.moveTo(extraLineStart.x, extraLineStart.y);
            ctx.lineTo(x, bottom + 3);
            ctx.strokeStyle = rgba(info.color, .88);
            ctx.stroke();
        }
        if (info.image) {
            var sprite = objectSprite(obj, now);
            var imageSize = Math.max(10, 20 * scaleUi);
            avatar(sprite, x - imageSize / 2, y - imageSize - Math.max(1, 2 * scaleUi), imageSize);
            y -= imageSize + Math.max(2, 4 * scaleUi);
        }
        if (info.distance) {
            var textSize = Math.max(5, 9 * scaleUi);
            label("[" + dist + " m]", x, y, info.color, textSize);
            y -= textSize + Math.max(1, scaleUi);
        }
        if (info.name) label(info.title, x, y, info.color, Math.max(5, 9 * scaleUi));
        if (info.kind === "boss" && Number.isFinite(obj.hp)) bar(left - 2, bottom + 4, width + 4, obj.hp, info.color);
        if (info.kind === "others" && /^(Fireball|PlasmaBall|Shuriken|Snowball|Bomb)$/.test(info.subtype || "")) {
            var ownerText = projectileOwner(obj);
            if (ownerText) {
                ctx.font = "600 9px Arial,sans-serif";
                ctx.textAlign = "center";
                ctx.textBaseline = "top";
                var labelY = bottom + Math.max(3, hudPx(4, 2));
                var tw = ctx.measureText(ownerText).width + 8;
                roundRect(x - tw / 2, labelY, tw, 14, 3);
                ctx.fillStyle = "rgba(14,15,18,.88)"; ctx.fill();
                ctx.fillStyle = info.color;
                ctx.fillText(ownerText, x, labelY + 2);
            }
        }
        ctx.restore();
    }

    function shortClock(seconds) {
        var value = Math.max(0, Math.ceil(Number(seconds) || 0));
        if (value < 60) return value + "s";
        return Math.floor(value / 60) + ":" + String(value % 60).padStart(2, "0");
    }

    function timerSprite(name) {
        try {
            var def = game.objectsDef && game.objectsDef[name];
            if (!def || !def.animations) return null;
            var anim = def.animations[def.animation] || def.animations.idle_right || def.animations.running_right || def.animations.flying_right || def.animations.none;
            if (!anim || !anim.sprites || !anim.sprites.length) return null;
            return game.sprites && game.sprites[anim.sprites[0]] || null;
        } catch (e) { return null; }
    }

    function foodClock(seconds) {
        var value = Math.max(0, Math.ceil(Number(seconds) || 0));
        return String(Math.floor(value / 60)).padStart(2, "0") + ":" + String(value % 60).padStart(2, "0");
    }

    function opaqueBounds(sprite) {
        if (!sprite || !sprite.width || !sprite.height) return null;
        try { if (spriteBounds.has(sprite)) return spriteBounds.get(sprite); } catch (e) {}
        var result = null;
        try {
            var sw = Number(sprite.naturalWidth) || Number(sprite.width) || 1;
            var sh = Number(sprite.naturalHeight) || Number(sprite.height) || 1;
            var scale = Math.min(1, 96 / Math.max(sw, sh));
            var w = Math.max(1, Math.round(sw * scale));
            var h = Math.max(1, Math.round(sh * scale));
            var canvas = document.createElement("canvas");
            canvas.width = w;
            canvas.height = h;
            var g = canvas.getContext("2d", { willReadFrequently: true });
            g.drawImage(sprite, 0, 0, w, h);
            var data = g.getImageData(0, 0, w, h).data;
            var minX = w, minY = h, maxX = -1, maxY = -1;
            for (var yy = 0; yy < h; yy++) {
                for (var xx = 0; xx < w; xx++) {
                    if (data[(yy * w + xx) * 4 + 3] < 12) continue;
                    if (xx < minX) minX = xx;
                    if (yy < minY) minY = yy;
                    if (xx > maxX) maxX = xx;
                    if (yy > maxY) maxY = yy;
                }
            }
            if (maxX >= minX && maxY >= minY) {
                result = {
                    x: minX / scale, y: minY / scale,
                    width: Math.max(1, (maxX - minX + 1) / scale),
                    height: Math.max(1, (maxY - minY + 1) / scale)
                };
            }
        } catch (e) {}
        try { spriteBounds.set(sprite, result); } catch (e) {}
        return result;
    }

    function drawFoodIcon(sprite, x, y, width, height) {
        if (!sprite || !sprite.width || !sprite.height) return;
        var bounds = opaqueBounds(sprite);
        var sx = bounds ? bounds.x : 0;
        var sy = bounds ? bounds.y : 0;
        var sw = bounds ? bounds.width : (Number(sprite.naturalWidth) || Number(sprite.width));
        var sh = bounds ? bounds.height : (Number(sprite.naturalHeight) || Number(sprite.height));
        var scale = Math.min(width / sw, height / sh);
        var dw = sw * scale;
        var dh = sh * scale;
        try { ctx.drawImage(sprite, sx, sy, sw, sh, x + (width - dw) / 2, y + (height - dh) / 2, dw, dh); } catch (e) {}
    }

    function drawSpawnClock(name, x, y, seconds, estimated, spriteOverride) {
        var def = game.objectsDef && game.objectsDef[name];
        if (!def) return;
        var pos = game.getRenderPosition(x, y);
        if (!pos || !Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return;
        var scale = hudScale();
        var card = Math.max(15, Math.round(36 * scale));
        var worldWidth = (Number(def.width) || card) * game.scaleX * game.zoom;
        var worldHeight = (Number(def.height) || card) * game.scaleY * game.zoom;
        var left = pos.x + (worldWidth - card) / 2;
        var top = pos.y - worldHeight / 2 - card / 2;
        if (left < -card || left > layer.width + card || top < -card || top > layer.height + card) return;
        var sprite = spriteOverride || timerSprite(name);
        ctx.save();
        ctx.globalAlpha = .58;
        drawFoodIcon(sprite, left, top, card, card);
        ctx.globalAlpha = 1;
        var fontSize = Math.max(5, 9 * scale);
        var text = (estimated ? "~" : "") + foodClock(seconds);
        ctx.font = "700 " + fontSize + "px monospace";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.lineJoin = "round";
        ctx.lineWidth = Math.max(1.2, fontSize * .3);
        ctx.strokeStyle = "rgba(0,0,0,.95)";
        ctx.strokeText(text, left + card / 2, top + card / 2);
        ctx.fillStyle = "#ffd166";
        ctx.fillText(text, left + card / 2, top + card / 2);
        ctx.restore();
    }

    function drawFoodTimers(now) {
        if (!cfg.extra.foodTimer || !ownPlayer()) return;
        var maxRange = 0;
        var raw = null;
        try { raw = typeof spawnTimerData !== "undefined" ? spawnTimerData : window.spawnTimerData; } catch (e) {}
        var used = new Set();
        if (Array.isArray(raw)) {
            for (var i = 0; i < raw.length; i++) {
                var row = raw[i];
                if (!row) continue;
                var x = Number(row.a), y = Number(row.b), name = row.c, left = Number(row.d);
                var def = game.objectsDef && game.objectsDef[name];
                if (!def || def.type !== objectType.FOOD || bossNames[name] || !staticFoodSpawn(name) || !canEatName(name) || !Number.isFinite(left) || left <= 0) continue;
                var pseudo = { position: { x: x, y: y }, width: def.width || 0, height: def.height || 0 };
                if (maxRange > 0 && meters(pseudo) > maxRange) continue;
                used.add(name + ":" + Math.round(x * 10) + ":" + Math.round(y * 10));
                drawSpawnClock(name, x, y, left, false);
            }
        }
        foodGone.forEach(function (item, key) {
            if (!item || !canEatName(item.name)) {
                foodGone.delete(key);
                return;
            }
            var seconds = (item.end - now) / 1000;
            if (seconds <= 0) {
                foodGone.delete(key);
                return;
            }
            var posKey = item.name + ":" + Math.round(item.x * 10) + ":" + Math.round(item.y * 10);
            if (used.has(posKey)) return;
            var def = game.objectsDef && game.objectsDef[item.name];
            var pseudo = { position: { x: item.x, y: item.y }, width: def && def.width || 0, height: def && def.height || 0 };
            if (maxRange > 0 && meters(pseudo) > maxRange) return;
            drawSpawnClock(item.name, item.x, item.y, seconds, false, item.sprite);
            used.add(posKey);
        });
        for (var j = 0; j < foodSpawns.length; j++) {
            var spawn = foodSpawns[j];
            if (!spawn || !canEatName(spawn.name)) {
                if (spawn) foodSpawnState.delete(spawn.key);
                continue;
            }
            var state = foodSpawnState.get(spawn.key);
            if (!state || !state.end) continue;
            var seconds = (state.end - now) / 1000;
            if (seconds <= 0) continue;
            var posKey = spawn.name + ":" + Math.round(spawn.obj.position.x * 10) + ":" + Math.round(spawn.obj.position.y * 10);
            if (used.has(posKey)) continue;
            if (maxRange > 0 && meters(spawn.obj) > maxRange) continue;
            drawSpawnClock(spawn.name, spawn.obj.position.x, spawn.obj.position.y, seconds, state.estimated);
        }
    }


    function staticFoodSpawn(name) {
        name = String(name || "");
        if (!name || !window.game || !game.objectsDef || !window.objectType) return null;
        if (staticFoodSpawnCache.has(name)) return staticFoodSpawnCache.get(name);
        var found = null;
        try {
            Object.keys(game.objectsDef).some(function (key) {
                var def = game.objectsDef[key];
                if (!def || def.type !== objectType.SPAWN || def.spawning !== name || !(Number(def.delay) > 0)) return false;
                found = { name: key, delay: Number(def.delay) };
                return true;
            });
        } catch (e) {}
        if (found) staticFoodSpawnCache.set(name, found);
        return found;
    }

    function nearestFoodSpawn(item) {
        if (!item || !staticFoodSpawn(item.name)) return null;
        var best = null;
        var bestDistance = Infinity;
        for (var i = 0; i < foodSpawns.length; i++) {
            var spawn = foodSpawns[i];
            if (!spawn || spawn.name !== item.name || !spawn.obj || !spawn.obj.position) continue;
            var dx = Number(spawn.obj.position.x) - Number(item.x);
            var dy = Number(spawn.obj.position.y) - Number(item.y);
            var distance = Math.sqrt(dx * dx + dy * dy);
            var limit = Math.max(100, Math.min(320, Math.max(Number(spawn.obj.width) || 0, Number(spawn.obj.height) || 0, 80) * 1.6));
            if (distance <= limit && distance < bestDistance) { bestDistance = distance; best = spawn; }
        }
        return best;
    }

    function foodDelay(name) {
        var spawn = staticFoodSpawn(name);
        return spawn ? spawn.delay : 0;
    }

    function rememberRemoval(data, object) {
        if (!data) return;
        try {
            var id = data.a != null ? data.a : data.id;
            var killerId = data.b != null ? data.b : data.killerId;
            var obj = object || (window.game && game.gameObjects ? game.gameObjects[id] : null);
            if (obj && window.objectType && obj.type === objectType.PLAYER && window.game && game.me && String(killerId) === String(game.me.id)) {
                if (cfg.misc.hitSound && performance.now() - lastHitSound > 35) {
                    lastHitSound = performance.now();
                    playHit();
                }
            }
            var item = obj && obj.position ? {
                name: obj.name, x: Number(obj.position.x) || 0, y: Number(obj.position.y) || 0,
                sprite: objectSprite(obj, performance.now())
            } : foodSeen.get(String(id));
            if (!item || !item.name || bossNames[item.name] || !canEatName(item.name)) return;
            var def = game.objectsDef && game.objectsDef[item.name];
            if (!def || !window.objectType || def.type !== objectType.FOOD) return;
            var spawn = nearestFoodSpawn(item);
            var delay = foodDelay(item.name);
            if (!spawn || !(delay > 0)) return;
            var sx = Number(spawn.obj.position.x) || 0;
            var sy = Number(spawn.obj.position.y) || 0;
            var key = "spawn:" + spawn.key;
            foodGone.set(key, { id: id, name: item.name, x: sx, y: sy, end: performance.now() + delay, sprite: item.sprite || null });
        } catch (e) {}
    }

    function noteFoodRemoval(response) {
        try {
            if (!response || !Array.isArray(response.c)) return;
            for (var i = 0; i < response.c.length; i++) {
                var message = response.c[i];
                if (!message || message.a != socketMsgType.REMOVEOBJECT || !message.b) continue;
                rememberRemoval(message.b, null);
            }
        } catch (e) {}
    }

    function attachRemoveHook() {
        try {
            if (typeof window.removeObject !== "function") return;
            if (window.removeObject === removeObjectWrap) return;
            if (removeObjectOriginal && window.removeObject !== removeObjectOriginal) removeObjectOriginal = null;
            removeObjectOriginal = window.removeObject;
            removeObjectWrap = function (data) {
                var obj = null;
                try {
                    var id = data && (data.a != null ? data.a : data.id);
                    obj = window.game && game.gameObjects ? game.gameObjects[id] : null;
                    rememberRemoval(data, obj);
                } catch (e) {}
                return removeObjectOriginal.apply(this, arguments);
            };
            window.removeObject = removeObjectWrap;
        } catch (e) {}
    }

    function attachFoodSync() {
        try {
            if (typeof gameServer === "undefined" || !gameServer || foodSyncSocket === gameServer) return;
            if (foodSyncSocket && foodSyncHandler) {
                if (typeof foodSyncSocket.off === "function") foodSyncSocket.off(socketMsgType.SYNC, foodSyncHandler);
                else if (typeof foodSyncSocket.removeListener === "function") foodSyncSocket.removeListener(socketMsgType.SYNC, foodSyncHandler);
            }
            foodSyncSocket = gameServer;
            foodSyncHandler = noteFoodRemoval;
            foodSyncSocket.on(socketMsgType.SYNC, foodSyncHandler);
        } catch (e) {}
    }

    function ownedSkins() {
        var map = Object.create(null);
        try {
            if (!window.user || typeof user.skins !== "string") return map;
            user.skins.split(";").forEach(function (part) {
                var bits = part.split(",");
                var name = bits.shift();
                if (!name) return;
                map[name] = new Set(bits.map(function (id) { return Number(id) || 0; }).filter(Boolean));
            });
        } catch (e) {}
        return map;
    }

    function cosmeticCatalog() {
        var out = Object.create(null);
        out.pets = new Set();
        for (var petId = 1; petId <= 41; petId++) out.pets.add(petId);
        var owned = ownedSkins();
        Object.keys(owned).forEach(function (name) {
            out[name] = out[name] || new Set();
            owned[name].forEach(function (id) { out[name].add(id); });
        });

        try {
            if (typeof skinsPreview !== "undefined" && skinsPreview) {
                Object.keys(skinsPreview).forEach(function (name) {
                    var list = skinsPreview[name];
                    if (!list) return;
                    Object.keys(list).forEach(function (id) {
                        id = Number(id) || 0;
                        if (!id) return;
                        out[name] = out[name] || new Set();
                        out[name].add(id);
                    });
                });
            }
        } catch (e) {}

        try {
            if (window.game && game.objectsDef && game.sprites) {
                var baseToName = Object.create(null);
                Object.keys(game.objectsDef).forEach(function (name) {
                    var def = game.objectsDef[name];
                    if (!def || !def.animations) return;
                    if (window.objectType && def.type !== objectType.PLAYER) return;
                    Object.keys(def.animations).forEach(function (anim) {
                        var row = def.animations[anim];
                        if (!row || !row.sprites) return;
                        var bases = Array.isArray(row.sprites) ? row.sprites : Object.keys(row.sprites).map(function (key) { return row.sprites[key]; });
                        bases.forEach(function (base) { if (base) baseToName[base] = name; });
                    });
                });
                function addCatalogKey(key) {
                    var pet = key.match(/^pet_(\d+)_/);
                    if (pet) {
                        out.pets = out.pets || new Set();
                        out.pets.add(Number(pet[1]));
                    }
                    var skin = key.match(/^(.*)_(\d+)$/);
                    if (!skin || !baseToName[skin[1]]) return;
                    var id = Number(skin[2]) || 0;
                    if (!id) return;
                    var name = baseToName[skin[1]];
                    out[name] = out[name] || new Set();
                    out[name].add(id);
                }
                [game.spritesToDownload, game.sprites].forEach(function (bank) {
                    if (!bank) return;
                    Object.keys(bank).forEach(addCatalogKey);
                });
                try {
                    if (typeof skinsPreview !== "undefined" && skinsPreview) {
                        Object.keys(skinsPreview).forEach(function (name) {
                            var list = skinsPreview[name];
                            var base = list && list[0];
                            if (!base || name === "pets") return;
                            [game.spritesToDownload, game.sprites].forEach(function (bank) {
                                if (!bank) return;
                                Object.keys(bank).forEach(function (key) {
                                    if (key.indexOf(String(base) + "_") !== 0) return;
                                    var id = Number(key.slice(String(base).length + 1));
                                    if (!Number.isFinite(id) || id <= 0) return;
                                    out[name] = out[name] || new Set();
                                    out[name].add(id);
                                });
                            });
                        });
                    }
                } catch (e) {}
                Object.keys(game.objectsDef).forEach(function (name) {
                    var pet = name.match(/^pet_(\d+)$/);
                    if (!pet) return;
                    out.pets = out.pets || new Set();
                    out.pets.add(Number(pet[1]));
                });
            }
        } catch (e) {}

        try {
            document.querySelectorAll('.offer.skin[data-skin][data-skin-id]').forEach(function (node) {
                var name = node.getAttribute('data-skin');
                var id = Number(node.getAttribute('data-skin-id')) || 0;
                if (!name || !id) return;
                out[name] = out[name] || new Set();
                out[name].add(id);
            });
        } catch (e) {}
        return out;
    }

    function prettyName(name) {
        try {
            var def = game.objectsDef && game.objectsDef[name];
            if (def && def.fullName) return def.fullName;
        } catch (e) {}
        return String(name || "").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, function (c) { return c.toUpperCase(); });
    }

    function directImage(src) {
        var value = String(src || "");
        if (/^data:image\//i.test(value)) return value;
        if (/^https?:\/\//i.test(value) && /\.(png|webp|gif|jpe?g|svg)(?:[?#]|$)/i.test(value)) return value;
        return "";
    }

    function previewSprite(name, id) {
        try {
            var previews = typeof skinsPreview !== "undefined" ? skinsPreview : null;
            var list = previews && previews[name];
            if (!list) return "";
            if (name === "pets") {
                if (list[id]) return list[id];
                if (list[0]) return String(list[0]).replace("(id)", id);
                return "";
            }
            if (list[id]) return list[id];
            if (list[0]) return String(list[0]) + "_" + id;
        } catch (e) {}
        return "";
    }

    function requestSpriteKey(key) {
        if (!key || !window.game) return;
        try {
            if (game.sprites && game.sprites[key] && game.sprites[key].loaded) return;
            var pending = game.spritesToDownload && game.spritesToDownload[key];
            if (!pending || typeof game.downloadSprite !== "function") return;
            var loadKey = "sprite:" + key;
            if (skinLoads.has(loadKey)) return;
            skinLoads.add(loadKey);
            game.downloadSprite(key, pending.url, pending.params, function () {
                skinLoads.delete(loadKey);
                if (!dead) refreshCosmeticImages();
            });
            setTimeout(function () {
                skinLoads.delete(loadKey);
                if (!dead) refreshCosmeticImages();
            }, 2500);
        } catch (e) {}
    }

    function skinImage(name, id) {
        try {
            var node = document.querySelector('.offer.skin[data-skin="' + CSS.escape(name) + '"][data-skin-id="' + id + '"] .sprite-image');
            if (node && node.naturalWidth > 0 && node.naturalHeight > 0) {
                var fromNode = spriteUrl(node);
                if (fromNode) return fromNode;
            }
        } catch (e) {}
        try {
            var previewName = previewSprite(name, id);
            if (previewName) {
                var preview = game.sprites && game.sprites[previewName];
                var previewUrl = spriteUrl(preview);
                if (previewUrl) return previewUrl;
                requestSpriteKey(previewName);
            }
            var defName = name;
            if (name === "pets" && id > 0) defName = "pet_" + id;
            var def = game.objectsDef && game.objectsDef[defName];
            if (!def || !def.animations) return "";
            var anim = def.animations[def.animation] || def.animations.flying_right || def.animations.none || def.animations.flapping_right || def.animations.idle_right || def.animations.running_right;
            if (!anim || !anim.sprites) return "";
            var bases = Array.isArray(anim.sprites) ? anim.sprites : Object.keys(anim.sprites).map(function (key) { return anim.sprites[key]; });
            if (!bases.length) return "";
            var base = bases[0];
            var spriteName = base + (name === "pets" ? "" : (id ? "_" + id : ""));
            var sprite = game.sprites && game.sprites[spriteName];
            var result = spriteUrl(sprite);
            if (result) return result;
            requestSpriteKey(spriteName);
            if (id > 0 && name !== "pets" && game && typeof game.loadSkin === "function") {
                var loadKey = name + ":" + id;
                if (!skinLoads.has(loadKey)) {
                    skinLoads.add(loadKey);
                    try {
                        def.skinsLoaded = def.skinsLoaded || [];
                        game.loadSkin(name, id);
                    } catch (e) {}
                    setTimeout(function () {
                        skinLoads.delete(loadKey);
                        if (!dead) refreshCosmeticImages();
                    }, 1200);
                }
            }
        } catch (e) {}
        return "";
    }

    function freeSkin() {
        if (!skinHold) return;
        var row = skinHold;
        skinHold = null;
        try {
            var desc = Object.getOwnPropertyDescriptor(row.obj, "skin");
            if (desc && desc.get === row.get) {
                delete row.obj.skin;
                row.obj.skin = row.last;
            }
        } catch (e) {}
    }

    function holdSkin(obj, id) {
        id = Number(id) || 0;
        if (skinHold && skinHold.obj === obj) {
            skinHold.pick = id;
            return;
        }
        freeSkin();
        var row = { obj: obj, pick: id, last: obj.skin, get: null, set: null };
        row.get = function () { return row.pick; };
        row.set = function (value) { row.last = value; };
        try {
            Object.defineProperty(obj, "skin", { configurable: true, enumerable: true, get: row.get, set: row.set });
            skinHold = row;
        } catch (e) {
            try { obj.skin = id; } catch (x) {}
        }
    }

    function hasPremium() {
        try {
            if (!window.user) return false;
            return user.premium === true || Number(user.premium) > 0 || String(user.premium).toLowerCase() === "true";
        } catch (e) { return false; }
    }

    function isOwned(name, id) {
        if (!id) return true;
        var map = ownedSkins();
        return !!(map[name] && map[name].has(Number(id)));
    }

    function officialSkin(name, id) {
        if (!isOwned(name, id)) return false;
        if (name === "pets" && !hasPremium()) return false;
        try {
            if (typeof selectedSkins !== "undefined") {
                if (id > 0) selectedSkins[name] = id;
                else delete selectedSkins[name];
                if (typeof selectedSkinsToString === "function" && window.user) {
                    user.selectedSkins = selectedSkinsToString(selectedSkins);
                    if (typeof setCookie === "function") setCookie("selectedSkins", user.selectedSkins, 365);
                }
            }
            if (id > 0 && name !== "pets" && game && typeof game.loadSkin === "function") game.loadSkin(name, id);
            if (typeof joinedGame !== "undefined" && joinedGame && typeof imDead !== "undefined" && !imDead && typeof gameServer !== "undefined" && typeof socketMsgType !== "undefined") {
                gameServer.emit(socketMsgType.CHANGESKIN, { skinId: id, skinName: name });
            }
            return true;
        } catch (e) { return false; }
    }

    function chooseSkin(name, id) {
        cfg.misc.skinPick[name] = id;
        if (isOwned(name, id)) officialSkin(name, id);
        saveCfg();
        applyCosmetics(performance.now(), true);
        renderSkinPanel(true);
    }

    function choosePet(id) {
        cfg.misc.petId = id;
        if (isOwned("pets", id) && hasPremium()) officialSkin("pets", id);
        saveCfg();
        applyCosmetics(performance.now(), true);
        renderPetMenu();
        updatePetButton();
    }

    function cosmeticState(name, id) {
        if (!id) return "Default";
        if (name === "pets" && !hasPremium()) return "Locked";
        return isOwned(name, id) ? "Owned" : "Not owned";
    }

    function cosmeticPreview(name, id) {
        var image = id ? skinImage(name, id) : skinImage(name, 0);
        var state = cosmeticState(name, id);
        return '<span class="ez-skin-preview">' + (image ? '<img src="' + image + '">' : '<em>#' + (id || 0) + '</em>') + '<small class="' + (state === 'Owned' ? 'ez-owned' : '') + '">' + state + '</small></span>';
    }

    function refreshCosmeticImages() {
        if (!menu) return;
        menu.querySelectorAll("[data-skin-select]").forEach(function (select) {
            var row = select.closest(".ez-skin-row");
            var preview = row && row.querySelector(".ez-skin-preview");
            if (!preview) return;
            var name = select.getAttribute("data-skin-select");
            var id = Number(select.value) || 0;
            var image = id ? skinImage(name, id) : skinImage(name, 0);
            var state = cosmeticState(name, id);
            preview.innerHTML = (image ? '<img src="' + image + '">' : '<em>#' + (id || 0) + '</em>') + '<small class="' + (state === 'Owned' ? 'ez-owned' : '') + '">' + state + '</small>';
        });
        var petMenu = menu.querySelector("[data-pet-menu]");
        if (!petMenu || !petMenu.classList.contains("ez-open")) renderPetMenu();
        updatePetButton();
    }

    function renderSkinPanel(force) {
        if (!menu) return;
        var holder = menu.querySelector("[data-skin-list]");
        if (!holder) return;
        var focused = document.activeElement;
        if (!force && focused && focused.matches && focused.matches("[data-skin-select]")) return;
        var catalog = cosmeticCatalog();
        var names = Object.keys(catalog).filter(function (name) {
            if (name === "pets" || name.indexOf("_effects") !== -1 || name === "tombstones" || name === "profile_themes") return false;
            try { return !!(game.objectsDef[name] && (!window.objectType || game.objectsDef[name].type === objectType.PLAYER)); } catch (e) { return false; }
        }).sort(function (a, b) { return prettyName(a).localeCompare(prettyName(b)); });
        var sign = names.map(function (name) { return name + ":" + Array.from(catalog[name]).sort(function (a,b) { return a-b; }).join("."); }).join("|");
        if (!force && sign === catalogSign) return;
        catalogSign = sign;
        if (!names.length) {
            holder.innerHTML = '<div class="ez-empty">Skin IDs are not ready yet</div>';
            return;
        }
        holder.innerHTML = names.map(function (name) {
            var ids = Array.from(catalog[name]).sort(function (a, b) { return a - b; });
            var selected = Number(cfg.misc.skinPick[name]);
            if (!Number.isFinite(selected)) {
                try { selected = typeof selectedSkins !== "undefined" && selectedSkins[name] ? Number(selectedSkins[name]) : 0; } catch (e) { selected = 0; }
            }
            var opts = '<option value="0"' + (!selected ? ' selected' : '') + '>Default</option>' + ids.map(function (id) {
                return '<option value="' + id + '"' + (selected === id ? ' selected' : '') + '>Skin ' + id + ' · ' + cosmeticState(name, id) + '</option>';
            }).join('');
            return '<label class="ez-skin-row"><span class="ez-skin-name">' + escapeHtml(prettyName(name)) + '</span>' + cosmeticPreview(name, selected) + '<select class="ez-select" data-skin-select="' + escapeHtml(name) + '">' + opts + '</select></label>';
        }).join('');
        renderPetMenu();
        updatePetButton();
    }

    function renderPetMenu() {
        if (!menu) return;
        var holder = menu.querySelector("[data-pet-menu]");
        if (!holder) return;
        var catalog = cosmeticCatalog();
        var ids = catalog.pets ? Array.from(catalog.pets) : [];
        ids = Array.from(new Set(ids)).filter(function (id) { return id > 0; }).sort(function (a, b) { return a - b; });
        var all = [0].concat(ids);
        holder.innerHTML = all.map(function (id) {
            var image = id ? skinImage("pet_" + id, 0) || skinImage("pets", id) : "";
            var state = cosmeticState("pets", id);
            var title = id ? "Pet " + id : "None";
            return '<button class="ez-pet-choice' + (Number(cfg.misc.petId) === id ? ' ez-active' : '') + '" type="button" data-pet-id="' + id + '" title="' + title + ' · ' + state + '">' + (image ? '<img src="' + image + '">' : '<span>' + title + '</span>') + '<small class="' + (state === 'Owned' ? 'ez-owned' : '') + '">' + state + '</small></button>';
        }).join('');
    }

    function updatePetButton() {
        if (!menu) return;
        var button = menu.querySelector("[data-pet-button]");
        if (!button) return;
        var id = Number(cfg.misc.petId) || 0;
        var image = id ? skinImage("pet_" + id, 0) || skinImage("pets", id) : "";
        var state = cosmeticState("pets", id);
        button.innerHTML = image ? '<img src="' + image + '">' + state : (id ? 'Pet ' + id + ' · ' + state : 'None');
        button.title = id ? 'Pet ' + id + ' · ' + state : 'No pet';
    }

    function removeLocalPet() {
        if (localPet && !localPet.deleted && window.game) {
            try { game.deleteObject(localPet); } catch (e) { localPet.deleted = true; }
        }
        localPet = null;
        localPetOwner = null;
        localPetMe = null;
        if (hiddenPet && !hiddenPet.deleted) hiddenPet.opacity = hiddenPetOpacity;
        hiddenPet = null;
    }

    function petAttack(data) {
        try {
            if (!data || !localPet || localPet.deleted || !localPetMe || localPetMe.deleted) return;
            if (String(data.attacker) !== String(localPetMe.id)) return;
            var victim = game.gameObjects && game.gameObjects[data.victim];
            if (!victim || victim.deleted) return;
            localPet.victim = victim;
            localPet.attackTime = game.time;
        } catch (e) {}
    }

    function attachPetAttack() {
        try {
            if (typeof gameServer === "undefined" || !gameServer || typeof socketMsgType === "undefined") return;
            if (petAttackSocket === gameServer) return;
            if (petAttackSocket && petAttackHandler) {
                if (typeof petAttackSocket.off === "function") petAttackSocket.off(socketMsgType.DOPETATTACK, petAttackHandler);
                else if (typeof petAttackSocket.removeListener === "function") petAttackSocket.removeListener(socketMsgType.DOPETATTACK, petAttackHandler);
            }
            petAttackSocket = gameServer;
            petAttackHandler = petAttack;
            petAttackSocket.on(socketMsgType.DOPETATTACK, petAttackHandler);
        } catch (e) {}
    }

    function makeLocalPet(me, id) {
        if (!window.game || !game.objectsDef || !game.objectsDef["pet_" + id] || !me || me.deleted) return;
        if (localPet && !localPet.deleted && Number(localPet.petID) === id && localPetMe === me) return;
        if (localPet && !localPet.deleted) {
            try { game.deleteObject(localPet); } catch (e) { localPet.deleted = true; }
        }
        localPet = null;
        localPetOwner = null;
        var owner = { petID: id, pet: null };
        ["deleted", "killed", "inHide", "direction", "position"].forEach(function (name) {
            Object.defineProperty(owner, name, { configurable: true, get: function () { return me[name]; } });
        });
        Object.defineProperty(owner, "petOffset", { configurable: true, get: function () {
            return me.petOffset && Number.isFinite(me.petOffset.x) && Number.isFinite(me.petOffset.y) ? me.petOffset : { x: 55, y: 55 };
        }});
        Object.defineProperty(owner, "moveSpeed", { configurable: true, get: function () {
            return me.moveSpeed || { x: 0, y: 0 };
        }});
        owner.getAllPositions = function () { return me.getAllPositions(); };
        var pet = game.newObject(game.objectsDef["pet_" + id]);
        var all = me.getAllPositions();
        var center = all.center;
        pet.id = 0;
        pet.petID = id;
        pet.position.x = center.x - pet.width / 2;
        pet.position.y = center.y - pet.height / 2;
        pet.owner = owner;
        pet.moveSpeed.x = me.moveSpeed ? me.moveSpeed.x : 0;
        pet.moveSpeed.y = me.moveSpeed ? me.moveSpeed.y : 0;
        localPet = game.addObject(pet);
        localPetOwner = owner;
        localPetMe = me;
        owner.pet = localPet;
    }

    function applyCosmetics(now, force) {
        if (!force && now - cosmeticAt < 60) return;
        cosmeticAt = now;
        attachPetAttack();
        var me = ownPlayer();
        if (!me) {
            freeSkin();
            removeLocalPet();
            cosmeticMe = null;
            return;
        }
        var spawned = cosmeticMe !== me;
        if (spawned) {
            freeSkin();
            cosmeticMe = me;
            appliedSkinSpawn = "";
            appliedPetSpawn = "";
            if (localPet && localPetMe !== me) removeLocalPet();
        }
        var spawnKey = String(me.id != null ? me.id : "me") + ":" + String(me.name || "");
        var canSend = false;
        try {
            canSend = typeof joinedGame !== "undefined" && joinedGame && typeof imDead !== "undefined" && !imDead && typeof gameServer !== "undefined" && typeof socketMsgType !== "undefined";
        } catch (e) {}

        var skin = cfg.misc.skinPick && Number(cfg.misc.skinPick[me.name]);
        if (Number.isFinite(skin)) {
            if (skin > 0 && game.loadSkin && !skinLoads.has(me.name + ":" + skin)) {
                skinLoads.add(me.name + ":" + skin);
                try { game.loadSkin(me.name, skin); } catch (e) {}
            }
            holdSkin(me, skin);
            var skinSpawnKey = spawnKey + ":" + skin;
            if (canSend && isOwned(me.name, skin) && appliedSkinSpawn !== skinSpawnKey) {
                officialSkin(me.name, skin);
                appliedSkinSpawn = skinSpawnKey;
            }
        } else if (skinHold && skinHold.obj === me) {
            freeSkin();
        }

        var petId = Number(cfg.misc.petId) || 0;
        var realPet = petId > 0 && isOwned("pets", petId) && hasPremium();
        if (cfg.misc.petChanger && petId > 0) {
            if (realPet) {
                removeLocalPet();
                if (Number(me.petID) !== petId) me.petID = petId;
                var petSpawnKey = spawnKey + ":" + petId;
                if (canSend && appliedPetSpawn !== petSpawnKey) {
                    officialSkin("pets", petId);
                    appliedPetSpawn = petSpawnKey;
                }
            } else {
                appliedPetSpawn = "";
                if (me.pet && me.pet !== localPet && !me.pet.deleted) {
                    if (hiddenPet !== me.pet) {
                        if (hiddenPet && !hiddenPet.deleted) hiddenPet.opacity = hiddenPetOpacity;
                        hiddenPet = me.pet;
                        hiddenPetOpacity = Number.isFinite(me.pet.opacity) ? me.pet.opacity : 1;
                    }
                    me.pet.opacity = 0;
                }
                makeLocalPet(me, petId);
                if (localPet && hiddenPet && !hiddenPet.deleted && hiddenPet.victim && !hiddenPet.victim.deleted) {
                    localPet.victim = hiddenPet.victim;
                    localPet.attackTime = hiddenPet.attackTime || game.time;
                }
            }
        } else {
            appliedPetSpawn = "";
            removeLocalPet();
        }
        if (now - catalogAt > 1800) {
            catalogAt = now;
            renderSkinPanel(false);
        }
    }

    function reaperSkill(obj) {
        try {
            var def = game.objectsDef && game.objectsDef[obj.name];
            var skill = obj.skill || (def && def.skill);
            return skill && skill.fullName === "scythe attack" ? skill : null;
        } catch (e) { return null; }
    }

    function frameName(obj) {
        try {
            var row = obj.animations && obj.animations[obj.animation];
            if (!row || !row.sprites || !row.sprites.length) return "";
            return row.sprites[obj.animationFrame % row.sprites.length];
        } catch (e) { return ""; }
    }

    function sweepRegion(sprite, obj, right) {
        var offsets = obj && obj.colliderRectangleOffset || {};
        var leftOffset = clamp(Number(offsets.left) || 0, 0, .49);
        var rightOffset = clamp(Number(offsets.right) || 0, 0, .49);
        var topOffset = clamp(Number(offsets.top) || 0, 0, .49);
        var bottomOffset = clamp(Number(offsets.bottom) || 0, 0, .49);
        var key = (right ? "r:" : "l:") + [leftOffset, rightOffset, topOffset, bottomOffset].join(":");

        if (!sprite || !sprite.width || !sprite.height) {
            return right
                ? { x: 1 - rightOffset - .025, y: .03, width: rightOffset + .025, height: .94 }
                : { x: 0, y: .03, width: leftOffset + .025, height: .94 };
        }

        var saved = bladeRegions.get(sprite);
        if (!saved) {
            saved = {};
            bladeRegions.set(sprite, saved);
        }
        if (saved[key]) return saved[key];

        try {
            var sw = Number(sprite.naturalWidth) || Number(sprite.width) || 1;
            var sh = Number(sprite.naturalHeight) || Number(sprite.height) || 1;
            var scale = Math.min(1, 256 / Math.max(sw, sh));
            var cw = Math.max(1, Math.round(sw * scale));
            var ch = Math.max(1, Math.round(sh * scale));
            var canvas = document.createElement("canvas");
            canvas.width = cw;
            canvas.height = ch;
            var g = canvas.getContext("2d", { willReadFrequently: true });
            g.drawImage(sprite, 0, 0, cw, ch);
            var data = g.getImageData(0, 0, cw, ch).data;

            var bodyEdge = (right ? 1 - rightOffset : leftOffset) * cw;
            var overlap = Math.max(2, cw * .025);
            var minX = cw, minY = ch, maxX = -1, maxY = -1;
            var x, y, a;

            for (y = 0; y < ch; y++) {
                for (x = 0; x < cw; x++) {
                    a = data[(y * cw + x) * 4 + 3];
                    if (a < 30) continue;
                    if (right ? x < bodyEdge - overlap : x > bodyEdge + overlap) continue;
                    if (x < minX) minX = x;
                    if (x > maxX) maxX = x;
                    if (y < minY) minY = y;
                    if (y > maxY) maxY = y;
                }
            }

            if (maxX < minX || maxY < minY) throw new Error("no weapon reach");

            var padX = Math.max(2, cw * .018);
            var padY = Math.max(2, ch * .035);
            var startX = right ? Math.min(bodyEdge - overlap, minX) : Math.max(0, minX - padX);
            var endX = right ? Math.min(cw, maxX + padX) : Math.max(bodyEdge + overlap, maxX);
            if (!right) {
                startX = Math.max(0, minX - padX);
                endX = Math.min(cw, Math.max(bodyEdge + overlap, maxX + padX));
            }

            var result = {
                x: clamp(startX / cw, 0, 1),
                y: clamp((minY - padY) / ch, 0, 1),
                width: clamp((endX - startX) / cw, .08, .58),
                height: clamp((maxY - minY + padY * 2) / ch, .18, 1)
            };
            if (result.x + result.width > 1) result.width = 1 - result.x;
            if (result.y + result.height > 1) result.height = 1 - result.y;
            saved[key] = result;
            return result;
        } catch (e) {
            var fallback = right
                ? { x: Math.max(0, 1 - rightOffset - .03), y: .03, width: Math.min(.5, rightOffset + .03), height: .94 }
                : { x: 0, y: .03, width: Math.min(.5, leftOffset + .03), height: .94 };
            saved[key] = fallback;
            return fallback;
        }
    }

    function attackSweepRegion(obj, right) {
        var def = null;
        try { def = game.objectsDef && game.objectsDef[obj.name]; } catch (e) {}
        var animations = obj.animations || (def && def.animations) || {};
        var row = animations[right ? "attacking_right" : "attacking_left"];
        var names = row && row.sprites ? row.sprites : [];
        var key = String(obj.name || "reaper") + ":" + (right ? "r" : "l") + ":" + names.length;
        var cached = attackRegions.get(key);
        if (cached && cached.ready) return cached.region;

        var union = null;
        var loaded = 0;
        for (var i = 0; i < names.length; i++) {
            var sprite = null;
            try { sprite = game.sprites && game.sprites[names[i]]; } catch (e) {}
            if (!sprite || !(sprite.width > 0) || !(sprite.height > 0)) continue;
            var region = sweepRegion(sprite, obj, right);
            if (!region) continue;
            loaded++;
            if (!union) {
                union = { x: region.x, y: region.y, width: region.width, height: region.height };
            } else {
                var x1 = Math.min(union.x, region.x);
                var y1 = Math.min(union.y, region.y);
                var x2 = Math.max(union.x + union.width, region.x + region.width);
                var y2 = Math.max(union.y + union.height, region.y + region.height);
                union.x = x1;
                union.y = y1;
                union.width = x2 - x1;
                union.height = y2 - y1;
            }
        }

        var offsets = obj.colliderRectangleOffset || (def && def.colliderRectangleOffset) || {};
        var leftOffset = clamp(Number(offsets.left) || 0, 0, .49);
        var rightOffset = clamp(Number(offsets.right) || 0, 0, .49);
        if (!union) {
            union = right
                ? { x: Math.max(0, 1 - rightOffset - .04), y: .04, width: rightOffset + .04, height: .92 }
                : { x: 0, y: .04, width: leftOffset + .04, height: .92 };
        }

        union.y = clamp(union.y - .055, 0, 1);
        union.height = clamp(union.height + .11, .45, 1 - union.y);
        if (right) {
            var bodyRight = 1 - rightOffset;
            var outerRight = Math.max(1, union.x + union.width);
            union.x = clamp(Math.min(union.x, bodyRight - .035), 0, 1);
            union.width = Math.max(.20, outerRight - union.x);
        } else {
            var bodyLeft = leftOffset;
            var outerLeft = Math.min(0, union.x);
            var end = Math.max(union.x + union.width, bodyLeft + .035);
            union.x = outerLeft;
            union.width = Math.max(.20, end - outerLeft);
        }

        var result = { x: union.x, y: union.y, width: union.width, height: union.height };
        attackRegions.set(key, { region: result, ready: names.length > 0 && loaded === names.length });
        return result;
    }

    function weaponBox(obj) {
        if (!obj || !obj.position) return null;
        var skill = reaperSkill(obj);
        if (!skill) return null;

        var animName = String(obj.animation || "");
        var right = animName.indexOf("_right") !== -1 ? true : (animName.indexOf("_left") !== -1 ? false : Number(obj.direction) === 1);
        var attacking = animName.indexOf("attacking_") === 0;
        var region = attackSweepRegion(obj, right);
        if (!region) return null;

        var pos = game.getRenderPosition(obj.position.x, obj.position.y);
        var sx = (Number(game.scaleX) || 1) * (Number(game.zoom) || 1);
        var sy = (Number(game.scaleY) || 1) * (Number(game.zoom) || 1);
        var objectW = (Number(obj.width) || 50) * sx;
        var objectH = (Number(obj.height) || 50) * sy;
        if (!pos || !Number.isFinite(pos.x) || !Number.isFinite(pos.y) || !(objectW > 0) || !(objectH > 0)) return null;

        var offsets = obj.colliderRectangleOffset || {};
        var leftOffset = clamp(Number(offsets.left) || 0, 0, .49);
        var rightOffset = clamp(Number(offsets.right) || 0, 0, .49);
        var reachWorld = Number(skill.params && skill.params[0]);
        if (!(reachWorld > 0)) reachWorld = 30;
        var reachX = reachWorld * sx;

        var y = pos.y - objectH + region.y * objectH;
        var height = Math.max(18, region.height * objectH);
        var x, width;
        if (right) {
            var bodyEdgeR = pos.x + (1 - rightOffset) * objectW;
            var visualEndR = pos.x + Math.max(1, region.x + region.width) * objectW;
            x = Math.min(bodyEdgeR - objectW * .025, pos.x + region.x * objectW);
            width = Math.max(24, visualEndR + reachX - x);
        } else {
            var bodyEdgeL = pos.x + leftOffset * objectW;
            var visualStartL = pos.x + Math.min(0, region.x) * objectW;
            var endL = Math.max(bodyEdgeL + objectW * .025, pos.x + (region.x + region.width) * objectW);
            x = visualStartL - reachX;
            width = Math.max(24, endL - x);
        }

        return {
            x: x,
            y: y,
            width: width,
            height: height,
            attacking: attacking
        };
    }

    function drawHitboxes() {
        if (!cfg.misc.hitbox || !window.game || !game.me) return;
        var list = [game.me].concat(players);
        for (var i = 0; i < list.length; i++) {
            var obj = list[i];
            if (!obj || obj.deleted || !obj.position) continue;
            var box = weaponBox(obj);
            if (!box) continue;
            ctx.save();
            ctx.fillStyle = rgba(cfg.misc.hitboxColor, box.attacking ? .12 : .055);
            ctx.strokeStyle = rgba(cfg.misc.hitboxColor, box.attacking ? 1 : .82);
            ctx.lineWidth = box.attacking ? 1.7 : 1.1;
            ctx.fillRect(box.x, box.y, box.width, box.height);
            ctx.strokeRect(box.x, box.y, box.width, box.height);
            ctx.restore();
        }
    }

    function openSoundDb() {
        if (soundDbPromise) return soundDbPromise;
        soundDbPromise = new Promise(function (resolve) {
            try {
                if (!window.indexedDB) return resolve(null);
                var request = indexedDB.open("ezsquad.audio", 1);
                request.onupgradeneeded = function () {
                    var db = request.result;
                    if (!db.objectStoreNames.contains("sounds")) db.createObjectStore("sounds", { keyPath: "id" });
                };
                request.onsuccess = function () { resolve(request.result); };
                request.onerror = function () { resolve(null); };
            } catch (e) { resolve(null); }
        });
        return soundDbPromise;
    }

    function soundTx(mode, callback) {
        return openSoundDb().then(function (db) {
            if (!db) return null;
            return new Promise(function (resolve) {
                try {
                    var tx = db.transaction("sounds", mode);
                    var store = tx.objectStore("sounds");
                    var result = callback(store);
                    tx.oncomplete = function () { resolve(result && result.result !== undefined ? result.result : result); };
                    tx.onerror = function () { resolve(null); };
                    tx.onabort = function () { resolve(null); };
                } catch (e) { resolve(null); }
            });
        });
    }

    function getSoundRows() {
        return openSoundDb().then(function (db) {
            if (!db) return [];
            return new Promise(function (resolve) {
                try {
                    var request = db.transaction("sounds", "readonly").objectStore("sounds").getAll();
                    request.onsuccess = function () { resolve(Array.isArray(request.result) ? request.result : []); };
                    request.onerror = function () { resolve([]); };
                } catch (e) { resolve([]); }
            });
        });
    }

    function renderSoundOptions() {
        if (!menu) return;
        [["hit", "hitSoundType", "Click"], ["kill", "killSoundType", "Bell"]].forEach(function (row) {
            var kind = row[0], path = row[1], fallback = row[2];
            var select = menu.querySelector(".ez-" + kind + "-select");
            if (!select) return;
            var current = String(cfg.misc[path] || fallback);
            if (soundsReady && current.indexOf("custom:") === 0) {
                var id = current.slice(7);
                if (!customSounds.some(function (item) { return item.id === id; })) {
                    current = fallback;
                    cfg.misc[path] = current;
                    saveCfg();
                }
            }
            select.innerHTML = soundOptionsHtml(kind);
            select.value = current;
        });
    }

    function refreshCustomSounds() {
        var token = ++soundLoadToken;
        return getSoundRows().then(function (rows) {
            if (token !== soundLoadToken) return;
            soundsReady = true;
            customSounds = rows.map(function (row) {
                return { id: String(row.id), name: cleanFileName(row.name), type: row.type || "audio/mpeg", blob: row.blob };
            }).sort(function (a, b) { return a.name.localeCompare(b.name); });
            renderSoundOptions();
        });
    }

    function putSoundRow(row) {
        return soundTx("readwrite", function (store) { return store.put(row); });
    }

    function removeSoundRow(id) {
        return soundTx("readwrite", function (store) { return store.delete(id); });
    }

    function soundId() {
        try { if (crypto && typeof crypto.randomUUID === "function") return crypto.randomUUID(); } catch (e) {}
        return Date.now().toString(36) + Math.random().toString(36).slice(2);
    }

    function uploadSound(file, kind) {
        if (!file) return;
        kind = kind === "kill" ? "kill" : "hit";
        var ext = String(file.name || "").split(".").pop().toLowerCase();
        if (!(file.type && file.type.indexOf("audio/") === 0) && ["mp3", "wav", "ogg", "oga", "m4a", "aac", "flac", "webm"].indexOf(ext) < 0) return;
        if (file.size > 12 * 1024 * 1024) return;
        var row = { id: soundId(), name: cleanFileName(file.name), type: file.type || "audio/" + ext, blob: file, updated: Date.now() };
        putSoundRow(row).then(function () {
            customSounds.push({ id: row.id, name: row.name, type: row.type, blob: row.blob });
            customSounds.sort(function (a, b) { return a.name.localeCompare(b.name); });
            cfg.misc[kind === "kill" ? "killSoundType" : "hitSoundType"] = "custom:" + row.id;
            saveCfg();
            renderSoundOptions();
            playSound(kind);
        });
    }

    function selectedSound(kind) {
        var path = kind === "kill" ? "killSoundType" : "hitSoundType";
        var fallback = kind === "kill" ? "Bell" : "Click";
        var value = String(cfg.misc[path] || fallback);
        if (value.indexOf("custom:") !== 0) return null;
        var id = value.slice(7);
        for (var i = 0; i < customSounds.length; i++) if (customSounds[i].id === id) return customSounds[i];
        return null;
    }

    function decodeSound(item) {
        if (!item || !item.blob) return Promise.resolve(null);
        if (soundBuffers.has(item.id)) return Promise.resolve(soundBuffers.get(item.id));
        ensureAudio();
        if (!audio) return Promise.resolve(null);
        return item.blob.arrayBuffer().then(function (data) {
            return audio.decodeAudioData(data.slice(0));
        }).then(function (buffer) {
            soundBuffers.set(item.id, buffer);
            return buffer;
        }).catch(function () { return null; });
    }

    function playCustom(item, volume) {
        decodeSound(item).then(function (buffer) {
            if (!buffer || !audio) return;
            try {
                var source = audio.createBufferSource();
                var gain = audio.createGain();
                gain.gain.value = clamp(volume, 0, 1);
                source.buffer = buffer;
                source.connect(gain).connect(audio.destination);
                source.start();
            } catch (e) {}
        });
    }

    function bytesToBase64(buffer) {
        var bytes = new Uint8Array(buffer);
        var out = "";
        var step = 32768;
        for (var i = 0; i < bytes.length; i += step) out += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(bytes.length, i + step)));
        return btoa(out);
    }

    function base64ToBytes(value) {
        var raw = atob(String(value || ""));
        var out = new Uint8Array(raw.length);
        for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
        return out;
    }

    function exportSounds() {
        return Promise.all(customSounds.map(function (item) {
            if (!item.blob) return null;
            return item.blob.arrayBuffer().then(function (data) {
                return { id: item.id, name: item.name, type: item.type, data: bytesToBase64(data) };
            }).catch(function () { return null; });
        })).then(function (rows) { return rows.filter(Boolean); });
    }

    function importSounds(rows) {
        if (!Array.isArray(rows) || !rows.length) return Promise.resolve();
        return Promise.all(rows.map(function (row) {
            try {
                if (!row || !row.id || !row.data) return Promise.resolve();
                var blob = new Blob([base64ToBytes(row.data)], { type: row.type || "audio/mpeg" });
                return putSoundRow({ id: String(row.id), name: cleanFileName(row.name), type: row.type || blob.type, blob: blob, updated: Date.now() });
            } catch (e) { return Promise.resolve(); }
        })).then(refreshCustomSounds);
    }

    function ensureAudio() {
        try {
            if (!audio) audio = new (window.AudioContext || window.webkitAudioContext)();
            if (audio.state === "suspended") audio.resume();
        } catch (e) {}
    }

    function tone(freq, end, duration, type, gain, delay) {
        if (!audio) return;
        var now = audio.currentTime + (delay || 0);
        var osc = audio.createOscillator();
        var vol = audio.createGain();
        osc.type = type || "sine";
        osc.frequency.setValueAtTime(freq, now);
        osc.frequency.exponentialRampToValueAtTime(Math.max(30, end), now + duration);
        vol.gain.setValueAtTime(gain, now);
        vol.gain.exponentialRampToValueAtTime(.0001, now + duration);
        osc.connect(vol).connect(audio.destination);
        osc.start(now);
        osc.stop(now + duration + .01);
    }

    function playSound(kind) {
        kind = kind === "kill" ? "kill" : "hit";
        ensureAudio();
        if (!audio) return;
        var volume = clamp(Number(cfg.misc[kind === "kill" ? "killVolume" : "hitVolume"]) || 0, 0, 100) / 100;
        if (!volume) return;
        var custom = selectedSound(kind);
        if (custom) { playCustom(custom, volume); return; }
        var gain = volume * .16;
        var type = cfg.misc[kind === "kill" ? "killSoundType" : "hitSoundType"];
        if (type === "Bubble") tone(360, 920, .085, "sine", gain);
        else if (type === "Bell") { tone(1050, 760, .14, "sine", gain); tone(1580, 1120, .11, "sine", gain * .55, .012); }
        else if (type === "Metal") { tone(1700, 260, .075, "triangle", gain); tone(920, 180, .09, "square", gain * .35); }
        else tone(900, 420, .045, "square", gain);
    }

    function playHit() { playSound("hit"); }
    function playKill() { playSound("kill"); }

    function noteOwnAttack(data) {
        try {
            if (!window.game || !game.me || !data || String(data.objectId) !== String(game.me.id)) return;
            ownAttackAt = performance.now();
            ownAttackDirection = Number(data.direction) || Number(game.me.direction) || 1;
            ownAttackTargets.clear();
            var lag = Number.isFinite(Number(window.latency)) ? Math.max(0, Number(window.latency)) : 35;
            fightNext = ownAttackAt + clamp(500 - lag * .5, 455, 500);
        } catch (e) {}
    }

    function attachAttack() {
        try {
            if (typeof gameServer !== "undefined" && gameServer && attackSocket !== gameServer) {
                if (attackSocket && attackHandler) {
                    if (typeof attackSocket.off === "function") attackSocket.off(socketMsgType.DOATTACK, attackHandler);
                    else if (typeof attackSocket.removeListener === "function") attackSocket.removeListener(socketMsgType.DOATTACK, attackHandler);
                }
                attackSocket = gameServer;
                attackHandler = noteOwnAttack;
                attackSocket.on(socketMsgType.DOATTACK, attackHandler);
            }
            if (typeof window.doAttack === "function" && window.doAttack !== attackDrawWrap) {
                attackDrawOriginal = window.doAttack;
                attackDrawWrap = function (object, direction) {
                    try {
                        if (window.game && game.me && object === game.me) noteOwnAttack({ objectId: game.me.id, direction: direction });
                    } catch (e) {}
                    return attackDrawOriginal.apply(this, arguments);
                };
                window.doAttack = attackDrawWrap;
            }
        } catch (e) {}
    }

    function targetInWeapon(obj) {
        if (!window.game || !game.me || !obj || !obj.position) return false;
        var box = weaponBox(game.me);
        if (box) {
            var pos = game.getRenderPosition(obj.position.x, obj.position.y);
            var width = (Number(obj.width) || 50) * game.scaleX * game.zoom;
            var height = (Number(obj.height) || 50) * game.scaleY * game.zoom;
            var cx = pos.x + width / 2;
            var cy = pos.y - height / 2;
            var growX = Math.max(18, box.width * .35);
            var growY = Math.max(18, box.height * .35);
            if (cx >= box.x - growX && cx <= box.x + box.width + growX && cy >= box.y - growY && cy <= box.y + box.height + growY) return true;
        }
        var distance = worldDistance(game.me, obj);
        var reach = Math.max(120, (Number(game.me.width) || 50) * .7 + (Number(obj.width) || 50) * .7);
        return distance <= reach;
    }

    function ownContact(obj) {
        if (!window.game || !game.me || !obj || typeof checkFoodChain !== "function") return false;
        try {
            var link = checkFoodChain(game.me, obj);
            return !!link && link.check === 1 && targetInWeapon(obj);
        } catch (e) { return false; }
    }

    function watchHits(now) {
        if (!window.game || !window.objectType || !game.me) return;
        var anim = String(game.me.animation || "");
        if (anim.indexOf("attacking_") === 0 && anim !== ownAttackAnimation) {
            ownAttackAt = now;
            ownAttackDirection = anim.indexOf("_right") !== -1 ? 1 : -1;
            ownAttackTargets.clear();
        }
        ownAttackAnimation = anim;

        var seen = new Set();
        for (var i = 0; i < players.length; i++) {
            var obj = players[i];
            if (!obj || obj.deleted || obj.type !== objectType.PLAYER || obj === game.me) continue;
            var key = obj.id != null ? "o:" + obj.id : "u:" + obj.userId;
            seen.add(key);
            var hp = Number.isFinite(obj.hp) ? obj.hp : 100;
            var armor = Number.isFinite(obj.armor) ? obj.armor : 0;
            var old = hitBank.get(key);
            var damaged = old && (hp < old.hp || armor < old.armor);
            var manual = now - ownAttackAt < 1050 && targetInWeapon(obj);
            var contact = ownContact(obj);
            var blocked = manual && ownAttackTargets.has(key);
            if (damaged && cfg.misc.hitSound && (manual || contact) && !blocked && now - lastHitSound > 35) {
                if (manual) ownAttackTargets.add(key);
                lastHitSound = now;
                playHit();
            }
            hitBank.set(key, { hp: hp, armor: armor, at: now });
        }
        hitBank.forEach(function (value, key) { if (!seen.has(key) && now - value.at > 3000) hitBank.delete(key); });
    }

    function rawNick(obj) {
        if (!obj) return "";
        return privacyNames.get(obj) || obj.nick || "";
    }

    function displayNick(obj) {
        var hidden = cfg.misc.privacy && ((window.game && obj === game.me && cfg.misc.privacyIdentity) || (obj !== (window.game && game.me) && cfg.misc.privacyPlayers));
        if (hidden) return "Player";
        return privacyNames.get(obj) || (obj && obj.nick) || "Player";
    }

    function protectedPlayers() {
        return !!(cfg.misc.privacy && cfg.misc.privacyPlayers);
    }

    function restoreProtectedDom() {
        protectedDom.forEach(function (value, node) {
            if (!node || !node.isConnected) return;
            if (value.kind === "text") node.nodeValue = value.value;
            else node.textContent = value.value;
        });
        protectedDom.clear();
    }

    function maskTextNode(node, name) {
        if (!node || !name || !node.nodeValue) return;
        var source = String(node.nodeValue);
        var escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
        var next = source.replace(new RegExp(escaped, "gi"), "Player");
        if (next === source) return;
        if (!protectedDom.has(node)) protectedDom.set(node, { kind: "text", value: source });
        node.nodeValue = next;
    }

    function protectedNames() {
        var names = new Set();
        players.forEach(function (obj) { var name = rawNick(obj); if (name) names.add(String(name)); });
        scans.forEach(function (item) { if (item && item.nick) names.add(String(item.nick)); });
        recentPlayers.forEach(function (item, name) { if (name) names.add(String(name)); });
        deathEntries.forEach(function (item) { if (item && item.name) names.add(String(item.name)); });
        killEntries.forEach(function (item) { if (item && item.name) names.add(String(item.name)); });
        if (lastKillerName) names.add(String(lastKillerName));
        return Array.from(names).filter(function (name) { return name && name.toLowerCase() !== "player"; }).sort(function (a, b) { return b.length - a.length; });
    }

    function useProtectedDom(now) {
        if (now - protectedAt < 160) return;
        protectedAt = now;
        if (!protectedPlayers()) {
            restoreProtectedDom();
            return;
        }
        document.querySelectorAll(".system-feed-high-exp-nick,.system-feed-high-exp-killer,.system-feed-player-name,.profile-theme-killer-name").forEach(function (node) {
            if (!protectedDom.has(node)) protectedDom.set(node, { kind: "element", value: node.textContent });
            node.textContent = "Player";
        });
        var names = protectedNames();
        document.querySelectorAll("#systemFeed,.endGameText,#chat-box").forEach(function (root) {
            var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
            var node;
            while ((node = walker.nextNode())) for (var i = 0; i < names.length; i++) maskTextNode(node, names[i]);
        });
        protectedDom.forEach(function (value, node) { if (!node || !node.isConnected) protectedDom.delete(node); });
    }

    function restorePrivacyNames() {
        privacyNames.forEach(function (name, obj) {
            if (obj && !obj.deleted) obj.nick = name;
        });
        privacyNames.clear();
    }

    function usePrivacy() {
        var enabled = !!cfg.misc.privacy;
        var dom = [
            ["#gameContainer > div.debugInfo > div.server", enabled && cfg.misc.privacyServer, "visibility"],
            ["#gameContainer > .leaderboard", enabled && cfg.misc.privacyLeaderboard, "display"]
        ];
        dom.forEach(function (item) {
            var node = document.querySelector(item[0]);
            if (!node) return;
            if (item[1]) {
                if (!privacyDom.has(node)) privacyDom.set(node, { display: node.style.display, visibility: node.style.visibility });
                if (item[2] === "display") node.style.display = "none";
                else node.style.visibility = "hidden";
            } else if (privacyDom.has(node)) {
                var old = privacyDom.get(node);
                node.style.display = old.display;
                node.style.visibility = old.visibility;
                privacyDom.delete(node);
            }
        });
        if (!enabled || (!cfg.misc.privacyIdentity && !cfg.misc.privacyPlayers)) {
            restorePrivacyNames();
            return;
        }
        var list = [];
        if (cfg.misc.privacyIdentity && window.game && game.me) list.push(game.me);
        if (cfg.misc.privacyPlayers) list = list.concat(players);
        var active = new Set(list);
        privacyNames.forEach(function (name, obj) {
            if (!active.has(obj) && obj && !obj.deleted) {
                obj.nick = name;
                privacyNames.delete(obj);
            }
        });
        list.forEach(function (obj) {
            if (!obj || obj.deleted) return;
            if (!privacyNames.has(obj) && obj.nick) privacyNames.set(obj, obj.nick);
            obj.nick = "";
        });
    }

    function spriteUrl(sprite) {
        if (!sprite || !sprite.width || !sprite.height) return "";
        var cacheable = typeof sprite === "object" || typeof sprite === "function";
        try {
            if (cacheable && spriteImages.has(sprite)) return spriteImages.get(sprite);
            var fallback = directImage(sprite.currentSrc || sprite.src);
            var canvas = document.createElement("canvas");
            canvas.width = Math.max(1, Math.min(128, Number(sprite.width) || Number(sprite.naturalWidth) || 1));
            canvas.height = Math.max(1, Math.min(128, Number(sprite.height) || Number(sprite.naturalHeight) || 1));
            var g = canvas.getContext("2d");
            var sw = Number(sprite.naturalWidth) || Number(sprite.width) || 1;
            var sh = Number(sprite.naturalHeight) || Number(sprite.height) || 1;
            var scale = Math.min(canvas.width / sw, canvas.height / sh);
            g.drawImage(sprite, (canvas.width - sw * scale) / 2, (canvas.height - sh * scale) / 2, sw * scale, sh * scale);
            var data = canvas.toDataURL("image/png");
            if (cacheable) spriteImages.set(sprite, data);
            return data;
        } catch (e) {
            var safe = directImage(sprite.currentSrc || sprite.src);
            if (cacheable && safe) spriteImages.set(sprite, safe);
            return safe;
        }
    }

    function onDeath(data) {
        var killer = data && data.killer != null ? String(data.killer) : "";
        lastKillerName = killer;
        if (!cfg.misc.deathLog || !killer) return;
        var lower = killer.toLowerCase();
        var match = null;
        var savedSprite = null;
        var scanMatch = null;
        for (var i = 0; i < players.length; i++) {
            if (String(rawNick(players[i])).toLowerCase() === lower) { match = players[i]; break; }
        }
        if (!match) {
            var recent = recentPlayers.get(lower);
            if (recent) { match = recent.obj; savedSprite = recent.sprite || null; }
        }
        if (!match) {
            for (var s = 0; s < scans.length; s++) {
                if (String(scans[s] && scans[s].nick || "").toLowerCase() === lower) {
                    scanMatch = scans[s];
                    break;
                }
            }
        }

        var isPlayer = false;
        if (match) {
            try { isPlayer = match.type === objectType.PLAYER || game.objectsDef[match.name].type === objectType.PLAYER; } catch (e) {}
        } else if (scanMatch) {
            try { isPlayer = !!(game.objectsDef[scanMatch.evolution] && game.objectsDef[scanMatch.evolution].type === objectType.PLAYER); } catch (e) {}
        }
        if (!isPlayer) return;

        var avatarSprite = match ? objectSprite(match, performance.now()) : (scanMatch ? scanSprite(scanMatch) : savedSprite);
        if (!avatarSprite && savedSprite) avatarSprite = savedSprite;
        if (!avatarSprite && match && window.game && game.objectsDef && game.objectsDef[match.name]) {
            try {
                var def = game.objectsDef[match.name];
                var anim = def.animations[def.animation] || def.animations.idle_right || def.animations.flying_right || def.animations.none;
                avatarSprite = anim && anim.sprites && game.sprites[anim.sprites[0]];
            } catch (e) {}
        }
        var created = Date.now();
        deathEntries.push({
            name: killer,
            image: avatarSprite ? spriteUrl(avatarSprite) : "",
            at: created,
            until: created + clamp(Number(cfg.misc.deathTime) || 60, 10, 180) * 1000
        });
        deathEntries = deathEntries.slice(-6);
        updateDeathPanel();
    }

    function attachDeath() {
        try {
            if (typeof gameServer === "undefined" || !gameServer || deathSocket === gameServer) return;
            if (deathSocket && deathHandler) {
                if (typeof deathSocket.off === "function") deathSocket.off("game:dead", deathHandler);
                else if (typeof deathSocket.removeListener === "function") deathSocket.removeListener("game:dead", deathHandler);
            }
            deathSocket = gameServer;
            deathHandler = onDeath;
            deathSocket.on("game:dead", deathHandler);
        } catch (e) {}
    }

    function envKind(obj) {
        var name = String(obj && obj.name || "").toLowerCase();
        if (!name) return "";
        if (/^cloud\d/.test(name)) return "cloudAlpha";
        if (name.indexOf("bush") !== -1) return "bushAlpha";
        if (name === "water" || name === "water2" || name === "water2_fill" || name === "toxic_water") return "waterAlpha";
        if (name === "swamp") return "swampAlpha";
        if (name === "lava" || name === "lava_rising") return "lavaAlpha";
        return "";
    }

    function clearScene() {
        try {
            if (!window.game) return;
            if (game.staticContext && game.staticCanvas) game.staticContext.clearRect(0, 0, game.staticCanvas.width, game.staticCanvas.height);
            if (typeof game.clearStaticObjects === "function") game.clearStaticObjects();
            if (game.staticCanvasRenderOffset) {
                game.staticCanvasRenderOffset.x = 0;
                game.staticCanvasRenderOffset.y = 0;
                game.staticCanvasRenderOffset.restX = 0;
                game.staticCanvasRenderOffset.restY = 0;
            }
        } catch (e) {}
    }

    function syncScanIcon() {
        var icon = document.getElementById("scan-players-icon");
        if (!icon) return;
        if (!scanIconBase || scanIconBase.node !== icon) {
            scanIconBase = { node: icon, display: icon.style.display, visibility: icon.style.visibility };
        }
        var closed = !!(menu && menu.classList.contains("ez-hide"));
        if (closed) {
            icon.style.setProperty("display", "none", "important");
            icon.style.setProperty("visibility", "hidden", "important");
        } else {
            icon.style.removeProperty("display");
            icon.style.removeProperty("visibility");
            if (scanIconBase.display) icon.style.display = scanIconBase.display;
            if (scanIconBase.visibility) icon.style.visibility = scanIconBase.visibility;
        }
    }

    function attachBossSync() {
        if (bossSyncSocket || typeof gameServer === "undefined" || typeof socketMsgType === "undefined") return;
        bossSyncSocket = gameServer;
        bossSyncHandler = function (packet) {
            try {
                if (packet && packet.b) {
                    if (packet.b.g != null) bossSync = packet.b.g;
                    if (packet.b.d != null && Number.isFinite(Number(packet.b.d))) {
                        skillSync = Number(packet.b.d);
                        skillSyncAt = performance.now();
                        skillLocalEnd = skillSyncAt + Math.max(0, skillSync);
                        skillLocalMe = window.game && game.me ? game.me.id : 0;
                    } else {
                        skillSync = null;
                        skillSyncAt = performance.now();
                        skillLocalEnd = 0;
                    }
                }
            } catch (e) {}
        };
        try { bossSyncSocket.on(socketMsgType.SYNC, bossSyncHandler); } catch (e) { bossSyncSocket = null; bossSyncHandler = null; }
    }

    function drawRemoteBosses() {
        if (!cfg.extra.boss || !window.game || !game.me) return;
        var raw = null;
        try { raw = typeof spawnTimerData !== "undefined" ? spawnTimerData : window.spawnTimerData; } catch (e) {}
        if (!Array.isArray(raw)) return;
        var range = Number(cfg.extra.bossRange) || 0;
        for (var i = 0; i < raw.length; i++) {
            var row = raw[i];
            if (!row || !bossNames[row.c]) continue;
            var def = game.objectsDef && game.objectsDef[row.c];
            if (!def) continue;
            var pseudo = { name: row.c, position: { x: Number(row.a), y: Number(row.b) }, width: def.width || 50, height: def.height || 50, hp: 100 };
            if (range > 0 && meters(pseudo) > range) continue;
            var loaded = false;
            for (var j = 0; j < extras.length; j++) {
                if (extras[j].kind === "boss" && extras[j].obj && extras[j].obj.name === row.c && worldDistance(extras[j].obj, pseudo) < 500) { loaded = true; break; }
            }
            if (loaded) continue;
            var left = Number(row.d);
            var info = extraStyle("boss", pseudo, "");
            if (!info) continue;
            info.title = objectTitle(def, "Boss") + (Number.isFinite(left) && left > 0 ? " · " + shortClock(left) : " · Active");
            drawExtra(pseudo, info, performance.now());
        }
    }

    function useBright() {
        try {
            if (!darknessDraw && typeof window.drawDarkness === "function") {
                darknessDraw = window.drawDarkness;
                darknessWrap = function () {
                    if (cfg.other.fullBright) return;
                    return darknessDraw.apply(this, arguments);
                };
            }
            if (darknessWrap && window.drawDarkness !== darknessWrap) window.drawDarkness = darknessWrap;
        } catch (e) {}

        if (!window.game || !game.context) return;
        if (brightContext === game.context && brightWrap) return;
        if (brightContext && brightDraw && brightContext.drawImage === brightWrap) {
            try { brightContext.drawImage = brightDraw; } catch (e) {}
        }
        brightContext = game.context;
        brightDraw = brightContext.drawImage;
        brightWrap = function () {
            try {
                if (cfg.other.fullBright && typeof darknessCanvas !== "undefined" && arguments[0] === darknessCanvas) return;
            } catch (e) {}
            return brightDraw.apply(this, arguments);
        };
        try { brightContext.drawImage = brightWrap; } catch (e) { brightWrap = null; }
    }

    function useZoom() {
        if (!window.game || !game.canvas || typeof game.setCanvasSize !== "function") return;
        if (!cfg.other.zoom) {
            if (!viewBase) return;
            game.originalWidth = viewBase.width;
            game.originalHeight = viewBase.height;
            game.zoom = viewBase.zoom;
            try { if (typeof gameZoom !== "undefined") gameZoom = viewBase.gameZoom; } catch (e) {}
            game.setCanvasSize(game.canvas.width, game.canvas.height);
            viewBase = null;
            return;
        }
        if (!viewBase) {
            viewBase = {
                width: Number(game.originalWidth) || 1920,
                height: Number(game.originalHeight) || 1080,
                zoom: Number(game.zoom) || 1,
                gameZoom: typeof gameZoom !== "undefined" ? Number(gameZoom) || 1 : 1
            };
        }
        var value = clamp(Number(cfg.other.zoomValue) || 1, .15, 1);
        var width = viewBase.width / value;
        var height = viewBase.height / value;
        var changed = Math.abs(game.originalWidth - width) > .1 || Math.abs(game.originalHeight - height) > .1 || Math.abs(game.zoom - 1) > .001;
        game.originalWidth = width;
        game.originalHeight = height;
        game.zoom = 1;
        try { if (typeof gameZoom !== "undefined") gameZoom = 1; } catch (e) {}
        if (changed) game.setCanvasSize(game.canvas.width, game.canvas.height);
    }

    function useWorld(now, force) {
        useBright();
        useZoom();
        if (window.game && game.canvas && canvasFilter == null) canvasFilter = game.canvas.style.filter || "";

        var changedEnv = cfg.other.cloudAlpha !== 100 || cfg.other.bushAlpha !== 100 || cfg.other.waterAlpha !== 100 || cfg.other.swampAlpha !== 100 || cfg.other.lavaAlpha !== 100;
        if (!force && !worldDirty && now - worldAt < (changedEnv ? 350 : 1000)) return;
        worldAt = now;
        worldDirty = false;
        var changed = false;

        if (!changedEnv) {
            envBase.forEach(function (value, obj) {
                if (obj && !obj.deleted && obj.opacity !== value) {
                    obj.opacity = value;
                    changed = true;
                }
            });
            envBase.clear();
        } else if (window.game && game.gameObjects) {
            var list = game.gameObjects;
            for (var id in list) {
                if (!Object.prototype.hasOwnProperty.call(list, id)) continue;
                var obj = list[id];
                if (!obj || obj.deleted) continue;
                var key = envKind(obj);
                if (!key) continue;
                if (!envBase.has(obj)) envBase.set(obj, Number.isFinite(obj.opacity) ? obj.opacity : 1);
                var value = envBase.get(obj) * clamp(Number(cfg.other[key]) || 0, 0, 100) / 100;
                if (Math.abs((Number(obj.opacity) || 0) - value) > .001) {
                    obj.opacity = value;
                    changed = true;
                }
            }
            envBase.forEach(function (value, obj) {
                if (!obj || obj.deleted) envBase.delete(obj);
            });
        }
        if (changed) clearScene();
    }

    function noteSkillUse(now) {
        try {
            if (!window.game || !game.me || game.me.deleted || !game.me.skill || !game.me.skill.active || !(Number(game.me.skill.cooldown) > 0)) return;
            var cooldown = Number(game.me.skill.cooldown) || 0;
            if (!(cooldown > 0)) return;
            skillLocalEnd = now + cooldown;
            skillLocalMe = game.me.id;
        } catch (e) {}
    }

    function onSkillPointer(event) {
        try {
            if (event.button === 2 && window.game && event.target === game.canvas) noteSkillUse(performance.now());
        } catch (e) {}
    }

    function attachSkillUse() {
        try {
            if (typeof window.skillUse !== "function" || window.skillUse === skillUseWrap) return;
            skillUseOriginal = window.skillUse;
            skillUseWrap = function () {
                noteSkillUse(performance.now());
                return skillUseOriginal.apply(this, arguments);
            };
            window.skillUse = skillUseWrap;
        } catch (e) {
            skillUseOriginal = null;
            skillUseWrap = null;
        }
    }

    function updateSkillClock(now) {
        var host = document.querySelector("#gameContainer > div.status div.skillRight") || document.getElementById("skillButton");
        if (host !== skillClockHost) {
            if (skillClock && skillClock.parentNode) skillClock.remove();
            if (skillClockHost && skillClockHostPosition !== "") skillClockHost.style.position = skillClockHostPosition;
            skillClock = null;
            skillClockHost = host || null;
            skillClockHostPosition = host ? host.style.position : "";
            if (host) {
                try { if (getComputedStyle(host).position === "static") host.style.position = "relative"; } catch (e) {}
                skillClock = document.createElement("span");
                skillClock.id = "ez-skill-clock";
                host.appendChild(skillClock);
            }
        }
        if (!skillClock || !window.game || !game.me || !game.me.skill || !(Number(game.me.skill.cooldown) > 0)) {
            if (skillClock) skillClock.style.display = "none";
            return;
        }
        var left = skillSync == null ? 0 : Math.max(0, skillSync - Math.max(0, now - skillSyncAt));
        if (skillLocalMe === game.me.id && skillLocalEnd > now) left = Math.max(left, skillLocalEnd - now);
        if (!(left > 20)) {
            try {
                var barNode = host.querySelector("div.bar") || document.querySelector("#gameContainer > div.status div.skillRight > div.bar");
                var total = Number(game.me.skill.cooldown) || 0;
                if (barNode && barNode.parentElement && total > 0) {
                    var barHeight = parseFloat(getComputedStyle(barNode).height) || 0;
                    var parentHeight = parseFloat(getComputedStyle(barNode.parentElement).height) || 0;
                    if (parentHeight > 0 && barHeight > 0) left = total * clamp(barHeight / parentHeight, 0, 1);
                }
            } catch (e) {}
        }
        if (!(left > 20)) {
            skillClock.style.display = "none";
            return;
        }
        var seconds = left / 1000;
        skillClock.textContent = seconds < 10 ? seconds.toFixed(1) : String(Math.ceil(seconds));
        skillClock.style.display = "flex";
    }

    var fightReapers = new Set(["grimReaper", "pumpkinGhost", "ghostlyReaper"]);
    var fightPass = new Set(["fly", "butterfly", "wasp", "mosquito", "dragonfly"]);
    var fightReach = {
        grimReaper: { x: 168, y: 88 },
        pumpkinGhost: { x: 178, y: 94 },
        ghostlyReaper: { x: 158, y: 90 }
    };

    function fightReady() {
        if (!window.game || !game.me || !game.gameObjects || game.me.deleted) return false;
        if (typeof gameServer === "undefined" || !gameServer || typeof socketMsgType === "undefined" || typeof objectType === "undefined") return false;
        try { if (typeof imDead !== "undefined" && imDead) return false; } catch (e) {}
        try { if (typeof joinedGame !== "undefined" && !joinedGame) return false; } catch (e) {}
        return true;
    }

    function fightSelf() {
        return fightReady() && fightReapers.has(game.me.name) && game.me.eatsSouls === true;
    }

    function fightCenter(obj) {
        try {
            var all = obj && typeof obj.getAllPositions === "function" ? obj.getAllPositions() : null;
            if (all && all.center && Number.isFinite(all.center.x) && Number.isFinite(all.center.y)) return all.center;
        } catch (e) {}
        return {
            x: Number(obj && obj.position && obj.position.x || 0) + Number(obj && obj.width || 0) / 2,
            y: Number(obj && obj.position && obj.position.y || 0) + Number(obj && obj.height || 0) / 2
        };
    }

    function fightSpeed(obj) {
        return {
            x: Number.isFinite(obj && obj.moveSpeed && obj.moveSpeed.x) ? obj.moveSpeed.x : 0,
            y: Number.isFinite(obj && obj.moveSpeed && obj.moveSpeed.y) ? obj.moveSpeed.y : 0
        };
    }

    function fightParty(obj) {
        try { return typeof isPartyMemberObject === "function" && isPartyMemberObject(obj); } catch (e) { return false; }
    }

    function fightCan(obj) {
        if (!obj || obj.type !== objectType.PLAYER || obj.immortal === true) return false;
        if (game.me && game.me.eatsSouls === true) return true;
        try { return typeof checkFoodChain !== "function" || checkFoodChain(game.me, obj).check === 1; } catch (e) { return true; }
    }

    function fightValid(obj) {
        if (!obj || obj === game.me || obj.deleted || obj.type !== objectType.PLAYER) return false;
        if (obj.inHide || obj.inSafeZone || obj.immortal || obj.isProtected || fightParty(obj)) return false;
        if (cfg.misc.reaperTargets) {
            if (!fightReapers.has(obj.name)) return false;
        } else if (fightPass.has(obj.name) || obj.reapersIgnoreCollision === true) return false;
        return fightCan(obj);
    }

    function fightPoint(obj) {
        var me = fightCenter(game.me);
        var at = fightCenter(obj);
        var mv = fightSpeed(game.me);
        var tv = fightSpeed(obj);
        var dx = at.x - me.x;
        var dy = at.y - me.y;
        var vx = tv.x - mv.x;
        var vy = tv.y - mv.y;
        var dist = Math.max(1, Math.hypot(dx, dy));
        var radial = (dx * vx + dy * vy) / dist;
        var lag = Number.isFinite(Number(window.latency)) ? Math.max(0, Number(window.latency)) : 35;
        var span = clamp(42 + lag * .45, 42, 82);
        return {
            obj: obj,
            dx: dx,
            dy: dy,
            d2: dx * dx + dy * dy,
            radial: radial,
            px: dx + vx * span / 1000,
            py: dy + vy * span / 1000
        };
    }

    function fightNear(item) {
        return Math.abs(item.dx) <= 305 && Math.abs(item.dy) <= 205;
    }

    function fightMetric(item) {
        var reach = fightReach[game.me.name] || { x: 168, y: 90 };
        var rx = reach.x + Math.min(20, Number(item.obj.width || 0) * .09);
        var ry = reach.y + Math.min(14, Number(item.obj.height || 0) * .07);
        return Math.max(Math.abs(item.px) / rx, Math.abs(item.py) / ry);
    }

    function pickFight(now) {
        if (fightLock != null) {
            var locked = game.gameObjects[fightLock];
            if (fightValid(locked)) {
                var keep = fightPoint(locked);
                if (fightNear(keep) && now <= fightLockUntil) return keep;
            }
            fightLock = null;
        }
        var best = null;
        var bestScore = Infinity;
        var list = game.gameObjects;
        for (var id in list) {
            if (!Object.prototype.hasOwnProperty.call(list, id)) continue;
            var obj = list[id];
            if (!fightValid(obj)) continue;
            var item = fightPoint(obj);
            if (!fightNear(item)) continue;
            var metric = fightMetric(item);
            var score = metric * 1000 + Math.sqrt(item.d2) - Math.max(0, item.radial) * .18;
            if (score < bestScore) {
                best = item;
                bestScore = score;
            }
        }
        if (best) {
            fightLock = best.obj.id;
            fightLockUntil = now + (best.radial > 25 ? 760 : 560);
        }
        return best;
    }

    function inFightRange(item) {
        var reach = fightReach[game.me.name] || { x: 168, y: 90 };
        var rx = reach.x + Math.min(20, Number(item.obj.width || 0) * .09);
        var ry = reach.y + Math.min(14, Number(item.obj.height || 0) * .07);
        var here = Math.max(Math.abs(item.dx) / rx, Math.abs(item.dy) / ry);
        var next = Math.max(Math.abs(item.px) / rx, Math.abs(item.py) / ry);
        if (item.radial > 28) return here <= .94 && next <= 1.01;
        if (item.radial < -28) return next <= .84;
        return here <= .88 && next <= .93;
    }

    function swingFight() {
        if (!fightSelf()) return;
        try { gameServer.emit(socketMsgType.SKILL, true); } catch (e) {}
        clearTimeout(fightRelease);
        fightRelease = setTimeout(function () {
            try { gameServer.emit(socketMsgType.SKILL, false); } catch (e) {}
        }, 38);
    }

    function fightBusy() {
        var anim = String(game.me && game.me.animation || "");
        return anim === "attacking_left" || anim === "attacking_right";
    }

    function runFight(now) {
        if (!fightSelf() || !cfg.misc.autoHit) return;
        var target = pickFight(now);
        if (!target || !inFightRange(target) || fightBusy()) return;
        var side = target.dx >= 0 ? 1 : -1;
        if (fightSide() !== side) return;
        if (now < fightNext || now - fightTry < 34) return;
        fightTry = now;
        clearTimeout(fightAttack);
        fightAttack = setTimeout(swingFight, 0);
    }

    function paint(now) {
        raf = requestAnimationFrame(paint);
        if (dead || document.hidden) return;
        if (!fit(now)) return;

        updateList(now);
        updateScan(now);
        doScan(now, false);
        syncScanIcon();
        attachBossSync();
        attachSkillUse();
        updateSkillClock(now);
        attachDeath();
        attachKillFeed();
        attachSpectators();
        requestSpectators(now);
        attachAttack();
        attachPetAttack();
        attachFoodSync();
        attachRemoveHook();
        applyCosmetics(now, false);
        runFight(now);
        watchHits(now);
        usePrivacy();
        useProtectedDom(now);
        useWorld(now, false);
        ctx.clearRect(0, 0, layer.width, layer.height);

        if (!window.game || !game.me || game.me.deleted || !window.objectType) return;

        drawHitboxes();

        if (cfg.esp.enabled) {
            for (var i = 0; i < players.length; i++) {
                var obj = players[i];
                if (!obj || obj.deleted || obj === game.me || obj.type !== objectType.PLAYER) continue;
                var style = styleFor(relation(obj, now));
                if (style) drawPlayer(obj, style, now);
            }
        }

        drawCached(now);
        for (var j = 0; j < extras.length; j++) {
            var item = extras[j];
            var info = extraStyle(item.kind, item.obj, item.subtype);
            if (info) drawExtra(item.obj, info, now);
        }
        drawFoodTimers(now);
        drawRemoteBosses();
        if (now - deathUiAt > 250) {
            deathUiAt = now;
            updateDeathPanel(Date.now());
        }
        if (now - killUiAt > 250) {
            killUiAt = now;
            updateKillPanel(Date.now());
        }
        if (now - spectatorUiAt > 250) {
            spectatorUiAt = now;
            updateSpectatorPanel(Date.now());
        }
    }

    function start() {
        if (dead) return;
        makeLayer();
        menu = makeMenu();
        makeKeys();
        makeSpectatorPanel();
        refreshCustomSounds();
        window.addEventListener("keydown", onKey, true);
        window.addEventListener("keyup", onKeyUp, true);
        document.addEventListener("pointerdown", onSkillPointer, true);
        window.addEventListener("resize", markFit, true);
        window.addEventListener("scroll", markFit, true);
        doScan(performance.now(), true);
        renderSkinPanel(true);
        attachDeath();
        attachKillFeed();
        attachSpectators();
        requestSpectators(performance.now());
        attachAttack();
        attachFoodSync();
        attachRemoveHook();
        paint(0);
    }

    function stop() {
        if (dead) return;
        dead = true;
        clearTimeout(bootTimer);
        cancelAnimationFrame(raf);
        clearTimeout(fightRelease);
        clearTimeout(fightAttack);
        try {
            if (typeof gameServer !== "undefined" && gameServer && typeof socketMsgType !== "undefined") {
                gameServer.emit(socketMsgType.SKILL, false);
            }
        } catch (e) {}
        window.removeEventListener("keydown", onKey, true);
        window.removeEventListener("keyup", onKeyUp, true);
        document.removeEventListener("pointerdown", onSkillPointer, true);
        document.removeEventListener("pointerdown", closeBindPop, true);
        window.removeEventListener("resize", markFit, true);
        window.removeEventListener("scroll", markFit, true);

        if (brightContext && brightDraw && brightContext.drawImage === brightWrap) {
            try { brightContext.drawImage = brightDraw; } catch (e) {}
        }
        brightContext = null;
        brightDraw = null;
        brightWrap = null;
        if (darknessDraw && window.drawDarkness === darknessWrap) {
            try { window.drawDarkness = darknessDraw; } catch (e) {}
        }
        darknessDraw = null;
        darknessWrap = null;
        if (window.game && game.canvas && canvasFilter != null) game.canvas.style.filter = canvasFilter;
        if (window.game && viewBase && game.canvas && typeof game.setCanvasSize === "function") {
            game.originalWidth = viewBase.width;
            game.originalHeight = viewBase.height;
            game.zoom = viewBase.zoom;
            try { if (typeof gameZoom !== "undefined") gameZoom = viewBase.gameZoom; } catch (e) {}
            try { game.setCanvasSize(game.canvas.width, game.canvas.height); } catch (e) {}
            viewBase = null;
        }
        restorePrivacyNames();
        restoreProtectedDom();
        privacyDom.forEach(function (old, node) {
            if (node) { node.style.display = old.display; node.style.visibility = old.visibility; }
        });
        privacyDom.clear();
        if (deathSocket && deathHandler) {
            try {
                if (typeof deathSocket.off === "function") deathSocket.off("game:dead", deathHandler);
                else if (typeof deathSocket.removeListener === "function") deathSocket.removeListener("game:dead", deathHandler);
            } catch (e) {}
        }
        deathSocket = null;
        deathHandler = null;
        if (killSocket && killHandler) {
            try {
                if (typeof killSocket.off === "function") killSocket.off("game:feed", killHandler);
                else if (typeof killSocket.removeListener === "function") killSocket.removeListener("game:feed", killHandler);
            } catch (e) {}
        }
        killSocket = null;
        killHandler = null;
        if (spectatorSocket && spectatorHandler) {
            try {
                if (typeof spectatorSocket.off === "function") spectatorSocket.off(socketMsgType.SPECTATORSLIST, spectatorHandler);
                else if (typeof spectatorSocket.removeListener === "function") spectatorSocket.removeListener(socketMsgType.SPECTATORSLIST, spectatorHandler);
            } catch (e) {}
        }
        spectatorSocket = null;
        spectatorHandler = null;
        spectatorEntries.clear();
        document.body.classList.remove("ez-spec-active");
        if (attackSocket && attackHandler) {
            try {
                if (typeof attackSocket.off === "function") attackSocket.off(socketMsgType.DOATTACK, attackHandler);
                else if (typeof attackSocket.removeListener === "function") attackSocket.removeListener(socketMsgType.DOATTACK, attackHandler);
            } catch (e) {}
        }
        attackSocket = null;
        attackHandler = null;
        if (petAttackSocket && petAttackHandler) {
            try {
                if (typeof petAttackSocket.off === "function") petAttackSocket.off(socketMsgType.DOPETATTACK, petAttackHandler);
                else if (typeof petAttackSocket.removeListener === "function") petAttackSocket.removeListener(socketMsgType.DOPETATTACK, petAttackHandler);
            } catch (e) {}
        }
        petAttackSocket = null;
        petAttackHandler = null;
        if (skillUseOriginal && window.skillUse === skillUseWrap) {
            try { window.skillUse = skillUseOriginal; } catch (e) {}
        }
        skillUseOriginal = null;
        skillUseWrap = null;
        skillLocalEnd = 0;
        skillLocalMe = 0;
        if (attackDrawOriginal && window.doAttack === attackDrawWrap) {
            try { window.doAttack = attackDrawOriginal; } catch (e) {}
        }
        attackDrawOriginal = null;
        attackDrawWrap = null;
        if (removeObjectOriginal && window.removeObject === removeObjectWrap) {
            try { window.removeObject = removeObjectOriginal; } catch (e) {}
        }
        removeObjectOriginal = null;
        removeObjectWrap = null;
        if (foodSyncSocket && foodSyncHandler) {
            try {
                if (typeof foodSyncSocket.off === "function") foodSyncSocket.off(socketMsgType.SYNC, foodSyncHandler);
                else if (typeof foodSyncSocket.removeListener === "function") foodSyncSocket.removeListener(socketMsgType.SYNC, foodSyncHandler);
            } catch (e) {}
        }
        foodSyncSocket = null;
        foodSyncHandler = null;
        if (audio) { try { audio.close(); } catch (e) {} audio = null; }
        soundBuffers.clear();
        customSounds.length = 0;
        if (bossSyncSocket && bossSyncHandler) {
            try {
                if (typeof bossSyncSocket.off === "function") bossSyncSocket.off(socketMsgType.SYNC, bossSyncHandler);
                else if (typeof bossSyncSocket.removeListener === "function") bossSyncSocket.removeListener(socketMsgType.SYNC, bossSyncHandler);
            } catch (e) {}
        }
        bossSyncSocket = null;
        bossSyncHandler = null;
        skillSync = null;
        if (skillClock && skillClock.parentNode) skillClock.remove();
        if (skillClockHost && skillClockHostPosition !== "") skillClockHost.style.position = skillClockHostPosition;
        skillClock = null;
        skillClockHost = null;
        skillClockHostPosition = "";
        if (scanIconBase && scanIconBase.node) {
            scanIconBase.node.style.removeProperty("display");
            scanIconBase.node.style.removeProperty("visibility");
            scanIconBase.node.style.display = scanIconBase.display || "";
            scanIconBase.node.style.visibility = scanIconBase.visibility || "";
        }
        scanIconBase = null;
        freeSkin();
        removeLocalPet();
        cosmeticMe = null;
        appliedSkinSpawn = "";
        appliedPetSpawn = "";
        bladeRegions = new WeakMap();
        attackRegions.clear();
        envBase.forEach(function (value, obj) {
            if (obj && !obj.deleted) obj.opacity = value;
        });
        if (envBase.size) clearScene();

        ["ez-menu", "ez-layer", "ez-style", "ez-keys", "ez-bind-pop", "ez-deaths", "ez-kills", "ez-spectators"].forEach(function (id) {
            var node = document.getElementById(id);
            if (node) node.remove();
        });
        players.length = 0;
        hides.length = 0;
        extras.length = 0;
        foodObjects.length = 0;
        foodSpawns.length = 0;
        scans.length = 0;
        live.clear();
        bank.clear();
        scanBank.clear();
        cardBank.clear();
        envBase.clear();
        hitBank.clear();
        deathEntries.length = 0;
        killEntries.length = 0;
        skillTracks.clear();
        projectileOwners.clear();
        projectileSkillSeen.clear();
        staticFoodSpawnCache.clear();
        skinLoads.clear();
        recentPlayers.clear();
        foodSpawnState.clear();
        foodSeen.clear();
        foodGone.clear();
        protectedDom.clear();
        spriteImages = new WeakMap();
        if (window.EZSQUAD && window.EZSQUAD.stop === stop) {
            try { delete window.EZSQUAD; } catch (e) { window.EZSQUAD = null; }
        }
    }

    window.EZSQUAD = {
        active: true,
        show: function () { if (menu) menu.classList.remove("ez-hide"); syncScanIcon(); updateKeys(); },
        hide: function () { if (menu) menu.classList.add("ez-hide"); syncScanIcon(); updateKeys(); },
        toggle: function () { if (menu) menu.classList.toggle("ez-hide"); syncScanIcon(); updateKeys(); },
        scan: function () { return doScan(performance.now(), true); },
        stop: stop,
        settings: cfg
    };

    var booted = false;
    var bootTimer = 0;
    var bootBody = null;
    var bootCanvas = null;
    var bootSince = 0;

    function readyGame() {
        return !!(document.body && window.game && game.canvas && game.canvas.isConnected && window.objectType && window.socketMsgType);
    }

    function boot() {
        if (dead || booted) return;
        if (!readyGame()) {
            bootBody = null;
            bootCanvas = null;
            bootSince = 0;
            clearTimeout(bootTimer);
            bootTimer = setTimeout(boot, 250);
            return;
        }
        if (bootBody !== document.body || bootCanvas !== game.canvas) {
            bootBody = document.body;
            bootCanvas = game.canvas;
            bootSince = Date.now();
            clearTimeout(bootTimer);
            bootTimer = setTimeout(boot, 700);
            return;
        }
        if (Date.now() - bootSince < 650) {
            clearTimeout(bootTimer);
            bootTimer = setTimeout(boot, 100);
            return;
        }
        booted = true;
        start();
    }

    window.addEventListener("load", function () {
        clearTimeout(bootTimer);
        bootTimer = setTimeout(boot, 250);
    }, { once: true });
    bootTimer = setTimeout(boot, 250);
})();