Arras Hackering Script

Advanced arras.io script — stacking, botting, sandbox, feeding, proxy manager. Press TAB to open

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Arras Hackering Script
// @namespace    http://tampermonkey.net/
// @version      4.0.0
// @description  Advanced arras.io script — stacking, botting, sandbox, feeding, proxy manager. Press TAB to open
// @author       GreninjaOP
// @match        *://arras.io/*
// @match        *://localhost:42789/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=arras.io
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
    if (location.hostname === "localhost" && location.port !== "42789") return;

    // ─── Electron detection ───────────────────────────────────────────────────
    const IS_ELECTRON = typeof window.electronBridge !== "undefined";
    const ELECTRON_CFG = IS_ELECTRON ? (window.__electronConfig || null) : null;
    if (IS_ELECTRON && ELECTRON_CFG?.isBot) {
        window.__is_bot = true;
        const nativeRAF = window.requestAnimationFrame.bind(window);
        const nativeCancelRAF = window.cancelAnimationFrame.bind(window);
        const rafTimers = new Map();
        let rafId = 1;
        window.requestAnimationFrame = cb => {
            const id = rafId++;
            const timeout = setTimeout(() => {
                rafTimers.delete(id);
                const nativeId = nativeRAF(ts => cb(ts));
                rafTimers.set(id, { nativeId });
            }, 200);
            rafTimers.set(id, { timeout });
            return id;
        };
        window.cancelAnimationFrame = id => {
            const item = rafTimers.get(id);
            if (!item) return;
            if (item.timeout) clearTimeout(item.timeout);
            if (item.nativeId) nativeCancelRAF(item.nativeId);
            rafTimers.delete(id);
        };
        window.channel = {
            clan: ELECTRON_CFG.clan || "",
            botName: ELECTRON_CFG.botName || "Cat Minion",
            tank: ELECTRON_CFG.tank || "none",
            customTankPath: ELECTRON_CFG.customTankPath || "",
            botBuildMode: ELECTRON_CFG.botBuildMode || "default",
            customBuild: ELECTRON_CFG.customBuild || "",
            disRender: ELECTRON_CFG.disRender || false,
            moving: ELECTRON_CFG.moving !== false,
            autoFire: !!ELECTRON_CFG.autoFire,
            autoFireActive: false,
            autoSpin: !!ELECTRON_CFG.autoSpin,
            autoSpinActive: false,
            mouseType: ELECTRON_CFG.mouseType || "copy",
            isCrashy: ELECTRON_CFG.isCrashy || false,
            disableAllOperations: !!ELECTRON_CFG.disableAllOperations,
            isCatAI:  ELECTRON_CFG.isCatAI  || false,
            index: ELECTRON_CFG.index ?? 0,
            lastWorldX: undefined, lastWorldY: undefined,
            message: () => {},
            reconnect: () => window.electronBridge.botReconnect(),
            feeed: bool => { if (typeof fedingCheckbox !== "undefined" && fedingCheckbox) fedingCheckbox.checked = bool; },
            feeedRESET: () => { window.fedPath = []; currentFedPath.length = 0; },
        };
    }

    // ─── Suppress ads ────────────────────────────────────────────────────────
    Node.prototype._nappend = Node.prototype.appendChild;
    Node.prototype.appendChild = function (node) {
        if (["pw-spawn-ad", "pw-respawn-ad"].includes(node?.id)) return node;
        return this._nappend(node);
    };

    // ─── Proxy Manager ───────────────────────────────────────────────────────
    const ProxyManager = {
        proxies: [],
        load() {
            try { const r = localStorage.getItem("pr_proxies"); if (r) this.proxies = JSON.parse(r); }
            catch { this.proxies = []; }
        },
        save() { localStorage.setItem("pr_proxies", JSON.stringify(this.proxies)); },
        setFromText(text) {
            this.proxies = text.trim().split("\n")
                .map(l => l.trim()).filter(Boolean)
                .map(l => {
                    if (/^https?:\/\//i.test(l)) {
                        try {
                            const u = new URL(l);
                            return {
                                ip: u.hostname,
                                port: u.port || (u.protocol === "https:" ? "443" : "80"),
                                user: u.username || "",
                                pass: u.password || ""
                            };
                        } catch {
                            return null;
                        }
                    }
                    const p = l.split(":");
                    if (p.length === 4) return { ip:p[0], port:p[1], user:p[2], pass:p[3] };
                    if (p.length === 2) return { ip:p[0], port:p[1], user:"", pass:"" };
                    return null;
                }).filter(Boolean);
            this.save();
        },
        getForBot(idx) {
            if (!this.proxies.length) return null;
            return this.proxies[Math.floor(idx / 3) % this.proxies.length];
        },
        formatProxy(p) {
            if (!p) return "None";
            return p.user ? `${p.ip}:${p.port} (${p.user})` : `${p.ip}:${p.port}`;
        },
        toText() {
            return this.proxies.map(p => p.user
                ? `${p.ip}:${p.port}:${p.user}:${p.pass}`
                : `${p.ip}:${p.port}`
            ).join("\n");
        }
    };
    ProxyManager.load();

    // ─── Utilities ───────────────────────────────────────────────────────────
    const utils = {
        getDir:  (x1,y1,x2,y2) => Math.atan2(y2-y1, x2-x1),
        getDist: (x1,y1,x2,y2) => Math.sqrt((x2-x1)**2 + (y2-y1)**2),
        randint: (a,b) => Math.floor(Math.random()*(b-a+1))+a,
        choice:  arr => arr[utils.randint(0, arr.length-1)],
        clamp:   (v,lo,hi) => Math.max(lo, Math.min(hi,v)),
        removeBrackets(s) {
            if (s.startsWith("[")) s = s.slice(1);
            if (s.endsWith("]"))   s = s.slice(0,-1);
            return s;
        }
    };

    // ─── Tank FOV table ──────────────────────────────────────────────────────
    const tankFOV = {
        "Base":1,"Sniper":1.200606585788562,"Director":1.1000866551126518,
        "Smasher":1.050693240901213,"Mega Smasher":1.1000866551126518,
        "Dual":1.200606585788562,"Musket":1.2010398613518198,
        "Nailgun":1.1009532062391683,"Gunner Trapper":1.2512998266897748,
        "Overgunner":1.1009532062391683,"Assassin":1.3513864818024266,
        "Ranger":1.501733102253033,"Falcon":1.2014731369150782,
        "Stalker":1.3513864818024266,"Auto-Assassin":1.3513864818024266,
        "Single":1.0004332755632583,"Deadeye":1.3513864818024266,
        "Hunter":1.3006932409012133,"Predator":1.3006932409012133,
        "X-Hunter":1.3006932409012133,"Poacher":1.3006932409012133,
        "Ordnance":1.3006932409012133,"Nimrod":1.3513864818024266,
        "Minigun":1.200606585788562,"Streamliner":1.3006932409012133,
        "Crop Duster":1.200606585788562,"Barricade":1.200606585788562,
        "Vulture":1.200606585788562,"Rifle":1.200606585788562,
        "Crossbow":1.200606585788562,"Armsman":1.200606585788562,
        "Revolver":1.200606585788562,"Marksman":1.200606585788562,
        "Fork":1.200606585788562,"Bushwhacker":1.200606585788562,
        "Field Gun":1.1507798960138649,"Banshee":1.1000866551126518,
        "Overseer":1.1000866551126518,"Overlord":1.1000866551126518,
        "Auto-Overseer":1.1000866551126518,"Overdrive":1.1000866551126518,
        "Commander":1.1507798960138649,"Cruiser":1.2014731369150782,
        "Carrier":1.2014731369150782,"Battleship":1.2512998266897748,
        "Fortress":1.2014731369150782,"Auto-Cruiser":1.2014731369150782,
        "Underseer":1.0710571923743502,"Necromancer":1.0710571923743502,
        "Maleficitor":1.0710571923743502,"Infestor":1.0710571923743502,
        "Spawner":1.1000866551126518,"Factory":1.1000866551126518,
        "Auto-Spawner":1.1000866551126518,"Manager":1.1000866551126518,
        "Big Cheese":1.1000866551126518,"Builder":1.1503466204506068,
        "Constructor":1.1503466204506068,"Auto-Builder":1.1503466204506068,
        "Engineer":1.1503466204506068,"Assembler":1.1503466204506068,
        "Boomer":1.1503466204506068,"Architect":1.1503466204506068,
        "Launcher":1.1507798960138649,"Skimmer":1.1507798960138649,
        "Twister":1.1507798960138649,"Swarmer":1.1507798960138649,
        "Sidewinder":1.3006932409012133,"Shotgun":1.1503466204506068,
        "Railgun":1.2010398613518198,"Finger":1.2010398613518198
    };

    // ─── Core state ──────────────────────────────────────────────────────────
    const getEl = id => document.getElementById(id);
    const keys  = {};
    const mouse = { x:0, y:0, dir:0, lock:false, lockX:0, lockY:0, down:false, buttons:{}, seen:false, worldX:undefined, worldY:undefined };
    const center = { get x(){ return innerWidth/2; }, get y(){ return innerHeight/2; } };
    const PI2 = Math.PI*2;
    const player = { fov:1 };
    const sinsay = {};
    const stealedClans = [];

    let mouseType    = "copy";
    let chatting     = false;
    let ping         = 20;
    let isConnecting = false;
    let bots         = [];
    let botsMsg      = () => {};
    let canvas, ctx;
    let stackCycle   = false;
    let hasUpgraded  = false;
    let spawned      = false;
    let firstSpawn   = true;
    let nrCycle      = 0;
    const nrTanks    = { launchers:"yuih", builders:"yihj" };
    let currentFedPath = [];
    let afkEnabled = false;
    let afkPos     = null; // { x, y }

    function isFinitePoint(x, y) {
        return Number.isFinite(x) && Number.isFinite(y);
    }
    function screenToWorld(x, y) {
        if (!isFinitePoint(x, y) || !isFinitePoint(player.x, player.y) || !Number.isFinite(player.fov)) return null;
        return {
            x:(x - center.x) / center.x * (23.09 * player.fov) + player.x,
            y:(y - center.y) / center.y * (12.98 * player.fov) + player.y,
        };
    }
    function rememberMouseWorld(x, y) {
        const world = screenToWorld(x, y);
        if (!world) return null;
        mouse.worldX = world.x;
        mouse.worldY = world.y;
        return world;
    }

    let tankSelect, botTankSelect, botMoving, fedingCheckbox, botMouseFollow;
    let XRayCheckbox, seeEverything, XRayAlpha;
    let botIframes;
    let botFeedingEnabled = false;
    let botFunMode = false;
    let botFunKind = "";
    let botReconnectWanted = false;
    let botLastCoordChange = Date.now();
    let botLastCoordSeen = Date.now();
    let botLastCoordX;
    let botLastCoordY;
    let seenPlayerNames = new Set();
    let teamUrlState = { entries: [], yourUrl: "" };
    let updateNamesDisplay = () => {};
    let inLeaderboard = false;
    let preLeaderboardRow = 0;
    let serverClients = 0;
    let lastServerClients = -1;
    let lastServerKey = "";
    let framePlayerNames = [];
    let framePlayerNameKeys = new Set();
    let pendingPlayerNames = [];
    let chatLog = [];
    let recentChatRows = [];
    let updateChatSidebar = () => {};
    let chatSidebarQueued = false;
    const CHAT_DEDUPE_SPAM_MS = 900;
    const ARRAS_UI_STRINGS = new Set([
        // Stat labels
        "Max Health","Health Regen","Body Damage","Bullet Speed","Bullet Penetration",
        "Bullet Damage","Reload","Movement Speed","Shield","Shield Regen",
        "Shield Capacity","Shield Regeneration",
        "Drone Count","Drone Speed","Drone Health","Trap Count","Swarm Speed",
        "Swarm Count","Turret Count","Turret Damage","Turret Reload","Regen Rate",
        "Respawn Time","Upgrade","Upgrade Points","Don't Upgrade","Click to play","Press Enter",
        "Your score","High score","Leaderboard","Players","Score","FPS","Arena closed",
        "Game over","Alpha","Beta","Closed","Open","Sandbox","Assault",
        "Maze","Tag","Arms Race","Domination","Mothership",
        "Speed","Health","Damage","Penetration","Count","Regen","Capacity","Regeneration",
        // Tank names
        "Basic","Twin","Sniper","Machine Gun","Flank Guard","Pounder","Director","Trapper",
        "Destroyer","Hunter","Gunner","Tri-Angle","Fighter","Skimmer","Booster","Hybrid",
        "Manager","Stalker","Ranger","Assassin","Predator","Spreadshot","Rocketeer",
        "Penta Shot","Triplet","Pentabarrel","Octo Tank","Auto 5","Necromancer",
        "Overlord","Overseer","Battleship","Auto Gunner","Streamliner","Spike",
        "Landmine","Auto Smasher","Mega Smasher","Smasher","Annihilator","Minigun",
        "Sharpshooter","Pelleter","Auto Trapper","Mega Trapper","Fortress","Builder",
        "Mechanic","Engineer","Inferno","Mortar","Artillery","Crossbow","Hewn Double",
        "Sprayer","Auto 3","Bent Double","Bent Hybrid","Carrier","Conqueror",
        "Constructor","Cruiser","Cyclone","Falcon","Factory","Glider","Hurricane",
        "Iota","Lagrange","Lancer","Multishot","Naval","Ordinator","Overgunner",
        "Paladin","Partisan","Phoenix","Quadruplet","Rimfire","Saw","Sidewinder",
        "Spawner","Spectre","Splitter","Squall","Subjugator","Switcheroo","Talon",
        "Tidal Wave","Trappist","Tri-Trapper","Turret","Whirlwind","X Hunter",
        "Zapper","Basic Tank","Twin Flank","Triplet","Auto Twin","Spread Shot",
    ]);

    // ─── Events ──────────────────────────────────────────────────────────────
    const events = {
        mouseDown(button, x, y) {},
        mouseUp(button, x, y) {},
        click(but, x, y) { this.mouseDown(but,x,y); this.mouseUp(but,x,y); },
        moveTo(x, y) {},
        moveToDir(dir) { this.moveTo(center.x+Math.cos(dir)*100, center.y+Math.sin(dir)*100); },
        _keyEvent(code, pressed) {
            const ev = new KeyboardEvent(pressed ? "keydown" : "keyup", {
                key:code.replace("Key","").toLowerCase(), code,
                bubbles:true, cancelable:true, composed:true, itsMe:true
            });
            ev.itsMe = true;
            try { dispatchEvent(ev); } catch {}
        },
        keyDown(code) { this._keyEvent(code, true); },
        keyUp(code)   { this._keyEvent(code, false); },
        press(key, func, ms) {
            this.keyDown(key);
            func && func();
            if (ms) setTimeout(() => this.keyUp(key), ms);
            else this.keyUp(key);
        }
    };

    const sleep = t => t ? new Promise(r => setTimeout(r, t*1000)) : Promise.resolve();
    const shouldRelayMouseButtonToBots = () => true;
    (function installSocketMasterHook() {
        if (!IS_ELECTRON || window.__is_bot || window.__socketMasterHooked) return;
        window.__socketMasterHooked = true;
        const NativeWS = window.WebSocket;
        let master = null;
        const toBytes = data => {
            try {
                if (data instanceof ArrayBuffer) return [...new Uint8Array(data)];
                if (ArrayBuffer.isView(data)) return [...new Uint8Array(data.buffer, data.byteOffset, data.byteLength)];
            } catch {}
            return null;
        };
        function WrappedWebSocket(url, protocols) {
            const ws = protocols === undefined ? new NativeWS(url) : new NativeWS(url, protocols);
            const isGameSocket = typeof url === "string" && /^wss?:\/\//i.test(url);
            if (isGameSocket && !master) {
                master = ws;
                window.electronBridge?.socketMasterEvent?.({
                    type:"open",
                    url:String(url),
                    protocols: protocols ?? null,
                    origin: (typeof window !== "undefined" && window.location?.origin)
                        ? String(window.location.origin)
                        : "",
                });
                const oldSend = ws.send.bind(ws);
                ws.send = function(data) {
                    const bytes = toBytes(data);
                    if (bytes) {
                        window.electronBridge?.socketMasterEvent?.({ type:"send", data:bytes });
                        window.__wsCaptureCallback?.(bytes, "send");
                    }
                    return oldSend(data);
                };
                ws.addEventListener("message", ev => {
                    const bytes = toBytes(ev.data);
                    if (bytes) {
                        window.electronBridge?.socketMasterEvent?.({ type:"recv", data:bytes });
                        window.__wsCaptureCallback?.(bytes, "recv");
                    }
                });
                ws.addEventListener("close", ev => {
                    window.electronBridge?.socketMasterEvent?.({ type:"close", code:ev.code, reason:ev.reason || "" });
                    if (master === ws) master = null;
                });
            }
            return ws;
        }
        WrappedWebSocket.prototype = NativeWS.prototype;
        Object.setPrototypeOf(WrappedWebSocket, NativeWS);
        window.WebSocket = WrappedWebSocket;
        window.MozWebSocket = WrappedWebSocket;
    })();

    // ─── Tank definitions ─────────────────────────────────────────────────────
    const Sbuild  = "0/3/5/8/8/8/7/3";
    const Sbuild2 = "0/3/5/8/8/9/6/3";
    const Sbuild3 = "0/3/2/8/8/9/9/3";

    const tanks = {
        spread:    { name:"Spreadshot",          path:"yuuk",  desc:"Right click.",                                     build:Sbuild  },
        penta:     { name:"Penta Shot",           path:"yuy",   desc:"Right click.",                                     build:Sbuild  },
        guntrap:   { name:"Gunner Trapper",       path:"hhu",   desc:"Use 9 reload.",                                    build:Sbuild3 },
        trapguard: { name:"Trap Guard",           path:"hh",    desc:"Use 9 reload. Useful for Bomber branch.",          build:Sbuild3 },
        conq:      { name:"Conqueror",            path:"kyy",   desc:"Works on some Arms Race upgrades.",                build:Sbuild3 },
        octo:      { name:"Octo Tank",            path:"hyy",   desc:"Right click.",                                     build:Sbuild  },
        orbit:     { name:"Orbital Strike",       path:"hyhi",  desc:"Right click. Cringe.",                             build:Sbuild  },
        cyclone:   { name:"Cyclone",              path:"hyui",                                                           build:Sbuild  },
        tornado:   { name:"Tornado",              path:"hyuy",  desc:"Cyclone but more stable.",                         build:Sbuild  },
        whirlwind: { name:"Whirlwind",            path:"hyuk",  desc:"Perfect (slow).",                                  build:Sbuild  },
        septaM:    { name:"Septa-Mech",           path:"hjkh",  desc:"Good.",                                            build:Sbuild  },
        septaMa:   { name:"Septa-Machine",        path:"hjiu",  desc:"Worse.",                                           build:Sbuild  },
        blockade:  { name:"Blockade",             path:"uiji",  desc:"Hold right click. Very useful in soccer.",         build:"0/9/9/1/0/0/6/9/8" },
        toppler:   { name:"Toppler",              path:"uijh",  desc:"Hold right click.",                                build:Sbuild2 },
        anni:      { name:"Annihilator",          path:"ekyue", desc:"Press G to instantly upgrade and shoot.",          firingWhenUpgrade:true, slowUpgrading:true, build:"0/6/9/9/9/9" },
        fastram:   { name:"Fast Ramming",                       desc:"Press G for fast lvl 45 + ram stats.",             firingWhenUpgrade:true, build:"9/9/0/0/0/0/0/9/6/9" },
        finger:    { name:"Finger",               path:"uky",   desc:"Press G — upgrades to Finger and shoots.",         firingWhenUpgrade:true, build:"0/0/9/9/9/9/6" },
        fighter:   { name:"Fighter",              path:"huy",   desc:"Stack for any Fighter branch in Arms Race. Right click.", build:"0/5/0/7/7/9/7/7" },
        nuke:      { name:"Nuker",                path:"huh",   desc:"Right click to reverse.",                          build:"0/5/0/7/7/9/7/7" },
        auto4:     { name:"Auto-4/6",             path:"hiiy",  desc:"(Not a stack)",                                    build:Sbuild  },
        auto4feed: { name:"Auto-4 (Feed Build)",  path:"hii",                                                            build:"0/0/0/0/0/0/0/9" },
        ultraspawn:{ name:"Ultra Spawner",        path:"jhiy",                                                           build:"0/3/6/8/8/9/5/3" },
        brig:      { name:"Brig",                 path:"yjjj",                                                           build:"0/3/3/9/9/9/9"   },
        overczar:  { name:"Overczar",             path:"jyyy",                                                           build:"0/3/7/8/8/9/4/3" },
        pursuer:   { name:"Pursuer (Feed Build)", path:"uyiy",                                                           build:"0/0/0/0/0/0/0/9" },
        phys:      { name:"Physician (N+M)",      path:"nm",                                                             build:"9/9/0/0/0/0/6/12/0/9" },
        dreadV2:    { name:"Peacekeeper",                        desc:"Stack for destroyer dreadnought with 7 reload.",   path:"", build:"" },
        septaTrap:  { name:"Septa Trapper",          path:"hku",   desc:"Upgrade path for Septa Trapper.",                build:Sbuild3 },
    };
    for (const k in tanks) if (Object.hasOwn(tanks,k)) tanks[k].id = k;

    const botBranches = {
        wiping:   { label:"Tri-Angle",             pick:() => ({ path:"hu"+utils.choice("yhjk"),    build:"0/4/2/7/7/9/7/6" }) },
        wipingAR: { label:"Tri-Angle (Arms Race)",  pick:() => ({ path:utils.choice(["huyj","hukk","hukh","huju","hujy","huhy","uikj","uikk","kkh","kkk"]), build:"0/4/2/7/7/9/7/6" }) },
        necro:    { label:"Underseer (Arms Race)",  pick:() => ({ path:"ji"+utils.choice(["yy","yk","yi","ki","hy","hj","hi","iy","iu"]), build:"0/5/3/7/7/9/7/4" }) },
        crash:    { label:"Crashing (Arms Race)",   pick:() => ({ path:"ce"+utils.choice(["hyuk","yjyk","hjiu","hyuh"]), build:"0/0/0/0/9/9/9/6" }) },
        feding:   { label:"Feeding",                pick:() => ({ path:"hu"+utils.choice("yuik"),   build:"0/0/0/0/0/0/9/9" }) },
        fedingAR: { label:"Feeding (Arms Race)",    pick:() => ({ path:"huu"+utils.choice("yuik"),  build:"0/0/0/0/0/0/9/9" }) },
        random:   { label:"Random",                 pick:() => { const all=Object.values(tanks).filter(t=>t.path); return all[Math.floor(Math.random()*all.length)]; } },
        chaosRandom:{ label:"Random Chaos",          pick:() => { const all=Object.values(tanks).filter(t=>t.path && !["dreadV2","fastram"].includes(t.id)); return all[Math.floor(Math.random()*all.length)]; } },
    };

    // ─── Cat AI Bot ───────────────────────────────────────────────────────────
    const CHAT_LIMIT = 60;
    const chatLimit = msg => String(msg ?? "").replace(/\s+/g, " ").trim().slice(0, CHAT_LIMIT);
    const chatChunks = (msg, max=3) => {
        const text = String(msg ?? "").replace(/\s+/g, " ").trim();
        const chunks = [];
        for (let i = 0; i < text.length && chunks.length < max; i += CHAT_LIMIT) {
            chunks.push(text.slice(i, i + CHAT_LIMIT));
        }
        return chunks.length ? chunks : [""];
    };
    const cleanArgs = args => String(args || "").trim();
    const splitWords = args => cleanArgs(args).split(/\s+/).filter(Boolean);
    const parseNums = args => splitWords(args).map(Number).filter(Number.isFinite);
    const fmtNum = n => Number.isInteger(n) ? String(n) : String(Math.round(n * 1000) / 1000);
    const truthy = args => /^(1|on|true|yes|y)$/i.test(cleanArgs(args));

    let catAICooldowns = Object.create(null);
    let catAIRecentOwn = [];
    let catAIChatMemory = [];
    let catAILastAct = 0;
    const catAIStartedAt = Date.now();
    let catAIGameState = {
        guess: null, trivia: null, scramble: null, math: null,
        streak: 0, score: 0, seen: 0, last: "", notes: [],
        votes: null, poll: null,
    };
    const catAICommands = Object.create(null);
    const catAISections = Object.create(null);
    function catAIAdd(section, names, usage, desc, run) {
        for (const name of names) {
            const cmd = name.startsWith("!") ? name.toLowerCase() : "!" + name.toLowerCase();
            catAICommands[cmd] = { section, usage, desc, run };
            (catAISections[section] ||= []).push(cmd);
        }
    }
    function catAICommandList(section) {
        const cmds = catAISections[section] || [];
        return cmds.slice(0, 7).join(" ") + (cmds.length > 7 ? " ..." : "");
    }
    function catAIHelp(args) {
        const q = cleanArgs(args).toLowerCase();
        const sections = Object.keys(catAISections);
        if (!q) return "Use !tree, !help <section>, !find <word>";
        if (/^\d+$/.test(q)) return catAICommandList(sections[Number(q) - 1] || sections[0]);
        if (catAISections[q]) return `${q}/ ${catAICommandList(q)}`;
        const cmd = q.startsWith("!") ? q : "!" + q;
        const meta = catAICommands[cmd];
        return meta ? `${cmd}: ${meta.usage}` : "No match. Try !find <word>";
    }
    function catAITree() {
        return Object.keys(catAISections).map((s, i) => `${i + 1}.${s}`).join(" ");
    }
    function catAIFind(args) {
        const q = cleanArgs(args).toLowerCase();
        if (!q) return "Use !find <word>";
        const hits = Object.keys(catAICommands).filter(c => c.includes(q) || catAICommands[c].desc.toLowerCase().includes(q));
        return hits.length ? hits.slice(0, 8).join(" ") : "No command matches.";
    }
    function catAIUsage(args) {
        const cmd = cleanArgs(args).toLowerCase();
        if (!cmd) return "Use !usage <command>";
        return catAIHelp(cmd);
    }
    function catAISection(args) {
        const q = cleanArgs(args).toLowerCase();
        const sections = Object.keys(catAISections);
        if (!q) return catAITree();
        return catAISections[q] ? catAICommandList(q) : "No section. Use !tree";
    }
    function catAIUptime() {
        const s = Math.floor((Date.now() - catAIStartedAt) / 1000);
        return `Uptime ${Math.floor(s / 60)}m ${s % 60}s`;
    }
    function catAIStatus() {
        return `Score ${catAIGameState.score}; cmds ${catAIGameState.seen}`;
    }
    function catAIMemory(args) {
        const n = utils.clamp(parseInt(args, 10) || 5, 1, 8);
        return catAIChatMemory.slice(-n).map(m => m.text).join(" | ") || "No memory yet.";
    }
    function catAIForget() {
        catAIChatMemory = [];
        return "Chat memory cleared.";
    }
    function catAIRememberChat(text) {
        const t = cleanArgs(text);
        if (!t || t.length < 2 || catAIIsOwnText(t)) return;
        if (/^(Rendering:|Coordinates:|Memory:|Speed:|Level\s)/i.test(t)) return;
        if (/ ms  |mspt|^\d+\s*players?$/i.test(t)) return;
        const last = catAIChatMemory[catAIChatMemory.length - 1];
        if (last && last.text === t && Date.now() - last.time < 5000) return;
        catAIChatMemory.push({ text: t.slice(0, 90), time: Date.now() });
        if (catAIChatMemory.length > 24) catAIChatMemory.shift();
    }
    function catAICommandSummary() {
        return Object.keys(catAISections)
            .map(s => `${s}: ${catAISections[s].slice(0, 12).join(" ")}`)
            .join("\n");
    }
    function catAIEnvContext() {
        const active = [];
        if (catAIGameState.guess) active.push("guess");
        if (catAIGameState.trivia) active.push("trivia");
        if (catAIGameState.scramble) active.push("scramble");
        if (catAIGameState.math) active.push("math");
        return [
            `bot=Cat AI Bot`,
            `commands=${Object.keys(catAICommands).length}`,
            `sections=${Object.keys(catAISections).join(",")}`,
            `pos=${fmtNum(player.x||0)},${fmtNum(player.y||0)} ping=${ping||0}`,
            `score=${catAIGameState.score} streak=${catAIGameState.streak}`,
            `activeGames=${active.join(",") || "none"}`,
            `notes=${catAIGameState.notes.join(" | ") || "none"}`,
            `recentChat=${catAIChatMemory.slice(-8).map(m => m.text).join(" || ") || "none"}`,
        ].join("\n");
    }
    function catAIEnv() {
        return `cmds:${Object.keys(catAICommands).length} ping:${ping||0} mem:${catAIChatMemory.length}`;
    }
    function catAINote(args) {
        const text = cleanArgs(args);
        if (!text) return "Use !note <text>";
        catAIGameState.notes.push(text.slice(0, 40));
        if (catAIGameState.notes.length > 5) catAIGameState.notes.shift();
        return `Saved note ${catAIGameState.notes.length}/5`;
    }
    function catAINotes() {
        return catAIGameState.notes.length ? catAIGameState.notes.join(" | ") : "No notes.";
    }
    function catAIClearNotes() {
        catAIGameState.notes = [];
        return "Notes cleared.";
    }
    function catAIMathOp(args, op) {
        const n = parseNums(args);
        if (!n.length) return `Use !${op} numbers`;
        const r = {
            add: n.reduce((a,b)=>a+b,0),
            sub: n.slice(1).reduce((a,b)=>a-b,n[0]),
            mul: n.reduce((a,b)=>a*b,1),
            div: n.slice(1).reduce((a,b)=>b?a/b:NaN,n[0]),
            mod: n.length>=2 ? n[0] % n[1] : NaN,
            avg: n.reduce((a,b)=>a+b,0)/n.length,
            min: Math.min(...n), max: Math.max(...n),
        }[op];
        return Number.isFinite(r) ? fmtNum(r) : "Bad math input.";
    }
    function catAIUnary(args, op) {
        const n = parseNums(args)[0];
        if (!Number.isFinite(n)) return `Use !${op} <num>`;
        const r = { sqrt:Math.sqrt(n), abs:Math.abs(n), floor:Math.floor(n), ceil:Math.ceil(n), round:Math.round(n) }[op];
        return Number.isFinite(r) ? fmtNum(r) : "Bad math input.";
    }
    function catAIPow(args) {
        const [a,b] = parseNums(args);
        return Number.isFinite(a) && Number.isFinite(b) ? fmtNum(a ** b) : "Use !pow <a> <b>";
    }
    function catAIPct(args) {
        const [part,total] = parseNums(args);
        return total ? `${fmtNum(part / total * 100)}%` : "Use !pct <part> <total>";
    }
    function catAIRand(args) {
        const [a=1,b=100] = parseNums(args);
        const lo = Math.min(a,b), hi = Math.max(a,b);
        return String(utils.randint(lo, hi));
    }
    function catAIDice(args) {
        const [count=1,sides=6] = parseNums(args);
        const c = utils.clamp(Math.floor(count), 1, 8);
        const s = utils.clamp(Math.floor(sides), 2, 100);
        const rolls = Array.from({length:c}, () => utils.randint(1, s));
        return `${rolls.join("+")}=${rolls.reduce((a,b)=>a+b,0)}`;
    }
    function catAIChoose(args) {
        const opts = cleanArgs(args).split(/\s*(?:,|\bor\b)\s*/i).filter(Boolean);
        return opts.length ? utils.choice(opts).slice(0, 55) : "Use !choose a,b,c";
    }
    function catAIShuffle(args) {
        const opts = splitWords(args);
        return opts.length ? opts.sort(()=>Math.random()-0.5).join(" ").slice(0, 60) : "Use !shuffle words";
    }
    function catAICoin(args) {
        const pick = cleanArgs(args).toLowerCase();
        const flip = Math.random() < 0.5 ? "heads" : "tails";
        return pick ? `${flip}; ${pick===flip ? "you win" : "you lose"}` : flip;
    }
    function catAI8Ball(args) {
        if (!cleanArgs(args)) return "Ask with !8ball <question>";
        return utils.choice(["yes","no","maybe","likely","unlikely","ask later"]);
    }
    function catAIGuess(args) {
        if (!catAIGameState.guess) {
            catAIGameState.guess = utils.randint(1, 20);
            return "Guess 1-20 with !guess <num>";
        }
        const n = parseInt(args, 10);
        if (!n) return "Use !guess <1-20>";
        if (n === catAIGameState.guess) {
            catAIGameState.guess = null; catAIGameState.score++; catAIGameState.streak++;
            return `Correct. Score ${catAIGameState.score}`;
        }
        return n < catAIGameState.guess ? "Too low." : "Too high.";
    }
    const catAITriviaDeck = [
        { q:"2+2? !answer <ans>", a:["4","four"] },
        { q:"Opposite of hot? !answer <ans>", a:["cold"] },
        { q:"Triangle sides? !answer <ans>", a:["3","three"] },
        { q:"Color mixing red+blue? !answer", a:["purple"] },
    ];
    function catAITrivia() {
        catAIGameState.trivia = utils.choice(catAITriviaDeck);
        return `Trivia: ${catAIGameState.trivia.q}`;
    }
    const catAIScrambleWords = ["tank","arras","pixel","score","arena","proxy","reload","damage"];
    function catAIScramble() {
        const word = utils.choice(catAIScrambleWords);
        catAIGameState.scramble = word;
        return `Unscramble ${word.split("").sort(()=>Math.random()-0.5).join("")}`;
    }
    function catAIMathGame(args) {
        if (!catAIGameState.math || !cleanArgs(args)) {
            const a=utils.randint(2,12), b=utils.randint(2,12), op=utils.choice(["+","*"]);
            catAIGameState.math = op==="+" ? a+b : a*b;
            return `${a}${op}${b}=? use !math <ans>`;
        }
        const n = parseInt(args, 10);
        if (n === catAIGameState.math) {
            catAIGameState.math = null; catAIGameState.score++; catAIGameState.streak++;
            return `Correct. Score ${catAIGameState.score}`;
        }
        return "Wrong. Try again.";
    }
    function catAIAnswer(args) {
        const guess = cleanArgs(args).toLowerCase();
        if (catAIGameState.trivia) {
            if (catAIGameState.trivia.a.includes(guess)) {
                catAIGameState.trivia = null; catAIGameState.score++; catAIGameState.streak++;
                return `Correct. Score ${catAIGameState.score}`;
            }
            return "No. Try again.";
        }
        if (catAIGameState.scramble) return catAIUnscramble(args);
        if (catAIGameState.math) return catAIMathGame(args);
        return "No active question.";
    }
    function catAIUnscramble(args) {
        const guess = cleanArgs(args).toLowerCase();
        if (!catAIGameState.scramble) return "Start with !scramble";
        if (guess === catAIGameState.scramble) {
            catAIGameState.scramble = null; catAIGameState.score++; catAIGameState.streak++;
            return `Correct. Score ${catAIGameState.score}`;
        }
        return "Wrong word.";
    }
    function catAIRps(args) {
        const user = cleanArgs(args).toLowerCase();
        if (!["rock","paper","scissors"].includes(user)) return "Use rock/paper/scissors";
        const bot = utils.choice(["rock","paper","scissors"]);
        const win = (user==="rock"&&bot==="scissors") || (user==="paper"&&bot==="rock") || (user==="scissors"&&bot==="paper");
        if (win) catAIGameState.score++;
        return user===bot ? `Tie: ${bot}` : `${bot}; ${win ? "you win" : "you lose"}`;
    }
    function catAIVote(args) {
        const opt = cleanArgs(args).toLowerCase();
        if (!opt) return "Use !vote <option>";
        catAIGameState.votes ||= Object.create(null);
        catAIGameState.votes[opt] = (catAIGameState.votes[opt] || 0) + 1;
        return `${opt}: ${catAIGameState.votes[opt]}`;
    }
    function catAIVotes() {
        const votes = catAIGameState.votes || {};
        const rows = Object.entries(votes).sort((a,b)=>b[1]-a[1]);
        return rows.length ? rows.map(([k,v])=>`${k}:${v}`).join(" ") : "No votes.";
    }
    function catAIText(args, mode) {
        const s = cleanArgs(args);
        if (!s) return `Use !${mode} <text>`;
        if (mode==="upper") return s.toUpperCase();
        if (mode==="lower") return s.toLowerCase();
        if (mode==="reverse") return [...s].reverse().join("");
        if (mode==="len") return `${[...s].length} chars`;
        if (mode==="words") return `${splitWords(s).length} words`;
        if (mode==="trim") return s;
        if (mode==="repeat") {
            const [n,...rest] = splitWords(s);
            return Array(utils.clamp(parseInt(n,10)||2,1,5)).fill(rest.join(" ")).join(" ");
        }
        return s;
    }
    function catAIEncode(args, mode) {
        const s = cleanArgs(args);
        if (!s) return `Use !${mode} <text>`;
        try {
            if (mode==="base64") return btoa(s).slice(0,60);
            if (mode==="unbase64") return atob(s).slice(0,60);
            if (mode==="url") return encodeURIComponent(s).slice(0,60);
            if (mode==="unurl") return decodeURIComponent(s).slice(0,60);
        } catch { return "Decode failed."; }
    }
    function catAITime(mode) {
        const d = new Date();
        if (mode==="date") return d.toLocaleDateString();
        if (mode==="iso") return d.toISOString().slice(0,19);
        return d.toLocaleTimeString();
    }
    const catAITips = {
        hp:"Regen helps after disengage.",
        armor:"Max health/shield increases survival.",
        speed:"Speed lets you choose fights.",
        reload:"Reload raises sustained fire.",
        damage:"Damage punishes bad positioning.",
        drone:"Keep drones between you and threats.",
        trap:"Layer traps where enemies must cross.",
        bullet:"Bullet builds reward aim and spacing.",
        missile:"Use missiles for burst and pressure.",
        upgrade:"Spend stats before risky fights.",
        respawn:"Press Enter to respawn.",
        arena:"Watch edges; escape routes matter.",
        team:"Focus targets with teammates.",
        solo:"Solo needs speed, vision, exits.",
        boss:"Kite bosses; do not facetank.",
        lag:"If lag spikes, stop chasing.",
        dc:"Reconnect if the button appears.",
    };
    function catAITip(args) {
        const key = cleanArgs(args).toLowerCase();
        if (!key) return "Tip topics: " + Object.keys(catAITips).slice(0,8).join(" ");
        return catAITips[key] || "No tip. Try !tips";
    }
    function catAITank(args) {
        const key = cleanArgs(args).toLowerCase();
        if (!key) return "Use !tank <name>, !build <name>, !tanks";
        const hit = Object.entries(tanks).find(([k,t]) => k===key || t.name.toLowerCase().includes(key));
        return hit ? `${hit[1].name}: ${hit[1].path || "no path"}` : "Tank not found.";
    }
    function catAIBuild(args) {
        const key = cleanArgs(args).toLowerCase();
        const hit = Object.entries(tanks).find(([k,t]) => k===key || t.name.toLowerCase().includes(key));
        return hit?.[1]?.build ? `${hit[1].name} ${hit[1].build}` : "No build found.";
    }
    function catAITanks(args) {
        const q = cleanArgs(args).toLowerCase();
        const list = Object.values(tanks).filter(t => !q || t.name.toLowerCase().includes(q)).map(t => t.name);
        return list.length ? list.slice(0,4).join(", ") : "No tanks.";
    }
    function catAIStats() {
        return `x:${fmtNum(player.x||0)} y:${fmtNum(player.y||0)} ping:${ping}`;
    }
    async function catAIAskOllama(args) {
        const text = cleanArgs(args);
        if (!text) return "Use !msg <text>";
        catAIRememberChat(`user asked Cat AI: ${text}`);
        const prompt = [
            "You are Cat AI Bot inside an Arras bot client.",
            "Be useful, terse, and chat-native. No markdown.",
            "Reply in at most 180 characters total.",
            "The client will split your reply into 60-char messages.",
            "Use recent chat context when relevant.",
            "Know these coded commands and suggest them when helpful:",
            catAICommandSummary(),
            "Current environment:",
            catAIEnvContext(),
        ].join("\n");
        try {
            if (IS_ELECTRON && window.electronBridge?.askOllama) {
                const res = await window.electronBridge.askOllama(text, prompt);
                return res?.ok ? chatChunks(res.text, 3) : `Ollama: ${res?.error || "failed"}`;
            }
            const tags = await fetch("http://127.0.0.1:11434/api/tags").then(r=>r.json());
            const model = tags.models?.[0]?.name;
            if (!model) return "Ollama: no model found.";
            const body = { model, stream:false, messages:[
                { role:"system", content:prompt },
                { role:"user", content:text }
            ]};
            const r = await fetch("http://127.0.0.1:11434/api/chat", { method:"POST", headers:{ "Content-Type":"application/json" }, body:JSON.stringify(body) });
            const json = await r.json();
            return chatChunks(json.message?.content || json.response || "Ollama: empty.", 3);
        } catch (e) { return `Ollama: ${e.message || "offline"}`; }
    }

    catAIAdd("nav", ["help","h"], "!help <section|num|command>", "show organized command help", catAIHelp);
    catAIAdd("nav", ["tree","sections","nav"], "!tree", "show command section tree", catAITree);
    catAIAdd("nav", ["commands","cmds","ls"], "!commands <section>", "list commands in a section", catAISection);
    catAIAdd("nav", ["find","search"], "!find <word>", "search command names and descriptions", catAIFind);
    catAIAdd("nav", ["usage","man"], "!usage <command>", "show a command usage", catAIUsage);
    catAIAdd("nav", ["count"], "!count", "count registered commands", () => `${Object.keys(catAICommands).length} commands`);
    catAIAdd("nav", ["about","info"], "!about", "show bot purpose", () => "Cat AI: tools, games, tips, and !msg");
    catAIAdd("nav", ["version","ver"], "!version", "show bot version", () => "Cat AI v2 organized commands");

    catAIAdd("ai", ["msg","ask","ollama"], "!msg <text>", "ask your local Ollama model", catAIAskOllama);
    catAIAdd("ai", ["model"], "!model", "show Ollama model source", () => "Ollama uses first /api/tags model.");
    catAIAdd("ai", ["prompt"], "!prompt", "show Ollama response rule", () => "Prompt: terse, max 60 chars.");

    catAIAdd("state", ["ping"], "!ping", "show bot latency state", () => `pong; game ping ${ping || 0}`);
    catAIAdd("state", ["uptime"], "!uptime", "show bot uptime", catAIUptime);
    catAIAdd("state", ["status"], "!status", "show bot state", catAIStatus);
    catAIAdd("state", ["score"], "!score", "show game score", () => `Score ${catAIGameState.score}; streak ${catAIGameState.streak}`);
    catAIAdd("state", ["last"], "!last", "show last handled command", () => catAIGameState.last || "No command yet.");
    catAIAdd("state", ["memory","mem"], "!memory [count]", "show recent Cat AI chat memory", catAIMemory);
    catAIAdd("state", ["forget"], "!forget", "clear Cat AI chat memory", catAIForget);
    catAIAdd("state", ["env"], "!env", "show compact environment state", catAIEnv);
    catAIAdd("state", ["note"], "!note <text>", "save a short note", catAINote);
    catAIAdd("state", ["notes"], "!notes", "list saved notes", catAINotes);
    catAIAdd("state", ["clearnotes"], "!clearnotes", "clear saved notes", catAIClearNotes);
    catAIAdd("state", ["reset"], "!reset", "reset Cat AI game state", () => {
        catAIGameState = { guess:null, trivia:null, scramble:null, math:null, streak:0, score:0, seen:catAIGameState.seen, last:"", notes:[], votes:null, poll:null };
        return "Cat AI game state reset.";
    });

    catAIAdd("games", ["games"], "!games", "list game commands", () => catAICommandList("games"));
    catAIAdd("games", ["guess"], "!guess [num]", "play number guessing", catAIGuess);
    catAIAdd("games", ["trivia"], "!trivia", "start trivia", catAITrivia);
    catAIAdd("games", ["answer","ans"], "!answer <text>", "answer active puzzle", catAIAnswer);
    catAIAdd("games", ["rps"], "!rps rock|paper|scissors", "play rock paper scissors", catAIRps);
    catAIAdd("games", ["scramble"], "!scramble", "start word scramble", catAIScramble);
    catAIAdd("games", ["unscramble"], "!unscramble <word>", "answer scramble", catAIUnscramble);
    catAIAdd("games", ["math"], "!math [answer]", "play arithmetic quiz", catAIMathGame);
    catAIAdd("games", ["vote"], "!vote <option>", "record a vote", catAIVote);
    catAIAdd("games", ["votes"], "!votes", "show vote totals", catAIVotes);

    for (const op of ["add","sum","sub","minus","mul","times","div","mod","avg","min","max"]) {
        const core = { sum:"add", minus:"sub", times:"mul" }[op] || op;
        catAIAdd("math", [op], `!${op} <numbers>`, `${core} numbers`, args => catAIMathOp(args, core));
    }
    catAIAdd("math", ["sqrt"], "!sqrt <num>", "square root", args => catAIUnary(args, "sqrt"));
    catAIAdd("math", ["abs"], "!abs <num>", "absolute value", args => catAIUnary(args, "abs"));
    catAIAdd("math", ["floor"], "!floor <num>", "round down", args => catAIUnary(args, "floor"));
    catAIAdd("math", ["ceil"], "!ceil <num>", "round up", args => catAIUnary(args, "ceil"));
    catAIAdd("math", ["round"], "!round <num>", "round nearest", args => catAIUnary(args, "round"));
    catAIAdd("math", ["pow"], "!pow <a> <b>", "raise a to b", catAIPow);
    catAIAdd("math", ["pct","percent"], "!pct <part> <total>", "calculate percent", catAIPct);
    catAIAdd("math", ["pi"], "!pi", "pi constant", () => "3.141592653589793");
    catAIAdd("math", ["tau"], "!tau", "tau constant", () => "6.283185307179586");
    catAIAdd("math", ["e"], "!e", "e constant", () => "2.718281828459045");

    catAIAdd("random", ["rand","random"], "!rand [min max]", "random integer", catAIRand);
    catAIAdd("random", ["dice","roll"], "!dice [count sides]", "roll dice", catAIDice);
    catAIAdd("random", ["coin","flip"], "!coin [heads|tails]", "coin flip with optional pick", catAICoin);
    catAIAdd("random", ["choose","pick"], "!choose a,b,c", "choose an option", catAIChoose);
    catAIAdd("random", ["shuffle"], "!shuffle words", "shuffle words", catAIShuffle);
    catAIAdd("random", ["8ball"], "!8ball <question>", "yes/no decision helper", catAI8Ball);
    catAIAdd("random", ["chance"], "!chance <pct>", "roll under a percent chance", args => {
        const n = parseNums(args)[0];
        return Number.isFinite(n) ? (Math.random()*100 < n ? "hit" : "miss") : "Use !chance <pct>";
    });

    for (const mode of ["upper","lower","reverse","len","words","trim","repeat"]) {
        catAIAdd("text", [mode], `!${mode} <text>`, `${mode} text`, args => catAIText(args, mode));
    }
    for (const mode of ["base64","unbase64","url","unurl"]) {
        catAIAdd("text", [mode], `!${mode} <text>`, `${mode} encode/decode`, args => catAIEncode(args, mode));
    }

    catAIAdd("time", ["time","clock"], "!time", "local time", () => catAITime("time"));
    catAIAdd("time", ["date"], "!date", "local date", () => catAITime("date"));
    catAIAdd("time", ["iso"], "!iso", "ISO timestamp", () => catAITime("iso"));
    catAIAdd("time", ["timer"], "!timer <sec>", "acknowledge a short timer", args => {
        const n = utils.clamp(parseInt(args,10)||0, 1, 60);
        setTimeout(() => catAISay(`Timer done: ${n}s`), n * 1000);
        return `Timer set: ${n}s`;
    });

    catAIAdd("arras", ["stats","pos","where"], "!stats", "show bot position and ping", catAIStats);
    catAIAdd("arras", ["tank"], "!tank <name>", "find tank path", catAITank);
    catAIAdd("arras", ["build"], "!build <name>", "find tank build", catAIBuild);
    catAIAdd("arras", ["tanks"], "!tanks [search]", "search tanks", catAITanks);
    catAIAdd("arras", ["tip","tips"], "!tip <topic>", "show Arras tip", catAITip);
    for (const k of Object.keys(catAITips)) {
        catAIAdd("arras", [k], `!${k}`, `${k} gameplay tip`, () => catAITips[k]);
    }

    function catAISay(msg) {
        if (Array.isArray(msg)) {
            msg.slice(0, 3).forEach((part, i) => {
                setTimeout(() => catAISay(part), i * 700);
            });
            return;
        }
        const out = chatLimit(msg);
        catAIRecentOwn.push({ text: out, time: Date.now() });
        if (catAIRecentOwn.length > 12) catAIRecentOwn.shift();
        chat(out);
    }
    function catAIIsOwnText(text) {
        const now = Date.now();
        catAIRecentOwn = catAIRecentOwn.filter(m => now - m.time < 15000);
        return catAIRecentOwn.some(m => m.text && String(text).includes(m.text));
    }
    function catAIHandleCommand(cmd, args="", raw="") {
        if (!window.channel?.isCatAI) return;
        const now = Date.now();
        const key = (raw || `${cmd} ${args}`).trim().toLowerCase();
        if (catAICooldowns[key] && now - catAICooldowns[key] < 3500) return;
        catAICooldowns[key] = now;
        catAILastAct = now;
        catAIGameState.seen++;
        catAIGameState.last = `${cmd}${args ? " " + args : ""}`;
        const meta = catAICommands[cmd];
        if (!meta) return;
        setTimeout(() => {
            Promise.resolve(meta.run(args)).then(catAISay).catch(e => catAISay(`Error: ${e.message || e}`));
        }, 400 + Math.random() * 400);
    }
    function catAIHandleText(text) {
        if (!window.channel?.isCatAI || catAIIsOwnText(text)) return;
        const m = String(text).match(/!(\w+)(?:\s+([^!]*))?/);
        if (!m) return;
        const cmd = "!" + m[1].toLowerCase();
        const args = (m[2] || "").trim();
        catAIHandleCommand(cmd, args, m[0]);
    }

    const tinyFont = {
        A:["01110","10001","10001","11111","10001","10001","10001"],
        B:["11110","10001","10001","11110","10001","10001","11110"],
        C:["01111","10000","10000","10000","10000","10000","01111"],
        D:["11110","10001","10001","10001","10001","10001","11110"],
        E:["11111","10000","10000","11110","10000","10000","11111"],
        F:["11111","10000","10000","11110","10000","10000","10000"],
        G:["01111","10000","10000","10011","10001","10001","01110"],
        H:["10001","10001","10001","11111","10001","10001","10001"],
        I:["11111","00100","00100","00100","00100","00100","11111"],
        J:["00111","00010","00010","00010","10010","10010","01100"],
        K:["10001","10010","10100","11000","10100","10010","10001"],
        L:["10000","10000","10000","10000","10000","10000","11111"],
        M:["10001","11011","10101","10101","10001","10001","10001"],
        N:["10001","11001","10101","10011","10001","10001","10001"],
        O:["01110","10001","10001","10001","10001","10001","01110"],
        P:["11110","10001","10001","11110","10000","10000","10000"],
        Q:["01110","10001","10001","10001","10101","10010","01101"],
        R:["11110","10001","10001","11110","10100","10010","10001"],
        S:["01111","10000","10000","01110","00001","00001","11110"],
        T:["11111","00100","00100","00100","00100","00100","00100"],
        U:["10001","10001","10001","10001","10001","10001","01110"],
        V:["10001","10001","10001","10001","10001","01010","00100"],
        W:["10001","10001","10001","10101","10101","10101","01010"],
        X:["10001","10001","01010","00100","01010","10001","10001"],
        Y:["10001","10001","01010","00100","00100","00100","00100"],
        Z:["11111","00001","00010","00100","01000","10000","11111"],
        0:["01110","10001","10011","10101","11001","10001","01110"],
        1:["00100","01100","00100","00100","00100","00100","01110"],
        2:["01110","10001","00001","00010","00100","01000","11111"],
        3:["11110","00001","00001","01110","00001","00001","11110"],
        4:["00010","00110","01010","10010","11111","00010","00010"],
        5:["11111","10000","10000","11110","00001","00001","11110"],
        6:["01110","10000","10000","11110","10001","10001","01110"],
        7:["11111","00001","00010","00100","01000","01000","01000"],
        8:["01110","10001","10001","01110","10001","10001","01110"],
        9:["01110","10001","10001","01111","00001","00001","01110"],
        "!":["00100","00100","00100","00100","00100","00000","00100"],
        "?":["01110","10001","00001","00010","00100","00000","00100"],
        ".":["00000","00000","00000","00000","00000","01100","01100"],
        ",":["00000","00000","00000","00000","01100","01100","01000"],
        ":":["00000","01100","01100","00000","01100","01100","00000"],
        ";":["00000","01100","01100","00000","01100","01100","01000"],
        "-":["00000","00000","00000","11111","00000","00000","00000"],
        "_":["00000","00000","00000","00000","00000","00000","11111"],
        "+":["00000","00100","00100","11111","00100","00100","00000"],
        "=":["00000","00000","11111","00000","11111","00000","00000"],
        "(":["00110","01000","10000","10000","10000","01000","00110"],
        ")":["11000","00100","00010","00010","00010","00100","11000"],
        "[":["01110","01000","01000","01000","01000","01000","01110"],
        "]":["01110","00010","00010","00010","00010","00010","01110"],
        "/":["00001","00001","00010","00100","01000","10000","10000"],
        "\\":["10000","10000","01000","00100","00010","00001","00001"],
        "*":["00000","10101","01110","11111","01110","10101","00000"],
        "#":["01010","01010","11111","01010","11111","01010","01010"],
        "@":["01110","10001","10111","10101","10111","10000","01110"],
        "$":["00100","01111","10100","01110","00101","11110","00100"],
        "%":["11000","11001","00010","00100","01000","10011","00011"],
        "^":["00100","01010","10001","00000","00000","00000","00000"],
        "&":["01100","10010","10100","01100","10101","10010","01101"],
        "~":["00000","00000","01000","10101","00010","00000","00000"],
        "<":["00001","00010","00100","01000","00100","00010","00001"],
        ">":["10000","01000","00100","00010","00100","01000","10000"],
        "|":["00100","00100","00100","00100","00100","00100","00100"],
        "'":["00100","00100","01000","00000","00000","00000","00000"],
        "\"":["01010","01010","00000","00000","00000","00000","00000"],
        "`":["01000","00100","00000","00000","00000","00000","00000"],
    };

    // ─── Helper functions ─────────────────────────────────────────────────────
    async function upgradeTank(path, slow) {
        if (!path) return;
        for (const key of path.toUpperCase()) {
            events.press("Key"+key);
            if (slow) await sleep(0.04);
        }
    }

    function upgradeStats(tank) {
        let build = typeof tank === "string" ? tank : tank?.build;
        if (!build) return;
        let i = 0;
        for (const stat of build.split("/")) {
            i++; if (i===10) i=0;
            if (stat==="0") continue;
            for (let j=0; j<Number(stat); j++) events.press("Digit"+i);
        }
    }

    function command(...ks) {
        events.press("Backquote", () => { for (const k of ks) events.press(k); });
    }

    function chat(msg) {
        msg = chatLimit(msg);
        events.press("Enter");
        const iv = setInterval(() => {
            const inp = document.querySelector("body > input[type=text]");
            if (!inp) return;
            clearInterval(iv);
            inp.value = msg;
            events.press("Enter");
        }, 40);
    }

    function currentServerKey() {
        return decodeURIComponent(location.hash.replace(/^#/, "").split(/[/?&]/)[0] || "").trim();
    }
    function setArrasStatusValues(values) {
        if (!Array.isArray(values)) return;
        window.__arrasStatusValues = values.filter(v => v && typeof v === "object");
    }
    function captureArrasStatusSource(value) {
        if (!value || typeof value !== "object") return;
        if (value.status && typeof value.status === "object") {
            setArrasStatusValues(Object.values(value.status));
        }
    }
    function unhideArrasStatus(value) {
        if (!value || typeof value !== "object") return value;
        if (value.status && typeof value.status === "object") {
            captureArrasStatusSource(value);
            for (const server of Object.values(value.status)) unhideArrasStatus(server);
        } else if ("hidden" in value) {
            try { value.hidden = false; } catch {}
        }
        return value;
    }
    function recordArrasStatus(values) {
        if (!Array.isArray(values)) return;
        setArrasStatusValues(values.map(v => unhideArrasStatus(v)));
    }
    function readArrasStatusFrame() {
        const candidates = [];
        const key = currentServerKey();
        const addTextCandidates = txt => {
            if (!txt || !key) return;
            const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
            const patterns = [
                new RegExp(`\\b${escaped}\\b\\s*:\\s*\\{[\\s\\S]{0,500}?\\bclients\\s*:\\s*(\\d+)`, "i"),
                new RegExp(`\\bname\\s*:\\s*["']${escaped}["'][\\s\\S]{0,500}?\\bclients\\s*:\\s*(\\d+)`, "i"),
                new RegExp(`\\bclients\\s*:\\s*(\\d+)[\\s\\S]{0,500}?\\bname\\s*:\\s*["']${escaped}["']`, "i"),
            ];
            for (const re of patterns) {
                const m = txt.match(re);
                if (m) candidates.push({ name:key, clients:Number(m[1]) || 0 });
            }
        };
        if (Array.isArray(window.__arrasStatusValues)) candidates.push(...window.__arrasStatusValues);
        for (const frame of Array.from(window.frames || [])) {
            try {
                const w = frame;
                for (const key of ["status","serverStatus","arrasStatus","__status"]) {
                    const obj = w[key];
                    if (obj?.status) candidates.push(...Object.values(obj.status));
                    else if (obj && typeof obj === "object") candidates.push(...Object.values(obj));
                }
                const txt = w.document?.body?.innerText?.trim();
                if (txt && txt.includes("status")) {
                    addTextCandidates(txt);
                    const json = JSON.parse(txt);
                    if (json?.status) candidates.push(...Object.values(json.status));
                }
            } catch {
                try {
                    const txt = frame.document?.documentElement?.textContent || frame.document?.body?.innerText || "";
                    addTextCandidates(txt);
                } catch {}
            }
        }
        return candidates.filter(v => v && typeof v === "object");
    }
    function statusMatchesServer(server, key) {
        if (!server || !key) return false;
        return [server.name, server.host, server.id, server.key].some(v => String(v || "") === key);
    }
    function refreshServerClients(force=false) {
        const key = currentServerKey();
        if (!key) return;
        if (!force && key === lastServerKey && Date.now() - (refreshServerClients.last || 0) < 4000) return;
        refreshServerClients.last = Date.now();
        const matches = readArrasStatusFrame().filter(s => statusMatchesServer(s, key));
        if (!matches.length) return;
        const next = Math.max(...matches.map(s => Number(s.clients || 0)).filter(Number.isFinite));
        if (!Number.isFinite(next) || next <= 0) return;
        if (key !== lastServerKey || next !== lastServerClients) {
            if (key !== lastServerKey) pendingPlayerNames = [];
            lastServerKey = key;
            lastServerClients = next;
            serverClients = next;
            framePlayerNames = [];
            framePlayerNameKeys = new Set();
        }
    }
    function queueRefreshServerClients(force=false) {
        if (queueRefreshServerClients.timer) return;
        queueRefreshServerClients.force = queueRefreshServerClients.force || force;
        queueRefreshServerClients.timer = setTimeout(() => {
            const shouldForce = !!queueRefreshServerClients.force;
            queueRefreshServerClients.timer = null;
            queueRefreshServerClients.force = false;
            refreshServerClients(shouldForce);
        }, 250);
    }
    function isScoreText(t) {
        return /\b\d{2}\.\d{2}k\b/i.test(t);
    }
    const FILTERED_CHAT_LINES = new Set([
        "protection of video game mechanics, and we",
        "sites, as well as various server outages.",
        "3AM Experiences LLC, the owner of diep.io.",
        "situation is available here:",
        "Announcement - December 13, 2025",
        "Over the past several weeks, arras.io has",
        "are working with Discord and our hosting",
        "A detailed, transparent summary of the",
        "notices overstate the scope of copyright",
        "providers to restore affected services.",
        "our official Discord server and most proxy",
        "We believe that the claims raised in these",
        "received multiple DMCA takedown notices from",
        "These notices resulted in the deletion of",
    ].map(s => s.toLowerCase()));
    function isFilteredChatLine(t) {
        return FILTERED_CHAT_LINES.has(String(t || "").trim().toLowerCase());
    }
    function isIgnoredCanvasText(t) {
        return /^Rendering:/.test(t) || /^Coordinates:/.test(t) ||
               /\bmspt\b/i.test(t) || / ms  /.test(t) ||
               /^Memory:/i.test(t) || /^Speed:/i.test(t) ||
               /^Level\s/.test(t) || /^\d+\s*players?$/i.test(t) ||
               /^Score:/i.test(t) || isScoreText(t) ||
               isFilteredChatLine(t) ||
               /^[0-9.,\s]+$/.test(t) || ARRAS_UI_STRINGS.has(t);
    }
    function cleanPlayerName(t) {
        return t.replace(/\[[^\[\]]+\]\s*/g,"").trim();
    }
    function looksLikeChatText(t) {
        return /:\s*\S/.test(t) || /^!/.test(t);
    }
    function isChatCanvasArea(_x, y) {
        return Number.isFinite(y) && y > innerHeight * 0.45;
    }
    function looksLikePlayerName(t) {
        const stripped = cleanPlayerName(t);
        return stripped.length >= 2 && stripped.length <= 24 &&
               /[a-zA-Z]/.test(stripped) &&
               !looksLikeChatText(stripped) &&
               !isScoreText(stripped) &&
               !ARRAS_UI_STRINGS.has(stripped) &&
               !/.+[-–]\s*.+:\s*[\d.]+[km]?$/i.test(stripped);
    }
    function replaceFetchedPlayers(names) {
        const byKey = new Map();
        for (const name of names) {
            const key = normalizeNameForDedupe(name);
            if (!key) continue;
            const prev = byKey.get(key);
            if (!prev || name.length > prev.length) byKey.set(key, name);
        }
        seenPlayerNames = new Set(byKey.values());
        updateNamesDisplay();
    }
    function commitPendingPlayers() {
        replaceFetchedPlayers(pendingPlayerNames);
    }
    function queueChatSidebarUpdate() {
        if (chatSidebarQueued) return;
        chatSidebarQueued = true;
        requestAnimationFrame(() => {
            chatSidebarQueued = false;
            updateChatSidebar();
        });
    }
    function normalizeChatForDedupe(text) {
        return String(text || "")
            .toLowerCase()
            .normalize("NFKD")
            .replace(/[\u0300-\u036f]/g, "")
            .replace(/\s+/g, " ")
            .trim();
    }
    function escapeChatHtml(text) {
        return String(text ?? "")
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }
    const escapeAttr = escapeChatHtml;
    function getScoreUpdateKey(text) {
        const match = String(text || "").match(/^(.+?)\s*[-–]\s*[^:]+:\s*\d+(?:\.\d+)?\s*[kmb]\b/i);
        if (!match) return "";
        return normalizeNameForDedupe(match[1]);
    }
    function normalizeNameForDedupe(text) {
        return cleanPlayerName(text)
            .toLowerCase()
            .normalize("NFKD")
            .replace(/[\u0300-\u036f]/g, "")
            .replace(/[^a-z0-9]/g, "");
    }
    function addChatRow(text) {
        const t = text.trim();
        if (!t || isIgnoredCanvasText(t)) return;
        const now = Date.now();
        const key = normalizeChatForDedupe(t);
        if (!key) return;
        const scoreKey = getScoreUpdateKey(t);
        if (scoreKey) {
            const logIndex = chatLog.findLastIndex
                ? chatLog.findLastIndex(entry => entry.scoreKey === scoreKey)
                : chatLog.map(entry => entry.scoreKey === scoreKey).lastIndexOf(true);
            if (logIndex !== -1) {
                const [logMatch] = chatLog.splice(logIndex, 1);
                chatLog.push({
                    ...logMatch,
                    text: t,
                    key,
                    time: now,
                    count: (logMatch.count || 1) + 1,
                });
                queueChatSidebarUpdate();
                return;
            }
            chatLog.push({ text:t, key, scoreKey, time:now, count:1 });
            queueChatSidebarUpdate();
            return;
        }
        recentChatRows = recentChatRows.filter(r => now - r.time < CHAT_DEDUPE_SPAM_MS);
        const recentMatch = recentChatRows.find(r => r.key === key);
        if (recentMatch) {
            recentMatch.time = now;
            const logIndex = chatLog.findLastIndex
                ? chatLog.findLastIndex(entry => entry.key === key && now - entry.time < CHAT_DEDUPE_SPAM_MS)
                : chatLog.map(entry => entry.key === key && now - entry.time < CHAT_DEDUPE_SPAM_MS).lastIndexOf(true);
            if (logIndex !== -1) {
                const [logMatch] = chatLog.splice(logIndex, 1);
                chatLog.push({
                    ...logMatch,
                    time: now,
                    count: (logMatch.count || 1) + 1,
                });
            }
            queueChatSidebarUpdate();
            return;
        }
        recentChatRows.push({ text:t, key, time:now });
        chatLog.push({ text:t, key, time:now, count:1 });
        queueChatSidebarUpdate();
    }

    function stopMoving() { for (const k of "WASD") events.keyUp("Key"+k); }
    const isMovementKey = code => ["KeyW","KeyA","KeyS","KeyD"].includes(code);
    function botCanMove() {
        return !window.__is_bot || (spawned && hasUpgraded && window.channel?.moving !== false);
    }
    function botCanAttack() {
        return !window.__is_bot || (spawned && hasUpgraded);
    }
    const botOperations = {
        disabled: false,
        requestAnimationFrame: null,
        cancelAnimationFrame: null,
    };
    function disableAllBotOperations() {
        if (!window.__is_bot || botOperations.disabled) return;
        botOperations.requestAnimationFrame = window.requestAnimationFrame;
        botOperations.cancelAnimationFrame = window.cancelAnimationFrame;
        window.requestAnimationFrame = () => 0;
        window.cancelAnimationFrame = () => {};
        botOperations.disabled = true;
    }
    function enableAllBotOperations() {
        if (!botOperations.disabled) return;
        window.requestAnimationFrame = botOperations.requestAnimationFrame || window.requestAnimationFrame;
        window.cancelAnimationFrame = botOperations.cancelAnimationFrame || window.cancelAnimationFrame;
        botOperations.disabled = false;
    }
    function setBotAutoFire(enabled) {
        if (!window.__is_bot || !window.channel) return;
        const desired = typeof enabled === "string" ? enabled === "true" : !!enabled;
        window.channel.autoFire = desired;
        if (!botCanAttack()) return;
        if (desired !== window.channel.autoFireActive) {
            events.press('KeyE');
            window.channel.autoFireActive = desired;
        }
    }
    function setBotAutoSpin(enabled) {
        if (!window.__is_bot || !window.channel) return;
        const desired = typeof enabled === "string" ? enabled === "true" : !!enabled;
        window.channel.autoSpin = desired;
        if (!botCanAttack()) return;
        if (desired !== window.channel.autoSpinActive) {
            events.press('KeyC');
            window.channel.autoSpinActive = desired;
        }
    }
    const isBotToggleKey = code => code === "KeyE" || code === "KeyC";
    function resetBotSpawnState() {
        spawned = false;
        hasUpgraded = false;
        if (window.channel) {
            window.channel.autoFireActive = false;
            window.channel.autoSpinActive = false;
        }
        if (typeof window.__resetBotInputs === "function") window.__resetBotInputs();
    }
    function reportBotTeamUrlIfReady() {
        if (!window.__is_bot || !IS_ELECTRON || !spawned || !hasUpgraded || !location.hash) return;
        window.electronBridge?.reportTeamUrl?.(location.href);
    }

    const BOT_FUN_WRITE_TOLERANCE = 0.5;

    function dirPathfind(angle) {
        stopMoving();
        if      (angle >= -Math.PI/8   && angle <  Math.PI/8)   events.keyDown("KeyD");
        else if (angle >=  Math.PI/8   && angle <  3*Math.PI/8) { events.keyDown("KeyS"); events.keyDown("KeyD"); }
        else if (angle >=  3*Math.PI/8 && angle <  5*Math.PI/8) events.keyDown("KeyS");
        else if (angle >=  5*Math.PI/8 && angle <  7*Math.PI/8) { events.keyDown("KeyS"); events.keyDown("KeyA"); }
        else if (angle >=  7*Math.PI/8 || angle < -7*Math.PI/8) events.keyDown("KeyA");
        else if (angle >= -7*Math.PI/8 && angle < -5*Math.PI/8) { events.keyDown("KeyW"); events.keyDown("KeyA"); }
        else if (angle >= -5*Math.PI/8 && angle < -3*Math.PI/8) events.keyDown("KeyW");
        else { events.keyDown("KeyW"); events.keyDown("KeyD"); }
    }

    function pathfind(x, y, minDist=2.5, exact=false) {
        stopMoving();
        if (!spawned || (window.__is_bot && (!hasUpgraded || !window.channel?.moving))) return;
        const dx = x - player.x;
        const dy = y - player.y;
        const dist = Math.hypot(dx, dy);
        if (!exact) {
            if (dist > minDist) dirPathfind(Math.atan2(dy, dx));
            return;
        }

        const vx = player.xv || 0;
        const vy = player.yv || 0;
        const speed = Math.hypot(vx, vy);

        // Hysteresis: only re-engage if drifted 0.8 past minDist, stops at settleDist
        const cushion = 0.8;
        const settleDist = Math.max(minDist, 1.05);
        const driftStop = 0.11;
        if (dist <= settleDist && speed <= driftStop) return;
        if (dist <= minDist + cushion) {
            if (speed > 0.18) dirPathfind(Math.atan2(-vy, -vx));
            return;
        }

        // Lag prediction: assume 80ms latency, predict where bot will be
        const lagMs = 80;
        const accel = 1.2; // reduced game acceleration estimate
        const lagSec = lagMs / 1000;
        const predictX = player.x + vx * lagSec + (dx / Math.max(0.1, dist)) * accel * lagSec * lagSec * 0.3;
        const predictY = player.y + vy * lagSec + (dy / Math.max(0.1, dist)) * accel * lagSec * lagSec * 0.3;
        const pdx = x - predictX;
        const pdy = y - predictY;

        const kp = 0.32;
        const kd = 7.5;
        const ax = pdx * kp - vx * kd;
        const ay = pdy * kp - vy * kd;
        const effort = Math.hypot(ax, ay);
        if (effort < 0.2) return; // increased from 0.16 to avoid micro-oscillations
        dirPathfind(Math.atan2(ay, ax));
    }

    let botChaosFiring = false;
    function setBotChaosFiring(enabled) {
        if (typeof events.mouseDown !== "function" || typeof events.mouseUp !== "function") return;
        const x = Number.isFinite(mouse.x) ? mouse.x : innerWidth / 2;
        const y = Number.isFinite(mouse.y) ? mouse.y : innerHeight / 2;
        if (enabled && !botChaosFiring) {
            if (!botCanAttack()) return;
            botChaosFiring = true;
            events.mouseDown(0, x, y);
        } else if (!enabled && botChaosFiring) {
            botChaosFiring = false;
            events.mouseUp(0, x, y);
        }
    }

    function resolveBotTank(value) {
        if (!value || value === "none") return null;
        if (typeof value === "object" && value.path) return value;
        if (typeof value === "string") {
            if (value === "custom") {
                const path = String(window.channel?.customTankPath || "").replace(/[^a-z]/gi, "").toLowerCase();
                return path ? { name:"Custom", path, build:"" } : null;
            }
            if (tanks[value])       return tanks[value];
            if (botBranches[value]) return botBranches[value].pick();
        }
        return null;
    }
    function applyBotBuildConfig(tank) {
        if (!tank || !window.__is_bot) return tank;
        const mode = window.channel?.botBuildMode || "default";
        if (mode === "default") return tank;
        const out = { ...tank };
        if (mode === "none") out.build = "";
        else if (mode === "custom") out.build = String(window.channel?.customBuild || "").trim();
        return out;
    }

    // ─── On spawn ─────────────────────────────────────────────────────────────
    function onSpawn() {
        botReconnectWanted = false;
        isConnecting = false;
        if (window.__is_bot) {
            spawned = false;
            hasUpgraded = false;
            if (window.channel?.isCatAI && !catAILastAct) catAILastAct = Date.now();
            if (window.channel?.isCrashy) {
                spawned = true; firstSpawn = false;
                setTimeout(() => {
                    disableAllBotOperations();
                    if (IS_ELECTRON) window.electronBridge?.crashyBotReady?.();
                    else window.channel?.onCrashyReady?.();
                }, 200);
                return;
            }
            stopMoving();
                setTimeout(async () => {
                    const resolved = applyBotBuildConfig(resolveBotTank(window.channel?.tank));
                    if (resolved) {
                        if (resolved.build) upgradeStats(resolved);
                        if (resolved.path)  await upgradeTank(resolved.path, resolved.slowUpgrading);
                    }
                    stopMoving();
                    hasUpgraded = true;
                    spawned = true;
                    setBotAutoFire(window.channel?.autoFire);
                    setBotAutoSpin(window.channel?.autoSpin);
                    if (typeof window.__applyBotInputState === "function") window.__applyBotInputState();
                    reportBotTeamUrlIfReady();
                }, 200);
        } else {
            console.log(firstSpawn ? "[PR] Spawned" : "[PR] Respawned");
        }

        if (fedingCheckbox?.checked || (window.__is_bot && botFeedingEnabled)) {
            currentFedPath = [...(window.fedPath || [])];
            for (let i=0; i<4; i++) events.press("Key"+utils.choice("YUIHJK"));
            events.press("KeyM", () => events.press("Digit8"));
            for (let i=0; i<35; i++) events.press("Digit"+utils.randint(3,7));
            events.press("KeyR");
        }
        spawned = true;
        firstSpawn = false;
    }

    // ─── Window.addEventListener override ────────────────────────────────────
    const _nWinListener = window.addEventListener;
    window.addEventListener = function (...args) {
        if (args[0]==="keydown" || args[0]==="keyup") {
            const cb = args[1];
            args[1] = function (...a) {
                const e = a[0];
                if (!e.itsMe && document.activeElement && getEl("scriptMenu")?.contains(document.activeElement)) return;
                // Let Cmd/Ctrl+C through when the user has text selected inside the chat sidebar
                if (!e.itsMe && (e.metaKey || e.ctrlKey) && e.code === "KeyC") {
                    const sel = window.getSelection();
                    if (sel && sel.rangeCount && getEl("chatSidebar")?.contains(sel.anchorNode)) return;
                }
                const ev = e.isTrusted ? e : { isTrusted:true, key:e.key, code:e.code, preventDefault(){} };
                return cb.apply(this, [ev]);
            };
        }
        return _nWinListener.apply(this, args);
    };

    // ─── Stacks ───────────────────────────────────────────────────────────────
    class Stacks {
        async rotating(anglePlus, delay, endDelay=0.1, num=12, useCycle) {
            while (true) {
                if (useCycle) stackCycle = !stackCycle;
                let angle = 0;
                for (let i=0; i<=num; i++) {
                    if (!mouse.down) { mouse.lock=false; events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                    events.moveToDir((PI2-angle*(useCycle && stackCycle ? 1:-1))+mouse.dir);
                    angle += anglePlus;
                    if (i===0) events.mouseDown(0,mouse.lockX,mouse.lockY);
                    await sleep(delay);
                }
                events.mouseUp(0,mouse.lockX,mouse.lockY);
                await sleep(endDelay);
            }
        }
        async septa(delay, endDelay=0.04) {
            while (true) {
                let angle=0;
                for (let i=0; i<=7; i++) {
                    if (!mouse.down) { mouse.lock=false; events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                    events.moveToDir((PI2-angle)+mouse.dir);
                    angle += (PI2/7)*2;
                    if (i===0) events.mouseDown(0,mouse.lockX,mouse.lockY);
                    await sleep(delay);
                }
                events.mouseUp(0,mouse.lockX,mouse.lockY);
                await sleep(endDelay);
            }
        }
        async fullrotating(delay, endDelay=0.1) { await this.rotating(PI2/50, delay, endDelay, 50); }
        async cyclone(delay) {
            while (true) {
                for (let angle of [0,2,1,3]) {
                    angle = (PI2/12)*angle;
                    if (!mouse.down) { mouse.lock=false; events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                    events.moveToDir((PI2-angle)+mouse.dir);
                    if (angle===0) events.mouseDown(0,mouse.lockX,mouse.lockY);
                    await sleep(delay);
                }
                events.mouseUp(0,mouse.lockX,mouse.lockY);
                await sleep(0.1);
            }
        }
        async subverter(...timings) {
            mouse.lock=false;
            events.mouseUp(2);
            events.keyDown("Space");
            for (const timing of timings) {
                await sleep(timing[0]);
                events.keyUp("Space");
                await sleep(timing[1]);
                events.keyDown("Space");
            }
            while (mouse.buttons[2]) await sleep(0.04);
            events.keyUp("Space");
        }
        async flank(delay) {
            while (true) {
                mouse.lock=true;
                events.moveToDir(mouse.dir+Math.PI);
                events.mouseDown(0,mouse.lockX,mouse.lockY);
                await sleep(0.12);
                events.moveToDir(mouse.dir);
                mouse.lock=false;
                await sleep(delay);
                if (!mouse.down) { mouse.lock=false; events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                events.mouseUp(0,mouse.lockX,mouse.lockY);
                await sleep(0.08);
            }
        }
    }
    const stacks = new Stacks();

    // ─── HTMLDivElement event overrides ──────────────────────────────────────
    HTMLDivElement.prototype._nListener = HTMLDivElement.prototype.addEventListener;
    HTMLDivElement.prototype.addEventListener = function (event, callback, ...opt) {
        if (event === "mousedown") {
            const ncb = callback;
            callback = function (e) {
                if (mouse.lock && !e.itsMe) { e.clientX=mouse.lockX; e.clientY=mouse.lockY; }
                ncb(e);
                if (!e.itsMe && tankSelect?.value==="nuke" && (e.button===2 || mouse.buttons[2]))
                    events.moveToDir(mouse.dir+Math.PI);
            };
            events.mouseDown = (button, x, y) => {
                if (!x) x=mouse.x; if (!y) y=mouse.y;
                if (shouldRelayMouseButtonToBots(button)) botsMsg({ type:"mousedown", button, x, y });
                callback({ isTrusted:true, clientX:x, clientY:y, button, preventDefault:()=>{}, itsMe:true });
            };
            this._nListener("mousedown", function (e) {
                mouse.down=true;
                mouse.buttons[e.button]=true;
                mouse.dir=utils.getDir(center.x,center.y,mouse.x,mouse.y);
                if (getEl("stackin")?.checked &&
                    !(["spread","penta","octo","orbit","blockade","toppler","fighter"].includes(tankSelect?.value) && e.button!==2) &&
                    tankSelect?.value!=="nuke") {
                    (async () => {
                        mouse.lock=true;
                        const tv=tankSelect?.value;
                        if      (tv==="septaM")    await stacks.septa(0.077);
                        else if (tv==="septaMa")   await stacks.septa(0.0485);
                        else if (tv==="spread")    await stacks.rotating(0.105,0.03,0.12,12,true);
                        else if (tv==="penta")     await stacks.rotating(0.0948,0.026,0.12,6,true);
                        else if (tv==="octo")      await stacks.fullrotating(0.025,0);
                        else if (tv==="orbit")     await stacks.fullrotating(0.064,0);
                        else if (tv==="guntrap")   await stacks.flank(0.38);
                        else if (tv==="trapguard") await stacks.flank(0.38);
                        else if (tv==="cyclone")   await stacks.cyclone(0.05);
                        else if (tv==="tornado")   await stacks.cyclone(0.068);
                        else if (tv==="whirlwind") await stacks.cyclone(0.13);
                        else if (tv==="blockade")  await stacks.subverter([0.125,0.1]);
                        else if (tv==="toppler")   await stacks.subverter(...(window.o||[[0.381,0.1]]));
                        else if (tv==="dreadV2")   await stacks.flank(window.o||0.8);
                        else if (tv==="conq") {
                            while (true) {
                                mouse.lock=true;
                                events.moveToDir(mouse.dir+Math.PI);
                                events.mouseDown(0,mouse.lockX,mouse.lockY);
                                await sleep(0.12);
                                events.moveToDir(mouse.dir);
                                mouse.lock=false;
                                await sleep(0.9);
                                if (!mouse.down) { mouse.lock=false; events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                                events.mouseUp(0,mouse.lockX,mouse.lockY);
                                await sleep(0.1);
                            }
                        } else if (tv==="fighter") {
                            setTimeout(()=>events.mouseDown(2),0.1);
                            while (true) {
                                stackCycle=!stackCycle;
                                mouse.lock=true;
                                let delta=Math.PI/2;
                                if (stackCycle) delta=-delta;
                                events.moveToDir(mouse.dir+delta);
                                events.mouseDown(0,mouse.lockX,mouse.lockY);
                                await sleep(0.08);
                                events.moveToDir(mouse.dir);
                                mouse.lock=false;
                                await sleep(0.179);
                                if (!mouse.down) { mouse.lock=false; events.mouseUp(2); events.mouseUp(0,mouse.lockX,mouse.lockY); return; }
                                events.mouseUp(0,mouse.lockX,mouse.lockY);
                                await sleep(0.08);
                            }
                        }
                    })();
                }
                if (shouldRelayMouseButtonToBots(e.button)) {
                    botsMsg({ type:"mousedown", button:e.button,
                        x:mouse.lock?mouse.lockX:e.clientX,
                        y:mouse.lock?mouse.lockY:e.clientY });
                }
                if (getEl("testinn")?.checked && e.button===0) {
                    if (!window._t) { window._t=[]; window._t2=Date.now(); }
                    const _time=Date.now();
                    if (window._t.length) window._t.at(-1)[1]=(_time-window._t2)/1000;
                    window._t2=_time;
                    const _dbg=getEl("debugg"); if(_dbg) _dbg.innerHTML=JSON.stringify(window._t);
                }
            });
        } else if (event==="mouseup") {
            const ncb=callback;
            callback=function(e) {
                if (mouse.lock && !e.itsMe) { e.clientX=mouse.lockX; e.clientY=mouse.lockY; }
                ncb(e);
            };
            events.mouseUp=(button,x,y)=>{
                if(!x) x=mouse.x; if(!y) y=mouse.y;
                if (shouldRelayMouseButtonToBots(button)) botsMsg({ type:"mouseup", button, x, y });
                callback({ isTrusted:true, clientX:x, clientY:y, button, preventDefault:()=>{}, itsMe:true });
            };
            this._nListener("mouseup", function(e) {
                mouse.down=false;
                mouse.buttons[e.button]=false;
                if (shouldRelayMouseButtonToBots(e.button)) {
                    botsMsg({ type:"mouseup", button:e.button,
                        x:mouse.lock?mouse.lockX:e.clientX,
                        y:mouse.lock?mouse.lockY:e.clientY });
                }
                if (getEl("testinn")?.checked && e.button===0) {
                    if (!window._t) { window._t=[]; window._t2=Date.now(); }
                    const _time=Date.now();
                    window._t.push([(_time-window._t2)/1000]);
                    window._t2=_time;
                    const _dbg=getEl("debugg"); if(_dbg) _dbg.innerHTML=JSON.stringify(window._t);
                }
            });
        } else if (event==="mousemove") {
            const ncb=callback;
            callback=function(e) {
                if (mouse.lock && !e.itsMe) return;
                if (!e.itsMe && tankSelect?.value==="nuke" && mouse.buttons[2])
                    events.moveToDir(mouse.dir+Math.PI);
                else ncb(e);
            };
            events.moveTo=(x,y)=>{
                mouse.lockX=x; mouse.lockY=y;
                botsMsg({ type:"mousemove", x, y, innerWidth, innerHeight });
                callback({ isTrusted:true, clientX:x, clientY:y, preventDefault:()=>{}, itsMe:true });
            };
            this._nListener("mousemove", function(e) {
                mouse.x=e.clientX; mouse.y=e.clientY;
                mouse.seen = true;
                mouse.dir=utils.getDir(center.x,center.y,mouse.x,mouse.y);
                if (bots.length && !mouse.lock) {
                    const world = rememberMouseWorld(mouse.x, mouse.y);
                    botsMsg({
                        type:"mousemove",
                        x:mouse.x,
                        y:mouse.y,
                        innerWidth,
                        innerHeight,
                        worldX:world?.x,
                        worldY:world?.y,
                    });
                }
            });
        }
        this._nListener(event, callback, ...opt);
    };

    // ─── fillText hook ────────────────────────────────────────────────────────
    CanvasRenderingContext2D.prototype._nFT = CanvasRenderingContext2D.prototype.fillText;
    CanvasRenderingContext2D.prototype.fillText = function (text, x, y) {
        // ── System text detection ───────────────────────────────────────────
        if (text.includes("Coordinates: (")) {
            const m = text.match(/Coordinates: \(([^)]+)\)/);
            if (m) {
                const parts = m[1].split(", ");
                if (!spawned && !firstSpawn) onSpawn();
                player.oldX=player.x; player.oldY=player.y;
                player.x  = parseFloat(parts[0]);
                player.y  = parseFloat(parts[1]);
                player.xv = player.x-player.oldX;
                player.yv = player.y-player.oldY;
                if (window.__is_bot) botLastCoordSeen = Date.now();
                if (window.__is_bot && (player.x !== botLastCoordX || player.y !== botLastCoordY)) {
                    botLastCoordX = player.x;
                    botLastCoordY = player.y;
                    botLastCoordChange = Date.now();
                }
            }
        } else if (text.startsWith("Level ")) {
            const parts=text.split(" ");
            if (parts.length>=3) {
                const name=parts.slice(2).join(" ");
                player.class=name;
                player.fov  =tankFOV[name]||1;
            }
        } else if (text.includes("You have spawned! Welcome to the game.")) {
            botReconnectWanted = false;
            if (firstSpawn || !spawned || !hasUpgraded) onSpawn();
        } else if (text.includes("Survived for ")) {
            if (!window.__is_bot) console.log("[PR] Died");
            resetBotSpawnState();
            if (window.__is_bot) {
                setBotAutoFire(window.channel.autoFire);
                setBotAutoSpin(window.channel.autoSpin);
            }
            if (window.__is_bot || getEl("autoRespawn")?.checked || fedingCheckbox?.checked) {
                const iv=setInterval(()=>{
                    if (spawned) { clearInterval(iv); return; }
                    events.press("Enter"); events.press("Escape");
                    stopMoving(); events.mouseUp(0);
                }, 1000);
            }
        } else if (text.includes(" ms  ")) {
            const m=text.match(/([0-9]+)\.[0-9]+ ms/);
            if (m) ping=Number(m[1]);
        } else if (text==="Connecting...") {
            isConnecting=true;
            botReconnectWanted = false;
            if (window.__is_bot) resetBotSpawnState();
            if (window.__is_bot) {
                setBotAutoFire(window.channel.autoFire);
                setBotAutoSpin(window.channel.autoSpin);
            }
        } else if (window.__is_bot && botLooksLikeReconnectScreenText(text)) {
            botReconnectWanted = true;
            resetBotSpawnState();
            if (window.__is_bot) {
                setBotAutoFire(window.channel.autoFire);
                setBotAutoSpin(window.channel.autoSpin);
            }
            stopMoving();
            events.mouseUp(0);
            botTryReconnect(/\breconnect\b/i.test(text) ? x : undefined, /\breconnect\b/i.test(text) ? y : undefined);
        }

        // ── Clan tag stealing (host only) ──────────────────────────────────
        if (!window.__is_bot) {
            const m=text.match(/(\[[^\[\]]+\])/g);
            if (m) {
                let stolen=false;
                for (const tag of m) {
                    if (!stealedClans.includes(tag) && !/\[\w\]/.test(tag)) {
                        stealedClans.push(tag); stolen=true;
                    }
                }
                if (stolen) { const el=getEl("stealedClans"); if(el) el.innerText=stealedClans.join(" "); }
            }
        }

        // Cat AI command detection
        if (window.channel?.isCatAI) {
            catAIRememberChat(text);
            if (text.includes("!")) catAIHandleText(text);
        }

        // "Players" is the leaderboard header. Anything after it is
        // leaderboard data, not player fetch/chat rows.
        if (/^Players$/.test(text.trim())) inLeaderboard = true;

        if (!window.__is_bot) {
            const t = text.trim();
            if (!inLeaderboard && !isIgnoredCanvasText(t) && t.length >= 1) {
                const stripped = cleanPlayerName(t);
                const chatArea = isChatCanvasArea(x, y);
                const possiblePlayerName = looksLikePlayerName(stripped);
                const canCollectPlayer = !chatArea && possiblePlayerName;
                if (canCollectPlayer) {
                    preLeaderboardRow++;
                    const key = normalizeNameForDedupe(stripped);
                    if (key && !framePlayerNameKeys.has(key)) {
                        framePlayerNameKeys.add(key);
                        framePlayerNames.push(stripped);
                    }
                } else if (chatArea || looksLikeChatText(t) || !possiblePlayerName) {
                    addChatRow(t);
                }
            }
        }

        this._nFT(text, x, y);
    };

    // Reset inLeaderboard at the start of each rendered frame.
    // Our rAF fires after the game's (game registered first), so it resets
    // after each full render and the next frame starts with inLeaderboard=false.
    if (!window.__is_bot) {
        (function resetLeaderboardBoundary() {
            if (framePlayerNames.length) {
                const seen = new Set(pendingPlayerNames.map(normalizeNameForDedupe));
                for (const name of framePlayerNames) {
                    const key = normalizeNameForDedupe(name);
                    if (key && !seen.has(key)) {
                        seen.add(key);
                        pendingPlayerNames.push(name);
                    }
                }
            }
            preLeaderboardRow = 0;
            framePlayerNames = [];
            framePlayerNameKeys = new Set();
            inLeaderboard = false;
            requestAnimationFrame(resetLeaderboardBoundary);
        })();
        setInterval(() => refreshServerClients(), 4000);
        setTimeout(() => refreshServerClients(true), 1000);
        addEventListener("hashchange", () => refreshServerClients(true));
    }

    // ─── X-Ray fill hook ──────────────────────────────────────────────────────
    const _nFill = CanvasRenderingContext2D.prototype.fill;
    CanvasRenderingContext2D.prototype.fill = function () {
        if (XRayCheckbox?.checked) this.globalAlpha = XRayAlpha?.value ?? 0.5;
        _nFill.call(this);
    };

    // ─── Canvas getContext hook ───────────────────────────────────────────────
    const _nGetCtx = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = function (type, ...rest) {
        // Force preserveDrawingBuffer on the host so we can read pixels anytime
        if (!window.__is_bot && (type === "webgl" || type === "webgl2")) {
            const opts = (rest[0] && typeof rest[0] === "object") ? rest[0] : {};
            rest[0] = { ...opts, preserveDrawingBuffer: true };
        }
        const context = _nGetCtx.call(this, type, ...rest);
        if (!canvas) { canvas = this; ctx = context; }
        return context;
    };

    // ─── WASM hook ────────────────────────────────────────────────────────────
    const _fetch = window.fetch;
    window.fetch = async function (...args) {
        const res = await _fetch.apply(this, args);
        const oldJson = res.json.bind(res);
        res.json = async () => {
            const json = await oldJson();
            const unhidden = unhideArrasStatus(json);
            queueRefreshServerClients(true);
            return unhidden;
        };
        return res;
    };

    const _WASM_iS = WebAssembly.instantiateStreaming;
    const _WASM_i = WebAssembly.instantiate;
    function patchWasmImports(imports) {
        if (!imports?.[0]) return imports;
        for (let i=0; i<imports[0].length; i++) {
            if (imports[0][i]?.toString()==="()=>crypto.getRandomValues(new Uint32Array(1))[0]") {
                imports[0][i] = ()=>[0];
            } else if (imports[0][i]?.toString().includes(".shift().status")) {
                const original = imports[0][i];
                imports[0][i] = (...args) => {
                    const values = original(...args);
                    recordArrasStatus(values);
                    queueRefreshServerClients(true);
                    return values;
                };
            } else if (/\.hidden\b/.test(imports[0][i]?.toString() || "")) {
                imports[0][i] = () => false;
            } else if (imports[0][i]?.toString().includes("].globalAlpha=") && !window.__is_bot) {
                imports[0][i] = (idx, val) => {
                    ctx.globalAlpha = seeEverything?.checked ? 1 : val;
                };
            }
        }
        return imports;
    }
    WebAssembly.instantiateStreaming = async function (wasm, imports) {
        patchWasmImports(imports);
        const instance = await _WASM_iS.call(this, wasm, imports);
        let mem_export;
        for (const k in instance.instance.exports)
            if (instance.instance.exports[k] instanceof WebAssembly.Memory) { mem_export=k; break; }
        Object.defineProperty(window, "HEAPU32", {
            get() { return new Uint32Array(instance.instance.exports[mem_export].buffer); }
        });
        window.xd = ()=>new Uint32Array(instance.instance.exports[mem_export].buffer);
        return instance;
    };
    WebAssembly.instantiate = async function (wasm, imports) {
        patchWasmImports(imports);
        return _WASM_i.call(this, wasm, imports);
    };

    // ─── Global key handlers ──────────────────────────────────────────────────
    addEventListener("keydown", function (e) {
        if (!(e instanceof KeyboardEvent)) return;
        if (!window.__is_bot) {
            if (e.key==="Tab") {
                const m=getEl("scriptMenu");
                if (m) m.style.display = m.style.display==="flex"?"none":"flex";
                e.preventDefault();
            } else if (e.key==="Enter") {
                const iv=setInterval(()=>{
                    const inp=document.querySelector("body > input[type=text]");
                    if (!inp) return;
                    clearInterval(iv);
                    inp.addEventListener("keydown",(ce)=>{
                        if (inp.value.toLowerCase().includes("ez") && getEl("allowEZ")?.checked)
                            inp.value=inp.value
                                .replace("ez","еz").replace("Ez","Еz")
                                .replace("eZ","еZ").replace("EZ","ЕZ");
                        if (ce.key==="Enter" && getEl("botsCopyChat")?.checked && inp.value.trim()) {
                            const msg=inp.value.trim();
                            setTimeout(()=>botsMsg({ type:"chat", msg }),50);
                        }
                    });
                },40);
            }
            const tank=tanks[tankSelect?.value];
            if (tank && !chatting && !keys["`"]) {
                if (e.key==="x") {
                    const el=getEl("stackin");
                    if (el) { el.checked=!el.checked; el.dispatchEvent(new Event("change")); }
                } else if (e.key==="z") {
                    mouse.lock=!mouse.lock;
                } else if (e.key===(tank.firingWhenUpgrade?"g":",")) {
                    if (tank.firingWhenUpgrade) {
                        for (let i=0; i<=45; i++) events.keyDown("KeyN");
                        events.keyUp("KeyN");
                        upgradeStats(tank);
                        upgradeTank(tank.path, tank.slowUpgrading);
                        events.press("Space",null,400);
                    } else {
                        if (getEl("autoStats")?.checked) upgradeStats(tank);
                        upgradeTank(tank.path);
                    }
                }
                if (getEl("sandboxTools")?.checked) {
                    if (e.key==="1") command("KeyC","KeyH");
                    else if (e.key==="5" && keys[4] && window._ea_army)
                        for (let i=0; i<50; i++) command("KeyC","KeyH");
                    else if (e.key==="8" && keys[6]) command("KeyW","KeyK");
                }
            }
            if (getEl("botsReplKeys")?.checked && !"wasd".includes(e.key.toLowerCase()) && !isBotToggleKey(e.code) && !chatting)
                botsMsg({ type:"keydown", code:e.code });
        }
        keys[e.key]=true;
    });

    addEventListener("keyup", function (e) {
        if (getEl("botsReplKeys")?.checked && !"wasd".includes(e.key.toLowerCase()) && !isBotToggleKey(e.code) && !chatting)
            botsMsg({ type:"keyup", code:e.code });
        keys[e.key]=false;
    });

    // ─── Sandbox loop ─────────────────────────────────────────────────────────
    (function sbxLoop() {
        chatting=Boolean(document.querySelector("body > input[type=text]"));
        if (getEl("sbxHeal")?.checked && mouse.down) command("KeyH");
        else if (!chatting && !keys["`"] && getEl("sandboxTools")?.checked) {
            if (keys["2"]) command("KeyX");
        }
        const nrTank=getEl("noReloadSelect")?.value;
        if (mouse.down && nrTank && nrTank!=="disabled") {
            switch (nrTank) {
                case "engineer":  command("KeyQ"); upgradeTank("kui"); break;
                case "surgeon":   command("KeyQ","Digit2"); events.press("KeyI"); break;
                case "launchers":
                    command("KeyQ"); upgradeTank("kh"+nrTanks.launchers[nrCycle]);
                    if (++nrCycle>=nrTanks.launchers.length) nrCycle=0; break;
                case "builders":
                    command("KeyQ"); upgradeTank("ku"+nrTanks.builders[nrCycle]);
                    if (++nrCycle>=nrTanks.builders.length) nrCycle=0; break;
            }
        }
        setTimeout(sbxLoop, 40);
    })();

    // ─── Main game loop ───────────────────────────────────────────────────────
    setInterval(()=>{
        // AFK: continuously pathfind back to saved position even when pushed
        if (!window.__is_bot && afkEnabled && afkPos && spawned) {
            pathfind(afkPos.x, afkPos.y, 0.5, true);
        }
        // Keep mouse world coords fresh as player moves (WASD changes world position under cursor)
        if (!window.__is_bot && mouse.seen && bots.length && !mouse.lock) {
            const world = rememberMouseWorld(mouse.x, mouse.y);
            if (world) botsMsg({ type:"mousemove", x:mouse.x, y:mouse.y, innerWidth, innerHeight, worldX:world.x, worldY:world.y });
        }
        if (window.__is_bot && isFinitePoint(player.x, player.y) && isFinitePoint(sinsay.x, sinsay.y)) {
            const minDist = Number.isFinite(sinsay.tolerance) ? sinsay.tolerance : (sinsay.exact ? 0.7 : 2.5);
            pathfind(sinsay.x, sinsay.y, minDist, sinsay.exact);
        } else if (bots.length && !botFunMode) {
            let tx=player.x+(player.xv||0)*20;
            let ty=player.y+(player.yv||0)*20;
            if (botMouseFollow?.checked && !mouse.lock) {
                const world = rememberMouseWorld(mouse.x, mouse.y);
                if (world) {
                    tx = world.x;
                    ty = world.y;
                } else if (isFinitePoint(mouse.worldX, mouse.worldY)) {
                    tx = mouse.worldX;
                    ty = mouse.worldY;
                }
            }
            if (isFinitePoint(tx, ty)) botsMsg({ type:"position", x:tx, y:ty });
        }
        if ((fedingCheckbox?.checked || (window.__is_bot && botFeedingEnabled)) && currentFedPath.length) {
            let loc=currentFedPath[0];
            if (utils.getDist(player.x,player.y,loc.x,loc.y)<2.5) {
                currentFedPath.shift();
                loc=currentFedPath[0];
            }
            if (loc) pathfind(loc.x, loc.y);
        }
    }, 100);

    // ─── UI ───────────────────────────────────────────────────────────────────
    function initUI() {
        tankSelect    = getEl("tankSelect");
        botTankSelect = getEl("botTankSelect");
        botMoving     = getEl("botsMoving");
        fedingCheckbox= getEl("doFed");
        XRayCheckbox  = getEl("xray");
        seeEverything = getEl("seeEverything");
        XRayAlpha     = getEl("xrayAlpha");

        if (!window.__is_bot) {
            const menu = document.createElement("div");
            menu.id = "scriptMenu";
            Object.assign(menu.style, {
                position:"fixed", top:"20px", left:"20px",
                width:"500px", maxHeight:"calc(100% - 40px)",
                background:"rgba(10,10,18,0.92)",
                backdropFilter:"blur(20px)",
                color:"#f0f0f0",
                fontFamily:"'Inter','Segoe UI',Ubuntu,system-ui",
                fontSize:"13px", borderRadius:"20px", padding:"16px",
                display:"none", gap:"12px", flexDirection:"row",
                userSelect:"none",
                boxShadow:"0 30px 60px -10px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.07)",
                border:"1px solid rgba(255,255,255,0.12)",
                zIndex:"999999", overflow:"hidden"
            });

            menu.innerHTML = `
<style>
#scriptMenu * { box-sizing:border-box; }
.tabButton {
    cursor:pointer; background:rgba(255,255,255,0.06);
    font-size:12px; width:108px; height:32px; border-radius:40px;
    color:rgba(255,255,255,0.85); border:none; transition:0.18s;
    font-weight:500; letter-spacing:0.2px;
}
.tabButton:hover { background:rgba(99,102,241,0.55); color:#fff; transform:scale(1.03); }
.tabButton.active { background:rgba(99,102,241,0.8); color:#fff; }
.v-sep { width:1px; background:rgba(255,255,255,0.1); flex-shrink:0; margin:0 4px; }
.h-sep { height:1px; background:rgba(255,255,255,0.08); margin:8px 0; }
#tabButtons { display:flex; flex-direction:column; gap:5px; flex-shrink:0; padding-top:14px; }
#menus { flex:1; overflow-y:auto; padding-left:12px; font-size:13px; max-height:70vh; }
#menus::-webkit-scrollbar { width:4px; }
#menus::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.15); border-radius:4px; }
#menus p { margin:6px 0; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
#menus label { opacity:0.85; }
#menus select, #menus input[type=text], #menus input[type=number], #menus textarea {
    background:rgba(0,0,0,0.45); border:1px solid rgba(255,255,255,0.15);
    border-radius:10px; padding:5px 10px; color:white; font-size:12px;
    outline:none; transition:0.15s;
}
#menus select:focus, #menus input[type=text]:focus, #menus input[type=number]:focus, #menus textarea:focus {
    border-color:rgba(99,102,241,0.7);
}
#menus input[type=checkbox] { accent-color:#6366f1; width:15px; height:15px; cursor:pointer; }
#menus button {
    cursor:pointer; transition:0.18s;
    background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.15);
    border-radius:10px; padding:5px 12px; color:white; font-size:12px; font-weight:500;
}
#menus button:hover { background:#6366f1; border-color:#6366f1; }
#menus button.danger { border-color:rgba(239,68,68,0.5); }
#menus button.danger:hover { background:#ef4444; border-color:#ef4444; }
#menus h4 { margin:8px 0 4px; opacity:0.7; font-size:11px; text-transform:uppercase; letter-spacing:0.8px; }
.subtabBar { display:flex; gap:4px; margin:4px 0 8px; }
.subtab { cursor:pointer; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); border-radius:8px; padding:3px 10px; color:rgba(255,255,255,0.7); font-size:11px; font-weight:500; transition:0.15s; }
.subtab:hover { background:rgba(99,102,241,0.4); color:#fff; }
.subtab.active { background:rgba(99,102,241,0.75); border-color:#6366f1; color:#fff; }
#dragHandle {
    position:absolute; top:0; left:0; right:0; height:14px;
    cursor:move; border-radius:20px 20px 0 0;
    background:rgba(255,255,255,0.04);
}
#pingDisplay {
    position:absolute; bottom:8px; right:12px;
    font-size:10px; opacity:0.45; background:rgba(0,0,0,0.3);
    padding:2px 8px; border-radius:40px; pointer-events:none;
}
.proxy-row {
    display:flex; align-items:center; gap:6px;
    background:rgba(0,0,0,0.3); border-radius:8px; padding:4px 8px;
    font-size:11px; border:1px solid rgba(255,255,255,0.08); margin:3px 0;
}
.proxy-row .proxy-label { opacity:0.55; }
.proxy-row .proxy-val { color:#a5b4fc; font-family:monospace; }
.proxy-dot { width:7px; height:7px; border-radius:50%; background:#22c55e; flex-shrink:0; }
.proxy-dot.none { background:#6b7280; }
.bot-row {
    display:flex; align-items:center; gap:5px;
    font-size:11px; padding:3px 0; border-bottom:1px solid rgba(255,255,255,0.05);
}
.tank-badge {
    font-size:10px; padding:1px 6px; border-radius:20px;
    background:rgba(99,102,241,0.25); color:#a5b4fc; font-family:monospace;
}
#storyWriterWindow {
    position:fixed; right:24px; bottom:24px; width:360px; max-width:calc(100vw - 48px);
    background:rgba(10,10,18,0.96); border:1px solid rgba(255,255,255,0.14);
    border-radius:14px; box-shadow:0 24px 54px rgba(0,0,0,0.55);
    padding:10px; z-index:1000000; display:none; flex-direction:column; gap:8px;
}
#storyWriterWindow textarea {
    width:100%; height:190px; resize:vertical; font-family:monospace; line-height:1.45;
}
#storyWriterWindow .story-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
#storyWriterWindow .story-title { font-size:12px; font-weight:600; opacity:0.9; }
#storyWriterWindow .story-meta { font-size:10px; opacity:0.55; }
#storyWriterWindow button {
    cursor:pointer; transition:0.18s;
    background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.15);
    border-radius:10px; padding:5px 12px; color:white; font-size:12px; font-weight:500;
}
#storyWriterWindow button:hover { background:#6366f1; border-color:#6366f1; }
#storyWriterWindow button.danger { border-color:rgba(239,68,68,0.5); }
#storyWriterWindow button.danger:hover { background:#ef4444; border-color:#ef4444; }
</style>
<div id="dragHandle"></div>
<div id="tabButtons">
    <button onclick="changeMenu('main')"   class="tabButton">⚙️ Main</button>
    <button onclick="changeMenu('bots')"   class="tabButton">🤖 Botting</button>
    <button onclick="changeMenu('socket')" class="tabButton">🔌 Socket</button>
    <button onclick="changeMenu('proxy')"  class="tabButton">🔁 Proxies</button>
    <button onclick="changeMenu('sbx')"    class="tabButton">🎮 Sandbox</button>
    <button onclick="changeMenu('fed')"    class="tabButton">🍖 Feeding</button>
    <button onclick="changeMenu('visual')" class="tabButton">👁️ Visuals</button>
    <button onclick="changeMenu('other')"  class="tabButton">🔧 Other</button>
    <div class="h-sep"></div>
    <button onclick="changeMenu('tutor')"  class="tabButton">📘 Help</button>
    <button onclick="changeMenu('test')"   class="tabButton">🧪 Test</button>
    <button onclick="toggleChatSidebar()" class="tabButton" id="chatToggleBtn">💬 Chat</button>
</div>
<div class="v-sep"></div>
<div id="menus">

    <!-- MAIN -->
    <div id="menu-main">
        <h4>Tank Setup</h4>
        <p><label>Tank:</label>
        <select id="tankSelect">
            <option value="spread">Spreadshot</option>
            <option value="penta">Penta Shot</option>
            <option value="guntrap">Gunner Trapper</option>
            <option value="trapguard">Trap Guard</option>
            <option value="conq">Conqueror</option>
            <option value="octo">Octo Tank</option>
            <option value="orbit">Orbital Strike</option>
            <option value="cyclone">Cyclone *</option>
            <option value="tornado">Tornado</option>
            <option value="whirlwind">Whirlwind *</option>
            <option value="septaM">Septa-Mech *</option>
            <option value="septaMa">Septa-Machine *</option>
            <option value="blockade">Blockade</option>
            <option value="anni">Annihilator</option>
            <option value="fastram">Fast Ramming</option>
            <option value="finger">Finger</option>
            <option value="fighter">Any Fighter</option>
            <option value="nuke">Nuker</option>
            <option value="auto4">Auto-4/6</option>
            <option value="dreadV2">Peacekeeper</option>
        </select></p>
        <div id="tankDesc" style="font-size:12px;opacity:0.65;padding:4px 0 6px;line-height:1.5;"></div>
        <div class="h-sep"></div>
        <p><label>🔁 Stacking</label> <input id="stackin" type=checkbox></p>
        <p><label>🔄 Auto respawn</label> <input id="autoRespawn" type=checkbox></p>
        <p><label>📈 Auto stats (, key)</label> <input id="autoStats" type=checkbox></p>
        <p><label>💬 Allow type "ez"</label> <input id="allowEZ" type=checkbox></p>
        <p id="soccerbotWrapper" hidden><label>⚽ Soccer bot</label> <input id="soccerbot" type=checkbox></p>
        <p id="timekillerWrapper" hidden><label>⏱️ Time killer</label> <input id="timekiller" type=checkbox></p>
        <p><button id="cleanRefreshArras">🧼 Clean refresh arras.io</button></p>
        <div class="h-sep"></div>
        <h4>AFK Mode</h4>
        <p>
            <button id="afkSetSpot">📍 Set spot</button>
            <span id="afkCoords" style="font-size:11px;opacity:0.55;margin-left:6px;">Not set</span>
        </p>
        <p><label>🏃 AFK</label> <input id="afkToggle" type=checkbox></p>
        <div class="h-sep"></div>
        <h4>Hotkeys</h4>
        <div style="font-size:11px;opacity:0.6;line-height:1.8;">
            <kbd>Tab</kbd> — toggle menu &nbsp;|&nbsp;
            <kbd>X</kbd> — toggle stacking<br>
            <kbd>Z</kbd> — toggle mouse lock &nbsp;|&nbsp;
            <kbd>,</kbd> / <kbd>G</kbd> — upgrade tank
        </div>
    </div>

    <!-- BOTS -->
    <div id="menu-bots" hidden>
        <h4>Bot Controls</h4>
        <p><label>⌨️ Copy keys</label> <input id="botsReplKeys" type=checkbox></p>
        <p><label>🚶 Moving</label> <input id="botsMoving" type=checkbox checked></p>
        <p><label>🔥 Autofire</label> <input id="botsAutoFire" type=checkbox></p>
        <p><label>🌀 Autospin</label> <input id="botsAutoSpin" type=checkbox></p>
        <p><label>🎲 Random move</label> <input id="botsRandomMove" type=checkbox></p>
        <p><label>🎨 Disable render</label> <input id="disRender" type=checkbox></p>
        <p><label>🎯 Mouse aim mode</label> <input id="mouseAimMode" type=checkbox></p>
        <p><label>🐭 Mouse follow mode</label> <input id="mouseFollowMode" type=checkbox></p>
        <div class="h-sep"></div>
        <div class="subtabBar">
            <button class="subtab active" id="subtab-normal" onclick="switchBotSubtab('normal')">Normal</button>
            <button class="subtab"        id="subtab-other" onclick="switchBotSubtab('other')">Other</button>
            <button class="subtab"        id="subtab-crashy" onclick="switchBotSubtab('crashy')">💀 Crashy</button>
        </div>

        <!-- NORMAL subtab -->
        <div id="botpanel-normal">
        <h4>Spawn Bots</h4>
        <p>
            <button id="createBot">➕ Create 1 bot</button>
            <input type="number" id="spawnCount" value="9" min="1" max="99" style="width:55px;">
            <button id="spawnMultiple">🐣 Spawn N</button>
        </p>
        <p>
            <button id="spawnOtherTeam">🎯 Fill other team</button>
            <span id="spawnOtherTeamStatus" style="font-size:10px;opacity:0.5;">Tries until one bot lands off your URL.</span>
        </p>
        <p>
            <button id="reconnectBots">🔌 Reconnect all</button>
            <button id="syncBots">🔄 Sync all</button>
            <button id="dcBots" class="danger">❌ Disconnect all</button>
        </p>
        <div class="h-sep"></div>
        <h4>Bot Config</h4>
        <p>
            <label>Tank:</label>
            <select id="botTankSelect"></select>
            <button id="applyBotTank" style="padding:4px 10px;font-size:11px;">✅ Apply to all</button>
        </p>
        <p><label>Path:</label> <input id="botCustomTankPath" style="width:130px;" placeholder="custom upgrade path"></p>
        <div id="botTankDesc" style="font-size:11px;opacity:0.55;padding:2px 0 4px;line-height:1.5;"></div>
        <p>
            <label>Build:</label>
            <select id="botBuildMode" style="width:95px;">
                <option value="default">Default</option>
                <option value="none">Unset</option>
                <option value="custom">Custom</option>
            </select>
            <input id="botCustomBuild" style="width:105px;" placeholder="0/3/5/8/8/8/7/3">
        </p>
        <p><label>🏷️ Clan tag:</label> <input id="botClan" style="width:90px;" placeholder="[PR]"></p>
        <p><label>🔤 Bot name:</label> <input id="botName" style="width:120px;" placeholder="Cat Minion"></p>
        <p>
            <label>Names:</label>
            <select id="botNamePattern" style="width:120px;">
                <option value="same">Same name</option>
                <option value="numbered">Base 1, 2, 3</option>
                <option value="padded">Base 001</option>
                <option value="letters">Base A, B, C</option>
                <option value="random">Random pattern</option>
                <option value="custom">Custom pattern</option>
            </select>
        </p>
        <p id="botNameTemplateRow" hidden>
            <label>Pattern:</label>
            <input id="botNameTemplate" style="width:150px;" maxlength="24" value="{base}-{n}" placeholder="{base}-{n}">
        </p>
        <p><label>📌 Server pin:</label> <input id="botServerPin" style="width:95px;" placeholder="wpd"> <span style="font-size:10px;opacity:0.45;">blank = current URL</span></p>
        <p><label>👥 Team URL:</label> <select id="botTeamSelect" style="width:145px;"><option value="">Current URL / pin</option></select></p>
        <div id="botTeamStatus" style="font-size:10px;opacity:0.5;margin:-2px 0 5px 0;">No team URLs captured yet.</div>
        <p><label>💬 Copy your chat</label> <input id="botsCopyChat" type=checkbox></p>
        <div class="h-sep"></div>
        <h4>Fun Tools</h4>
        <p>
            <input id="botWriteText" style="width:100px;" maxlength="18" placeholder="TEXT">
            <input id="botWriteScale" type="number" value="3" min="1" max="12" style="width:45px;">
            <button id="botWriteWords">✏️ Write</button>
            <span style="margin-left:8px;font-size:10px;opacity:0.5;">or</span>
        </p>
        <p>
            <label style="font-size:11px;opacity:0.7;">Symbols:</label>
            <button id="symArrowUp" style="font-size:10px;padding:3px 6px;">↑</button>
            <button id="symArrowDown" style="font-size:10px;padding:3px 6px;">↓</button>
            <button id="symArrowLeft" style="font-size:10px;padding:3px 6px;">←</button>
            <button id="symArrowRight" style="font-size:10px;padding:3px 6px;">→</button>
            <button id="symCircle" style="font-size:10px;padding:3px 6px;">●</button>
            <button id="symSquare" style="font-size:10px;padding:3px 6px;">■</button>
            <button id="symX" style="font-size:10px;padding:3px 6px;">✕</button>
            <button id="symPlus" style="font-size:10px;padding:3px 6px;">+</button>
            <button id="symStar" style="font-size:10px;padding:3px 6px;">★</button>
        </p>
        <p>
            <button id="openStoryWriter">📖 Story writer</button>
            <button id="botStopFun" class="danger">⏹ Stop fun</button>
        </p>
        <div class="h-sep"></div>
        <h4>Player Names</h4>
        <p>
            <button id="fetchNames">🔎 Fetch</button>
            <button id="clearNames" class="danger">🗑️ Clear</button>
            <button id="copyNames">📋 Copy</button>
            <span id="namesCount" style="font-size:11px;opacity:0.5;">0 names</span>
        </p>
        <div id="scannedNames" style="font-size:11px;opacity:0.65;max-height:50px;overflow-y:auto;word-break:break-word;margin-bottom:4px;">None yet — names collect while you play.</div>
        <p><button id="spawnPerName">👥 Spawn 1 bot per name</button></p>
        <div class="h-sep"></div>
        <h4>Active Bots (<span id="botCount">0</span>)</h4>
        <div id="botList" style="max-height:120px;overflow-y:auto;"></div>
        </div><!-- /botpanel-normal -->

        <!-- OTHER subtab -->
        <div id="botpanel-other" hidden>
        <h4>Other Bots</h4>
        <p>
            <button id="createCatAI">🤖 Cat AI Bot</button>
        </p>
        <div style="font-size:11px;opacity:0.6;line-height:1.45;">
            Spawns one Cat AI Bot. It responds to ! commands in chat.
        </div>
        </div><!-- /botpanel-other -->

        <!-- CRASHY subtab -->
        <div id="botpanel-crashy" hidden>
        <p style="font-size:11px;opacity:0.6;line-height:1.6;margin-bottom:2px;">
            Minimal lightweight bots. They spawn together, then freeze their game loop to reduce overhead.
        </p>
        <div class="h-sep"></div>
        <h4>Spawn</h4>
        <p>
            <input type="number" id="crashyCount" value="5" min="1" max="99" style="width:55px;">
            <button id="spawnCrashy">💀 Spawn crashy</button>
        </p>
        <p><label>⛔ Disable all operations</label> <input id="crashyDisableOpsCheck" type=checkbox></p>
        <p>
            <button id="dcBotsCrashy" class="danger">❌ Disconnect all</button>
        </p>
        </div><!-- /botpanel-crashy -->
    </div>

    <!-- SOCKET -->
    <div id="menu-socket" hidden>
        <h4>Socket Bots</h4>
        <div style="font-size:11px;opacity:0.65;line-height:1.55;margin-bottom:8px;">
            Raw WebSocket bots. Uses the current captured game socket and routes through WhiteProxies via Electron sessions.
        </div>
        <p>
            <label>Spawn rate/sec:</label>
            <input type="number" id="socketSpawnRate" value="5" min="0.1" step="0.1" style="width:70px;">
            <button id="launchSocketBots">Start</button>
            <button id="stopSocketBots" class="danger">Stop</button>
        </p>
        <div id="socketStatus" style="font-size:11px;opacity:0.72;line-height:1.45;">Waiting for master socket...</div>
        <pre id="socketLog" style="max-height:120px;overflow:auto;background:rgba(0,0,0,0.3);border-radius:8px;padding:6px;font-size:10px;white-space:pre-wrap;"></pre>
    </div>

    <!-- PROXY -->
    <div id="menu-proxy" hidden>
        <h4>Proxy Manager</h4>
        <div style="font-size:11px;opacity:0.6;line-height:1.6;margin-bottom:8px;">
            One proxy per line. Format: <code style="background:rgba(255,255,255,0.08);padding:1px 5px;border-radius:4px;">ip:port:user:pass</code> or <code style="background:rgba(255,255,255,0.08);padding:1px 5px;border-radius:4px;">ip:port</code><br>
            Every <b>3 bots</b> share 1 proxy (round-robin).
        </div>
        <textarea id="proxyInput" style="width:100%;height:110px;font-family:monospace;font-size:11px;resize:vertical;" placeholder="194.54.183.98:8080:user:pass&#10;10.0.0.1:3128"></textarea>
        <p>
            <button id="saveProxies">💾 Save proxies</button>
            <button id="fetchProxies">🌐 Use WhiteProxies</button>
            <button id="clearProxies" class="danger">🗑️ Clear</button>
        </p>
        <div id="proxyStatus" style="font-size:11px;margin-top:4px;opacity:0.7;"></div>
        <div class="h-sep"></div>
        <h4>Current Proxy Assignments</h4>
        <div id="proxyAssignList" style="max-height:140px;overflow-y:auto;font-size:11px;"></div>
    </div>

    <!-- SANDBOX -->
    <div id="menu-sbx" hidden>
        <h4>Sandbox Tools</h4>
        <p><label>🛠️ Sandbox tools</label> <input id="sandboxTools" type=checkbox></p>
        <p><label>⚡ No reload:</label>
        <select id="noReloadSelect">
            <option value="disabled">Disabled</option>
            <option value="engineer">Engineer</option>
            <option value="surgeon">Surgeon</option>
            <option value="launchers">Launchers</option>
            <option value="builders">Builders</option>
        </select></p>
        <p><button id="setSpawn">📍 Set spawnpoint</button></p>
        <p><label>💚 Healing (\`+H)</label> <input id="sbxHeal" type=checkbox></p>
        <p>
            <button id="randomTanksBtn">🎲 Random Tanks</button>
            <input type="number" id="randomTanksRate" value="100" min="1" max="1000" style="width:60px;">
            <label style="font-size:11px;opacity:0.65;">/sec</label>
            <button id="randomTanksStop" class="danger">⏹ Stop</button>
        </p>
        <div class="h-sep"></div>
        <h4>Teleport Zone</h4>
        <p>
            <button id="tpZoneSetCenter">📍 Set center</button>
            <span id="tpZoneCenterLabel" style="font-size:11px;opacity:0.55;margin-left:4px;">Not set</span>
        </p>
        <p>
            <label>Radius:</label>
            <input type="number" id="tpZoneRadius" value="50" min="1" max="5000" style="width:65px;">
            <label style="margin-left:6px;">Rate:</label>
            <input type="number" id="tpZoneRate" value="2" min="0.1" max="30" step="0.1" style="width:55px;">
            <label style="font-size:11px;opacity:0.65;">/sec</label>
        </p>
        <p>
            <button id="tpZoneStart">▶ Start</button>
            <button id="tpZoneStop" class="danger">⏹ Stop</button>
            <button id="tpZoneClear" style="font-size:11px;padding:4px 8px;">🗑 Clear trail</button>
        </p>
        <canvas id="tpZoneCanvas" width="220" height="110" style="display:block;background:rgba(0,0,0,0.35);border-radius:8px;border:1px solid rgba(255,255,255,0.08);margin-top:2px;"></canvas>
        <div style="font-size:10px;opacity:0.4;margin-top:3px;">Green = you · Purple dots = teleport trail · Ring = zone</div>
        <div class="h-sep"></div>
        <h4>Entity Painter</h4>
        <div class="subtabBar">
            <button class="subtab active" id="epTabText" onclick="epSwitchTab('text')">Text</button>
            <button class="subtab" id="epTabDraw" onclick="epSwitchTab('draw')">Draw</button>
        </div>
        <!-- Text mode -->
        <div id="epPanelText">
            <p>
                <input id="epText" style="width:120px;" maxlength="18" placeholder="HELLO">
                <input type="number" id="epTextScale" value="5" min="0.1" max="60" step="0.1" style="width:45px;" title="Spacing (5 = default)">
            </p>
            <p>
                <label>Delay:</label><input type="number" id="epTextDelay" value="60" min="10" max="2000" style="width:55px;"> ms
                <button id="epSpawnText">🎯 Spawn text</button>
                <button id="epStopText" class="danger">⏹</button>
            </p>
        </div>
        <!-- Draw mode -->
        <div id="epPanelDraw" hidden>
            <p>
                <label>Brush:</label><input type="number" id="epBrush" value="4" min="1" max="20" style="width:45px;">
                <label style="margin-left:6px;">Scale:</label><input type="number" id="epDrawScale" value="3" min="0.5" max="30" step="0.5" style="width:50px;" title="World units per canvas pixel">
                <button id="epClearDraw" class="danger" style="font-size:11px;padding:3px 8px;">Clear</button>
            </p>
            <p>
                <label>Delay:</label><input type="number" id="epDrawDelay" value="60" min="10" max="2000" style="width:55px;"> ms
                <button id="epSpawnDraw">🎯 Spawn drawing</button>
                <button id="epStopDraw" class="danger">⏹</button>
            </p>
        </div>
        <canvas id="epCanvas" width="220" height="90" style="display:block;background:rgba(0,0,0,0.45);border-radius:8px;border:1px solid rgba(255,255,255,0.1);cursor:crosshair;margin-bottom:4px;touch-action:none;"></canvas>
        <p><label>🔗 Joint entities</label> <input id="epJoint" type=checkbox></p>
        <div id="epStatus" style="font-size:10px;opacity:0.55;min-height:14px;"></div>
        <div class="h-sep"></div>
        <h4>Snaaakee!!!</h4>
        <p>
            <label>Dots:</label>
            <input type="number" id="snakeCount" value="20" min="2" max="120" style="width:55px;">
            <label style="margin-left:6px;">Spacing:</label>
            <input type="number" id="snakeSpacing" value="0.5" min="0.1" max="5" step="0.1" style="width:50px;">
            <label style="font-size:11px;opacity:0.65;">wu</label>
        </p>
        <p>
            <label>Delay:</label>
            <input type="number" id="snakeDelay" value="80" min="10" max="2000" style="width:60px;"> ms
        </p>
        <p>
            <button id="snakeGo">🐍 Make snake</button>
            <button id="snakeStop" class="danger">⏹</button>
        </p>
        <div id="snakeStatus" style="font-size:10px;opacity:0.55;min-height:14px;"></div>
        <div class="h-sep"></div>
        <h4>Image Painter</h4>
        <p><input type="file" id="imgFile" accept="image/*" style="font-size:11px;width:100%;"></p>
        <canvas id="imgCanvas" width="220" height="110" style="display:block;background:rgba(0,0,0,0.45);border-radius:8px;border:1px solid rgba(255,255,255,0.08);margin-bottom:6px;"></canvas>
        <p>
            <label>Width:</label>
            <input type="number" id="imgWorldW" value="20" min="1" max="500" style="width:50px;"> wu
            <label style="margin-left:6px;">Res:</label>
            <input type="number" id="imgRes" value="40" min="5" max="120" style="width:45px;" title="Sample resolution (width in pixels)"> px
        </p>
        <p>
            <label>Delay:</label>
            <input type="number" id="imgDelay" value="80" min="10" max="2000" style="width:55px;"> ms
        </p>
        <p><label>🎨 Color entities</label> <input id="imgColor" type=checkbox checked></p>
        <p>
            <button id="imgSpawn">🖼 Spawn image</button>
            <button id="imgStop" class="danger">⏹</button>
        </p>
        <div id="imgStatus" style="font-size:10px;opacity:0.55;min-height:14px;"></div>
        <div class="h-sep"></div>
        <div style="font-size:11px;opacity:0.6;line-height:1.9;">
            <b>Sandbox hotkeys</b> (sandbox tools on):<br>
            <kbd>1</kbd> — spawn Unknown Entity<br>
            <kbd>2</kbd> — fast walls<br>
            <kbd>6</kbd>+<kbd>8</kbd> — clear everything<br>
            Hold mouse — no reload
        </div>
    </div>

    <!-- FEEDING -->
    <div id="menu-fed" hidden>
        <h4>Feed Bot</h4>
        <div style="font-size:11px;opacity:0.65;line-height:1.7;margin-bottom:8px;">
            Follows a custom path and auto-respawns.<br>
            Press <kbd>L</kbd> first so the script knows your location.
        </div>
        <p><label>Enabled</label> <input id="doFed" type=checkbox></p>
        <p>
            <button id="addFedLocation">➕ Add waypoint here</button>
            <button id="resetFedPath" class="danger">🔄 Reset path</button>
        </p>
        <div id="fedPathInfo" style="font-size:11px;opacity:0.6;margin-top:4px;">Path: 0 waypoints</div>
    </div>

    <!-- VISUALS -->
    <div id="menu-visual" hidden>
        <h4>Rendering</h4>
        <div style="font-size:11px;opacity:0.6;margin-bottom:8px;">Switch to Canvas2D rendering in Arras settings first.</div>
        <p><label>🔬 X-Ray mode</label> <input id="xray" type=checkbox></p>
        <p><label>Alpha:</label> <input type="range" id="xrayAlpha" min="0" max="1" step="0.05" value="0.5" style="width:100px;border:none;background:none;padding:0;"></p>
        <p><label>👁️ Remove transparency</label> <input id="seeEverything" type=checkbox></p>
    </div>

    <!-- OTHER -->
    <div id="menu-other" hidden>
        <h4>Tools</h4>
        <p>
            <button id="pibuild">🔢 PI build</button>
            <button id="copyClans">📋 Copy clan tags</button>
        </p>
        <div class="h-sep"></div>
        <h4>🏷️ Leaked Clan Tags</h4>
        <textarea id="stealedClans" style="width:100%;height:60px;font-size:11px;resize:none;" readonly>Nothing yet</textarea>
        <div class="h-sep"></div>
        <h4>Debug</h4>
        <div id="debugInfo" style="font-size:10px;opacity:0.5;line-height:1.8;font-family:monospace;"></div>
    </div>

    <!-- HELP -->
    <div id="menu-tutor" hidden>
        <h4>📖 Stacking</h4>
        <div style="font-size:12px;opacity:0.75;line-height:1.8;">
            Select a tank → enable Stacking → hold shoot button.<br>
            Tanks with <b>*</b> have anti-stack outside Arms Race.<br>
            Toggle stacking with <kbd>X</kbd> mid-game.
        </div>
        <div class="h-sep"></div>
        <h4>🤖 Botting</h4>
        <div style="font-size:12px;opacity:0.75;line-height:1.8;">
            Botting tab → Create bot → press <kbd>L</kbd> in bot window.<br>
            Select a tank in <b>Bot Config</b> — applies automatically on next spawn.<br>
            Hit <b>Apply to all</b> to push a tank change to already-running bots.<br>
            <b>Mouse aim</b>: bots aim at the same world point as you.<br>
            <b>Mouse follow</b>: bots move toward your cursor position.
        </div>
        <div class="h-sep"></div>
        <h4>🔧 Other</h4>
        <div style="font-size:12px;opacity:0.75;line-height:1.8;">
            <kbd>,</kbd> or <kbd>G</kbd> — upgrade tank &amp; stats.<br>
            PI build — enables secret stats in Arras settings.<br>
            X-Ray — requires Canvas2D rendering mode.
        </div>
    </div>

    <!-- TEST / DEBUG -->
    <div id="menu-test" hidden>
        <h4>Click Timing Debug</h4>
        <p><label>🖱️ Enable timing</label> <input id="testinn" type=checkbox></p>
        <div id="debugg" style="font-size:10px;font-family:monospace;word-break:break-all;opacity:0.7;max-height:80px;overflow-y:auto;"></div>
        <div class="h-sep"></div>
        <h4>WASM Memory Diff</h4>
        <p>
            <button id="test_wasm">📸 Snapshot / Diff</button>
            <label>Same values only</label> <input id="wasmcringe" type=checkbox>
        </p>
        <div id="wasmOut" style="font-size:10px;font-family:monospace;opacity:0.6;max-height:60px;overflow-y:auto;"></div>
        <div class="h-sep"></div>
        <h4>Kick Packet Capture</h4>
        <div style="font-size:11px;opacity:0.6;line-height:1.5;margin-bottom:6px;">
            Logs every WebSocket packet sent/received during a kick. Open the in-game player menu first, then use Auto-kick to kick one unnamed player automatically.
        </div>
        <p>
            <button id="wsAutoKickTest">🔬 Auto-kick 1 unnamed</button>
            <button id="wsCaptureStart">▶ Manual capture</button>
            <button id="wsCaptureStop" class="danger">⏹ Stop</button>
        </p>
        <p>
            <button id="wsCopyCaptured" style="font-size:11px;padding:3px 8px;">📋 Copy log</button>
            <button id="wsClearCapture" style="font-size:11px;padding:3px 8px;" class="danger">🗑 Clear</button>
        </p>
        <div id="wsCaptureStatus" style="font-size:10px;opacity:0.55;min-height:14px;">Idle — open player menu first.</div>
        <div id="wsCaptureLog" style="font-family:monospace;font-size:10px;background:rgba(0,0,0,0.3);border-radius:8px;padding:6px;max-height:150px;overflow-y:auto;margin-top:4px;word-break:break-all;line-height:1.7;"></div>
        <div class="h-sep"></div>
        <h4>Color Angle Tester</h4>
        <div style="font-size:11px;opacity:0.6;line-height:1.5;margin-bottom:6px;">
            Spawns entities in a ring, colors each at its angle using backtick+C. Hover the wheel to see angle&#8594;color mapping.
        </div>
        <p>
            <label>Steps:</label>
            <input type="number" id="ctSteps" value="36" min="4" max="72" style="width:50px;">
            <label style="margin-left:6px;">Radius:</label>
            <input type="number" id="ctRadius" value="5" min="1" max="30" step="0.5" style="width:50px;"> wu
        </p>
        <p>
            <label>Delay:</label>
            <input type="number" id="ctDelay" value="150" min="50" max="2000" style="width:60px;"> ms
            <button id="ctRun">🎨 Run test</button>
            <button id="ctStop" class="danger">⏹</button>
        </p>
        <canvas id="ctCanvas" width="220" height="220" style="display:block;background:rgba(0,0,0,0.4);border-radius:10px;border:1px solid rgba(255,255,255,0.1);margin-top:4px;cursor:crosshair;"></canvas>
        <div id="ctHoverLabel" style="font-size:11px;opacity:0.7;min-height:16px;margin-top:3px;font-family:monospace;"></div>
        <div class="h-sep" style="margin:6px 0;"></div>
        <h4>Annotate step</h4>
        <p>
            <label>Step:</label>
            <input type="number" id="ctAnnotateStep" value="1" min="1" max="72" style="width:50px;">
            <label style="margin-left:6px;">Color:</label>
            <input id="ctAnnotateInput" style="width:100px;" placeholder="red, #ff4400">
            <button id="ctAnnotateSave" style="font-size:11px;padding:3px 8px;">✅ Save</button>
        </p>
        <div id="ctStatus" style="font-size:10px;opacity:0.55;min-height:14px;"></div>
        <div class="h-sep"></div>
        <h4>Saved Colors</h4>
        <div id="ctSaved" style="font-size:10px;font-family:monospace;opacity:0.7;max-height:80px;overflow-y:auto;line-height:1.7;"></div>
        <p>
            <button id="ctClearSaved" class="danger" style="font-size:11px;padding:3px 8px;">Clear saved</button>
            <button id="ctCopySaved" style="font-size:11px;padding:3px 8px;">📋 Copy</button>
        </p>
        <div class="h-sep"></div>
        <h4>🧪 Standalone Spawn Test</h4>
        <div style="font-size:11px;opacity:0.6;line-height:1.5;margin-bottom:6px;">
            Opens raw WebSocket connections and sends a spawn packet with the given
            name &amp; party. Bots idle after spawning — no movement, no AI, and
            <b>no link to the rest of the script.</b> <i>Experimental:</i> arras may
            encrypt outgoing packets via WASM, so raw sends might not register on the
            live server.
        </div>
        <p>
            <label>Name:</label>
            <input id="sstName" value="test" maxlength="16" style="width:100px;">
            <label style="margin-left:6px;">Party:</label>
            <input id="sstParty" value="" maxlength="40" style="width:80px;" placeholder="(none)">
        </p>
        <p>
            <label>Count:</label>
            <input type="number" id="sstCount" value="5" min="1" max="200" style="width:55px;">
            <button id="sstSpawn">🐣 Spawn bots</button>
            <button id="sstKill" class="danger">⏹ Disconnect all</button>
        </p>
        <p style="font-size:11px;">
            <label>Server URL:</label>
            <input id="sstUrl" style="width:230px;" placeholder="auto-captured from game">
        </p>
        <div id="sstStatus" style="font-size:10px;opacity:0.6;min-height:14px;">No bots.</div>
        <div id="sstLog" style="font-family:monospace;font-size:10px;background:rgba(0,0,0,0.3);border-radius:8px;padding:6px;max-height:120px;overflow-y:auto;margin-top:4px;word-break:break-all;line-height:1.6;"></div>
    </div>

</div>
<div id="pingDisplay">🏓 <span id="pingVal">?</span>ms</div>`;

            document.body.appendChild(menu);

            const storyWindow = document.createElement("div");
            storyWindow.id = "storyWriterWindow";
            storyWindow.innerHTML = `
<div class="story-head">
    <div>
        <div class="story-title">📖 Story Writer</div>
        <div id="storyWriterMeta" class="story-meta">0 chunks · 24 chars per bot</div>
    </div>
    <button id="closeStoryWriter" class="danger" style="padding:3px 8px;">✕</button>
</div>
<textarea id="storyWriterText" maxlength="2400" placeholder="Type a story. Every 24 characters becomes one bot name."></textarea>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
    <button id="clearStoryWriter" class="danger">Clear story bots</button>
    <span id="storyWriterStatus" class="story-meta">Idle</span>
</div>`;
            document.body.appendChild(storyWindow);

            // ── Chat sidebar ──────────────────────────────────────────────
            const chatPanel = document.createElement("div");
            chatPanel.id = "chatSidebar";
            Object.assign(chatPanel.style, {
                position:"fixed", top:"20px", right:"10px",
                width:"280px", maxHeight:"420px",
                background:"rgba(10,10,18,0.92)",
                backdropFilter:"blur(20px)",
                color:"#f0f0f0",
                fontFamily:"'Inter','Segoe UI',Ubuntu,system-ui",
                fontSize:"12px", borderRadius:"16px", padding:"10px",
                display:"none", flexDirection:"column", gap:"6px",
                boxShadow:"0 20px 40px -8px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.07)",
                border:"1px solid rgba(255,255,255,0.12)",
                zIndex:"999998",
                userSelect:"text", WebkitUserSelect:"text",
            });
            chatPanel.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
    <span style="font-weight:600;opacity:0.9;">💬 Chat Log</span>
    <button onclick="toggleChatSidebar()" style="background:none;border:none;color:#f0f0f0;cursor:pointer;font-size:14px;opacity:0.6;padding:0 4px;">✕</button>
</div>
<div id="chatMessages" style="overflow-y:auto;max-height:360px;display:flex;flex-direction:column;gap:3px;user-select:text;-webkit-user-select:text;cursor:text;"></div>`;
            document.body.appendChild(chatPanel);

            window.toggleChatSidebar = function() {
                const p = getEl("chatSidebar");
                if (!p) return;
                const visible = p.style.display === "flex";
                p.style.display = visible ? "none" : "flex";
                const btn = getEl("chatToggleBtn");
                if (btn) btn.classList.toggle("active", !visible);
            };

            updateChatSidebar = () => {
                const el = getEl("chatMessages");
                if (!el) return;
                el.innerHTML = chatLog.map(entry => {
                    const t = new Date(entry.time);
                    const ts = `${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}`;
                    const repeat = entry.count > 1 ? ` <span style="opacity:0.35;">x${entry.count}</span>` : "";
                    return `<div style="background:rgba(255,255,255,0.05);border-radius:6px;padding:3px 7px;line-height:1.4;">
                        <span style="opacity:0.35;font-size:10px;margin-right:4px;">${ts}</span>${escapeChatHtml(entry.text)}${repeat}</div>`;
                }).join("");
                el.scrollTop = el.scrollHeight;
            };

            if (!IS_ELECTRON) {
                botIframes = document.createElement("div");
                Object.assign(botIframes.style, { position:"absolute", bottom:"20px", left:"200px", zIndex:"999999" });
                document.body.appendChild(botIframes);
            }

            // Re-grab refs after DOM injection
            tankSelect     = getEl("tankSelect");
            botTankSelect  = getEl("botTankSelect");
            botMoving      = getEl("botsMoving");
            botMouseFollow = getEl("mouseFollowMode");
            fedingCheckbox = getEl("doFed");
            XRayCheckbox   = getEl("xray");
            seeEverything  = getEl("seeEverything");
            XRayAlpha      = getEl("xrayAlpha");

            // ── Dragging ──────────────────────────────────────────────────
            let dragOffX=0, dragOffY=0, dragging=false;
            getEl("dragHandle").addEventListener("mousedown", e=>{
                dragging=true;
                dragOffX=e.clientX-menu.offsetLeft;
                dragOffY=e.clientY-menu.offsetTop;
                e.preventDefault();
            });
            document.addEventListener("mousemove", e=>{
                if (!dragging) return;
                menu.style.left=(e.clientX-dragOffX)+"px";
                menu.style.top =(e.clientY-dragOffY)+"px";
            });
            document.addEventListener("mouseup", ()=>{ dragging=false; });

            // ── Tab switching ─────────────────────────────────────────────
            const menuNames=["main","bots","socket","proxy","sbx","fed","visual","other","tutor","test"];
            window.changeMenu=function(name){
                menuNames.forEach(n=>{
                    const el=getEl("menu-"+n);
                    if (el) el.hidden=(el.id!=="menu-"+name);
                });
                document.querySelectorAll(".tabButton").forEach(b=>{
                    b.classList.toggle("active", b.getAttribute("onclick")===`changeMenu('${name}')`);
                });
                stored.tab=name; saveData();
                if (name==="proxy") refreshProxyUI();
                if (name==="bots")  refreshBotList();
            };

            // ── Persistence ───────────────────────────────────────────────
            function saveData() { localStorage.setItem("pr-script-data", JSON.stringify(stored)); }
            let stored={};
            try {
                const raw=localStorage.getItem("pr-script-data");
                stored=raw?JSON.parse(raw):{ elements:{}, tab:"main" };
            } catch { stored={ elements:{}, tab:"main" }; }

            window.changeMenu(stored.tab||"main");

            for (const key in stored.elements) {
                if (!Object.hasOwn(stored.elements,key)) continue;
                const el=getEl(key); if(!el) continue;
                const d=stored.elements[key];
                if      (d.type==="checkbox") el.checked=d.checked;
                else if (d.type==="select")   el.value=d.value;
                else if (d.type==="text")     el.value=d.value;
            }

            ["autoStats","autoRespawn","botsReplKeys","botsAutoFire","botsAutoSpin","botsRandomMove","disRender","allowEZ","seeEverything","mouseFollowMode","sandboxTools","sbxHeal","botsCopyChat"].forEach(id=>{
                const el=getEl(id); if(!el) return;
                el.addEventListener("change",()=>{
                    stored.elements[id]={ type:"checkbox", checked:el.checked };
                    saveData();
                });
            });

            // ── Main tank select ──────────────────────────────────────────
            const desc=getEl("tankDesc");
            const updateTankSelect=()=>{
                if (desc) desc.innerHTML=tanks[tankSelect.value]?.desc||"";
                getEl("soccerbotWrapper").hidden=tankSelect.value!=="blockade";
                if (tankSelect.value!=="blockade") getEl("soccerbot").checked=false;
                getEl("timekillerWrapper").hidden=tankSelect.value!=="fighter";
                if (tankSelect.value!=="fighter") getEl("timekiller").checked=false;
                stored.elements["tankSelect"]={ type:"select", value:tankSelect.value };
                saveData();
            };
            tankSelect.addEventListener("change", updateTankSelect);
            updateTankSelect();

            // ── Bot tank select ───────────────────────────────────────────
            const botTankDescEl = getEl("botTankDesc");
            const botBuildMode = getEl("botBuildMode");
            const botCustomBuild = getEl("botCustomBuild");
            const botCustomTankPath = getEl("botCustomTankPath");
            botTankSelect.innerHTML = `<option value="none">None</option><option value="custom">Custom path</option>`;

            for (const k in tanks) {
                if (!Object.hasOwn(tanks,k)) continue;
                botTankSelect.innerHTML += `<option value="${k}">${tanks[k].name}</option>`;
            }

            botTankSelect.innerHTML += `<option disabled>──────</option><option disabled>Branches:</option>`;
            for (const k in botBranches) {
                if (!Object.hasOwn(botBranches,k)) continue;
                botTankSelect.innerHTML += `<option value="${k}">${botBranches[k].label}</option>`;
            }

            const updateBotTankDesc = () => {
                const v = botTankSelect.value;
                const mode = botBuildMode?.value || "default";
                let text = "";
                if (v === "custom") text = `Custom path: <code style="background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;">${botCustomTankPath?.value || ""}</code>`;
                else if (tanks[v]?.desc) text = tanks[v].desc;
                else if (tanks[v]?.build) text = `Build: <code style="background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;">${tanks[v].build}</code>`;
                if (botBranches[v]) text = `Random upgrade path each spawn.`;
                if (mode === "none") text = `${text ? text + "<br>" : ""}Skill build: unset`;
                if (mode === "custom") text = `${text ? text + "<br>" : ""}Skill build: <code style="background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;">${botCustomBuild?.value || ""}</code>`;
                if (botTankDescEl) botTankDescEl.innerHTML = text;
                stored.elements["botTankSelect"]={ type:"select", value:v };
                if (botCustomTankPath) stored.elements["botCustomTankPath"]={ type:"input", value:botCustomTankPath.value };
                if (botBuildMode) stored.elements["botBuildMode"]={ type:"select", value:mode };
                if (botCustomBuild) stored.elements["botCustomBuild"]={ type:"input", value:botCustomBuild.value };
                saveData();
            };

            if (stored.elements["botTankSelect"]) botTankSelect.value=stored.elements["botTankSelect"].value;
            if (stored.elements["botCustomTankPath"] && botCustomTankPath) botCustomTankPath.value=stored.elements["botCustomTankPath"].value;
            if (stored.elements["botBuildMode"] && botBuildMode) botBuildMode.value=stored.elements["botBuildMode"].value;
            if (stored.elements["botCustomBuild"] && botCustomBuild) botCustomBuild.value=stored.elements["botCustomBuild"].value;
            const syncCustomTankPath = () => {
                syncBotConfig();
                updateBotTankDesc();
            };
            const syncBotBuildConfig = () => {
                syncBotConfig();
                updateBotTankDesc();
            };
            botCustomTankPath?.addEventListener("input", syncCustomTankPath);
            botBuildMode?.addEventListener("change", syncBotBuildConfig);
            botCustomBuild?.addEventListener("input", syncBotBuildConfig);
            botTankSelect.addEventListener("change", ()=>{
                syncBotConfig();
                updateBotTankDesc();
                refreshBotList();
            });
            updateBotTankDesc();

            getEl("applyBotTank").addEventListener("click", ()=>{
                syncBotConfig(currentBotConfig(), { forceUpgrade:true });
                updateBotTankDesc();
                refreshBotList();
            });

            // ── Bot list renderer ──────────────────────────────────────────
            function getBotTankLabel(v) {
                if (!v || v==="none") return "None";
                if (v === "custom") return "Custom path";
                if (tanks[v]) return tanks[v].name;
                if (botBranches[v]) return botBranches[v].label;
                if (typeof v==="object" && v.name) return v.name;
                return String(v);
            }

            function refreshBotList() {
                const list=getEl("botList");
                const count=getEl("botCount");
                if (!list) return;
                count.textContent=bots.length;
                list.innerHTML=bots.map((b,i)=>{
                    let proxyStr, hasPx, tv, botNum;
                    if (IS_ELECTRON) {
                        proxyStr = b.proxyLabel || "direct";
                        hasPx    = b.proxyLabel && b.proxyLabel !== "direct";
                        tv       = b.config?.isCatAI ? "Cat AI Bot" : (b.config?.tank || "none");
                        botNum   = b.index + 1;
                    } else {
                        const p  = ProxyManager.getForBot(i);
                        proxyStr = ProxyManager.formatProxy(p);
                        hasPx    = !!p;
                        tv       = b.iframe?.contentWindow?.channel?.isCatAI ? "Cat AI Bot" : b.iframe?.contentWindow?.channel?.tank;
                        botNum   = i + 1;
                    }
                    return `<div class="bot-row">
                        <div class="proxy-dot ${hasPx?'':'none'}"></div>
                        <span style="opacity:0.6;width:48px;">Bot #${botNum}</span>
                        <span class="tank-badge">${getBotTankLabel(tv)}</span>
                        <span class="proxy-val" style="font-size:10px;opacity:0.5;">${proxyStr}</span>
                    </div>`;
                }).join("")||`<div style="opacity:0.4;font-size:11px;padding:4px;">No bots active.</div>`;
            }

            let funRandomInterval = null;
            let funRandomTanksInterval = null;
            let funWriteInterval = null;
            function botIndexOf(b, fallback) { return IS_ELECTRON ? b.index : fallback; }
            function activeBotIndices() { return bots.map((b,i)=>botIndexOf(b,i)); }
            function selectedBotTankValue() {
                return getEl("botTankSelect")?.value || "none";
            }
            function stopFunTools() {
                if (funRandomInterval) clearInterval(funRandomInterval);
                funRandomInterval = null;
                if (storyFormationInterval) clearInterval(storyFormationInterval);
                storyFormationInterval = null;
                if (funRandomTanksInterval) { clearInterval(funRandomTanksInterval); funRandomTanksInterval = null; }
                if (funWriteInterval) { clearInterval(funWriteInterval); funWriteInterval = null; }

                botFunMode = false;
                botFunKind = "";

                botsMsg({ type:"chaosMode", value:false });
                botsMsg({ type:"formationStop" });
                botsMsg({ type:"setTank", value:selectedBotTankValue() });
                botsMsg({ type:"forceUpgrade", tank:selectedBotTankValue() });

                if (getEl("botsMoving")?.checked) {
                    botsMsg({ type:"setMoving", value:true });
                }
                const rmCb = getEl("botsRandomMove");
                if (rmCb) rmCb.checked = false;
            }
            const SYMBOLS = {
                arrowUp: [
                    [0,0,1,0,0], [0,1,1,1,0], [1,1,1,1,1],
                    [0,0,1,0,0], [0,0,1,0,0], [0,0,1,0,0],
                ],
                arrowDown: [
                    [0,0,1,0,0], [0,0,1,0,0], [0,0,1,0,0],
                    [1,1,1,1,1], [0,1,1,1,0], [0,0,1,0,0],
                ],
                arrowLeft: [
                    [0,0,1,0,0], [0,1,0,0,0], [1,0,0,0,0],
                    [0,1,0,0,0], [0,0,1,0,0], [0,0,0,0,0],
                ],
                arrowRight: [
                    [0,0,1,0,0], [0,0,0,1,0], [0,0,0,0,1],
                    [0,0,0,1,0], [0,0,1,0,0], [0,0,0,0,0],
                ],
                circle: [
                    [0,1,1,1,0], [1,0,0,0,1], [1,0,0,0,1],
                    [1,0,0,0,1], [0,1,1,1,0],
                ],
                square: [
                    [1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1],
                    [1,0,0,0,1], [1,1,1,1,1],
                ],
                X: [
                    [1,0,0,0,1], [0,1,0,1,0], [0,0,1,0,0],
                    [0,1,0,1,0], [1,0,0,0,1],
                ],
                plus: [
                    [0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1],
                    [0,0,1,0,0], [0,0,1,0,0],
                ],
                star: [
                    [0,0,1,0,0], [0,1,0,1,0], [1,0,0,0,1],
                    [0,1,0,1,0], [0,0,1,0,0],
                ],
            };
            function makeSymbolPoints(symbolName, scale) {
                const symbol = SYMBOLS[symbolName];
                if (!symbol) return [];
                const points = [];
                for (let y=0; y<symbol.length; y++) {
                    for (let x=0; x<symbol[y].length; x++) {
                        if (symbol[y][x]) points.push({ x:x*scale, y:y*scale });
                    }
                }
                if (!points.length) return [];
                const minX = Math.min(...points.map(p=>p.x));
                const maxX = Math.max(...points.map(p=>p.x));
                const minY = Math.min(...points.map(p=>p.y));
                const maxY = Math.max(...points.map(p=>p.y));
                const cx = (minX + maxX) / 2;
                const cy = (minY + maxY) / 2;
                return points
                    .sort((a,b) => (a.x - b.x) || (a.y - b.y))
                    .map(p => ({ x:p.x - cx, y:p.y - cy }));
            }
            function makeWordPoints(text, scale) {
                text = (text || "").toUpperCase().slice(0, 18);
                const points = [];
                let cursor = 0;
                for (const ch of text) {
                    if (ch === " ") { cursor += 4; continue; }
                    const glyph = tinyFont[ch];
                    if (!glyph) { cursor += 4; continue; }
                    for (let y=0; y<glyph.length; y++) {
                        for (let x=0; x<glyph[y].length; x++) {
                            if (glyph[y][x] === "1") points.push({ x:(cursor+x)*scale, y:y*scale });
                        }
                    }
                    cursor += 6;
                }
                if (!points.length) return [];
                const minX = Math.min(...points.map(p=>p.x));
                const maxX = Math.max(...points.map(p=>p.x));
                const minY = Math.min(...points.map(p=>p.y));
                const maxY = Math.max(...points.map(p=>p.y));
                const cx = (minX + maxX) / 2;
                const cy = (minY + maxY) / 2;
                // Return as relative offsets, not absolute positions
                return points
                    .sort((a,b) => (a.x - b.x) || (a.y - b.y))
                    .map(p => ({ x:p.x - cx, y:p.y - cy }));
            }
            function sendFormation(points, opts={}) {
                const ids = activeBotIndices();
                if (!ids.length || !points.length) return;
                const assignments = ids.map((index, i) => {
                    const p = points[Math.floor(i * points.length / ids.length) % points.length];
                    return {
                        index,
                        x: p.x,
                        y: p.y,
                        exact: opts.exact,
                        tolerance: opts.tolerance,
                    };
                });
                botsMsg({ type:"formation", assignments });
            }

            // ── Bot messaging + Electron sync ──────────────────────────────
            if (IS_ELECTRON) {
                botsMsg = msg => window.electronBridge.sendToAllBots(msg);
                window.electronBridge.onSocketState?.(state => {
                    const status = getEl("socketStatus");
                    const log = getEl("socketLog");
                    if (status) status.innerHTML = [
                        `Master: <b>${state.master ? "captured" : "waiting"}</b>`,
                        `Socket bots: <b>${state.total}</b> connected: <b>${state.connected}</b> spawned: <b>${state.spawned}</b>`,
                        `Spawner: <b>${state.spawning ? "running" : "stopped"}</b>${state.spawning ? ` at <b>${state.spawnRate}</b>/sec` : ""}`,
                        `Proxies: <b>${state.proxies}</b>`,
                    ].join("<br>");
                    if (log) log.textContent = (state.log || []).join("\n");
                });
                window.electronBridge.onBotsChanged(list => {
                    bots = list;
                    refreshBotList();
                    refreshProxyUI();
                });
                window.electronBridge.onTeamUrls?.(state => {
                    teamUrlState = state || { entries: [], yourUrl: "" };
                    refreshTeamUrlUI();
                });
                window.electronBridge.getTeamUrls?.().then(state => {
                    teamUrlState = state || { entries: [], yourUrl: "" };
                    refreshTeamUrlUI();
                }).catch(() => {});
                window.electronBridge.onFoundName(name => {
                    if (!serverClients && !seenPlayerNames.has(name) && !ARRAS_UI_STRINGS.has(name)) {
                        seenPlayerNames.add(name);
                        updateNamesDisplay();
                    }
                });
                window.electronBridge.onFoundChat(text => {
                    addChatRow(text);
                });
            } else {
                botsMsg = function(msg) {
                    for (const b of bots) b.iframe.contentWindow.channel.message(msg);
                };
            }

            function refreshTeamUrlUI() {
                const select = getEl("botTeamSelect");
                const status = getEl("botTeamStatus");
                if (!select) return;
                const previous = select.value;
                const entries = Array.isArray(teamUrlState.entries) ? teamUrlState.entries : [];
                select.innerHTML = `<option value="">Current URL / pin</option>` + entries.map(entry => {
                    const label = `${entry.label}${entry.isYours ? " (you)" : ""}`;
                    return `<option value="${escapeAttr(entry.url)}">${label}</option>`;
                }).join("");
                if ([...select.options].some(opt => opt.value === previous)) select.value = previous;
                if (status) {
                    if (!IS_ELECTRON) {
                        status.textContent = "Team URL capture requires Electron mode.";
                    } else if (!entries.length) {
                        status.textContent = "No team URLs captured yet.";
                    } else {
                        const yours = entries.find(entry => entry.isYours);
                        status.textContent = yours
                            ? `You are on ${yours.label}.`
                            : `Your URL: ${teamUrlState.yourUrl || "not captured yet"}`;
                    }
                }
            }

            function botPinOrCurrentUrl() {
                const pin = getEl("botServerPin")?.value.trim() || "";
                if (!pin) return location.href;
                if (/^https?:\/\//i.test(pin)) return pin;
                const hashIndex = pin.indexOf("#");
                const cleanPin = (hashIndex !== -1 ? pin.slice(hashIndex + 1) : pin)
                    .trim()
                    .replace(/^#+/, "")
                    .replace(/[^A-Za-z0-9_-]/g, "");
                return cleanPin ? `https://arras.io/#${cleanPin}` : location.href;
            }

            function botTargetUrl() {
                const teamUrl = getEl("botTeamSelect")?.value.trim() || "";
                return teamUrl || botPinOrCurrentUrl();
            }

            let botNameSequence = 0;
            const BOT_NAME_WORDS = [
                "Nova","Pixel","Dash","Echo","Flux","Orbit","Vex","Quill",
                "Byte","Comet","Rift","Bolt","Rune","Drift","Jolt","Zen"
            ];
            function botBaseName() {
                return getEl("botName")?.value.trim() || "Cat Minion";
            }
            function letterName(num) {
                let n = Math.max(1, Number(num) || 1);
                let out = "";
                while (n > 0) {
                    n--;
                    out = String.fromCharCode(65 + (n % 26)) + out;
                    n = Math.floor(n / 26);
                }
                return out;
            }
            function currentNamePattern() {
                const mode = getEl("botNamePattern")?.value || "same";
                if (mode === "same") return null;
                return {
                    mode,
                    base: botBaseName(),
                    template: getEl("botNameTemplate")?.value.trim() || "{base}-{n}",
                    words: BOT_NAME_WORDS,
                    start: botNameSequence + 1,
                };
            }
            function formatPatternName(pattern, seq) {
                const base = pattern?.base || botBaseName();
                const n = Math.max(1, Number(seq) || 1);
                const word = (pattern?.words || BOT_NAME_WORDS)[(n - 1) % (pattern?.words || BOT_NAME_WORDS).length] || "Bot";
                const rand = Math.floor(100 + Math.random() * 900);
                let name = base;
                if (pattern?.mode === "numbered") name = `${base} ${n}`;
                else if (pattern?.mode === "padded") name = `${base} ${String(n).padStart(3, "0")}`;
                else if (pattern?.mode === "letters") name = `${base} ${letterName(n)}`;
                else if (pattern?.mode === "random") name = `${word} ${rand}`;
                else if (pattern?.mode === "custom") {
                    name = (pattern.template || "{base}-{n}")
                        .replaceAll("{base}", base)
                        .replaceAll("{n}", String(n))
                        .replaceAll("{i}", String(n - 1))
                        .replaceAll("{letter}", letterName(n))
                        .replaceAll("{word}", word)
                        .replaceAll("{rand}", String(rand));
                }
                return String(name).replace(/\s+/g, " ").trim().slice(0, 24) || base;
            }
            function nextBotName() {
                const pattern = currentNamePattern();
                if (!pattern) return botBaseName();
                botNameSequence++;
                return formatPatternName(pattern, botNameSequence);
            }
            function updateNamePatternUI() {
                const row = getEl("botNameTemplateRow");
                if (row) row.hidden = (getEl("botNamePattern")?.value || "same") !== "custom";
            }

            function currentBotConfig(nameOverride) {
                const pattern = nameOverride == null ? currentNamePattern() : null;
                return {
                    clan: utils.removeBrackets(getEl("botClan").value || ""),
                    botName: nameOverride ?? (getEl("botName").value.trim() || "Cat Minion"),
                    namePattern: pattern,
                    tank: botTankSelect.value,
                    customTankPath: getEl("botCustomTankPath")?.value.trim() || "",
                    botBuildMode: getEl("botBuildMode")?.value || "default",
                    customBuild: getEl("botCustomBuild")?.value.trim() || "",
                    disRender: getEl("disRender").checked,
                    moving: botMoving.checked,
                    autoFire: !!getEl("botsAutoFire")?.checked,
                    autoSpin: !!getEl("botsAutoSpin")?.checked,
                    mouseType,
                    targetTeamUrl: getEl("botTeamSelect")?.value.trim() || "",
                    targetPin: getEl("botServerPin")?.value.trim() || "",
                    targetUrl: botTargetUrl(),
                };
            }
            function applyConfigToIframeBot(bot, config, opts={}) {
                const ch = bot?.iframe?.contentWindow?.channel;
                if (!ch) return;
                ch.clan = config.clan;
                if (opts.names !== false) ch.botName = config.botName;
                ch.disRender = config.disRender;
                ch.tank = config.tank;
                ch.customTankPath = config.customTankPath;
                ch.botBuildMode = config.botBuildMode;
                ch.customBuild = config.customBuild;
                ch.moving = config.moving;
                ch.autoFire = !!config.autoFire;
                ch.autoSpin = !!config.autoSpin;
                ch.mouseType = config.mouseType;
            }
            function syncBotConfig(config=currentBotConfig(), opts={}) {
                const syncNames = opts.names === true || !config.namePattern;
                if (IS_ELECTRON) {
                    botsMsg({ type:"setClan", value:config.clan });
                    if (syncNames) botsMsg({ type:"setName", value:config.botName });
                    botsMsg({ type:"setDisRender", value:config.disRender });
                    botsMsg({ type:"setMoving", value:config.moving });
                    botsMsg({ type:"setAutoFire", value:!!config.autoFire });
                    botsMsg({ type:"setAutoSpin", value:!!config.autoSpin });
                    botsMsg({ type:"setTank", value:config.tank });
                    botsMsg({ type:"setCustomTankPath", path:config.customTankPath });
                    botsMsg({ type:"setBotBuild", mode:config.botBuildMode, build:config.customBuild });
                    botsMsg({ type:"mouseType", value:config.mouseType });
                    if (opts.forceUpgrade) botsMsg({ type:"forceUpgrade", tank:config.tank });
                } else {
                    for (const b of bots) applyConfigToIframeBot(b, config, { names: syncNames });
                    if (opts.forceUpgrade) botsMsg({ type:"forceUpgrade", tank:config.tank });
                    botsMsg({ type:"mouseType", value:config.mouseType });
                    botsMsg({ type:"setAutoFire", value:!!config.autoFire });
                    botsMsg({ type:"setAutoSpin", value:!!config.autoSpin });
                }
                refreshBotList();
            }

            // ── Proxy UI ───────────────────────────────────────────────────
            getEl("proxyInput").value = IS_ELECTRON ? "" : ProxyManager.toText();

            function refreshProxyUI() {
                const status=getEl("proxyStatus");
                const list  =getEl("proxyAssignList");
                if (!status) return;
                if (IS_ELECTRON) {
                    status.textContent = bots.length
                        ? `✅ ${bots.length} bot(s) active — proxies managed by launcher.`
                        : "Enter proxies below or fetch from WhiteProxies, then click Save.";
                } else {
                    const count=ProxyManager.proxies.length;
                    status.textContent=count
                        ?`✅ ${count} proxi${count>1?"es":"y"} loaded. Covers ${count*3} bot slots.`
                        :"⚠️ No proxies loaded — bots use your direct connection.";
                }
                if (!list) return;
                if (!bots.length) { list.innerHTML=`<div style="opacity:0.4;">Spawn bots first to see assignments.</div>`; return; }
                list.innerHTML=bots.map((b,i)=>{
                    const proxyStr = IS_ELECTRON ? (b.proxyLabel||"direct") : ProxyManager.formatProxy(ProxyManager.getForBot(i));
                    const hasPx    = IS_ELECTRON ? (b.proxyLabel && b.proxyLabel!=="direct") : !!ProxyManager.getForBot(i);
                    const botNum   = IS_ELECTRON ? b.index+1 : i+1;
                    return `<div class="proxy-row">
                        <div class="proxy-dot ${hasPx?'':'none'}"></div>
                        <span class="proxy-label">Bot #${botNum}</span>
                        <span class="proxy-val">${proxyStr}</span>
                    </div>`;
                }).join("");
            }

            getEl("saveProxies").addEventListener("click",()=>{
                const text = getEl("proxyInput").value;
                ProxyManager.setFromText(text);
                if (IS_ELECTRON) window.electronBridge.setProxies(text);
                refreshProxyUI(); refreshBotList();
                const s=getEl("proxyStatus");
                if(s){ s.style.color="#86efac"; setTimeout(()=>{ s.style.color=""; refreshProxyUI(); },1500); }
            });

            getEl("clearProxies").addEventListener("click",()=>{
                if (!confirm("Clear all saved proxies?")) return;
                ProxyManager.proxies=[]; ProxyManager.save();
                getEl("proxyInput").value="";
                if (IS_ELECTRON) window.electronBridge.setProxies("");
                refreshProxyUI();
            });

            if (IS_ELECTRON) {
                getEl("fetchProxies").addEventListener("click", async () => {
                    const btn=getEl("fetchProxies");
                    btn.disabled=true;
                    try {
                        const text = await window.electronBridge.fetchProxies();
                        getEl("proxyInput").value=text;
                        ProxyManager.setFromText(text);
                        window.electronBridge.setProxies(text);
                        refreshProxyUI();
                        const s=getEl("proxyStatus");
                        if(s){ s.textContent=`✅ Loaded ${ProxyManager.proxies.length} WhiteProxies gateway${ProxyManager.proxies.length>1?"s":""}.`; s.style.color="#86efac"; setTimeout(()=>{ s.style.color=""; refreshProxyUI(); },2000); }
                    } catch(err) {
                        const s=getEl("proxyStatus");
                        if(s){ s.textContent="❌ Fetch failed: "+err.message; s.style.color="#f87171"; setTimeout(()=>{ s.style.color=""; },3000); }
                    }
                    btn.disabled=false;
                });
            } else {
                getEl("fetchProxies").remove();
            }

            // ── Bot management ─────────────────────────────────────────────
            function createBotWithName(name) {
                botFunMode = false;
                botFunKind = "";
                botsMsg({ type:"formationStop" });
                const config = currentBotConfig(name);
                if (IS_ELECTRON) {
                    return window.electronBridge.spawnBot(config).catch(() => null);
                } else {
                    const idx=bots.length;
                    const proxy=ProxyManager.getForBot(idx);
                    const iframe=document.createElement("iframe");
                    iframe.src=config.targetUrl || location.href;
                    iframe.width=60; iframe.height=40;
                    botIframes.appendChild(iframe);
                    const win=iframe.contentWindow;
                    win.__is_bot=true;
                    win.channel={ message:()=>{} };
                    win.channel.index    =idx;
                    win.channel.proxyInfo=proxy;
                    win.channel.notifyName = n => {
                        if (!serverClients && !seenPlayerNames.has(n) && !ARRAS_UI_STRINGS.has(n)) {
                            seenPlayerNames.add(n); updateNamesDisplay();
                        }
                    };
                    win.channel.notifyChat = t => {
                        addChatRow(t);
                    };
                const entry = { iframe, proxyInfo:proxy, index:idx, config };
                bots.push(entry);
                applyConfigToIframeBot(entry, config);
                win.channel.autoFireActive = false;
                win.channel.autoSpinActive = false;
                refreshBotList();
                    if (proxy) console.log(`[PR] Bot #${idx+1} → ${proxy.ip}:${proxy.port}`);
                    return entry;
                }
            }
            function createBot() { createBotWithName(nextBotName()); }
            function reindexIframeBots() {
                if (IS_ELECTRON) return;
                bots.forEach((b, i) => {
                    b.index = i;
                    if (b.iframe?.contentWindow?.channel) b.iframe.contentWindow.channel.index = i;
                });
            }
            async function closeOneBot(entry) {
                if (!entry) return;
                if (IS_ELECTRON) {
                    const index = typeof entry === "number" ? entry : entry.index;
                    if (Number.isInteger(index)) await window.electronBridge.closeBot(index).catch(() => {});
                    return;
                }
                const botEntry = entry.bot || entry;
                if (botEntry?.iframe?.parentNode) botEntry.iframe.parentNode.removeChild(botEntry.iframe);
                const idx = bots.indexOf(botEntry);
                if (idx !== -1) bots.splice(idx, 1);
                reindexIframeBots();
                refreshBotList();
                refreshProxyUI();
            }

            let storyBots = [];
            let storyUpdateSeq = 0;
            let storyDebounce = null;
            let storyFormationInterval = null;
            function storyChunks(text) {
                const chunks = [];
                const raw = String(text || "").replace(/\r\n/g, "\n");
                for (let i=0; i<raw.length; i+=24) {
                    const piece = raw.slice(i, i + 24).replace(/\s+/g, " ");
                    chunks.push(piece.trim() ? piece : ".");
                }
                return chunks;
            }
            function storyBotIndex(entry) {
                if (!entry) return undefined;
                if (IS_ELECTRON) return entry.index;
                return bots.indexOf(entry.bot);
            }
            function placeStoryBots() {
                const live = storyBots
                    .map(entry => ({ index: storyBotIndex(entry) }))
                    .filter(entry => Number.isInteger(entry.index) && entry.index >= 0);
                if (!live.length) return;
                if (!player.x || !player.y) events.press("KeyL");
                const cols = Math.min(5, Math.max(1, Math.ceil(Math.sqrt(live.length))));
                const spacingX = 18;
                const spacingY = 14;
                const startX = (player.x || 0) - ((cols - 1) * spacingX) / 2;
                const startY = (player.y || 0) - 18;
                botsMsg({ type:"formation", assignments: live.map((entry, i) => ({
                    index: entry.index,
                    x: startX + (i % cols) * spacingX,
                    y: startY + Math.floor(i / cols) * spacingY,
                    exact: false,
                    tolerance: BOT_FUN_WRITE_TOLERANCE,
                })) });
            }
            function keepPlacingStoryBots() {
                if (storyFormationInterval) clearInterval(storyFormationInterval);
                storyFormationInterval = setInterval(() => {
                    if (storyBots.length) placeStoryBots();
                }, 1500);
            }
            async function spawnStoryBot(name) {
                const config = {
                    ...currentBotConfig(name),
                    botName: name,
                    clan: "",
                    tank: "none",
                    moving: true,
                    targetTeamUrl: getEl("botTeamSelect")?.value.trim() || "",
                    targetPin: getEl("botServerPin")?.value.trim() || "",
                    targetUrl: botTargetUrl(),
                };
                if (IS_ELECTRON) {
                    const result = await window.electronBridge.spawnBot(config).catch(() => null);
                    return result ? { index: result.index, name } : null;
                }
                const bot = createBotWithName(name);
                return bot ? { bot, name } : null;
            }
            async function clearStoryBots() {
                if (storyFormationInterval) clearInterval(storyFormationInterval);
                storyFormationInterval = null;
                const existing = storyBots.slice();
                storyBots = [];
                for (const entry of existing) await closeOneBot(entry);
                botsMsg({ type:"formationStop" });
                botFunMode = false;
                botFunKind = "";
                refreshBotList();
                refreshProxyUI();
            }
            async function updateStoryBotsNow() {
                const seq = ++storyUpdateSeq;
                const text = getEl("storyWriterText")?.value || "";
                const chunks = storyChunks(text);
                const status = getEl("storyWriterStatus");
                const meta = getEl("storyWriterMeta");
                if (meta) meta.textContent = `${chunks.length} chunk${chunks.length!==1?"s":""} · ${text.length} chars`;
                if (status) status.textContent = "Updating...";
                stopFunTools();
                botFunMode = true;
                botFunKind = "story";

                for (let i=chunks.length; i<storyBots.length; i++) {
                    await closeOneBot(storyBots[i]);
                    if (seq !== storyUpdateSeq) return;
                }
                storyBots.length = chunks.length;

                for (let i=0; i<chunks.length; i++) {
                    if (storyBots[i]?.name === chunks[i]) continue;
                    if (storyBots[i]) await closeOneBot(storyBots[i]);
                    if (seq !== storyUpdateSeq) return;
                    const spawned = await spawnStoryBot(chunks[i]);
                    if (seq !== storyUpdateSeq) {
                        await closeOneBot(spawned);
                        return;
                    }
                    storyBots[i] = spawned;
                    await new Promise(r => setTimeout(r, IS_ELECTRON ? 120 : 60));
                }

                if (chunks.length) {
                    placeStoryBots();
                    keepPlacingStoryBots();
                } else {
                    if (storyFormationInterval) clearInterval(storyFormationInterval);
                    storyFormationInterval = null;
                    botFunMode = false;
                    botFunKind = "";
                }
                if (status) status.textContent = chunks.length ? `${chunks.length} story bot${chunks.length!==1?"s":""}` : "Idle";
                refreshBotList();
                refreshProxyUI();
            }
            function scheduleStoryUpdate() {
                clearTimeout(storyDebounce);
                storyDebounce = setTimeout(updateStoryBotsNow, 350);
                const chunks = storyChunks(getEl("storyWriterText")?.value || "");
                const meta = getEl("storyWriterMeta");
                if (meta) meta.textContent = `${chunks.length} chunk${chunks.length!==1?"s":""} · 24 chars per bot`;
            }
            function createCatAIBot() {
                if (IS_ELECTRON) {
                    window.electronBridge.spawnBot({
                        isCatAI: true,
                        clan: "",
                        botName: "Cat AI Bot",
                        tank: "none",
                        disRender: false,
                        moving: botMoving.checked,
                        autoFire: !!getEl("botsAutoFire")?.checked,
                        autoSpin: !!getEl("botsAutoSpin")?.checked,
                        targetTeamUrl: getEl("botTeamSelect")?.value.trim() || "",
                        targetPin: getEl("botServerPin")?.value.trim() || "",
                        targetUrl: botTargetUrl(),
                    });
                } else {
                    const idx=bots.length;
                    const proxy=ProxyManager.getForBot(idx);
                    const iframe=document.createElement("iframe");
                    iframe.src=botTargetUrl();
                    iframe.width=60; iframe.height=40;
                    botIframes.appendChild(iframe);
                    const win=iframe.contentWindow;
                    win.__is_bot=true;
                    win.channel={ message:()=>{} };
                    win.channel.clan="";
                    win.channel.botName="Cat AI Bot";
                    win.channel.disRender=false;
                    win.channel.tank="none";
                    win.channel.moving=botMoving.checked;
                    win.channel.autoFire=!!getEl("botsAutoFire")?.checked;
                    win.channel.autoFireActive=false;
                    win.channel.autoSpin=!!getEl("botsAutoSpin")?.checked;
                    win.channel.autoSpinActive=false;
                    win.channel.isCatAI=true;
                    win.channel.index=idx;
                    win.channel.proxyInfo=proxy;
                    bots.push({ iframe, proxyInfo:proxy, index:idx, config:{ botName:"Cat AI Bot", tank:"none", isCatAI:true } });
                    refreshBotList();
                    if (proxy) console.log(`[PR] Cat AI Bot #${idx+1} → ${proxy.ip}:${proxy.port}`);
                    setTimeout(() => botsMsg({ type:"mouseType", value:mouseType }), 500);
                }
            }

            // ── Subtab switching ──────────────────────────────────────────
            window.switchBotSubtab = function(name) {
                ["normal","other","crashy"].forEach(n => {
                    getEl("botpanel-"+n).hidden = (n !== name);
                    const btn = getEl("subtab-"+n);
                    if (btn) btn.classList.toggle("active", n === name);
                });
            };

            // ── Crashy bots ───────────────────────────────────────────────
            let crashyDisableAllOperations = false;
            function updateCrashyDisableOpsButton() {
                const cb = getEl("crashyDisableOpsCheck");
                if (cb) cb.checked = crashyDisableAllOperations;
            }
            function crashyConfig() {
                const config = currentBotConfig("");
                return {
                    ...config,
                    isCrashy: true,
                    lightweightView: true,
                    disableAllOperations: crashyDisableAllOperations,
                };
            }
            function createCrashyBot() {
                const config = crashyConfig();
                if (IS_ELECTRON) {
                    window.electronBridge.spawnCrashyBots({ count: 1, config });
                } else {
                    const idx = bots.length;
                    const proxy = ProxyManager.getForBot(idx);
                    const iframe = document.createElement("iframe");
                    iframe.src = config.targetUrl || botTargetUrl();
                    iframe.width = 60; iframe.height = 40;
                    botIframes.appendChild(iframe);
                    const win = iframe.contentWindow;
                    win.__is_bot = true;
                    win.channel = {
                        message: () => {},
                        ...config,
                        isCrashy: true,
                        index: idx,
                        proxyInfo: proxy,
                    };
                    bots.push({ iframe, proxyInfo: proxy, index: idx });
                    refreshBotList();
                }
            }
            getEl("spawnCrashy").addEventListener("click", () => {
                const count = parseInt(getEl("crashyCount").value, 10);
                if (isNaN(count) || count < 1) return;
                if (IS_ELECTRON) {
                    window.electronBridge.spawnCrashyBots({ count, config: crashyConfig() });
                } else {
                    for (let i = 0; i < count; i++) createCrashyBot();
                }
            });
            getEl("crashyDisableOpsCheck")?.addEventListener("change", () => {
                crashyDisableAllOperations = getEl("crashyDisableOpsCheck").checked;
                botsMsg({ type:"setDisableAllOperations", value:crashyDisableAllOperations });
            });
            updateCrashyDisableOpsButton();
            getEl("dcBotsCrashy").addEventListener("click", () => getEl("dcBots").click());

            getEl("createBot").addEventListener("click", createBot);
            getEl("createCatAI").addEventListener("click", createCatAIBot);
            getEl("spawnMultiple").addEventListener("click",()=>{
                const count=parseInt(getEl("spawnCount").value,10);
                if (isNaN(count)||count<1) return;
                if (IS_ELECTRON) {
                    for (let i=0; i<count; i++) createBot();
                    return;
                }
                let n=0;
                function next(){ if(n++>=count) return; createBot(); setTimeout(next,60); }
                next();
            });
            getEl("spawnOtherTeam").addEventListener("click", async () => {
                const status = getEl("spawnOtherTeamStatus");
                const btn = getEl("spawnOtherTeam");
                const count = parseInt(getEl("spawnCount").value, 10);
                if (isNaN(count) || count < 1) return;
                if (!IS_ELECTRON) {
                    if (status) status.textContent = "Requires Electron mode.";
                    return;
                }
                botFunMode = false;
                botFunKind = "";
                botsMsg({ type:"formationStop" });
                const config = {
                    ...currentBotConfig(),
                    targetTeamUrl: "",
                    targetUrl: botPinOrCurrentUrl(),
                };
                btn.disabled = true;
                if (status) status.textContent = "Probing up to 4 bots at a time for another team...";
                try {
                    const result = await window.electronBridge.spawnOtherTeamBots({ count, config });
                    if (result?.ok) {
                        if (config.namePattern) {
                            botNameSequence += Math.max(result.count || 0, (result.attempts || 0) + (result.count || 0) - 1);
                        }
                        if (status) status.textContent = `${result.count} bot${result.count === 1 ? "" : "s"} sent to ${result.team}.`;
                        const select = getEl("botTeamSelect");
                        if (select && result.teamUrl) {
                            await window.electronBridge.getTeamUrls?.().then(state => {
                                teamUrlState = state || teamUrlState;
                                refreshTeamUrlUI();
                            }).catch(() => {});
                            select.value = result.teamUrl;
                        }
                    } else if (status) {
                        status.textContent = result?.error || "Could not find another team.";
                    }
                } finally {
                    btn.disabled = false;
                }
            });
            getEl("reconnectBots").addEventListener("click",()=>{
                syncBotConfig();
                if (IS_ELECTRON) {
                    botsMsg({ type:"reconnect" });
                    setTimeout(() => syncBotConfig(), 2000);
                    setTimeout(() => syncBotConfig(), 4500);
                } else {
                    for (const b of bots) b.iframe.contentWindow.channel?.reconnect();
                    setTimeout(() => syncBotConfig(), 750);
                    setTimeout(() => syncBotConfig(), 2500);
                }
            });
            getEl("syncBots").addEventListener("click",()=>{
                botsMsg({ type:"sync" });
            });
            function afkSetSpotNow() {
                if (!isFinitePoint(player.x, player.y)) return;
                afkPos = { x: player.x, y: player.y };
                const el = getEl("afkCoords");
                if (el) el.textContent = `${player.x.toFixed(1)}, ${player.y.toFixed(1)}`;
            }
            getEl("afkSetSpot")?.addEventListener("click", () => {
                if (!isFinitePoint(player.x, player.y)) { events.press("KeyL"); setTimeout(afkSetSpotNow, 150); }
                else afkSetSpotNow();
            });
            getEl("afkToggle")?.addEventListener("change", () => {
                afkEnabled = !!getEl("afkToggle").checked;
                if (!afkEnabled) stopMoving();
            });

            getEl("cleanRefreshArras").addEventListener("click", async ()=>{
                try {
                    if (IS_ELECTRON) await window.electronBridge.closeAllBots();
                    else if (botIframes) botIframes.innerHTML = "";
                } catch {}
                bots = [];
                refreshBotList();
                refreshProxyUI();
                location.replace("https://arras.io/");
            });
            getEl("dcBots").addEventListener("click", async ()=>{
                storyBots = [];
                if (IS_ELECTRON) {
                    bots = [];
                    refreshBotList();
                    refreshProxyUI();
                    await window.electronBridge.closeAllBots();
                } else {
                    botIframes.innerHTML=""; bots=[];
                    refreshBotList(); refreshProxyUI();
                }
            });
            getEl("botClan").addEventListener("input",()=>{
                syncBotConfig();
            });
            getEl("botName").addEventListener("input",()=>{
                syncBotConfig();
            });
            getEl("botNamePattern")?.addEventListener("change",()=>{
                updateNamePatternUI();
                syncBotConfig();
            });
            getEl("botNameTemplate")?.addEventListener("input",()=>{
                syncBotConfig();
            });
            updateNamePatternUI();
            getEl("launchSocketBots")?.addEventListener("click", async ()=>{
                if (!IS_ELECTRON) return alert("Socket bots require Electron mode.");
                const spawnRate = Math.max(0.1, Number(getEl("socketSpawnRate").value)||1);
                const res = await window.electronBridge.launchSocketBots({ spawnRate });
                if (res && res.ok === false) alert(res.error || "Socket bots failed to launch.");
            });
            getEl("stopSocketBots")?.addEventListener("click", async ()=>{
                if (IS_ELECTRON) await window.electronBridge.stopSocketBots();
            });
            // ── Player names ───────────────────────────────────────────────
            updateNamesDisplay = () => {
                const cnt=getEl("namesCount");
                const el=getEl("scannedNames");
                if (cnt) cnt.textContent=`${seenPlayerNames.size} name${seenPlayerNames.size!==1?"s":""}`;
                if (el) el.textContent=seenPlayerNames.size?[...seenPlayerNames].join(", "):"None yet — names collect while you play.";
            };
            getEl("fetchNames").addEventListener("click",()=>{
                commitPendingPlayers();
            });
            getEl("clearNames").addEventListener("click",()=>{
                seenPlayerNames.clear(); updateNamesDisplay();
            });
            getEl("copyNames").addEventListener("click",()=>{
                if (!seenPlayerNames.size) return;
                navigator.clipboard.writeText([...seenPlayerNames].join("\n")).then(()=>{
                    const btn=getEl("copyNames");
                    btn.textContent="✅ Copied";
                    setTimeout(()=>btn.textContent="📋 Copy",1500);
                });
            });
            getEl("spawnPerName").addEventListener("click",()=>{
                const names=[...seenPlayerNames];
                if (!names.length) return;
                if (IS_ELECTRON) {
                    for (const n of names) createBotWithName(n);
                } else {
                    let i=0;
                    function next(){ if(i>=names.length) return; createBotWithName(names[i++]); setTimeout(next,60); }
                    next();
                }
            });
            getEl("disRender").addEventListener("change",()=>{
                syncBotConfig();
            });
            getEl("mouseAimMode").addEventListener("change",()=>{
                mouseType=getEl("mouseAimMode").checked?"aim":"copy";
                syncBotConfig();
            });
            getEl("mouseFollowMode").addEventListener("change",()=>{
                if (getEl("mouseFollowMode").checked) stopFunTools();
            });
            getEl("botsMoving").addEventListener("change",()=>{
                syncBotConfig();
            });
            getEl("botsAutoFire").addEventListener("change",()=>{
                syncBotConfig();
            });
            getEl("botsAutoSpin").addEventListener("change",()=>{
                syncBotConfig();
            });
            getEl("botsRandomMove")?.addEventListener("change",()=>{
                const enabled = getEl("botsRandomMove").checked;
                if (enabled) {
                    stopFunTools();
                    getEl("botsRandomMove").checked = true;
                    botFunMode = true;
                    botFunKind = "randomMove";
                    const tick = () => {
                        const ids = activeBotIndices();
                        if (!ids.length) return;
                        const radius = 18 + Math.min(55, ids.length * 2);
                        botsMsg({ type:"formation", assignments: ids.map(index => ({
                            index,
                            x:(player.x||0) + utils.randint(-radius, radius),
                            y:(player.y||0) + utils.randint(-radius, radius),
                            exact:false,
                            tolerance:2.5,
                        })) });
                    };
                    tick();
                    funRandomInterval = setInterval(tick, 1600);
                } else {
                    if (funRandomInterval) clearInterval(funRandomInterval);
                    funRandomInterval = null;
                    botFunMode = false;
                    botFunKind = "";
                    botsMsg({ type:"formationStop" });
                }
            });
            getEl("randomTanksBtn")?.addEventListener("click",()=>{
                if (funRandomTanksInterval) clearInterval(funRandomTanksInterval);
                const rate = Math.max(1, Math.min(1000, Number(getEl("randomTanksRate")?.value) || 100));
                const ms = Math.round(1000 / rate);
                const allTankKeys = Object.keys(tanks).filter(k => tanks[k].path && !["dreadV2","fastram"].includes(k));
                funRandomTanksInterval = setInterval(() => {
                    const t = allTankKeys[Math.floor(Math.random() * allTankKeys.length)];
                    command("KeyQ");
                    upgradeTank(tanks[t].path);
                }, ms);
            });
            getEl("randomTanksStop")?.addEventListener("click",()=>{
                if (funRandomTanksInterval) clearInterval(funRandomTanksInterval);
                funRandomTanksInterval = null;
            });

            // ── Teleport Zone ──────────────────────────────────────────────
            let tpZoneInterval = null;
            let tpZoneCenter = null;
            let tpZoneTrail = [];

            function tpZoneWorldToCanvas(wx, wy, canvasW, canvasH, radius) {
                const scale = Math.min(canvasW, canvasH) / 2 / (radius * 1.15);
                return {
                    x: canvasW/2 + (wx - tpZoneCenter.x) * scale,
                    y: canvasH/2 + (wy - tpZoneCenter.y) * scale,
                    scale,
                };
            }
            function drawTpZoneMap() {
                const canvas = getEl("tpZoneCanvas");
                if (!canvas) return;
                const ctx2 = canvas.getContext("2d");
                const W = canvas.width, H = canvas.height;
                ctx2.clearRect(0, 0, W, H);
                if (!tpZoneCenter) return;
                const radius = Math.max(1, Number(getEl("tpZoneRadius")?.value) || 50);
                const scale = Math.min(W, H) / 2 / (radius * 1.15);

                // Zone circle
                ctx2.strokeStyle = "rgba(99,102,241,0.65)";
                ctx2.lineWidth = 1.5;
                ctx2.beginPath();
                ctx2.arc(W/2, H/2, radius * scale, 0, Math.PI*2);
                ctx2.stroke();

                // Center crosshair
                ctx2.strokeStyle = "rgba(255,255,255,0.15)";
                ctx2.lineWidth = 0.8;
                ctx2.beginPath(); ctx2.moveTo(W/2-6,H/2); ctx2.lineTo(W/2+6,H/2); ctx2.stroke();
                ctx2.beginPath(); ctx2.moveTo(W/2,H/2-6); ctx2.lineTo(W/2,H/2+6); ctx2.stroke();

                // Trail dots (fade older ones)
                tpZoneTrail.forEach((pt, i) => {
                    const alpha = 0.2 + 0.6 * (i / tpZoneTrail.length);
                    ctx2.fillStyle = `rgba(99,102,241,${alpha})`;
                    ctx2.beginPath();
                    ctx2.arc(W/2 + (pt.x-tpZoneCenter.x)*scale, H/2 + (pt.y-tpZoneCenter.y)*scale, 2, 0, Math.PI*2);
                    ctx2.fill();
                });

                // Player position
                if (isFinitePoint(player.x, player.y)) {
                    const px = W/2 + (player.x - tpZoneCenter.x) * scale;
                    const py = H/2 + (player.y - tpZoneCenter.y) * scale;
                    ctx2.fillStyle = "#86efac";
                    ctx2.beginPath();
                    ctx2.arc(px, py, 3.5, 0, Math.PI*2);
                    ctx2.fill();
                }
            }
            function tpZoneSetCenter() {
                tpZoneCenter = { x: player.x || 0, y: player.y || 0 };
                const el = getEl("tpZoneCenterLabel");
                if (el) el.textContent = `${Math.round(tpZoneCenter.x)}, ${Math.round(tpZoneCenter.y)}`;
                drawTpZoneMap();
            }
            getEl("tpZoneSetCenter")?.addEventListener("click", () => {
                if (!isFinitePoint(player.x, player.y)) { events.press("KeyL"); setTimeout(tpZoneSetCenter, 120); }
                else tpZoneSetCenter();
            });
            getEl("tpZoneRadius")?.addEventListener("input", drawTpZoneMap);
            getEl("tpZoneStart")?.addEventListener("click", () => {
                if (tpZoneInterval) clearInterval(tpZoneInterval);
                if (!tpZoneCenter) tpZoneSetCenter();
                const rate = Math.max(0.1, Math.min(30, Number(getEl("tpZoneRate")?.value) || 2));
                const ms = Math.round(1000 / rate);
                tpZoneInterval = setInterval(() => {
                    const radius = Math.max(1, Number(getEl("tpZoneRadius")?.value) || 50);
                    // Uniform random point inside circle
                    const angle = Math.random() * Math.PI * 2;
                    const r = radius * Math.sqrt(Math.random());
                    const wx = tpZoneCenter.x + Math.cos(angle) * r;
                    const wy = tpZoneCenter.y + Math.sin(angle) * r;
                    // World → screen
                    const sx = ((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2;
                    const sy = ((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2;
                    events.moveTo(utils.clamp(sx, 0, innerWidth), utils.clamp(sy, 0, innerHeight));
                    command("KeyE");
                    tpZoneTrail.push({ x: wx, y: wy });
                    if (tpZoneTrail.length > 40) tpZoneTrail.shift();
                    drawTpZoneMap();
                }, ms);
            });
            getEl("tpZoneStop")?.addEventListener("click", () => {
                if (tpZoneInterval) clearInterval(tpZoneInterval);
                tpZoneInterval = null;
            });
            getEl("tpZoneClear")?.addEventListener("click", () => {
                tpZoneTrail = [];
                drawTpZoneMap();
            });

            // ── Entity Painter ─────────────────────────────────────────────
            let epAbortText = false, epAbortDraw = false;
            let epMode = "text";
            let epDrawnPts = []; // { dx, dy } offsets from canvas center in canvas pixels
            let epIsDrawing = false;

            window.epSwitchTab = function(name) {
                epMode = name;
                ["text","draw"].forEach(n => {
                    const panel = getEl("epPanel" + n[0].toUpperCase() + n.slice(1));
                    if (panel) panel.hidden = (n !== name);
                    const btn = getEl("epTab" + n[0].toUpperCase() + n.slice(1));
                    if (btn) btn.classList.toggle("active", n === name);
                });
                epRedrawCanvas();
            };

            // Render char to offscreen canvas, downsample to gw×gh grid, cache by "ch_GWxGH"
            const epGlyphCache = {};
            function epDetectGlyph(ch, gw = 5, gh = 7) {
                if (gw === 5 && gh === 7 && tinyFont[ch]) return tinyFont[ch];
                const key = `${ch}_${gw}x${gh}`;
                if (epGlyphCache[key]) return epGlyphCache[key];
                const samp = 8;
                const TW = gw * samp, TH = gh * samp;
                const tmp = document.createElement("canvas");
                tmp.width = TW; tmp.height = TH;
                const tc = tmp.getContext("2d");
                tc.fillStyle = "#000";
                tc.fillRect(0, 0, TW, TH);
                tc.fillStyle = "#fff";
                tc.font = `bold ${Math.round(TH * 0.85)}px monospace`;
                tc.textAlign = "center";
                tc.textBaseline = "middle";
                tc.fillText(ch, TW / 2, TH / 2);
                const img = tc.getImageData(0, 0, TW, TH).data;
                const rows = [];
                for (let gy = 0; gy < gh; gy++) {
                    let row = "";
                    for (let gx = 0; gx < gw; gx++) {
                        let sum = 0;
                        for (let sy = 0; sy < samp; sy++)
                            for (let sx = 0; sx < samp; sx++)
                                sum += img[((gy * samp + sy) * TW + gx * samp + sx) * 4];
                        row += sum / (samp * samp) > 40 ? "1" : "0";
                    }
                    rows.push(row);
                }
                epGlyphCache[key] = rows;
                return rows;
            }

            // Grid resolution from scale input: larger scale → finer grid → more emoji detail
            function epGlyphRes(inputVal) {
                const v = Math.max(0.1, inputVal);
                const gw = Math.min(30, Math.max(5, Math.round(v)));
                const gh = Math.min(42, Math.max(7, Math.round(v * 1.4)));
                return { gw, gh };
            }

            // Returns centered { dx, dy } offsets for given text+scale
            function epTextOffsets(text, scale) {
                const inputVal = Math.max(0.1, Number(getEl("epTextScale")?.value) || 5);
                const { gw, gh } = epGlyphRes(inputVal);
                text = (text || "").toUpperCase().slice(0, 18);
                const pts = [];
                let cursor = 0;
                for (const ch of text) {
                    if (ch === " ") { cursor += Math.round(gw * 0.7); continue; }
                    const glyph = epDetectGlyph(ch, gw, gh);
                    for (let gy = 0; gy < glyph.length; gy++) {
                        for (let gx = 0; gx < glyph[gy].length; gx++) {
                            if (glyph[gy][gx] === "1") pts.push({ dx:(cursor+gx)*scale, dy:gy*scale });
                        }
                    }
                    cursor += gw + 1;
                }
                if (!pts.length) return [];
                const cx = (Math.min(...pts.map(p=>p.dx)) + Math.max(...pts.map(p=>p.dx))) / 2;
                const cy = (Math.min(...pts.map(p=>p.dy)) + Math.max(...pts.map(p=>p.dy))) / 2;
                return pts.map(p => ({ dx: p.dx - cx, dy: p.dy - cy }));
            }

            // Redraw the preview canvas (text mode only — draw mode paints directly)
            function epRedrawCanvas() {
                const cv = getEl("epCanvas");
                if (!cv) return;
                const c = cv.getContext("2d");
                const W = cv.width, H = cv.height;
                c.clearRect(0, 0, W, H);
                if (epMode === "text") {
                    const scale = (Math.max(0.1, Number(getEl("epTextScale")?.value) || 5)) / 5;
                    const offs = epTextOffsets(getEl("epText")?.value || "", scale);
                    c.fillStyle = "#fff";
                    for (const { dx, dy } of offs) {
                        c.fillRect(W/2 + dx - Math.max(0.5, scale/2), H/2 + dy - Math.max(0.5, scale/2), Math.max(1, scale), Math.max(1, scale));
                    }
                } else {
                    // Redraw tracked draw points (used after Clear)
                    c.fillStyle = "#fff";
                    for (const { dx, dy } of epDrawnPts) {
                        c.fillRect(W/2 + dx - 1, H/2 + dy - 1, 2, 2);
                    }
                }
            }

            // Spawn helper: takes array of {dx,dy} world offsets, returns spawned {wx,wy} positions
            async function epSpawnOffsets(offsets, delay, abortKey) {
                const status = getEl("epStatus");
                const startX = player.x || 0, startY = player.y || 0;
                const spawned = [];
                for (let i = 0; i < offsets.length; i++) {
                    if (abortKey()) break;
                    const wx = startX + offsets[i].dx;
                    const wy = startY + offsets[i].dy;
                    spawned.push({ wx, wy });
                    const sx = ((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2;
                    const sy = ((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2;
                    events.moveTo(utils.clamp(sx, 0, innerWidth), utils.clamp(sy, 0, innerHeight));
                    command("KeyC", "KeyH");
                    if (status) status.textContent = `Spawning ${i+1}/${offsets.length}…`;
                    await new Promise(r => setTimeout(r, delay));
                }
                return spawned;
            }

            // Joint chain: connect each adjacent pair in positions[]
            function epWorldToScreen(wx, wy) {
                return {
                    x: utils.clamp(((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2, 2, innerWidth-2),
                    y: utils.clamp(((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2, 2, innerHeight-2),
                };
            }
            async function epJointChain(positions, delay, abortKey) {
                const status = getEl("epStatus");
                for (let i = 1; i < positions.length; i++) {
                    if (abortKey()) break;
                    if (status) status.textContent = `Jointing ${i}/${positions.length-1}…`;
                    const sp = epWorldToScreen(positions[i-1].wx, positions[i-1].wy);
                    const sc = epWorldToScreen(positions[i].wx,   positions[i].wy);
                    events.moveTo(sp.x, sp.y);
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.4)));
                    events.keyDown("Backquote");
                    events.keyDown("KeyJ");
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.6)));
                    events.moveTo(sc.x, sc.y);
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.6)));
                    events.keyUp("KeyJ");
                    events.keyUp("Backquote");
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.8)));
                }
            }

            // Text spawn
            getEl("epSpawnText")?.addEventListener("click", async () => {
                const text = getEl("epText")?.value || "";
                const scale = (Math.max(0.1, Number(getEl("epTextScale")?.value) || 5)) / 5;
                const offs = epTextOffsets(text, scale);
                const status = getEl("epStatus");
                if (!offs.length) { if (status) status.textContent = "No text entered."; return; }
                epAbortText = false;
                const delay = Math.max(10, Number(getEl("epTextDelay")?.value) || 60);
                const spawned = await epSpawnOffsets(offs, delay, () => epAbortText);
                if (!epAbortText && getEl("epJoint")?.checked && spawned.length >= 2)
                    await epJointChain(spawned, delay, () => epAbortText);
                if (status) status.textContent = epAbortText ? "Stopped." : `Done — ${spawned.length} entities.`;
                epAbortText = false;
            });
            getEl("epStopText")?.addEventListener("click", () => {
                epAbortText = true;
                events.keyUp("KeyJ"); events.keyUp("Backquote");
            });

            // Draw spawn
            getEl("epSpawnDraw")?.addEventListener("click", async () => {
                if (!epDrawnPts.length) { const s = getEl("epStatus"); if (s) s.textContent = "Nothing drawn."; return; }
                const worldScale = Math.max(0.5, Number(getEl("epDrawScale")?.value) || 3);
                const offs = epDrawnPts.map(p => ({ dx: p.dx * worldScale, dy: p.dy * worldScale }));
                const status = getEl("epStatus");
                epAbortDraw = false;
                const delay = Math.max(10, Number(getEl("epDrawDelay")?.value) || 60);
                const spawned = await epSpawnOffsets(offs, delay, () => epAbortDraw);
                if (!epAbortDraw && getEl("epJoint")?.checked && spawned.length >= 2)
                    await epJointChain(spawned, delay, () => epAbortDraw);
                if (status) status.textContent = epAbortDraw ? "Stopped." : `Done — ${spawned.length} entities.`;
                epAbortDraw = false;
            });
            getEl("epStopDraw")?.addEventListener("click", () => {
                epAbortDraw = true;
                events.keyUp("KeyJ"); events.keyUp("Backquote");
            });

            // Canvas draw interaction — use document-level listeners so mouseup outside canvas still works
            {
                const cv = getEl("epCanvas");
                if (cv) {
                    const onDown = e => {
                        if (epMode !== "draw") return;
                        e.preventDefault(); e.stopPropagation();
                        epIsDrawing = true;
                        addEpPoint(e, cv);
                    };
                    const onMove = e => {
                        if (!epIsDrawing || epMode !== "draw") return;
                        e.preventDefault(); e.stopPropagation();
                        addEpPoint(e, cv);
                    };
                    const onUp = () => { epIsDrawing = false; };
                    cv.addEventListener("mousedown", onDown, true);
                    cv.addEventListener("mousemove", onMove, true);
                    document.addEventListener("mouseup", onUp);
                }
            }
            function addEpPoint(e, cv) {
                const rect = cv.getBoundingClientRect();
                const px = e.clientX - rect.left;
                const py = e.clientY - rect.top;
                const dx = px - cv.width/2;
                const dy = py - cv.height/2;
                const r = Math.max(1, Number(getEl("epBrush")?.value) || 4);
                const step = Math.max(1, Math.round(r * 0.75));
                const c = cv.getContext("2d");
                c.fillStyle = "#fff";
                for (let oy = -r; oy <= r; oy += step) {
                    for (let ox = -r; ox <= r; ox += step) {
                        if (ox*ox + oy*oy <= r*r) {
                            epDrawnPts.push({ dx: dx + ox, dy: dy + oy });
                            c.fillRect(px + ox - 1, py + oy - 1, 2, 2);
                        }
                    }
                }
            }

            getEl("epClearDraw")?.addEventListener("click", () => { epDrawnPts = []; epRedrawCanvas(); });
            getEl("epText")?.addEventListener("input", () => { if (epMode === "text") epRedrawCanvas(); });
            getEl("epTextScale")?.addEventListener("input", () => { if (epMode === "text") epRedrawCanvas(); });

            epRedrawCanvas();

            // ── Snaaakee!!! ────────────────────────────────────────────────
            let snakeAbort = false;
            function snakeToScreen(wx, wy) {
                return {
                    x: utils.clamp(((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2, 2, innerWidth - 2),
                    y: utils.clamp(((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2, 2, innerHeight - 2),
                };
            }
            getEl("snakeGo")?.addEventListener("click", async () => {
                snakeAbort = false;
                const count   = Math.max(2, Math.min(120, Number(getEl("snakeCount")?.value)   || 20));
                const spacing = Math.max(0.1, Number(getEl("snakeSpacing")?.value) || 0.5);
                const delay   = Math.max(10, Number(getEl("snakeDelay")?.value) || 80);
                const status  = getEl("snakeStatus");
                const px = player.x || 0, py = player.y || 0;
                const fov = player.fov || 1;
                const visHW = 23.09 * fov * 0.88;

                // Build horizontal line of positions, capped to visible area
                const positions = [];
                const totalW = (count - 1) * spacing;
                for (let i = 0; i < count; i++) {
                    const wx = px - totalW / 2 + i * spacing;
                    if (Math.abs(wx - px) > visHW) break;
                    positions.push({ wx, wy: py - 3 });
                }
                if (positions.length < 2) {
                    if (status) status.textContent = "Too wide for screen — reduce count or spacing.";
                    return;
                }

                // Spawn first dot
                const s0 = snakeToScreen(positions[0].wx, positions[0].wy);
                events.moveTo(s0.x, s0.y);
                command("KeyC", "KeyH");
                await new Promise(r => setTimeout(r, delay));

                for (let i = 1; i < positions.length; i++) {
                    if (snakeAbort) break;

                    // Spawn next dot
                    if (status) status.textContent = `Spawning ${i+1}/${positions.length}…`;
                    const si    = snakeToScreen(positions[i].wx,   positions[i].wy);
                    const sprev = snakeToScreen(positions[i-1].wx, positions[i-1].wy);
                    events.moveTo(si.x, si.y);
                    command("KeyC", "KeyH");
                    await new Promise(r => setTimeout(r, delay));
                    if (snakeAbort) break;

                    // Joint previous ↔ current: hold backtick+J on prev, release on curr
                    if (status) status.textContent = `Jointing ${i}↔${i+1}…`;
                    events.moveTo(sprev.x, sprev.y);
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.4)));
                    events.keyDown("Backquote");
                    events.keyDown("KeyJ");
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.6)));
                    events.moveTo(si.x, si.y);
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.6)));
                    events.keyUp("KeyJ");
                    events.keyUp("Backquote");
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.8)));
                }

                if (status) status.textContent = snakeAbort
                    ? "Stopped." : `🐍 Done — ${positions.length} dots jointed.`;
                snakeAbort = false;
            });
            getEl("snakeStop")?.addEventListener("click", () => {
                snakeAbort = true;
                events.keyUp("KeyJ");
                events.keyUp("Backquote");
            });

            // ── Image Painter ──────────────────────────────────────────────
            let imgAbort = false;
            let imgSampled = null; // { w, h, data: Uint8ClampedArray }

            // Arras sandbox backtick+C color palette: angle (degrees) → RGB
            // Angle formula: rawRad = (deg - 90) * π/180 → aimX/Y = entityPos + cos/sin(rawRad)*80
            const IMG_PALETTE = [
                { deg:5,   r:30,  g:100, b:255 }, // blue
                { deg:25,  r:0,   g:200, b:50  }, // green
                { deg:45,  r:255, g:30,  b:30  }, // red
                { deg:65,  r:150, g:0,   b:200 }, // purple
                { deg:80,  r:255, g:255, b:0   }, // bright yellow
                { deg:95,  r:105, g:105, b:105 }, // grey #696969
                { deg:115, r:255, g:255, b:255 }, // white
                { deg:135, r:255, g:210, b:0   }, // yellow
                { deg:155, r:255, g:140, b:0   }, // orange
                { deg:170, r:80,  g:0,   b:140 }, // indigo
                { deg:185, r:0,   g:128, b:128 }, // teal
                { deg:205, r:255, g:100, b:180 }, // pink
                { deg:225, r:100, g:255, b:0   }, // lime
                { deg:245, r:0,   g:240, b:240 }, // aqua
                { deg:275, r:85,  g:85,  b:85  }, // dark grey
                { deg:315, r:255, g:243, b:218 }, // warm white
                { deg:335, r:245, g:245, b:245 }, // near-white
                { deg:350, r:15,  g:15,  b:15  }, // black
            ];
            function imgClosestDeg(r, g, b) {
                let best = IMG_PALETTE[0], bestDist = Infinity;
                for (const p of IMG_PALETTE) {
                    const d = (r-p.r)**2 + (g-p.g)**2 + (b-p.b)**2;
                    if (d < bestDist) { bestDist = d; best = p; }
                }
                return best.deg;
            }

            function imgUpdatePreview() {
                const cv = getEl("imgCanvas");
                if (!cv || !imgSampled) return;
                const c = cv.getContext("2d");
                const W = cv.width, H = cv.height;
                c.clearRect(0, 0, W, H);
                const { w, h, data } = imgSampled;
                const scale = Math.min(W / w, H / h);
                const dw = w * scale, dh = h * scale;
                const dx = (W - dw) / 2, dy = (H - dh) / 2;
                // Draw sampled image into preview
                const tmp = document.createElement("canvas");
                tmp.width = w; tmp.height = h;
                const tc = tmp.getContext("2d");
                const id = tc.createImageData(w, h);
                id.data.set(data);
                tc.putImageData(id, 0, 0);
                c.imageSmoothingEnabled = false;
                c.drawImage(tmp, dx, dy, dw, dh);
                // Estimate dot count
                const maxDots = Math.max(1, Number(getEl("imgMaxDots")?.value) || 4);
                let total = 0;
                for (let i = 0; i < data.length; i += 4) {
                    const lum = (0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2]) * (data[i+3]/255);
                    total += Math.round(lum / 255 * maxDots);
                }
                const delay = Math.max(10, Number(getEl("imgDelay")?.value) || 50);
                const secs = Math.round(total * delay / 1000);
                const status = getEl("imgStatus");
                if (status) status.textContent = `~${total.toLocaleString()} dots · ~${secs}s at ${delay}ms delay`;
            }

            getEl("imgFile")?.addEventListener("change", e => {
                const file = e.target.files[0];
                if (!file) return;
                const img = new Image();
                img.onload = () => {
                    const res = Math.max(5, Math.min(120, Number(getEl("imgRes")?.value) || 40));
                    const scale = Math.min(1, res / Math.max(img.width, img.height));
                    const tw = Math.max(1, Math.round(img.width * scale));
                    const th = Math.max(1, Math.round(img.height * scale));
                    const tmp = document.createElement("canvas");
                    tmp.width = tw; tmp.height = th;
                    const tc = tmp.getContext("2d");
                    tc.drawImage(img, 0, 0, tw, th);
                    const id = tc.getImageData(0, 0, tw, th);
                    imgSampled = { w: tw, h: th, data: id.data.slice() };
                    URL.revokeObjectURL(img.src);
                    imgUpdatePreview();
                };
                img.src = URL.createObjectURL(file);
            });
            getEl("imgRes")?.addEventListener("change", () => {
                // Re-sample at new resolution if file is still selected
                const fileInput = getEl("imgFile");
                if (fileInput?.files[0]) fileInput.dispatchEvent(new Event("change"));
            });
            getEl("imgMaxDots")?.addEventListener("input", imgUpdatePreview);
            getEl("imgDelay")?.addEventListener("input", imgUpdatePreview);

            getEl("imgSpawn")?.addEventListener("click", async () => {
                if (!imgSampled) { const s = getEl("imgStatus"); if (s) s.textContent = "No image loaded."; return; }
                const worldW   = Math.max(1, Number(getEl("imgWorldW")?.value) || 20);
                const maxDots  = Math.max(1, Math.min(12, Number(getEl("imgMaxDots")?.value) || 4));
                const delay    = Math.max(10, Number(getEl("imgDelay")?.value) || 80);
                const doColor  = !!getEl("imgColor")?.checked;
                const status   = getEl("imgStatus");
                const { w: iw, h: ih, data } = imgSampled;
                const worldH   = worldW * ih / iw;
                const startX   = player.x || 0, startY = player.y || 0;

                // Build queue: each entry has world pos + color angle
                const queue = [];
                for (let py = 0; py < ih; py++) {
                    for (let px = 0; px < iw; px++) {
                        const idx = (py * iw + px) * 4;
                        const r = data[idx], g = data[idx+1], b = data[idx+2], a = data[idx+3];
                        if (a < 20) continue;
                        const n = 1;
                        const colorDeg = doColor ? imgClosestDeg(r, g, b) : null;
                        const wx = startX - worldW/2 + (px + 0.5) / iw * worldW;
                        const wy = startY - worldH/2 + (py + 0.5) / ih * worldH;
                        for (let k = 0; k < n; k++) queue.push({ wx, wy, colorDeg });
                    }
                }
                if (!queue.length) { if (status) status.textContent = "Image too dark — nothing to spawn."; return; }

                imgAbort = false;
                for (let i = 0; i < queue.length; i++) {
                    if (imgAbort) break;
                    const { wx, wy, colorDeg } = queue[i];
                    const sx = utils.clamp(((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2, 2, innerWidth-2);
                    const sy = utils.clamp(((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2, 2, innerHeight-2);

                    // Spawn entity
                    events.moveTo(sx, sy);
                    command("KeyC", "KeyH");
                    await new Promise(r => setTimeout(r, Math.round(delay * 0.7)));
                    if (imgAbort) break;

                    // Color entity
                    if (colorDeg !== null) {
                        events.moveTo(sx, sy);
                        await new Promise(r => setTimeout(r, Math.round(delay * 0.2)));
                        events.keyDown("Backquote"); events.keyDown("KeyC");
                        await new Promise(r => setTimeout(r, Math.round(delay * 0.3)));
                        const rawRad = (colorDeg - 90) * Math.PI / 180;
                        events.moveTo(
                            utils.clamp(sx + Math.cos(rawRad) * 80, 2, innerWidth-2),
                            utils.clamp(sy + Math.sin(rawRad) * 80, 2, innerHeight-2)
                        );
                        await new Promise(r => setTimeout(r, Math.round(delay * 0.3)));
                        events.keyUp("KeyC"); events.keyUp("Backquote");
                        await new Promise(r => setTimeout(r, Math.round(delay * 0.2)));
                    }

                    if (i % 10 === 0 && status) status.textContent = `Spawning ${i+1}/${queue.length}…`;
                }
                if (status) status.textContent = imgAbort ? "Stopped." : `Done — ${queue.length} entities.`;
                imgAbort = false;
            });
            getEl("imgStop")?.addEventListener("click", () => { imgAbort = true; });

            fedingCheckbox.addEventListener("change",()=>{
                if (IS_ELECTRON) {
                    botsMsg({ type:"feeed", value:fedingCheckbox.checked });
                } else {
                    for (const b of bots) b.iframe.contentWindow.channel?.feeed(fedingCheckbox.checked);
                }
            });
            function startWriteFormation(relOffsets) {
                if (!player.x || !player.y) events.press("KeyL");
                if (funWriteInterval) { clearInterval(funWriteInterval); funWriteInterval = null; }
                botFunMode = true;
                botFunKind = "write";
                const ids = activeBotIndices();
                if (!ids.length || !relOffsets.length) return;
                // Send formation EVERY 100ms, just like AFK calls pathfind every 100ms
                const doSend = () => {
                    const px = player.x || 0;
                    const py = player.y || 0;
                    const assignments = ids.map((index, i) => {
                        const offset = relOffsets[i % relOffsets.length];
                        return {
                            index,
                            x: px + offset.x,
                            y: py + offset.y,
                            exact: true,
                            tolerance: 0.5,
                        };
                    });
                    botsMsg({ type:"formation", assignments });
                };
                doSend();
                funWriteInterval = setInterval(doSend, 100);
            }
            getEl("botWriteWords").addEventListener("click",()=>{
                const scale = Math.max(1, Math.min(12, Number(getEl("botWriteScale").value)||3));
                const pts = makeWordPoints(getEl("botWriteText").value || "OP CAT", scale);
                startWriteFormation(pts);
            });
            ["arrowUp","arrowDown","arrowLeft","arrowRight","circle","square","X","plus","star"].forEach(sym => {
                getEl("sym"+sym.charAt(0).toUpperCase()+sym.slice(1))?.addEventListener("click", () => {
                    const scale = Math.max(1, Math.min(12, Number(getEl("botWriteScale").value)||3));
                    const pts = makeSymbolPoints(sym, scale);
                    startWriteFormation(pts);
                });
            });
            getEl("openStoryWriter").addEventListener("click",()=>{
                const win = getEl("storyWriterWindow");
                if (!win) return;
                win.style.display = win.style.display === "flex" ? "none" : "flex";
                if (win.style.display === "flex") getEl("storyWriterText")?.focus();
            });
            getEl("closeStoryWriter").addEventListener("click",()=>{
                const win = getEl("storyWriterWindow");
                if (win) win.style.display = "none";
            });
            getEl("storyWriterText").addEventListener("input", scheduleStoryUpdate);
            getEl("clearStoryWriter").addEventListener("click", async ()=>{
                getEl("storyWriterText").value = "";
                ++storyUpdateSeq;
                clearTimeout(storyDebounce);
                await clearStoryBots();
                const meta = getEl("storyWriterMeta");
                const status = getEl("storyWriterStatus");
                if (meta) meta.textContent = "0 chunks · 24 chars per bot";
                if (status) status.textContent = "Idle";
            });
            getEl("botStopFun").addEventListener("click", async ()=>{
                ++storyUpdateSeq;
                clearTimeout(storyDebounce);
                await clearStoryBots();
                stopFunTools();
            });

            // ── Feeding ────────────────────────────────────────────────────
            window.fedPath=[];
            const fedInfo=getEl("fedPathInfo");
            getEl("addFedLocation").addEventListener("click",()=>{
                const perf=()=>{
                    window.fedPath.push({ x:player.x, y:player.y });
                    if (IS_ELECTRON) {
                        botsMsg({ type:"addFedWaypoint", x:player.x, y:player.y });
                    } else {
                        for (const b of bots) b.iframe.contentWindow.fedPath?.push({ x:player.x, y:player.y });
                    }
                    if (fedInfo) fedInfo.textContent=`Path: ${window.fedPath.length} waypoints`;
                };
                if (player.x && player.y) perf();
                else { events.press("KeyL"); setTimeout(perf,100); }
            });
            getEl("resetFedPath").addEventListener("click",()=>{
                window.fedPath=[]; currentFedPath=[];
                if (IS_ELECTRON) {
                    botsMsg({ type:"fedReset" });
                } else {
                    for (const b of bots) b.iframe.contentWindow.channel?.feeedRESET();
                }
                if (fedInfo) fedInfo.textContent="Path: 0 waypoints";
            });


            // ── Other ──────────────────────────────────────────────────────
            getEl("setSpawn").addEventListener("click",()=>{
                if (!player.x) { alert("Press L first to register your coordinates."); return; }
                chat(`$arena spawnpoint ${player.x} ${player.y}`);
            });
            getEl("pibuild").addEventListener("click",()=>{
                upgradeStats(Math.PI.toString().replace(".","").split("").slice(0,10).join("/"));
            });
            let _wasmMem;
            getEl("test_wasm").addEventListener("click",()=>{
                if (!_wasmMem) {
                    _wasmMem=new Uint32Array(HEAPU32);
                    const wo=getEl("wasmOut"); if(wo) wo.textContent="Snapshot taken ("+_wasmMem.length+" u32s)";
                    return;
                }
                const cur=HEAPU32;
                const same=getEl("wasmcringe")?.checked;
                const out=[];
                for (let i=0; i<Math.min(_wasmMem.length,cur.length); i++)
                    if (same?_wasmMem[i]===cur[i]:_wasmMem[i]!==cur[i]) out.push(cur[i]);
                _wasmMem=new Uint32Array(cur);
                const wo=getEl("wasmOut"); if(wo) wo.textContent=out.slice(0,200).join(", ");
            });
            window._t=[]; window._t2=Date.now();

            // ── Kick Packet Capture ────────────────────────────────────────
            let wsCaptureLogs = [];

            function wsUpdateCaptureDisplay() {
                const el = getEl("wsCaptureLog");
                if (!el) return;
                if (!wsCaptureLogs.length) { el.textContent = "No packets yet."; return; }
                el.innerHTML = wsCaptureLogs.map((entry, i) => {
                    const hex = entry.bytes.map(b => b.toString(16).padStart(2,"0")).join(" ");
                    const arr = "[" + entry.bytes.join(", ") + "]";
                    const col = entry.dir === "send" ? "#86efac" : "#a5b4fc";
                    return `<div style="margin-bottom:3px;padding-bottom:3px;border-bottom:1px solid rgba(255,255,255,0.06);">` +
                        `<span style="opacity:0.45;">#${i+1} ${entry.dir} ${entry.bytes.length}B</span> ` +
                        `<span style="color:${col};">${hex}</span><br>` +
                        `<span style="opacity:0.35;">${arr}</span></div>`;
                }).join("");
                el.scrollTop = el.scrollHeight;
            }

            function wsStartCapture(status) {
                wsCaptureLogs = [];
                wsUpdateCaptureDisplay();
                if (status) status.textContent = "Capturing…";
                window.__wsCaptureCallback = (bytes, dir) => {
                    wsCaptureLogs.push({ dir, bytes: Array.from(bytes), time: Date.now() });
                    wsUpdateCaptureDisplay();
                    const s = getEl("wsCaptureStatus");
                    if (s) s.textContent = `Capturing… ${wsCaptureLogs.length} packet(s)`;
                };
            }

            function wsStopCapture() {
                window.__wsCaptureCallback = null;
                const s = getEl("wsCaptureStatus");
                if (s) s.textContent = `Stopped. ${wsCaptureLogs.length} packet(s) captured.`;
            }

            getEl("wsCaptureStart")?.addEventListener("click", () => {
                wsStartCapture(getEl("wsCaptureStatus"));
            });
            getEl("wsCaptureStop")?.addEventListener("click", wsStopCapture);
            getEl("wsClearCapture")?.addEventListener("click", () => {
                wsCaptureLogs = [];
                window.__wsCaptureCallback = null;
                wsUpdateCaptureDisplay();
                const s = getEl("wsCaptureStatus");
                if (s) s.textContent = "Cleared.";
            });
            getEl("wsCopyCaptured")?.addEventListener("click", () => {
                if (!wsCaptureLogs.length) return;
                const text = wsCaptureLogs.map((e, i) =>
                    `#${i+1} ${e.dir} (${e.bytes.length}B): ${e.bytes.map(b=>b.toString(16).padStart(2,"0")).join(" ")}\n  bytes: [${e.bytes.join(", ")}]`
                ).join("\n");
                navigator.clipboard.writeText(text).then(() => {
                    const btn = getEl("wsCopyCaptured");
                    if (btn) { btn.textContent = "✅ Copied"; setTimeout(() => btn.textContent = "📋 Copy log", 1500); }
                }).catch(() => {});
            });

            getEl("wsAutoKickTest")?.addEventListener("click", async () => {
                const status = getEl("wsCaptureStatus");
                // Start capturing then wait for user to click a kick button manually
                wsStartCapture(status);
                if (status) status.textContent = "Capturing — click a Kick button in the player menu now. Auto-stops in 5s.";
                await new Promise(r => setTimeout(r, 5000));
                if (window.__wsCaptureCallback) wsStopCapture();
            });

            wsUpdateCaptureDisplay();

            // ── Color Angle Tester ─────────────────────────────────────────
            let ctAbort = false;
            let ctSavedColors = [];
            let ctResults = []; // { step, angleDeg, color:{hex,name,r,g,b} }

            function ctWorldToScreen(wx, wy) {
                return {
                    x: utils.clamp(((wx - player.x) / (23.09 * player.fov)) * (innerWidth/2) + innerWidth/2, 2, innerWidth-2),
                    y: utils.clamp(((wy - player.y) / (12.98 * player.fov)) * (innerHeight/2) + innerHeight/2, 2, innerHeight-2),
                };
            }

            // Manual color annotation — no canvas pixel reading needed
            function ctDetectColor() { return Promise.resolve(null); }

            // Run a single step: spawn 4 entities + color each, then detect
            async function ctRunStep(i, steps, radius, delay) {
                const entityAngle = (i / steps) * Math.PI * 2 - Math.PI/2;
                const angleDeg = Math.round(((entityAngle + Math.PI/2) * 180/Math.PI + 360) % 360);
                const wx = (player.x||0) + Math.cos(entityAngle) * radius;
                const wy = (player.y||0) + Math.sin(entityAngle) * radius;
                const se = ctWorldToScreen(wx, wy);
                const aimX = utils.clamp(se.x + Math.cos(entityAngle)*80, 2, innerWidth-2);
                const aimY = utils.clamp(se.y + Math.sin(entityAngle)*80, 2, innerHeight-2);

                for (let k = 0; k < 1; k++) {
                    if (ctAbort) break;
                    // Spawn
                    events.moveTo(se.x, se.y);
                    command("KeyC", "KeyH");
                    await new Promise(r => setTimeout(r, delay));
                    if (ctAbort) break;
                    // Color
                    events.moveTo(se.x, se.y);
                    await new Promise(r => setTimeout(r, Math.round(delay*0.3)));
                    events.keyDown("Backquote"); events.keyDown("KeyC");
                    await new Promise(r => setTimeout(r, Math.round(delay*0.4)));
                    events.moveTo(aimX, aimY);
                    await new Promise(r => setTimeout(r, Math.round(delay*0.4)));
                    events.keyUp("KeyC"); events.keyUp("Backquote");
                    await new Promise(r => setTimeout(r, Math.round(delay*0.2)));
                }

                // Wait for the game to render the entity, then read inside the next rAF
                await new Promise(r => setTimeout(r, Math.max(300, delay)));
                const color = await ctDetectColor(se.x, se.y);
                return { step: i+1, angleDeg, color };
            }

            function ctDrawWheel(highlightIdx = -1) {
                const cv = getEl("ctCanvas");
                if (!cv) return;
                const c = cv.getContext("2d");
                const W = cv.width, H = cv.height;
                const cx = W/2, cy = H/2, r = W/2 - 18;
                c.clearRect(0, 0, W, H);
                const steps = Math.max(4, Number(getEl("ctSteps")?.value) || 36);

                for (let i = 0; i < steps; i++) {
                    const a = (i / steps) * Math.PI * 2 - Math.PI/2;
                    const highlight = i === highlightIdx;
                    const res = ctResults[i];
                    c.strokeStyle = highlight ? "#a5b4fc" : "rgba(255,255,255,0.12)";
                    c.lineWidth = highlight ? 2 : 1;
                    c.beginPath(); c.moveTo(cx, cy);
                    c.lineTo(cx + Math.cos(a)*r, cy + Math.sin(a)*r);
                    c.stroke();

                    const dotR = highlight ? 6 : 4;
                    c.fillStyle = res?.color?.hex ?? (highlight ? "#a5b4fc" : "rgba(255,255,255,0.2)");
                    c.beginPath();
                    c.arc(cx + Math.cos(a)*r, cy + Math.sin(a)*r, dotR, 0, Math.PI*2);
                    c.fill();
                    if (res?.color) {
                        c.strokeStyle = "rgba(0,0,0,0.4)"; c.lineWidth = 1;
                        c.stroke();
                    }

                    const deg = Math.round((i / steps) * 360);
                    if (deg % 30 === 0 || steps <= 12) {
                        c.fillStyle = "rgba(255,255,255,0.4)";
                        c.font = "9px monospace"; c.textAlign = "center"; c.textBaseline = "middle";
                        c.fillText(deg+"°", cx + Math.cos(a)*(r+11), cy + Math.sin(a)*(r+11));
                    }
                }
                c.fillStyle = "rgba(255,255,255,0.25)";
                c.beginPath(); c.arc(cx, cy, 3, 0, Math.PI*2); c.fill();
            }

            function ctUpdateSavedDisplay() {
                const el = getEl("ctSaved");
                if (!el) return;
                if (!ctSavedColors.length) { el.textContent = "No results yet."; return; }
                el.innerHTML = ctSavedColors.map(r => {
                    const swatch = r.color ? `<span style="display:inline-block;width:10px;height:10px;background:${r.color.hex};border:1px solid rgba(255,255,255,0.3);border-radius:2px;vertical-align:middle;margin-right:4px;"></span>` : "";
                    const colorStr = r.color ? `${r.color.name} ${r.color.hex}` : "?";
                    return `<div>${swatch}<b>${r.angleDeg}°</b> → ${colorStr}</div>`;
                }).join("");
            }

            // Hover — update label and auto-fill the step number input
            getEl("ctCanvas")?.addEventListener("mousemove", e => {
                const cv = getEl("ctCanvas");
                const rect = cv.getBoundingClientRect();
                const dx = e.clientX - rect.left - cv.width/2;
                const dy = e.clientY - rect.top  - cv.height/2;
                if (Math.hypot(dx, dy) < 10) { getEl("ctHoverLabel").textContent = ""; return; }
                const deg = ((Math.atan2(dy,dx) * 180/Math.PI + 90 + 360) % 360).toFixed(1);
                const steps = Math.max(4, Number(getEl("ctSteps")?.value) || 36);
                const idx = Math.round((Number(deg)/360)*steps) % steps;
                const res = ctResults[idx];
                const colorStr = res?.color ? ` · ${res.color.name}` : "";
                getEl("ctHoverLabel").textContent = `step ${idx+1} · ${deg}°${colorStr}`;
                const stepInp = getEl("ctAnnotateStep");
                if (stepInp) stepInp.value = idx + 1;
                ctDrawWheel(idx);
            });
            getEl("ctCanvas")?.addEventListener("mouseleave", () => {
                getEl("ctHoverLabel").textContent = ""; ctDrawWheel();
            });

            function ctSaveAnnotation() {
                const steps = Math.max(4, Number(getEl("ctSteps")?.value) || 36);
                const i = Math.max(0, Math.min(steps-1, Number(getEl("ctAnnotateStep")?.value||1) - 1));
                const inp = getEl("ctAnnotateInput");
                const colorLabel = inp?.value.trim();
                if (!colorLabel) return;
                const hex = /^#[0-9a-f]{3,6}$/i.test(colorLabel) ? colorLabel : null;
                const angleDeg = Math.round((i / steps) * 360);
                const result = ctResults[i] || { step: i+1, angleDeg };
                result.color = { name: colorLabel, hex: hex || colorLabel };
                ctResults[i] = result;
                ctSavedColors.push({ ...result });
                ctUpdateSavedDisplay(); ctDrawWheel();
                if (inp) inp.value = "";
                const stepInp = getEl("ctAnnotateStep");
                if (stepInp) stepInp.value = Math.min(steps, i + 2);
                const status = getEl("ctStatus");
                if (status) status.textContent = `Saved step ${i+1}: ${angleDeg}° → ${colorLabel}`;
            }
            getEl("ctAnnotateSave")?.addEventListener("click", ctSaveAnnotation);
            getEl("ctAnnotateInput")?.addEventListener("keydown", e => {
                if (e.key === "Enter") { e.stopPropagation(); ctSaveAnnotation(); }
            });

            getEl("ctRun")?.addEventListener("click", async () => {
                ctAbort = false; ctResults = [];
                const steps  = Math.max(4, Math.min(72, Number(getEl("ctSteps")?.value)  || 36));
                const radius = Math.max(1, Number(getEl("ctRadius")?.value) || 5);
                const delay  = Math.max(50, Number(getEl("ctDelay")?.value) || 150);
                const status = getEl("ctStatus");
                ctDrawWheel();
                for (let i = 0; i < steps; i++) {
                    if (ctAbort) break;
                    if (status) status.textContent = `Step ${i+1}/${steps} — spawning 4× at ${Math.round((i/steps)*360)}°…`;
                    const result = await ctRunStep(i, steps, radius, delay);
                    ctResults[i] = result;
                    ctSavedColors.push(result);
                    ctDrawWheel(i); ctUpdateSavedDisplay();
                }
                if (status) status.textContent = ctAbort ? "Stopped." : `Done — ${ctResults.filter(Boolean).length}/${steps} steps detected.`;
                ctAbort = false; ctDrawWheel();
            });

            getEl("ctStop")?.addEventListener("click", () => {
                ctAbort = true;
                events.keyUp("KeyC");
                events.keyUp("Backquote");
            });
            getEl("ctClearSaved")?.addEventListener("click", () => {
                ctSavedColors = []; ctResults = [];
                ctUpdateSavedDisplay(); ctDrawWheel();
            });
            getEl("ctCopySaved")?.addEventListener("click", () => {
                const text = ctSavedColors.map(r => `${r.angleDeg}° → ${r.color?.name ?? "?"} ${r.color?.hex ?? "no color detected"}`).join("\n");
                navigator.clipboard.writeText(text).then(() => {
                    const btn = getEl("ctCopySaved");
                    if (btn) { btn.textContent = "✅ Copied"; setTimeout(() => btn.textContent = "📋 Copy", 1500); }
                }).catch(() => {});
            });

            ctDrawWheel();
            ctUpdateSavedDisplay();

            // ── Standalone Spawn Test ──────────────────────────────────────
            // Fully self-contained sandbox. Does NOT touch any of the script's
            // bot manager, sockets, or state. It captures the live game's
            // WebSocket URL, then opens its own raw connections and sends a
            // spawn packet built exactly like the reversed-protocol reference.
            // Bots do nothing after spawning.
            (function standaloneSpawnTest(){
                const $ = id => document.getElementById(id);
                if (!$("sstSpawn")) return;

                // Grab a guaranteed-native WebSocket from a hidden iframe so our
                // test connections fully bypass the page's WebSocket wrapper and
                // can never be mistaken for the game's master socket.
                let NativeWS = window.WebSocket;
                try {
                    const frame = document.createElement("iframe");
                    frame.style.display = "none";
                    document.body.appendChild(frame);
                    if (frame.contentWindow && frame.contentWindow.WebSocket) {
                        NativeWS = frame.contentWindow.WebSocket;
                    }
                    // Keep the frame attached — detaching can invalidate the ctor.
                } catch {}

                // Passively record the most recent game socket URL by chaining
                // over whatever WebSocket constructor is currently installed.
                let capturedUrl = null;
                (function recordGameUrl(){
                    const Cur = window.WebSocket;
                    function Recorder(url, protocols){
                        if (typeof url === "string" && /^wss?:\/\//i.test(url)) {
                            capturedUrl = url;
                            const f = $("sstUrl");
                            if (f && !f.value) f.placeholder = url;
                        }
                        return protocols === undefined ? new Cur(url) : new Cur(url, protocols);
                    }
                    Recorder.prototype = Cur.prototype;
                    try { Object.setPrototypeOf(Recorder, Cur); } catch {}
                    window.WebSocket = Recorder;
                    try { window.MozWebSocket = Recorder; } catch {}
                })();

                // The spawn packet, verbatim from the reversed-protocol reference.
                function construct_spawn_packet(name, party){
                    const encoded_name  = new TextEncoder().encode(name);
                    const encoded_party = new TextEncoder().encode(party);
                    const packet = new Uint8Array(encoded_name.byteLength + encoded_party.byteLength + 4);
                    packet[1] = 192 + encoded_name.byteLength;
                    packet.set(encoded_name, 2);
                    packet[2 + encoded_name.length] = 192 + encoded_party.byteLength;
                    packet.set(encoded_party, 3 + encoded_name.length);
                    packet[packet.byteLength - 1] = 1;
                    packet[0] = 115;
                    return packet;
                }

                const testBots = [];
                function sstLog(msg){
                    const box = $("sstLog");
                    if (!box) return;
                    const line = document.createElement("div");
                    line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
                    box.appendChild(line);
                    box.scrollTop = box.scrollHeight;
                }
                function sstUpdateStatus(){
                    const open = testBots.filter(ws => ws.readyState === 1).length;
                    const st = $("sstStatus");
                    if (st) st.textContent = testBots.length
                        ? `${testBots.length} test bot${testBots.length!==1?"s":""} — ${open} open.`
                        : "No bots.";
                }
                function killAll(){
                    for (const ws of testBots) { try { ws.close(); } catch {} }
                    testBots.length = 0;
                    sstUpdateStatus();
                    sstLog("Disconnected all test bots.");
                }

                $("sstSpawn").addEventListener("click", () => {
                    const url = ($("sstUrl").value || "").trim() || capturedUrl;
                    if (!url) {
                        sstLog("No server URL — join a game first (URL auto-captures) or paste one.");
                        return;
                    }
                    if (!/^wss?:\/\//i.test(url)) { sstLog("URL must start with ws:// or wss://"); return; }
                    const name  = $("sstName").value || "";
                    const party = $("sstParty").value || "";
                    const count = Math.max(1, Math.min(200, Number($("sstCount").value) || 1));
                    sstLog(`Spawning ${count} bot${count!==1?"s":""} → ${url}`);
                    for (let i = 0; i < count; i++) {
                        let ws;
                        try { ws = new NativeWS(url); }
                        catch (e) { sstLog(`#${i+1} failed to open: ${e.message}`); continue; }
                        ws.binaryType = "arraybuffer";
                        testBots.push(ws);
                        ws.addEventListener("open", () => {
                            try {
                                ws.send(construct_spawn_packet(name, party));
                                sstLog(`#${i+1} open → sent spawn packet (${name||"<blank>"}).`);
                            } catch (e) { sstLog(`#${i+1} send error: ${e.message}`); }
                            sstUpdateStatus();
                        });
                        ws.addEventListener("close", ev => {
                            sstLog(`#${i+1} closed (${ev.code}${ev.reason?": "+ev.reason:""}).`);
                            sstUpdateStatus();
                        });
                        ws.addEventListener("error", () => sstLog(`#${i+1} socket error.`));
                        // Incoming packets are intentionally ignored — bots do nothing.
                    }
                    sstUpdateStatus();
                });

                $("sstKill").addEventListener("click", killAll);
            })();

            getEl("copyClans").addEventListener("click",()=>{
                navigator.clipboard.writeText(stealedClans.join("\n")).then(()=>alert("Copied!")).catch(()=>{});
            });

            // ── Debug ticker ───────────────────────────────────────────────
            setInterval(()=>{
                const pv=getEl("pingVal"); if(pv) pv.textContent=ping;
                const di=getEl("debugInfo");
                if(di) di.innerHTML=
                    `pos: ${player.x?.toFixed(1)||"?"}, ${player.y?.toFixed(1)||"?"}<br>`+
                    `class: ${player.class||"?"}<br>`+
                    `fov: ${player.fov?.toFixed(3)}<br>`+
                    `bots: ${bots.length} | proxies: ${IS_ELECTRON?"(launcher)":ProxyManager.proxies.length}`;
                
                // Report master lag to electron process for power allocation
                if (IS_ELECTRON && !window.__is_bot && typeof window.electronBridge?.reportMasterLag === "function") {
                    window.electronBridge.reportMasterLag(ping);
                }
            },500);

            // ── Soccer bot ─────────────────────────────────────────────────
            let sbCycle=false;
            setInterval(()=>{
                if(getEl("soccerbot")?.checked){
                    events._keyEvent("KeyW",sbCycle);
                    events._keyEvent("KeyS",!sbCycle);
                    sbCycle=!sbCycle;
                }
            },5500);

            // ── Time killer ────────────────────────────────────────────────
            setInterval(()=>{
                if(getEl("timekiller")?.checked){
                    stopMoving();
                    dirPathfind(mouse.dir+Math.PI);
                    setTimeout(()=>{ stopMoving(); dirPathfind(mouse.dir); },800);
                }
            },2000);
        }

        // Bot name injection
        const nameIv=setInterval(()=>{
            const inp=document.querySelector("body > input[type=text]");
            if (!inp) return;
            clearInterval(nameIv);
            player.name=inp.value;
            if (window.__is_bot) {
                let name=window.channel?.botName||"Cat Minion";
                if (window.channel?.clan) name=`[${window.channel.clan}] ${name}`;
                inp.value=name;
            }
        },77);
    }

    let botReconnectLastClick = 0;
    let botReconnectLastCheck = 0;
    function botLooksLikeReconnectScreenText(text) {
        const now = Date.now();
        // Only check for reconnect keywords every 2 seconds to avoid spam checks
        if (now - botReconnectLastCheck < 2000) return false;
        botReconnectLastCheck = now;

        return /\breconnect\b/i.test(text) ||
               /\bdisconnected\b/i.test(text) ||
               /\bkicked\b/i.test(text) ||
               /\bconnection closed\b/i.test(text) ||
               /\bclosed due to\b/i.test(text) ||
               /\bdue to an error\b/i.test(text);
    }
    function botClickReconnectAt(x, y) {
        const now = Date.now();
        if (now - botReconnectLastClick < 300) return false;
        if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
        botReconnectLastClick = now;
        events.click(0, x, y);
        return true;
    }
    function botClickReconnectCanvasArea(textX, textY) {
        const now = Date.now();
        if (now - botReconnectLastClick < 300) return false;
        botReconnectLastClick = now;
        const points = [];
        if (Number.isFinite(textX) && Number.isFinite(textY)) points.push([textX, textY]);
        const cx = innerWidth * 0.545;
        const cy = innerHeight * 0.655;
        points.push(
            [cx, cy],
            [innerWidth * 0.535, cy],
            [innerWidth * 0.555, cy],
            [cx, innerHeight * 0.645],
            [cx, innerHeight * 0.665],
        );
        points.forEach(([x, y], i) => setTimeout(() => events.click(0, x, y), i * 35));
        return true;
    }
    function botClickReconnectButton() {
        const now = Date.now();
        if (now - botReconnectLastClick < 300) return false;
        const candidates = [...document.querySelectorAll("button,input[type=button],input[type=submit],[role=button],a")];
        const btn = candidates.find(el => /\breconnect\b/i.test(el.textContent || el.value || el.getAttribute("aria-label") || ""));
        if (!btn) return false;
        botReconnectLastClick = now;
        btn.click();
        return true;
    }
    function botClickReconnectFallback() {
        const now = Date.now();
        if (now - botReconnectLastClick < 300) return false;
        botReconnectLastClick = now;
        const x = innerWidth * 0.545;
        const y = innerHeight * 0.655;
        events.click(0, x, y);
        setTimeout(() => events.click(0, x, y), 80);
        return true;
    }
    function botTryReconnect(x, y) {
        if (botClickReconnectCanvasArea(x, y)) return true;
        if (botClickReconnectButton()) return true;
        return botClickReconnectFallback();
    }
    function botReconnect() {
        if (botClickReconnectButton()) return;
        const now = Date.now();
        if (now - botReconnectLastClick < 300) return;
        botReconnectLastClick = now;
        const x = innerWidth * 0.545;
        const y = innerHeight * 0.655;
        events.click(0, x, y);
        setTimeout(() => events.click(0, x, y), 80);
    }

    // ─── Bot-specific setup ───────────────────────────────────────────────────
    if (window.__is_bot) {
        function disableBotRendering() {
            WebGLRenderingContext.prototype.drawElements = ()=>{};
            WebGLRenderingContext.prototype.drawArrays   = ()=>{};
            CanvasRenderingContext2D.prototype.fill       = ()=>{};
            CanvasRenderingContext2D.prototype.fillRect   = ()=>{};
            CanvasRenderingContext2D.prototype.stroke     = ()=>{};
            CanvasRenderingContext2D.prototype.strokeRect = ()=>{};
            CanvasRenderingContext2D.prototype.roundRect  = ()=>{};
            CanvasRenderingContext2D.prototype.lineTo     = ()=>{};
            CanvasRenderingContext2D.prototype.arc        = ()=>{};
        }
        if (window.channel?.disRender) disableBotRendering();

        const _oldSet=localStorage.setItem;
        localStorage.setItem=function(key,value){
            if (key==="arras.io") return;
            _oldSet.call(this,key,value);
        };

        const inner={ width:innerWidth, height:innerHeight };
        const desiredMouseButtons = {};
        const activeMouseButtons = {};
        const lastBotMouse = { x:inner.width / 2, y:inner.height / 2 };

        function releaseActiveBotMouseButtons() {
            for (const key of Object.keys(activeMouseButtons)) {
                if (!activeMouseButtons[key]) continue;
                events.mouseUp(Number(key), lastBotMouse.x, lastBotMouse.y);
                activeMouseButtons[key] = false;
            }
        }

        function applyBotMouseButtons() {
            if (!botCanAttack()) {
                releaseActiveBotMouseButtons();
                return;
            }
            for (const key of new Set([...Object.keys(desiredMouseButtons), ...Object.keys(activeMouseButtons)])) {
                const button = Number(key);
                if (desiredMouseButtons[key] && !activeMouseButtons[key]) {
                    events.mouseDown(button, lastBotMouse.x, lastBotMouse.y);
                    activeMouseButtons[key] = true;
                } else if (!desiredMouseButtons[key] && activeMouseButtons[key]) {
                    events.mouseUp(button, lastBotMouse.x, lastBotMouse.y);
                    activeMouseButtons[key] = false;
                }
            }
        }

        window.__resetBotInputs = releaseActiveBotMouseButtons;
        window.__applyBotInputState = applyBotMouseButtons;

        if (!IS_ELECTRON) {
            window.channel.feeed      = bool=>{ if(fedingCheckbox) fedingCheckbox.checked=bool; };
            window.channel.feeedRESET = ()=>{ window.fedPath=[]; currentFedPath=[]; };
        }

        const spawnIv=setInterval(()=>{
            if (isConnecting){ events.press("KeyL"); clearInterval(spawnIv); return; }
            events.press("Enter");
        },200);
        setInterval(() => {
            applyBotMouseButtons();
            if (botReconnectWanted) botTryReconnect();
            else botClickReconnectButton();
            if (window.channel?.isCatAI && spawned && Date.now() - catAILastAct >= 30000) {
                catAILastAct = Date.now();
                catAISay("Hello! I am Cat AI! !help for help!");
            }
            reportBotTeamUrlIfReady();
        }, 1000);
        setInterval(() => {
            const now = Date.now();
            const coordsStale = now - botLastCoordChange >= 10000;
            const coordsMissing = now - botLastCoordSeen >= 10000;
            if (!coordsStale && !coordsMissing) return;
            if (!coordsMissing && !botReconnectWanted) return;
            resetBotSpawnState();
            if (window.__is_bot) {
                setBotAutoFire(window.channel.autoFire);
                setBotAutoSpin(window.channel.autoSpin);
            }
            stopMoving();
            events.mouseUp(0);
            botReconnectWanted = true;
            botTryReconnect();
        }, 10000);

        function handleBotMessage(msg) {
            if (msg.targetIndex !== undefined && msg.targetIndex !== window.channel?.index) return;
            switch(msg.type){
                case "mousedown":
                    lastBotMouse.x = Number.isFinite(msg.x) ? msg.x : lastBotMouse.x;
                    lastBotMouse.y = Number.isFinite(msg.y) ? msg.y : lastBotMouse.y;
                    desiredMouseButtons[msg.button] = true;
                    applyBotMouseButtons();
                    break;
                case "mouseup":
                    lastBotMouse.x = Number.isFinite(msg.x) ? msg.x : lastBotMouse.x;
                    lastBotMouse.y = Number.isFinite(msg.y) ? msg.y : lastBotMouse.y;
                    desiredMouseButtons[msg.button] = false;
                    applyBotMouseButtons();
                    break;
                case "mousemove":
                    window.channel.lastWorldX=msg.worldX;
                    window.channel.lastWorldY=msg.worldY;
                    if (window.channel?.chaosMode) break;
                    if (window.channel?.mouseType==="aim" && isFinitePoint(msg.worldX, msg.worldY) && isFinitePoint(player.x, player.y)) {
                        let sx=((msg.worldX-player.x)/(23.09*player.fov)*(innerWidth/2))+(innerWidth/2);
                        let sy=((msg.worldY-player.y)/(12.98*player.fov)*(innerHeight/2))+(innerHeight/2);
                        events.moveTo(utils.clamp(sx,0,innerWidth), utils.clamp(sy,0,innerHeight));
                    } else if (isFinitePoint(msg.x, msg.y) && Number.isFinite(msg.innerWidth) && Number.isFinite(msg.innerHeight)) {
                        lastBotMouse.x = (msg.x/msg.innerWidth)*inner.width;
                        lastBotMouse.y = (msg.y/msg.innerHeight)*inner.height;
                        events.moveTo(lastBotMouse.x, lastBotMouse.y);
                    }
                    break;
                case "mouseType":
                    mouseType=msg.value;
                    if (window.channel) window.channel.mouseType=msg.value;
                    break;
                case "chaosMode":
                    if (window.channel) window.channel.chaosMode=msg.value;
                    break;
                case "keydown":
                    if (isBotToggleKey(msg.code)) break;
                    if (msg.code !== "KeyL" && (!isMovementKey(msg.code) || botCanMove())) events.keyDown(msg.code);
                    break;
                case "keyup":      if (msg.code!=="KeyL" && !isBotToggleKey(msg.code)) events.keyUp(msg.code); break;
                case "position":
                    if (isFinitePoint(msg.x, msg.y)) {
                        sinsay.x=msg.x; sinsay.y=msg.y; sinsay.exact=false; sinsay.tolerance=undefined;
                    }
                    break;
                case "formation":
                    {
                        const a = msg.assignments?.find(p => p.index === window.channel?.index);
                        if (a) {
                            sinsay.x=a.x;
                            sinsay.y=a.y;
                            sinsay.exact=a.exact !== false;
                            sinsay.tolerance=Number.isFinite(a.tolerance) ? a.tolerance : undefined;
                        }
                    }
                    break;
                case "formationStop": sinsay.x=undefined; sinsay.y=undefined; sinsay.exact=false; sinsay.tolerance=undefined; stopMoving(); break;
                case "setTank":    if (window.channel) window.channel.tank=msg.value; break;
                case "setCustomTankPath":
                    if (window.channel) window.channel.customTankPath = String(msg.path || "");
                    break;
                case "setBotBuild":
                    if (window.channel) {
                        window.channel.botBuildMode = msg.mode || "default";
                        window.channel.customBuild = String(msg.build || "");
                    }
                    break;
                case "setClan":
                    if (window.channel) window.channel.clan=msg.value;
                    {
                        const inp=document.querySelector("body > input[type=text]");
                        if (inp) {
                            let name=window.channel?.botName||"Cat Minion";
                            if (window.channel?.clan) name=`[${window.channel.clan}] ${name}`;
                            inp.value=name;
                        }
                    }
                    break;
                case "setName":
                    if (window.channel) window.channel.botName=msg.value;
                    {
                        const inp=document.querySelector("body > input[type=text]");
                        if (inp) {
                            let name=window.channel?.botName||"Cat Minion";
                            if (window.channel?.clan) name=`[${window.channel.clan}] ${name}`;
                            inp.value=name;
                        }
                    }
                    break;
                case "setMoving":  if (window.channel) window.channel.moving=msg.value; break;
                case "setDisableAllOperations":
                    if (window.channel) window.channel.disableAllOperations = !!msg.value;
                    if (window.channel?.isCrashy) {
                        if (msg.value) disableAllBotOperations();
                        else enableAllBotOperations();
                    }
                    break;
                case "setAutoFire":
                    setBotAutoFire(!!msg.value);
                    break;
                case "setAutoSpin":
                    setBotAutoSpin(!!msg.value);
                    break;
                case "setDisRender":
                    if (window.channel?.isCatAI) break;
                    if (window.channel) window.channel.disRender=msg.value;
                    if (msg.value) disableBotRendering();
                    break;
                case "feeed":
                    botFeedingEnabled=msg.value;
                    if (fedingCheckbox) fedingCheckbox.checked=msg.value;
                    break;
                case "addFedWaypoint":
                    window.fedPath=window.fedPath||[];
                    window.fedPath.push({ x:msg.x, y:msg.y }); break;
                case "fedReset":   window.fedPath=[]; currentFedPath=[]; break;
                case "reconnect":  botReconnect(); break;
                case "sync":
                    // Sync: ESC, stop moving for 5s, then upgrade
                    (async () => {
                        events.press("Escape");
                        stopMoving();
                        await new Promise(r => setTimeout(r, 5000));
                        if (spawned && !hasUpgraded) {
                            const resolved = resolveBotTank();
                            if (resolved) {
                                hasUpgraded = true;
                                if (resolved.build) upgradeStats(resolved);
                                if (resolved.path) await upgradeTank(resolved.path, resolved.slowUpgrading);
                            }
                        }
                    })();
                    break;
                case "forceUpgrade":
                    if (spawned && hasUpgraded) {
                        hasUpgraded = false;
                        stopMoving();
                        const resolved = applyBotBuildConfig(resolveBotTank(msg.tank));
                        (async () => {
                            if (resolved) {
                                if (resolved.build) upgradeStats(resolved);
                                if (resolved.path)  await upgradeTank(resolved.path, resolved.slowUpgrading);
                            }
                            stopMoving();
                            hasUpgraded = true;
                        })();
                    }
                    if (window.channel) window.channel.tank=msg.tank; break;
                case "chat":
                    if (window.__is_bot && !spawned) break;
                    if (msg.msg) {
                        events.press("Enter");
                        const civ=setInterval(()=>{
                            const inp=document.querySelector("body > input[type=text]");
                            if (!inp) return;
                            clearInterval(civ);
                            inp.value=chatLimit(msg.msg);
                            events.press("Enter");
                        },40);
                    }
                    break;
            }
        }

        if (IS_ELECTRON) {
            window.channel.reconnect = botReconnect;
            window.electronBridge.onBotMessage(handleBotMessage);
            window.electronBridge.botReady();
        } else {
            window.channel.message  = handleBotMessage;
            window.channel.reconnect = botReconnect;
        }

        addEventListener("keydown",function(e){
            if(e.key==="Tab"||e.key==="Escape"){
                const m=getEl("scriptMenu");
                if(m) m.style.display=m.style.display==="flex"?"none":"flex";
            }
        },true);
    }

    // ─── Init ─────────────────────────────────────────────────────────────────
    if (document.body) initUI();
    else document.addEventListener("DOMContentLoaded", initUI);

})();