Greasy Fork

Greasy Fork is available in English.

Meeland Enhancement Suite

Meeland.io cheat script with auto-lock, speed boost, infinite jump, teleportation & more! Works on CrazyGames, twoplayergames.org, meeland.io, iogames.onl, sprunki-game.io, gameflare.com and other sites hosting Meeland

当前为 2026-03-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Meeland Enhancement Suite
// @namespace    meeland-script
// @version      5.5.0
// @match        *://*/*
// @run-at       document-end
// @license      MIT
// @grant        none
// @description Meeland.io cheat script with auto-lock, speed boost, infinite jump, teleportation & more! Works on CrazyGames, twoplayergames.org, meeland.io, iogames.onl, sprunki-game.io, gameflare.com and other sites hosting Meeland
// ==/UserScript==

(function () {
    'use strict';

    const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    if (!W.pc?.app?.root) return;

    const STORE_KEY = 'ml_wp';
    const CFG_KEY   = 'ml_cfg';
    let ACCEL          = 6;
    let FLY_MIN_SPEED  = 10;
    let SPEED_CAP      = 100;
    let GRAVITY        = -18;
    let SPEED_DEFAULT  = 7;

    let flyActive   = false;
    const slots = new Array(10).fill(null);
    let flyUp       = false;
    let flyDown     = false;
    let flyVelY     = 0;
    let prevTick    = Date.now();
    let sprinting   = false;
    let sprintSpeed = SPEED_DEFAULT;
    let homePos     = null;
    let backPos     = null;
    let cuddleTarget = null;
    let cuddling    = false;
    let accelEnabled = true;

    let featFly     = true;
    let featSprint  = true;
    let featWaypoints = true;
    let featCuddle  = true;
    let featCuddleFollow = true;
    let featPets    = true;
    let featAutoLock = true;
    let featAntiKnockback = true;
    let featNoclip = false;
    let featFastSync = false;

    let autoRefresh    = true;
    let refreshInterval = 10;
    let kbListeningRow = null;
    let petFilter = '';
    let petAutoRefresh = true;
    let petRefreshInterval = 1;

    const KEYBINDS = {
        fly:      'Space',
        flyDown:  'KeyF',
        setHome:  'KeyQ',
        home:     'Backquote',
        back:     'KeyZ',
        cuddle:   'KeyJ',
        settings: 'KeyM',
        pets:     'KeyK',
    };
    const DEFAULT_KEYBINDS = { ...KEYBINDS };

    function saveSettings() {
        try {
            localStorage.setItem(CFG_KEY, JSON.stringify({
                ACCEL, SPEED_CAP, SPEED_DEFAULT, accelEnabled,
                refreshInterval, autoRefresh,
                featFly, featSprint, featWaypoints, featCuddle, featCuddleFollow, featPets, featAutoLock,
                featAntiKnockback, featNoclip, featFastSync,
                keybinds: { ...KEYBINDS },
                petFilter, petAutoRefresh, petRefreshInterval,
            }));
        } catch (_) {}
    }

    function loadSettings() {
        try {
            const raw = localStorage.getItem(CFG_KEY);
            if (!raw) return;
            const d = JSON.parse(raw);
            const assign = (key, fn) => { if (d[key] !== undefined) fn(d[key]); };
            assign('ACCEL', v => ACCEL = v);
            assign('SPEED_CAP', v => SPEED_CAP = v);
            assign('SPEED_DEFAULT', v => SPEED_DEFAULT = v);
            assign('accelEnabled', v => accelEnabled = v);
            assign('refreshInterval', v => refreshInterval = v);
            assign('autoRefresh', v => autoRefresh = v);
            assign('featFly', v => featFly = v);
            assign('featSprint', v => featSprint = v);
            assign('featWaypoints', v => featWaypoints = v);
            assign('featCuddle', v => featCuddle = v); assign('featVisit', v => featCuddle = v);
            assign('featCuddleFollow', v => featCuddleFollow = v); assign('featStalk', v => featCuddleFollow = v);
            assign('featPets', v => featPets = v);
            assign('featAutoLock', v => featAutoLock = v);
            assign('featAntiKnockback', v => featAntiKnockback = v);
            assign('featNoclip', v => featNoclip = v);
            assign('featFastSync', v => featFastSync = v);
            assign('petFilter', v => petFilter = v);
            assign('petAutoRefresh', v => petAutoRefresh = v);
            assign('petRefreshInterval', v => petRefreshInterval = v);
            if (d.keybinds) {
                for (const k of Object.keys(KEYBINDS)) {
                    if (typeof d.keybinds[k] === 'string') KEYBINDS[k] = d.keybinds[k];
                }
            }
        } catch (_) {}
    }
    loadSettings();

    const getPlayer = () => W.pc?.app?.root?.findByName('Player') ?? null;
    const getKcc    = p  => p?.script?.kcc ?? null;
    const getPC     = p  => p?.script?.playerController ?? null;
    const $ = id => document.getElementById(id);
    const flash = id => { const el = $(id); if (el) { el.classList.add('fresh'); setTimeout(() => el.classList.remove('fresh'), 400); } };
    const syncSlider = (id, valId, v) => { const el = $(id), ve = $(valId); if (el) el.value = v; if (ve) ve.textContent = v; };
    const vec3 = v => ({ x: v.x, y: v.y, z: v.z });

    const FEAT_IDS = ['ml-f-fly','ml-f-sprint','ml-f-waypoints','ml-f-cuddle','ml-f-cuddle-follow','ml-f-pets','ml-f-autolock','ml-f-antiknockback','ml-f-noclip','ml-f-fastsync'];
    const MOVE_KEYS = new Set(['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight']);

    function getPlayerState() {
        const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
        if (!nm?.room?.state?.players) return null;
        return nm.room.state.players.get(nm.room.sessionId) ?? null;
    }

    const setSpeed = (pc, kcc, v) => {
        if (pc) { pc.currentSpeed = v; pc.speed = v; }
        if (kcc.speed !== undefined) kcc.speed = v;
        const ps = getPlayerState();
        if (ps && ps.movementSpeed !== undefined) ps.movementSpeed = v / SPEED_DEFAULT;
    };

    function teleport(player, pos) {
        player.setPosition(pos.x, pos.y, pos.z);
        player.rigidbody?.teleport(pos.x, pos.y, pos.z);
    }

    function saveWaypoints() {
        try {
            localStorage.setItem(STORE_KEY, JSON.stringify({
                home:  homePos ? vec3(homePos) : null,
                back:  backPos ? vec3(backPos) : null,
                slots: slots.map(s => s ? vec3(s) : null),
            }));
        } catch (_) {}
    }

    function loadWaypoints() {
        try {
            const raw = localStorage.getItem(STORE_KEY);
            if (!raw) return;
            const d = JSON.parse(raw);
            if (d.home)  homePos = d.home;
            if (d.back)  backPos = d.back;
            if (Array.isArray(d.slots)) d.slots.forEach((s, i) => { if (s) slots[i] = s; });
        } catch (_) {}
    }

    function flyOn(kcc) {
        flyActive = true;
        flyUp     = true;
        flyVelY   = 0;
        kcc.gravity = 0;
        kcc._velY   = 0;
    }

    function flyOff(kcc, resetVel = true) {
        flyActive = false;
        flyUp     = false;
        flyDown   = false;
        flyVelY   = 0;
        kcc.gravity = GRAVITY;
        if (resetVel) kcc._velY = 0;
    }

    const DESCRIPTION = `
<h1>Meeland Script — Cheat &amp; Enhancement Suite</h1>
<p>General-purpose cheat script for Meeland.io. Works across all game modes — <strong>Meeland Hub</strong>, <strong>Escape Waves</strong>, <strong>Steal a Pet</strong>, and <strong>Obby Tower</strong> — on all supported platforms.</p>
<p><strong>Designed for PC.</strong> Mobile has basic support (collapsible HUD) but keybinds require a keyboard.</p>
<p><em>Sorry for the long gap between updates — a rating helps if it works for you. ⭐</em></p>
<h2>Features</h2>
<ul>
<li>🚀 <strong>Fly Hack</strong> — Press <kbd>Space</kbd> to activate. Hold <kbd>Space</kbd> to ascend, <kbd>F</kbd> to descend. Throttle-based acceleration with configurable speed cap and accel rate. Auto-lands on ground contact.</li>
<li>⚡ <strong>Speed Hack</strong> — Hold <kbd>Shift</kbd> for accelerating speed boost. Configurable cap and base speed.</li>
<li>📍 <strong>Waypoints</strong> — <kbd>Q</kbd> saves home, <kbd>\`</kbd> teleports to home, <kbd>Z</kbd> toggles back position. 10 save slots via <kbd>Ctrl+Numpad 0-9</kbd>, recall with <kbd>Numpad 0-9</kbd>.</li>
<li>🐾 <strong>Cuddle Panel</strong> — <kbd>J</kbd> opens a player list sorted by distance. Click a player to teleport to them. Continuous cuddle follows the target in real time (cancelled by moving).</li>
<li>🐕 <strong>Pet Browser</strong> — <kbd>K</kbd> opens a full pet table with sortable columns (name, mutation, rarity, owner, worth, income, distance). Filter pets with a powerful search supporting AND, OR, and brackets. Teleport to any pet or grab wild/others' pets directly from the table. Configurable auto-refresh interval.</li>
<li>🔒 <strong>Auto-Lock Base</strong> — Automatically locks your base in Steal a Pet when the lockdown timer expires. Shows lock status and countdown in the HUD.</li>
<li>🛡️ <strong>Anti-Disconnect</strong> — Prevents getting kicked when entering another player's locked base by spoofing your position to the server.</li>
<li>💀 <strong>Anti-Death</strong> — Immune to falling off the map and wave kills in Escape Waves.</li>
<li>💪 <strong>Anti-Knockback</strong> — Immune to attacks and knockback from other players.</li>
<li>👻 <strong>Noclip</strong> — Walk through walls and obstacles. Off by default.</li>
<li>📡 <strong>Fast Sync</strong> — 4× faster position updates to the server. Off by default.</li>
<li>⚙️ <strong>Settings Panel</strong> — <kbd>M</kbd> opens settings. Adjust speed cap, accel rate, base speed, cuddle panel refresh interval, and toggle individual features on/off. Reset to defaults with one click.</li>
<li>🎮 <strong>Rebindable Keybindings</strong> — All hotkeys can be rebound from the settings panel. Changes persist across sessions.</li>
<li>🖱️ <strong>Draggable &amp; Resizable Panels</strong> — All panels (cuddle, settings, keybinds, pets, help) can be dragged by their headers and resized. Positions reset on close.</li>
</ul>
<h2>Keyboard Shortcuts</h2>
<p><em>All keybindings are rebindable via Settings → Keybindings.</em></p>
<ul>
<li><kbd>Space</kbd> — Toggle fly / fly up</li>
<li><kbd>F</kbd> (hold) — Fly down</li>
<li><kbd>Shift</kbd> (hold) — Speed hack</li>
<li><kbd>Q</kbd> — Set home waypoint</li>
<li><kbd>\`</kbd> — Teleport to home</li>
<li><kbd>Z</kbd> — Teleport back (toggle)</li>
<li><kbd>J</kbd> — Cuddle panel (player list)</li>
<li><kbd>K</kbd> — Pet browser</li>
<li><kbd>M</kbd> — Settings panel</li>
<li><kbd>?</kbd> — This help dialog</li>
<li><kbd>Ctrl+Numpad 0-9</kbd> — Save position to slot</li>
<li><kbd>Numpad 0-9</kbd> — Recall position from slot</li>
</ul>
<h2>Pet Browser</h2>
<ul>
<li>Click column headers to sort (name, mutation, rarity, owner, worth, income, distance)</li>
<li>Use the search bar to filter — words are AND'd, use OR for alternatives, brackets to group</li>
<li>➜ button teleports to a pet</li>
<li>✋ button grabs a pet (teleports, buys, returns, drops at your position)</li>
<li>Grabbed pets show ✔ for 5 seconds</li>
<li>Toggle auto-refresh and adjust interval (1-10s) in the header</li>
</ul>
<h2>Installation</h2>
<ol>
<li>Install a userscript manager:
<ul>
<li><strong>Chrome/Brave/Edge:</strong> <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo">Tampermonkey</a> or <a href="https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag">Violentmonkey</a></li>
<li><strong>Firefox:</strong> <a href="https://addons.mozilla.org/firefox/addon/tampermonkey/">Tampermonkey</a> or <a href="https://addons.mozilla.org/firefox/addon/violentmonkey/">Violentmonkey</a></li>
</ul>
</li>
<li>Click Install above</li>
<li>Load any Meeland game — the script activates automatically</li>
</ol>
<h2>Supported Sites</h2>
<ul>
<li>✅ meeland.io</li>
<li>✅ CrazyGames</li>
<li>✅ twoplayergames.org</li>
<li>✅ iogames.onl</li>
<li>✅ sprunki-game.io</li>
<li>✅ gameflare.com</li>
<li>✅ Any site embedding Meeland in an iframe</li>
</ul>
<h2>Privacy</h2>
<ul>
<li>✅ Client-side only — no data collected, no external requests</li>
</ul>
<h2>Disclaimer</h2>
<blockquote>
<p>For educational and entertainment purposes. Use at your own risk.</p>
</blockquote>`;

    function createHUD() {
        if ($('ml-hud')) return;
        const s = document.createElement('style');
        s.textContent = [
            '#ml-hud{position:fixed;bottom:14px;right:14px;display:flex;flex-wrap:wrap-reverse;justify-content:flex-end;align-items:flex-end;gap:5px;z-index:99999;pointer-events:none;font-family:monospace;user-select:none;max-width:calc(100vw - 28px)}',
            '#ml-hud-toggle{display:none;pointer-events:auto;cursor:pointer;background:rgba(0,0,0,.6);color:#daa520;font-weight:900;font-size:11px;border:1px solid rgba(218,165,32,.4);border-radius:99px;padding:3px 8px;font-family:monospace;line-height:1}',
            '#ml-hud.collapsed .ml-b{display:none}',
            '#ml-hud.collapsed #ml-hud-toggle{display:inline-block}',
            '@media (pointer:coarse),(max-width:768px){#ml-hud{bottom:4px;right:4px;gap:3px;max-width:calc(100vw - 8px)}#ml-hud-toggle{display:inline-block}.ml-b{font-size:9px;padding:2px 4px}}',
            '.ml-b{padding:2px 5px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:0;background:rgba(0,0,0,.45);color:#daa520;transition:color .12s,background .12s;position:relative}',
            '.ml-b.act{pointer-events:auto;cursor:pointer}',
            '.ml-b.act:hover{color:#fff;background:rgba(255,255,255,.1)}',
            '.ml-b.on{color:#fff;background:rgba(60,220,140,.55)}',
            '.ml-b.fresh{color:#fff;background:rgba(60,220,140,.75)}',
            '.ml-b.disabled{color:rgba(255,60,60,.45);text-decoration:line-through;text-decoration-color:rgba(255,40,40,.7);text-decoration-thickness:2px;background:rgba(120,0,0,.25)}',
            '#ml-dialog{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.75);z-index:100000;display:none;overflow:auto;padding:40px 20px;box-sizing:border-box}',
            '#ml-dialog.open{display:block}',
            '#ml-inner{background:#181818;color:#ccc;max-width:580px;margin:0 auto;border-radius:8px;padding:24px 28px;font-family:sans-serif;font-size:14px;line-height:1.65;position:relative}',
            '#ml-inner h1{color:#fff;font-size:1.2em;margin:0 0 10px}',
            '#ml-inner h2{color:#aaa;font-size:.85em;text-transform:uppercase;letter-spacing:.08em;margin:18px 0 6px;border-top:1px solid #333;padding-top:12px}',
            '#ml-inner li{margin:4px 0}',
            '#ml-inner kbd{background:#2a2a2a;border:1px solid #444;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.9em}',
            '#ml-inner a{color:#4af}',
            '#ml-inner blockquote{border-left:3px solid #333;margin:8px 0 0;padding:6px 12px;color:#888;font-size:.9em}',
            '#ml-close{position:absolute;top:10px;right:14px;cursor:pointer;color:#555;font-size:20px;background:none;border:none;font-family:monospace;line-height:1}',
            '#ml-close:hover{color:#fff}',

            '#ml-plist{position:fixed;top:50%;right:20px;transform:translateY(-50%);width:260px;max-height:70vh;display:none;z-index:100001;pointer-events:auto;overflow:hidden;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:linear-gradient(170deg,rgba(15,20,35,.94) 0%,rgba(20,28,50,.96) 50%,rgba(12,16,30,.97) 100%);backdrop-filter:blur(24px) saturate(1.4);-webkit-backdrop-filter:blur(24px) saturate(1.4);border:1px solid rgba(100,180,255,.22);box-shadow:0 0 0 1px rgba(60,140,255,.08),0 4px 24px rgba(0,0,0,.7),0 12px 48px rgba(0,0,0,.5),inset 0 1px 0 rgba(140,200,255,.15),inset 0 -1px 0 rgba(0,0,0,.3),0 0 20px rgba(60,140,255,.06);user-select:none}',
            '#ml-plist.open{display:flex;flex-direction:column}',
            '#ml-plist-head{padding:11px 14px 9px;border-bottom:1px solid rgba(80,160,255,.2);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(40,100,200,.12) 0%,rgba(30,70,150,.04) 100%)}',
            '#ml-plist-title{font-size:12px;font-weight:800;color:rgba(200,225,255,.95);text-transform:uppercase;letter-spacing:.12em;text-shadow:0 0 8px rgba(80,160,255,.3),0 1px 2px rgba(0,0,0,.5)}',
            '#ml-plist-timer{font-size:10px;color:rgba(120,180,255,.55);font-variant-numeric:tabular-nums;margin-left:6px;text-shadow:0 0 4px rgba(60,140,255,.2)}',
            '#ml-plist-refresh{cursor:pointer;background:linear-gradient(180deg,rgba(50,110,200,.2) 0%,rgba(40,90,170,.12) 100%);border:1px solid rgba(80,160,255,.28);color:rgba(170,210,255,.8);font-size:12px;padding:3px 9px;border-radius:6px;font-family:inherit;transition:all .15s;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
            '#ml-plist-refresh:hover{background:linear-gradient(180deg,rgba(50,120,220,.35) 0%,rgba(40,100,190,.2) 100%);color:#e0edff;border-color:rgba(80,170,255,.45);box-shadow:0 0 10px rgba(60,140,255,.15)}',
            '#ml-plist-close{cursor:pointer;background:none;border:none;color:rgba(180,210,255,.45);font-size:18px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
            '#ml-plist-close:hover{color:#ff6b6b;text-shadow:0 0 8px rgba(255,80,80,.4)}',
            '#ml-plist-body{overflow-y:auto;flex:1;padding:4px 8px 6px;scrollbar-width:thin;scrollbar-color:rgba(80,160,255,.2) transparent}',
            '#ml-plist-body::-webkit-scrollbar{width:5px}',
            '#ml-plist-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.25),rgba(80,160,255,.15));border-radius:3px}',
            '#ml-plist-empty{color:rgba(140,190,255,.45);text-align:center;padding:20px 8px;font-size:12px;font-style:italic;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
            '.ml-prow{display:flex;align-items:center;gap:8px;padding:6px 9px;margin:2px 0;border-radius:7px;cursor:pointer;transition:all .15s;border:1px solid transparent;background:rgba(20,35,60,.25)}',
            '.ml-prow:hover{background:linear-gradient(90deg,rgba(40,90,180,.2) 0%,rgba(50,110,200,.12) 100%);border-color:rgba(80,160,255,.2);box-shadow:0 0 8px rgba(60,140,255,.08),inset 0 0 12px rgba(60,140,255,.04)}',
            '.ml-prow:active{background:rgba(50,110,200,.28);transform:scale(.98);border-color:rgba(80,160,255,.3)}',
            '.ml-pnum{font-size:10px;color:rgba(100,170,255,.6);font-weight:700;min-width:16px;text-align:right;font-variant-numeric:tabular-nums;text-shadow:0 0 4px rgba(60,140,255,.15)}',
            '.ml-pinfo{flex:1;overflow:hidden}',
            '.ml-pname{font-size:12.5px;font-weight:600;color:rgba(230,240,255,.95);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3;text-shadow:0 1px 3px rgba(0,0,0,.4)}',
            '.ml-pdist{font-size:10px;color:rgba(120,180,255,.6);line-height:1.2;text-shadow:0 1px 2px rgba(0,0,0,.3)}',
            '.ml-parrow{color:rgba(80,160,255,.3);font-size:14px;transition:all .15s;text-shadow:0 0 4px rgba(60,140,255,.1)}',
            '.ml-prow:hover .ml-parrow{color:rgba(100,180,255,.75);transform:translateX(3px);text-shadow:0 0 8px rgba(60,140,255,.3)}',

            '#ml-settings{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:340px;max-height:80vh;display:none;z-index:100002;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
            '#ml-settings.open{display:flex;flex-direction:column}',
            '#ml-settings-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),inset 2px 0 0 rgba(100,160,240,.06),inset -2px 0 0 rgba(100,160,240,.06),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75),0 0 30px rgba(40,100,200,.08);border:none}',
            '#ml-settings-bg::before{content:"";position:absolute;inset:0;border-radius:4px;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(100,170,255,.018) 2px,rgba(100,170,255,.018) 3px),repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(100,170,255,.012) 40px,rgba(100,170,255,.012) 41px);pointer-events:none}',
            '#ml-settings-bg::after{content:"";position:absolute;inset:0;border-radius:4px;background:radial-gradient(ellipse at 30% 0%,rgba(60,140,255,.12) 0%,transparent 45%),radial-gradient(ellipse at 70% 100%,rgba(40,100,200,.08) 0%,transparent 40%),radial-gradient(circle at 50% 50%,rgba(50,120,220,.02) 0%,transparent 70%);pointer-events:none}',
            '#ml-settings-head{position:relative;padding:14px 18px 11px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,rgba(30,70,150,.05) 60%,transparent 100%);box-shadow:0 1px 0 rgba(60,140,255,.1)}',
            '#ml-settings-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 0 28px rgba(60,140,255,.15),0 1px 2px rgba(0,0,0,.6)}',
            '#ml-settings-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
            '#ml-settings-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
            '#ml-settings-reset{cursor:pointer;background:linear-gradient(180deg,rgba(50,100,180,.18) 0%,rgba(35,75,140,.1) 100%);border:1px solid rgba(60,140,255,.3);color:rgba(160,205,255,.7);font-size:9px;padding:4px 12px;border-radius:2px;font-family:"Courier New",Courier,monospace;letter-spacing:.12em;text-transform:uppercase;transition:all .15s;text-shadow:0 0 6px rgba(60,140,255,.2);box-shadow:0 0 0 1px rgba(60,140,255,.05)}',
            '#ml-settings-reset:hover{color:#e0edff;background:linear-gradient(180deg,rgba(50,110,200,.3) 0%,rgba(40,90,170,.18) 100%);border-color:rgba(60,150,255,.5);box-shadow:0 0 12px rgba(60,140,255,.15),0 0 0 1px rgba(60,140,255,.1)}',
            '#ml-settings-body{position:relative;overflow-y:auto;flex:1;padding:10px 14px 14px;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
            '#ml-settings-body::-webkit-scrollbar{width:4px}',
            '#ml-settings-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.3),rgba(80,160,255,.15));border-radius:2px}',
            '.ml-sgroup{margin:0 0 8px}',
            '.ml-slabel{font-size:9.5px;color:rgba(80,160,255,.7);text-transform:uppercase;letter-spacing:.2em;padding:8px 2px 4px;font-weight:800;border-bottom:1px solid rgba(60,140,255,.15);text-shadow:0 0 8px rgba(60,140,255,.2);background:linear-gradient(90deg,rgba(60,140,255,.04) 0%,transparent 80%)}',
            '.ml-srow{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-radius:3px;transition:all .12s;border:1px solid transparent}',
            '.ml-srow:hover{background:linear-gradient(90deg,rgba(40,100,200,.12) 0%,rgba(50,110,200,.06) 100%);border-color:rgba(60,140,255,.08)}',
            '.ml-srow-label{font-size:11.5px;color:rgba(200,220,250,.85);flex:1;text-shadow:0 1px 2px rgba(0,0,0,.4)}',
            '.ml-srow-value{font-size:10.5px;color:rgba(100,175,255,.75);font-variant-numeric:tabular-nums;min-width:34px;text-align:right;margin-right:6px;text-shadow:0 0 6px rgba(60,140,255,.15)}',
            '.ml-toggle{position:relative;width:34px;height:18px;border-radius:2px;background:linear-gradient(180deg,rgba(25,35,55,.9) 0%,rgba(20,30,48,.95) 100%);border:1px solid rgba(60,130,220,.3);cursor:pointer;transition:all .18s;flex-shrink:0;box-shadow:inset 0 1px 3px rgba(0,0,0,.4),0 0 0 1px rgba(60,140,255,.05)}',
            '.ml-toggle::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:2px;background:linear-gradient(180deg,rgba(100,160,240,.3) 0%,rgba(80,140,220,.2) 100%);transition:all .18s;box-shadow:0 1px 4px rgba(0,0,0,.4)}',
            '.ml-toggle.on{background:linear-gradient(180deg,rgba(30,70,140,.6) 0%,rgba(25,60,120,.7) 100%);border-color:rgba(60,150,255,.55);box-shadow:inset 0 1px 3px rgba(0,0,0,.3),0 0 8px rgba(60,140,255,.12)}',
            '.ml-toggle.on::after{left:18px;background:linear-gradient(180deg,rgba(120,190,255,.95) 0%,rgba(80,160,255,.85) 100%);box-shadow:0 0 10px rgba(60,150,255,.5),0 0 20px rgba(60,140,255,.2)}',
            '.ml-slider{-webkit-appearance:none;appearance:none;width:84px;height:4px;border-radius:2px;background:linear-gradient(90deg,rgba(30,50,80,.6) 0%,rgba(40,70,120,.4) 100%);outline:none;cursor:pointer;border:1px solid rgba(60,140,255,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}',
            '.ml-slider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:2px;background:linear-gradient(180deg,rgba(140,200,255,.9) 0%,rgba(100,170,255,.75) 100%);border:1px solid rgba(60,150,255,.6);box-shadow:0 0 8px rgba(60,150,255,.35),0 0 16px rgba(60,140,255,.12),0 1px 3px rgba(0,0,0,.4);cursor:pointer}',
            '.ml-slider::-moz-range-thumb{width:14px;height:14px;border-radius:2px;background:linear-gradient(180deg,rgba(140,200,255,.9) 0%,rgba(100,170,255,.75) 100%);border:1px solid rgba(60,150,255,.6);box-shadow:0 0 8px rgba(60,150,255,.35),0 0 16px rgba(60,140,255,.12),0 1px 3px rgba(0,0,0,.4);cursor:pointer}',

            '#ml-keybinds{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:320px;max-height:70vh;display:none;z-index:100003;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
            '#ml-keybinds.open{display:flex;flex-direction:column}',
            '#ml-keybinds-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75)}',
            '#ml-keybinds-head{position:relative;padding:14px 18px 11px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,rgba(30,70,150,.05) 60%,transparent 100%)}',
            '#ml-keybinds-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 1px 2px rgba(0,0,0,.6)}',
            '#ml-keybinds-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
            '#ml-keybinds-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
            '#ml-keybinds-body{position:relative;overflow-y:auto;flex:1;padding:10px 14px 14px;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
            '.ml-krow{display:flex;align-items:center;justify-content:space-between;padding:7px 8px;border-radius:3px;cursor:pointer;transition:all .12s;border:1px solid transparent}',
            '.ml-krow:hover{background:linear-gradient(90deg,rgba(40,100,200,.12) 0%,rgba(50,110,200,.06) 100%);border-color:rgba(60,140,255,.08)}',
            '.ml-krow.listening{background:rgba(255,180,40,.1);border-color:rgba(255,180,40,.35)}',
            '.ml-krow-action{font-size:11.5px;color:rgba(200,220,250,.85);text-shadow:0 1px 2px rgba(0,0,0,.4)}',
            '.ml-krow-key{font-size:11px;color:rgba(100,175,255,.8);font-weight:700;padding:2px 8px;border-radius:3px;background:rgba(30,50,80,.5);border:1px solid rgba(60,140,255,.2);min-width:40px;text-align:center;transition:all .15s}',
            '.ml-krow.listening .ml-krow-key{color:rgba(255,200,80,.9);background:rgba(120,80,0,.2);border-color:rgba(255,180,40,.4)}',

            '#ml-pets{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:680px;max-height:82vh;display:none;z-index:100004;pointer-events:auto;overflow:hidden;border-radius:4px;font-family:"Courier New",Courier,monospace;user-select:none}',
            '#ml-pets.open{display:flex;flex-direction:column}',
            '#ml-pets-bg{position:absolute;inset:0;border-radius:4px;background:linear-gradient(175deg,#10141e 0%,#171d2c 15%,#1a2236 30%,#1e2840 50%,#1a2236 70%,#171d2c 85%,#10141e 100%);box-shadow:inset 0 2px 0 rgba(160,200,255,.18),inset 0 -2px 0 rgba(0,0,0,.5),0 0 0 1px rgba(70,130,220,.3),0 0 0 3px rgba(10,15,25,.8),0 0 0 4px rgba(70,130,220,.15),0 20px 60px rgba(0,0,0,.75)}',
            '#ml-pets-bg::before{content:"";position:absolute;inset:0;border-radius:4px;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(100,170,255,.018) 2px,rgba(100,170,255,.018) 3px);pointer-events:none}',
            '#ml-pets-head{position:relative;padding:12px 16px 10px;border-bottom:2px solid rgba(60,140,255,.25);display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,rgba(50,110,200,.14) 0%,transparent 100%)}',
            '#ml-pets-title{font-size:14px;font-weight:800;color:rgba(180,215,255,.95);letter-spacing:.2em;text-transform:uppercase;text-shadow:0 0 14px rgba(60,140,255,.4),0 1px 2px rgba(0,0,0,.6)}',
            '#ml-pets-count{font-size:10px;color:rgba(100,170,255,.5);margin-left:8px;letter-spacing:.1em}',
            '#ml-pets-close{cursor:pointer;background:none;border:none;color:rgba(160,200,255,.4);font-size:20px;padding:0 4px;font-family:monospace;line-height:1;transition:all .15s}',
            '#ml-pets-close:hover{color:#ff5555;text-shadow:0 0 12px rgba(255,60,60,.5)}',
            '#ml-pets-filter{position:relative;padding:8px 14px;border-bottom:1px solid rgba(60,140,255,.12)}',
            '#ml-pets-search{width:100%;background:rgba(20,30,50,.6);border:1px solid rgba(60,140,255,.2);color:rgba(200,225,255,.9);font-size:11px;padding:5px 10px;border-radius:3px;font-family:inherit;outline:none;box-sizing:border-box}',
            '#ml-pets-search:focus{border-color:rgba(60,150,255,.45);box-shadow:0 0 8px rgba(60,140,255,.12)}',
            '#ml-pets-search::placeholder{color:rgba(100,160,255,.35)}',
            '#ml-pets-body{position:relative;overflow-y:auto;flex:1;padding:0;scrollbar-width:thin;scrollbar-color:rgba(60,140,255,.25) transparent}',
            '#ml-pets-body::-webkit-scrollbar{width:4px}',
            '#ml-pets-body::-webkit-scrollbar-thumb{background:linear-gradient(180deg,rgba(60,140,255,.3),rgba(80,160,255,.15));border-radius:2px}',
            '#ml-ptable{width:100%;border-collapse:collapse;font-size:11px}',
            '#ml-ptable th{position:sticky;top:0;background:linear-gradient(180deg,rgba(25,35,55,.98),rgba(20,30,48,.95));color:rgba(100,170,255,.7);font-size:9.5px;text-transform:uppercase;letter-spacing:.15em;padding:7px 8px;text-align:left;cursor:pointer;border-bottom:1px solid rgba(60,140,255,.2);white-space:nowrap;transition:color .12s;z-index:1}',
            '#ml-ptable th:hover{color:rgba(140,200,255,.95)}',
            '#ml-ptable th.sort-asc::after{content:" ▲";font-size:8px}',
            '#ml-ptable th.sort-desc::after{content:" ▼";font-size:8px}',
            '#ml-ptable td{padding:5px 8px;border-bottom:1px solid rgba(60,140,255,.06);color:rgba(200,220,250,.8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}',
            '#ml-ptable tr:hover td{background:rgba(40,90,180,.12)}',
            '#ml-ptable .own-you{color:rgba(80,255,120,.9);font-weight:600}',
            '#ml-ptable .own-wild{color:rgba(255,190,60,.85);font-style:italic}',
            '#ml-ptable .pet-var{color:rgba(160,120,255,.85);font-style:italic}',
            '#ml-ptable .pet-mut-Golden{color:rgba(255,215,0,.95)}',
            '#ml-ptable .pet-mut-Diamond{color:rgba(185,242,255,.95)}',
            '#ml-ptable .pet-mut-Emerald{color:rgba(80,255,120,.95)}',
            '#ml-ptable .pet-mut-Rainbow{color:rgba(255,120,200,.95)}',
            '#ml-ptable .pet-mut-Galaxy{color:rgba(200,140,255,.95)}',
            '#ml-ptable .pet-rar{font-size:10px;letter-spacing:.05em}',
            '.pet-tp{background:rgba(60,140,255,.2);border:1px solid rgba(60,140,255,.3);color:rgba(140,200,255,.8);font-size:11px;padding:2px 6px;border-radius:3px;cursor:pointer;transition:all .12s;line-height:1}',
            '.pet-tp:hover{background:rgba(60,140,255,.4);color:#fff;border-color:rgba(80,170,255,.6)}',
            '.pet-grab{background:rgba(60,200,60,.2);border:1px solid rgba(60,200,60,.3);color:rgba(140,255,140,.8);font-size:11px;padding:2px 6px;border-radius:3px;cursor:pointer;transition:all .12s;line-height:1;margin-left:3px}',
            '.pet-grab:hover{background:rgba(60,200,60,.4);color:#fff;border-color:rgba(80,230,80,.6)}',
            '#ml-pets-empty{color:rgba(140,190,255,.45);text-align:center;padding:30px 8px;font-size:12px;font-style:italic}',
        ].join('');
        document.head.appendChild(s);
        const h = document.createElement('div');
        h.id = 'ml-hud';
        h.innerHTML = '<span id="ml-hud-toggle">ML</span><span class="ml-b" id="ml-fly">FLY (SPC)</span><span class="ml-b" id="ml-spr">SPR (Shft)</span><span class="ml-b" id="ml-home">SETHOME (Q)</span><span class="ml-b" id="ml-go">HOME (`)</span><span class="ml-b" id="ml-back">BACK (Z)</span><span class="ml-b" id="ml-slots">SLOTS (0/10)</span><span class="ml-b" id="ml-lock">🔒 LOCK</span><span class="ml-b act" id="ml-tp">CUDDLE (J)</span><span class="ml-b act" id="ml-pets-btn">PETS (K)</span><span class="ml-b act" id="ml-cfg">⚙ (M)</span><span class="ml-b act" id="ml-help">?</span>';
        document.body.appendChild(h);
        const d = document.createElement('div');
        d.id = 'ml-dialog';
        d.innerHTML = `<div id="ml-inner"><button id="ml-close">✕</button>${DESCRIPTION}</div>`;
        document.body.appendChild(d);

        const plist = document.createElement('div');
        plist.id = 'ml-plist';
        plist.innerHTML = '<div id="ml-plist-head"><div style="display:flex;align-items:baseline;gap:6px"><span id="ml-plist-title">Players</span><span id="ml-plist-timer">10s</span></div><div style="display:flex;gap:6px;align-items:center"><button id="ml-plist-refresh">↻</button><button id="ml-plist-close">✕</button></div></div><div id="ml-plist-body"><div id="ml-plist-empty">No other players found</div></div>';
        document.body.appendChild(plist);

        const settings = document.createElement('div');
        settings.id = 'ml-settings';
        settings.innerHTML = `<div id="ml-settings-bg"></div>
<div id="ml-settings-head"><span id="ml-settings-title">Settings</span><div style="display:flex;gap:6px;align-items:center"><button id="ml-settings-reset">Reset</button><button id="ml-settings-close">✕</button></div></div>
<div id="ml-settings-body">
<div class="ml-sgroup"><div class="ml-slabel">Movement</div>
<div class="ml-srow"><span class="ml-srow-label">Acceleration</span><div class="ml-toggle on" id="ml-s-accel-en"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Speed Cap</span><span class="ml-srow-value" id="ml-sv-cap">${SPEED_CAP}</span><input type="range" class="ml-slider" id="ml-s-cap" min="10" max="300" value="${SPEED_CAP}"></div>
<div class="ml-srow"><span class="ml-srow-label">Accel Rate</span><span class="ml-srow-value" id="ml-sv-accel">${ACCEL}</span><input type="range" class="ml-slider" id="ml-s-accel" min="5" max="60" value="${ACCEL}"></div>
<div class="ml-srow"><span class="ml-srow-label">Base Speed</span><span class="ml-srow-value" id="ml-sv-base">${SPEED_DEFAULT}</span><input type="range" class="ml-slider" id="ml-s-base" min="3" max="20" value="${SPEED_DEFAULT}"></div>
</div>
<div class="ml-sgroup"><div class="ml-slabel">Cuddle Panel</div>
<div class="ml-srow"><span class="ml-srow-label">Auto-Refresh</span><div class="ml-toggle on" id="ml-s-autorefresh"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Refresh Interval</span><span class="ml-srow-value" id="ml-sv-interval">10</span><input type="range" class="ml-slider" id="ml-s-interval" min="3" max="30" value="10"></div>
</div>
<div class="ml-sgroup"><div class="ml-slabel">Features</div>
<div class="ml-srow"><span class="ml-srow-label">Fly Hack</span><div class="ml-toggle on" id="ml-f-fly"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Speed Hack</span><div class="ml-toggle on" id="ml-f-sprint"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Waypoints</span><div class="ml-toggle on" id="ml-f-waypoints"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Cuddle Panel</span><div class="ml-toggle on" id="ml-f-cuddle"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Continuous Cuddle</span><div class="ml-toggle on" id="ml-f-cuddle-follow"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Pet Browser</span><div class="ml-toggle on" id="ml-f-pets"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Auto-Lock Base</span><div class="ml-toggle on" id="ml-f-autolock"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Anti-Knockback</span><div class="ml-toggle on" id="ml-f-antiknockback"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Noclip</span><div class="ml-toggle" id="ml-f-noclip"></div></div>
<div class="ml-srow"><span class="ml-srow-label">Fast Sync (4×)</span><div class="ml-toggle" id="ml-f-fastsync"></div></div>
</div>
<div class="ml-sgroup"><button id="ml-s-keybinds" style="width:100%;cursor:pointer;background:rgba(60,80,110,.15);border:1px solid rgba(100,130,170,.2);color:rgba(140,170,210,.5);font-size:10px;padding:6px 0;border-radius:3px;font-family:inherit;letter-spacing:.1em;text-transform:uppercase;transition:all .12s">Keybindings</button></div>
</div>`;
        document.body.appendChild(settings);

        const kbPanel = document.createElement('div');
        kbPanel.id = 'ml-keybinds';

        const KB_LABELS = {
            fly: 'Fly (toggle/up)',
            flyDown: 'Fly Down',
            setHome: 'Set Home',
            home: 'Teleport Home',
            back: 'Back Teleport',
            cuddle: 'Cuddle Panel',
            settings: 'Settings',
            pets: 'Pet Browser',
        };

        function keyCodeLabel(code) {
            if (code === 'Space') return 'Space';
            if (code === 'Backquote') return '`';
            if (code.startsWith('Key')) return code.slice(3);
            if (code.startsWith('Digit')) return code.slice(5);
            return code;
        }

        function buildKeybindsHTML() {
            let rows = '';
            for (const [action, code] of Object.entries(KEYBINDS)) {
                rows += '<div class="ml-krow" data-action="' + action + '"><span class="ml-krow-action">' + (KB_LABELS[action] || action) + '</span><span class="ml-krow-key">' + keyCodeLabel(code) + '</span></div>';
            }
            kbPanel.innerHTML = '<div id="ml-keybinds-bg"></div><div id="ml-keybinds-head"><span id="ml-keybinds-title">Keybindings</span><button id="ml-keybinds-close">✕</button></div><div id="ml-keybinds-body">' + rows + '</div>';
        }
        buildKeybindsHTML();
        document.body.appendChild(kbPanel);

        const petsPanel = document.createElement('div');
        petsPanel.id = 'ml-pets';
        petsPanel.innerHTML = '<div id="ml-pets-bg"></div><div id="ml-pets-head"><div style="display:flex;align-items:baseline;gap:6px"><span id="ml-pets-title">Pets</span><span id="ml-pets-count"></span></div><div style="display:flex;gap:8px;align-items:center"><label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:10px;color:rgba(140,190,255,.6)"><input type="checkbox" id="ml-pets-auto"> Auto</label><span style="display:flex;align-items:center;gap:3px;font-size:10px;color:rgba(140,190,255,.5)"><input type="range" id="ml-pets-interval" min="1" max="10" value="' + petRefreshInterval + '" style="width:50px;height:3px" class="ml-slider"><span id="ml-pets-iv">' + petRefreshInterval + '</span>s</span><button id="ml-pets-close">✕</button></div></div><div id="ml-pets-filter"><input id="ml-pets-search" type="text" placeholder="e.g. wild kitty, golden OR diamond, (golden OR emerald) wild"><div style="font-size:9px;color:rgba(140,190,255,.35);margin-top:2px">Words are AND\u2019d together. Use OR for alternatives, brackets to group.</div></div><div id="ml-pets-body"></div>';
        document.body.appendChild(petsPanel);

        [plist, settings, d, kbPanel, petsPanel].forEach(el => {
            ['mousedown','mouseup','click','pointerdown','pointerup','mousemove','mouseover','mouseenter','mouseleave','wheel','contextmenu','pointermove','pointerover','pointerenter'].forEach(evt => {
                el.addEventListener(evt, e => e.stopPropagation());
            });
        });

        function makeDraggable(panel, handle) {
            let dragging = false, ox = 0, oy = 0;
            handle.style.cursor = 'grab';
            panel.style.resize = 'both';
            panel.style.overflow = 'auto';
            handle.addEventListener('mousedown', e => {
                if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'LABEL') return;
                dragging = true;
                handle.style.cursor = 'grabbing';
                const rect = panel.getBoundingClientRect();
                panel.style.left = rect.left + 'px';
                panel.style.top = rect.top + 'px';
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
                panel.style.transform = 'none';
                ox = e.clientX - rect.left;
                oy = e.clientY - rect.top;
                e.preventDefault();
            });
            window.addEventListener('mousemove', e => {
                if (!dragging) return;
                panel.style.left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - 40)) + 'px';
                panel.style.top = Math.max(0, Math.min(e.clientY - oy, window.innerHeight - 40)) + 'px';
            }, true);
            window.addEventListener('mouseup', () => {
                if (dragging) { dragging = false; handle.style.cursor = 'grab'; }
            }, true);
            panel._resetPos = () => {
                panel.style.left = '';
                panel.style.top = '';
                panel.style.right = '';
                panel.style.bottom = '';
                panel.style.transform = '';
                panel.style.width = '';
                panel.style.height = '';
            };
        }
        makeDraggable(plist, $('ml-plist-head'));
        makeDraggable(settings, $('ml-settings-head'));
        makeDraggable(kbPanel, $('ml-keybinds-head'));
        makeDraggable(petsPanel, $('ml-pets-head'));
        makeDraggable(d.querySelector('#ml-inner'), d.querySelector('#ml-inner h1'));

        function syncSettingsUI() {
            syncSlider('ml-s-cap', 'ml-sv-cap', SPEED_CAP);
            syncSlider('ml-s-accel', 'ml-sv-accel', ACCEL);
            syncSlider('ml-s-base', 'ml-sv-base', SPEED_DEFAULT);
            syncSlider('ml-s-interval', 'ml-sv-interval', refreshInterval);
            const ae = $('ml-s-accel-en');
            if (ae) ae.classList.toggle('on', accelEnabled);
            const ar = $('ml-s-autorefresh');
            if (ar) ar.classList.toggle('on', autoRefresh);
            const featVals = [featFly, featSprint, featWaypoints, featCuddle, featCuddleFollow, featPets, featAutoLock, featAntiKnockback, featNoclip, featFastSync];
            FEAT_IDS.forEach((id, i) => { const el = $(id); if (el) el.classList.toggle('on', featVals[i]); });
        }
        syncSettingsUI();

        function getPlayers() {
            const holder = W.pc?.app?.root?.findByName('EnemyHolder');
            if (!holder) return [];
            const player = getPlayer();
            const playerPos = player?.getPosition();
            const results = [];
            for (const enemy of holder.children) {
                const pos = enemy.getPosition();
                let username = null;
                try { username = enemy.script?.enemy?.usernameEntity?.element?.text; } catch (_) {}
                if (!username) {
                    const ue = enemy.findByName?.('Username');
                    if (ue?.element?.text) username = ue.element.text;
                }
                if (!username || username === 'Enemy') username = enemy.name !== 'Enemy' ? enemy.name : null;
                if (!username) username = 'Player';
                const dist = playerPos ? Math.floor(playerPos.distance(pos)) : '?';
                results.push({ name: username, dist, pos: vec3(pos), entity: enemy });
            }
            results.sort((a, b) => (typeof a.dist === 'number' && typeof b.dist === 'number') ? a.dist - b.dist : 0);
            return results;
        }

        function refreshPlayerList() {
            const body = $('ml-plist-body');
            if (!body) return;
            const players = getPlayers();
            if (players.length === 0) {
                body.innerHTML = '<div id="ml-plist-empty">No other players found</div>';
                return;
            }
            body.innerHTML = '';
            players.forEach((p, i) => {
                const row = document.createElement('div');
                row.className = 'ml-prow';
                row.innerHTML = `<span class="ml-pnum">${i + 1}</span><div class="ml-pinfo"><div class="ml-pname"></div><div class="ml-pdist">${p.dist}m</div></div><span class="ml-parrow">→</span>`;
                row.querySelector('.ml-pname').textContent = p.name;
                row.addEventListener('click', () => {
                    const player = getPlayer();
                    if (!player) return;
                    const freshPos = p.entity?.getPosition();
                    const target = freshPos || p.pos;
                    backPos = player.getPosition().clone();
                    teleport(player, vec3(target));
                    saveWaypoints();
                    if (featCuddleFollow) {
                        cuddleTarget = p.entity;
                        cuddling = true;
                        console.log(`[cuddle] locked → ${p.name}`);
                    }
                    flash('ml-back');
                    const app = W.pc?.app;
                    if (app) app.fire('GameManager:GameResumed');
                });
                body.appendChild(row);
            });
        }

        function getPetOwnerId(pet) {
            if (!pet) return null;
            const d = pet.owner;
            if (d === false) return false;
            if (d != null) return d;
            return pet?.data?.owner ?? pet?.ownerId ?? pet?.data?.ownerId ?? null;
        }

        let _ownerMapCache = null;
        let _ownerMapTime = 0;
        function buildOwnerMap() {
            const now = Date.now();
            if (_ownerMapCache && now - _ownerMapTime < 2000) return _ownerMapCache;
            const map = {};
            const myId = W.pc?.sessionId;
            if (myId) map[myId] = 'Yours';
            const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
            if (nm?.sessionId && !map[nm.sessionId]) map[nm.sessionId] = 'Yours';
            const holder = W.pc?.app?.root?.findByName('EnemyHolder');
            if (holder) {
                for (const child of holder.children) {
                    let id = child.id || child.sessionId || child.playerId;
                    if (!id && child.script) {
                        for (const sk of Object.keys(child.script._scriptsIndex || {})) {
                            const inst = child.script[sk];
                            id = inst?.id || inst?.sessionId || inst?.playerId;
                            if (id) break;
                        }
                    }
                    if (!id) continue;
                    let name = null;
                    try { name = child.script?.enemy?.usernameEntity?.element?.text; } catch (_) {}
                    if (!name) { const ue = child.findByName?.('Username'); if (ue?.element?.text) name = ue.element.text; }
                    if (name && name !== 'Enemy') map[id] = name;
                }
            }
            _ownerMapCache = map;
            _ownerMapTime = now;
            return map;
        }

        function getPetOwnerName(pet, ownerMap) {
            const ownerId = getPetOwnerId(pet);
            if (ownerId === false || ownerId == null) return 'Wild';
            if (ownerMap?.[ownerId]) return ownerMap[ownerId];
            return ownerId.length > 10 ? ownerId.substring(0, 8) + '…' : ownerId;
        }

        let _pmCache = null;
        let _pmCacheTime = 0;
        function getPetsManager() {
            const now = Date.now();
            if (_pmCache && now - _pmCacheTime < 5000 && (_pmCache.activePets || _pmCache.basePets)) return _pmCache;
            const pmEntity = W.pc?.app?.root?.findByName('PetsManager');
            let pm = pmEntity?.script?.petsManager;
            if (!pm && pmEntity?.script) {
                for (const k of Object.keys(pmEntity.script)) {
                    const s = pmEntity.script[k];
                    if (s?.activePets || s?.basePets) { pm = s; break; }
                }
            }
            if (!pm) {
                const root = W.pc?.app?.root;
                if (!root) return null;
                root.find(e => {
                    if (!e.script) return false;
                    for (const k of Object.keys(e.script)) {
                        const s = e.script[k];
                        if (s?.activePets || s?.basePets) { pm = s; return true; }
                    }
                    return false;
                });
            }
            _pmCache = pm || null;
            _pmCacheTime = now;
            return _pmCache;
        }

        function getAllPets() {
            const pm = getPetsManager();
            if (!pm) return [];
            hookPetSpawn();
            const ownerMap = buildOwnerMap();
            const pets = [];
            const scan = (map, type) => {
                if (!map) return;
                map.forEach((pet, token) => {
                    const name = pet.name || String(token).substring(0, 8);
                    const sd = petSpawnData.get(token);
                    let mutation = sd?.mutation ?? '';
                    let rarity = sd?.rarity ?? '';
                    let profit = sd?.profit ?? null;
                    if (mutation === 'Default') mutation = '';
                    if (!mutation) {
                        try {
                            const mutNames = ['Golden', 'Diamond', 'Emerald', 'Rainbow', 'Galaxy'];
                            for (const mn of mutNames) {
                                if (pet.findByName?.(`Mutation ${mn} Effect`)) { mutation = mn; break; }
                            }
                        } catch (_) {}
                    }
                    if (!profit) {
                        try {
                            const statsEl = pet.findByName?.('PetStats');
                            if (statsEl) {
                                const profitEl = statsEl.findByName?.('Profit') || statsEl.findByName?.('PetProfit');
                                if (profitEl?.element?.text) {
                                    const m = profitEl.element.text.match(/\$\s*([\d,.]+)\s*([kmbtqsxi]*)/);
                                    if (m) {
                                        let v = parseFloat(m[1].replace(/,/g, ''));
                                        const suf = m[2]?.toLowerCase();
                                        if (suf === 'k') v *= 1e3;
                                        else if (suf === 'm') v *= 1e6;
                                        else if (suf === 'b') v *= 1e9;
                                        else if (suf === 't') v *= 1e12;
                                        else if (suf === 'q') v *= 1e15;
                                        profit = v;
                                    }
                                }
                            }
                        } catch (_) {}
                    }
                    const pos = pet.getPosition ? pet.getPosition() : pet.position;
                    const price = pet.price ?? 0;
                    pets.push({
                        token,
                        name,
                        price,
                        income: profit ?? 0,
                        owner: getPetOwnerName(pet, ownerMap),
                        ownerId: getPetOwnerId(pet),
                        type,
                        mutation,
                        rarity,
                        x: pos?.x ?? 0,
                        y: pos?.y ?? 0,
                        z: pos?.z ?? 0,
                    });
                });
            };
            scan(pm.activePets, 'active');
            scan(pm.basePets, 'base');
            return pets;
        }

        let petSortCol = 'income';
        let petSortDir = -1;

        let lastRenderedPets = [];
        const grabbedTokens = new Map(); // token → expiry timestamp

        function renderPetTable() {
            const body = $('ml-pets-body');
            const countEl = $('ml-pets-count');
            if (!body) return;
            let pets = getAllPets();
            if (countEl) countEl.textContent = `(${pets.length})`;
            if (pets.length === 0) {
                lastRenderedPets = [];
                body.innerHTML = '<div id="ml-pets-empty">No pets found in this room</div>';
                return;
            }
            if (petFilter) pets = pets.filter(p => petMatchesFilter(p, petFilter));
            const playerPos = getPlayer()?.getPosition();
            if (playerPos) pets.forEach(p => { p._dist = Math.hypot(p.x - playerPos.x, p.y - playerPos.y, p.z - playerPos.z); });
            pets.sort((a, b) => {
                const key = petSortCol === 'dist' ? '_dist' : petSortCol;
                let av = a[key], bv = b[key];
                if (av == null) av = Infinity;
                if (bv == null) bv = Infinity;
                if (typeof av === 'string') av = av.toLowerCase();
                if (typeof bv === 'string') bv = bv.toLowerCase();
                if (av < bv) return -petSortDir;
                if (av > bv) return petSortDir;
                return 0;
            });
            lastRenderedPets = pets;
            const cols = ['name','mutation','rarity','owner','price','income','dist','go'];
            const labels = { name:'Name', mutation:'Mutation', rarity:'Rarity', owner:'Owner', price:'Worth', income:'Income/s', dist:'Dist', go:'' };
            let html = '<table id="ml-ptable"><thead><tr>';
            cols.forEach(c => {
                const cls = petSortCol === c ? (petSortDir === 1 ? 'sort-asc' : 'sort-desc') : '';
                html += c === 'go' ? '<th></th>' : `<th data-col="${c}" class="${cls}">${labels[c]}</th>`;
            });
            html += '</tr></thead><tbody>';
            const now = Date.now();
            pets.forEach(p => {
                const ownerCls = p.owner === 'Yours' ? 'own-you' : p.owner === 'Wild' ? 'own-wild' : '';
                const mutCls = p.mutation ? `pet-mut-${p.mutation}` : 'pet-var';
                const tokenStr = String(p.token);
                const grabbed = grabbedTokens.has(tokenStr) && grabbedTokens.get(tokenStr) > now;
                const grabBtn = p.owner !== 'Yours' ? `<button class="pet-grab" data-token="${esc(p.token)}"${grabbed ? ' disabled' : ''} title="Pick up">${grabbed ? '\u2714' : '\u270B'}</button>` : '';
                html += `<tr><td>${esc(p.name)}</td><td class="${mutCls}">${esc(p.mutation) || '\u2014'}</td><td class="pet-rar">${esc(p.rarity) || '\u2014'}</td><td class="${ownerCls}">${esc(p.owner)}</td><td>${numFmt(p.price)}</td><td>${numFmt(p.income)}/s</td><td>${p._dist != null ? numFmt(Math.round(p._dist)) : '\u2014'}</td><td><button class="pet-tp" data-token="${esc(p.token)}" title="Teleport">\u279C</button>${grabBtn}</td></tr>`;
            });
            html += '</tbody></table>';
            body.innerHTML = html;
        }

        function findPetByToken(token) {
            return lastRenderedPets.find(p => String(p.token) === token) || null;
        }

        $('ml-pets-body').addEventListener('click', e => {
            const th = e.target.closest('th[data-col]');
            if (th) {
                const col = th.dataset.col;
                if (petSortCol === col) petSortDir *= -1;
                else { petSortCol = col; petSortDir = col === 'price' || col === 'income' || col === 'dist' ? -1 : 1; }
                renderPetTable();
                return;
            }
            const tpBtn = e.target.closest('.pet-tp');
            if (tpBtn) {
                const p = findPetByToken(tpBtn.dataset.token);
                if (!p) return;
                const player = getPlayer();
                if (!player) return;
                player.setPosition(p.x, p.y + 1, p.z);
                togglePetsPanel(false);
                return;
            }
            const grabBtn = e.target.closest('.pet-grab');
            if (grabBtn) {
                const p = findPetByToken(grabBtn.dataset.token);
                if (!p) return;
                const app = W.pc?.app;
                if (!app) return;
                const player = getPlayer();
                if (!player) return;
                const tokenStr = String(p.token);
                const orig = player.getPosition().clone();
                player.setPosition(p.x, p.y + 0.5, p.z);
                grabBtn.disabled = true;
                grabBtn.textContent = '…';
                setTimeout(() => {
                    app.fire('ModeOverlay:BuyPet', p.token);
                    console.log(`[ml] grab pet: ${p.name} (${p.token})`);
                    setTimeout(() => {
                        player.setPosition(orig.x, orig.y, orig.z);
                        setTimeout(() => {
                            const tgt = app.graphicsDevice?.canvas || document;
                            const opts = { code: 'KeyX', key: 'x', keyCode: 88, which: 88, bubbles: true, cancelable: true };
                            tgt.dispatchEvent(new KeyboardEvent('keydown', opts));
                            setTimeout(() => {
                                tgt.dispatchEvent(new KeyboardEvent('keyup', opts));
                                console.log(`[ml] drop pet at origin`);
                                grabbedTokens.set(tokenStr, Date.now() + 5000);
                                renderPetTable();
                            }, 50);
                        }, 150);
                    }, 150);
                }, 100);
            }
        });

        function esc(s) {
            return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
        }
        function numFmt(n) {
            if (n >= 1e15) return (n / 1e15).toFixed(1) + 'q';
            if (n >= 1e12) return (n / 1e12).toFixed(1) + 't';
            if (n >= 1e9) return (n / 1e9).toFixed(1) + 'b';
            if (n >= 1e6) return (n / 1e6).toFixed(1) + 'm';
            if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
            return n % 1 === 0 ? String(n) : n.toFixed(1);
        }
        function petMatchesFilter(p, raw) {
            if (!raw) return true;
            const text = [p.name, p.owner, p.mutation, p.rarity].join(' ').toLowerCase();
            const tokens = raw.match(/\(|\)|AND|OR|[^\s()]+/gi) || [];
            function parseOr(i) {
                let [result, j] = parseAnd(i);
                while (j < tokens.length && tokens[j]?.toUpperCase() === 'OR') {
                    const [r2, j2] = parseAnd(j + 1);
                    result = result || r2;
                    j = j2;
                }
                return [result, j];
            }
            function parseAnd(i) {
                let [result, j] = parseAtom(i);
                while (j < tokens.length) {
                    const up = tokens[j]?.toUpperCase();
                    if (up === 'AND') { j++; }
                    else if (up === 'OR' || tokens[j] === ')') break;
                    const [r2, j2] = parseAtom(j);
                    result = result && r2;
                    j = j2;
                }
                return [result, j];
            }
            function parseAtom(i) {
                if (i >= tokens.length) return [false, i];
                if (tokens[i] === '(') {
                    const [result, j] = parseOr(i + 1);
                    return [result, j < tokens.length && tokens[j] === ')' ? j + 1 : j];
                }
                const t = tokens[i].toLowerCase();
                return [text.includes(t), i + 1];
            }
            try { return parseOr(0)[0]; } catch (_) { return text.includes(raw.toLowerCase()); }
        }

        let petRefreshTimer = null;
        function startPetRefresh() {
            stopPetRefresh();
            petRefreshTimer = setInterval(() => renderPetTable(), petRefreshInterval * 1000);
        }
        function stopPetRefresh() {
            if (petRefreshTimer) { clearInterval(petRefreshTimer); petRefreshTimer = null; }
        }
        function resumeGame() {
            const app = W.pc?.app;
            const canvas = app?.graphicsDevice?.canvas;
            if (canvas) canvas.requestPointerLock();
            if (app) app.fire('GameManager:GameResumed');
        }
        function togglePetsPanel(forceOpen) {
            const open = forceOpen !== undefined ? forceOpen : !petsPanel.classList.contains('open');
            petsPanel.classList.toggle('open', open);
            if (open) {
                $('ml-pets-search').value = petFilter;
                $('ml-pets-auto').checked = petAutoRefresh;
                syncSlider('ml-pets-interval', 'ml-pets-iv', petRefreshInterval);
                renderPetTable();
                if (petAutoRefresh) startPetRefresh();
                if (document.pointerLockElement) document.exitPointerLock();
            } else {
                stopPetRefresh();
                petsPanel._resetPos?.();
                resumeGame();
            }
        }

        $('ml-pets-btn').addEventListener('click', () => togglePetsPanel());
        $('ml-pets-close').addEventListener('click', () => togglePetsPanel(false));
        $('ml-pets-search').addEventListener('input', e => {
            petFilter = e.target.value;
            saveSettings();
            renderPetTable();
        });
        $('ml-pets-auto').addEventListener('change', e => {
            petAutoRefresh = e.target.checked;
            saveSettings();
            if (petAutoRefresh && petsPanel.classList.contains('open')) startPetRefresh();
            else stopPetRefresh();
        });
        $('ml-pets-interval').addEventListener('input', e => {
            petRefreshInterval = parseInt(e.target.value);
            const iv = $('ml-pets-iv');
            if (iv) iv.textContent = petRefreshInterval;
            saveSettings();
            if (petAutoRefresh && petsPanel.classList.contains('open')) startPetRefresh();
        });

        function toggleCuddlePanel(forceOpen) {
            const wasOpen = plist.classList.contains('open');
            const open = forceOpen !== undefined ? forceOpen : !wasOpen;
            plist.classList.toggle('open', open);
            if (open) {
                refreshPlayerList();
                if (document.pointerLockElement) document.exitPointerLock();
                if (!wasOpen && autoRefresh) startRefreshTimer();
            } else if (wasOpen) {
                stopRefreshTimer();
                plist._resetPos?.();
                resumeGame();
            }
        }

        const origRequestPointerLock = Element.prototype.requestPointerLock;
        Element.prototype.requestPointerLock = function() {
            if (plist.classList.contains('open') || settings.classList.contains('open') || kbPanel.classList.contains('open') || petsPanel.classList.contains('open') || d.classList.contains('open')) return;
            return origRequestPointerLock.call(this);
        };

        const anyPanelOpen = () => plist.classList.contains('open') || settings.classList.contains('open') || kbPanel.classList.contains('open') || petsPanel.classList.contains('open') || d.classList.contains('open');
        document.addEventListener('pointerlockchange', e => {
            if (anyPanelOpen() && !document.pointerLockElement) {
                e.stopImmediatePropagation();
            }
        }, true);

        let refreshCountdown = refreshInterval;
        let refreshTimer = null;

        function startRefreshTimer() {
            stopRefreshTimer();
            refreshCountdown = refreshInterval;
            const timerEl = $('ml-plist-timer');
            if (timerEl) timerEl.textContent = refreshCountdown + 's';
            refreshTimer = setInterval(() => {
                refreshCountdown--;
                if (timerEl) timerEl.textContent = refreshCountdown + 's';
                if (refreshCountdown <= 0) {
                    refreshPlayerList();
                    refreshCountdown = refreshInterval;
                    if (timerEl) timerEl.textContent = refreshCountdown + 's';
                }
            }, 1000);
        }
        function stopRefreshTimer() {
            if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
        }

        $('ml-tp').addEventListener('click', () => toggleCuddlePanel());
        $('ml-plist-refresh').addEventListener('click', () => {
            refreshPlayerList();
            if (autoRefresh && plist.classList.contains('open')) startRefreshTimer();
        });
        $('ml-plist-close').addEventListener('click', () => toggleCuddlePanel(false));

        function toggleSettings(forceOpen) {
            const open = forceOpen !== undefined ? forceOpen : !settings.classList.contains('open');
            settings.classList.toggle('open', open);
            if (open && document.pointerLockElement) document.exitPointerLock();
            else if (!open) { settings._resetPos?.(); resumeGame(); }
        }
        $('ml-cfg').addEventListener('click', () => toggleSettings());
        $('ml-settings-close').addEventListener('click', () => toggleSettings(false));

        const DEFAULTS = { ACCEL: 6, SPEED_CAP: 100, SPEED_DEFAULT: 7, refreshInterval: 10, petRefreshInterval: 1 };
        $('ml-settings-reset').addEventListener('click', () => {
            ACCEL = DEFAULTS.ACCEL;
            SPEED_CAP = DEFAULTS.SPEED_CAP;
            SPEED_DEFAULT = DEFAULTS.SPEED_DEFAULT;
            refreshInterval = DEFAULTS.refreshInterval;
            petRefreshInterval = DEFAULTS.petRefreshInterval;
            accelEnabled = true;
            autoRefresh = true;
            featFly = true; featSprint = true; featWaypoints = true; featCuddle = true; featCuddleFollow = true; featPets = true; featAutoLock = true;
            featAntiKnockback = true; featNoclip = false; featFastSync = false;
            Object.assign(KEYBINDS, DEFAULT_KEYBINDS);
            syncSettingsUI();
            updateHUDBadgeLabels();
            console.log('[settings] reset');
            saveSettings();
        });

        const sliderMap = [
            { id: 'ml-s-cap',     valId: 'ml-sv-cap',      apply: v => { SPEED_CAP = v; } },
            { id: 'ml-s-accel',   valId: 'ml-sv-accel',    apply: v => { ACCEL = v; } },
            { id: 'ml-s-base',    valId: 'ml-sv-base',     apply: v => { SPEED_DEFAULT = v; } },
            { id: 'ml-s-interval',valId: 'ml-sv-interval', apply: v => { refreshInterval = v; } },
        ];
        sliderMap.forEach(s => {
            const el = $(s.id);
            const valEl = $(s.valId);
            if (!el) return;
            el.addEventListener('input', () => {
                const v = parseInt(el.value);
                if (valEl) valEl.textContent = v;
                s.apply(v);
                saveSettings();
            });
        });

        const arToggle = $('ml-s-autorefresh');
        if (arToggle) {
            arToggle.addEventListener('click', () => {
                autoRefresh = !autoRefresh;
                arToggle.classList.toggle('on', autoRefresh);
                saveSettings();
                const timerEl = $('ml-plist-timer');
                if (!autoRefresh) {
                    stopRefreshTimer();
                    if (timerEl) timerEl.textContent = 'off';
                } else if (plist.classList.contains('open')) {
                    startRefreshTimer();
                }
            });
        }

        const accelToggle = $('ml-s-accel-en');
        if (accelToggle) {
            accelToggle.addEventListener('click', () => {
                accelEnabled = !accelEnabled;
                accelToggle.classList.toggle('on', accelEnabled);
                saveSettings();
            });
        }

        const featToggles = [
            { id: 'ml-f-fly',       get: () => featFly,       set: v => { featFly = v; if (!v) { const p = getPlayer(); const k = getKcc(p); if (k && flyActive) flyOff(k); } } },
            { id: 'ml-f-sprint',    get: () => featSprint,    set: v => { featSprint = v; if (!v) sprinting = false; } },
            { id: 'ml-f-waypoints', get: () => featWaypoints, set: v => { featWaypoints = v; } },
            { id: 'ml-f-cuddle',       get: () => featCuddle,       set: v => { featCuddle = v; if (!v) toggleCuddlePanel(false); } },
            { id: 'ml-f-cuddle-follow', get: () => featCuddleFollow, set: v => { featCuddleFollow = v; if (!v) { cuddling = false; cuddleTarget = null; } } },
            { id: 'ml-f-pets',      get: () => featPets,      set: v => { featPets = v; if (!v) togglePetsPanel(false); } },
            { id: 'ml-f-autolock',  get: () => featAutoLock,  set: v => { featAutoLock = v; } },
            { id: 'ml-f-antiknockback', get: () => featAntiKnockback, set: v => { featAntiKnockback = v; if (!v) antiKnockbackReady = false; } },
            { id: 'ml-f-noclip', get: () => featNoclip, set: v => { featNoclip = v; } },
            { id: 'ml-f-fastsync', get: () => featFastSync, set: v => { featFastSync = v; } },
        ];
        featToggles.forEach(ft => {
            const el = $(ft.id);
            if (!el) return;
            el.addEventListener('click', () => {
                const nv = !ft.get();
                ft.set(nv);
                el.classList.toggle('on', nv);
                saveSettings();
                console.log(`[feat] ${ft.id.replace('ml-f-','')} = ${nv}`);
            });
        });

        function toggleKeybinds(forceOpen) {
            const open = forceOpen !== undefined ? forceOpen : !kbPanel.classList.contains('open');
            kbPanel.classList.toggle('open', open);
            if (open) {
                buildKeybindsHTML();
                if (document.pointerLockElement) document.exitPointerLock();
            } else {
                kbListeningRow = null;
                kbPanel._resetPos?.();
                resumeGame();
            }
        }
        kbPanel.addEventListener('click', e => {
            if (e.target.closest('#ml-keybinds-close')) { toggleKeybinds(false); return; }
            const row = e.target.closest('.ml-krow');
            if (!row) return;
            kbPanel.querySelectorAll('.ml-krow.listening').forEach(r => r.classList.remove('listening'));
            if (kbListeningRow === row) { kbListeningRow = null; return; }
            row.classList.add('listening');
            row.querySelector('.ml-krow-key').textContent = '...';
            kbListeningRow = row;
        });

        window.addEventListener('keydown', e => {
            if (!kbListeningRow) return;
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            const action = kbListeningRow.dataset.action;
            if (!action || !(action in KEYBINDS)) return;
            if (e.code === 'Escape') {
                kbListeningRow.classList.remove('listening');
                kbListeningRow.querySelector('.ml-krow-key').textContent = keyCodeLabel(KEYBINDS[action]);
                kbListeningRow = null;
                return;
            }
            KEYBINDS[action] = e.code;
            kbListeningRow.querySelector('.ml-krow-key').textContent = keyCodeLabel(e.code);
            kbListeningRow.classList.remove('listening');
            kbListeningRow = null;
            saveSettings();
            updateHUDBadgeLabels();
            console.log('[keybind] ' + action + ' → ' + e.code);
        }, true);

        function updateHUDBadgeLabels() {
            const map = {
                'ml-fly':  ['FLY', 'fly'],
                'ml-spr':  ['SPR', null],
                'ml-home': ['SETHOME', 'setHome'],
                'ml-go':   ['HOME', 'home'],
                'ml-back': ['BACK', 'back'],
                'ml-tp':   ['CUDDLE', 'cuddle'],
                'ml-pets-btn': ['PETS', 'pets'],
                'ml-cfg':  ['⚙', 'settings'],
            };
            for (const [id, [label, action]] of Object.entries(map)) {
                const el = $(id);
                if (!el) continue;
                if (!action) { el.textContent = label + ' (Shft)'; continue; }
                el.textContent = label + ' (' + keyCodeLabel(KEYBINDS[action]) + ')';
            }
        }
        updateHUDBadgeLabels();

        $('ml-s-keybinds').addEventListener('click', () => {
            settings.classList.remove('open');
            toggleKeybinds(true);
        });

        $('ml-hud-toggle').addEventListener('click', e => {
            if (e.target._wasDragged) { e.target._wasDragged = false; return; }
            const hud = $('ml-hud');
            const wasCollapsed = hud.classList.contains('collapsed');
            hud.classList.toggle('collapsed');
            if (wasCollapsed) {
                hud.style.left = '';
                hud.style.top = '';
                hud.style.right = '';
                hud.style.bottom = '';
            }
        });
        (function() {
            const toggle = $('ml-hud-toggle');
            const hud = $('ml-hud');
            let dragging = false, ox = 0, oy = 0, moved = false;
            toggle.addEventListener('pointerdown', e => {
                dragging = true; moved = false;
                const rect = hud.getBoundingClientRect();
                ox = e.clientX - rect.left;
                oy = e.clientY - rect.top;
                toggle.setPointerCapture(e.pointerId);
            });
            toggle.addEventListener('pointermove', e => {
                if (!dragging) return;
                moved = true;
                hud.style.left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - 40)) + 'px';
                hud.style.top = Math.max(0, Math.min(e.clientY - oy, window.innerHeight - 40)) + 'px';
                hud.style.right = 'auto';
                hud.style.bottom = 'auto';
            });
            toggle.addEventListener('pointerup', () => {
                dragging = false;
                if (moved) toggle._wasDragged = true;
            });
        })();

        $('ml-help').addEventListener('click', () => {
            d.classList.add('open');
            if (document.pointerLockElement) document.exitPointerLock();
        });
        $('ml-close').addEventListener('click', () => { d.classList.remove('open'); d.querySelector('#ml-inner')._resetPos?.(); resumeGame(); });
        d.addEventListener('click', e => { if (e.target === d) { d.classList.remove('open'); d.querySelector('#ml-inner')._resetPos?.(); resumeGame(); } });
        d.addEventListener('wheel', e => { e.stopPropagation(); e.preventDefault(); d.scrollTop += e.deltaY; }, { passive: false });

        document.addEventListener('click', e => {
            if (plist.classList.contains('open') && !plist.contains(e.target) && e.target.id !== 'ml-tp') {
                toggleCuddlePanel(false);
            }
            if (settings.classList.contains('open') && !settings.contains(e.target) && e.target.id !== 'ml-cfg') {
                toggleSettings(false);
            }
            if (kbPanel.classList.contains('open') && !kbPanel.contains(e.target)) {
                toggleKeybinds(false);
            }
            if (petsPanel.classList.contains('open') && !petsPanel.contains(e.target) && e.target.id !== 'ml-pets-btn') {
                togglePetsPanel(false);
            }
        });
    }
    const petSpawnData = new Map();
    const PET_SPAWN_CAP = 2000;
    function hookPetSpawn() {
        const app = W.pc?.app;
        if (!app) return;
        if (app._mlPetSpawnHooked) return;
        app._mlPetSpawnHooked = true;
        const onPetsSpawn = data => {
            if (!Array.isArray(data)) return;
            if (petSpawnData.size > PET_SPAWN_CAP) {
                const excess = petSpawnData.size - PET_SPAWN_CAP + data.length;
                const it = petSpawnData.keys();
                for (let i = 0; i < excess; i++) petSpawnData.delete(it.next().value);
            }
            for (const p of data) {
                if (!p.token) continue;
                petSpawnData.set(p.token, {
                    profit: p.profit ?? null,
                    rarity: p.rarity ?? null,
                    mutation: p.mutation ?? null,
                    isEgg: !!p.isEgg,
                });
            }
        };
        app.on('PetsManager:PetsSpawn', onPetsSpawn);
        app.on('BasesManager:PetsSpawn', onPetsSpawn);
        console.log('[ml] pet spawn hook installed');
    }

    // ── Auto-Lock Base ──────────────────────────────────────
    function getMyBase() {
        const app = W.pc?.app;
        if (!app?.root) return null;
        const basesEntity = app.root.findByName('Bases');
        const basesScript = basesEntity?.script?.petTycoonBasesManager;
        if (!basesScript?.activeBases) return null;
        const nm = app.root.findByName('NetworkManager')?.script?.networkManager;
        const sessionId = nm?.room?.sessionId || W.pc?.sessionId;
        if (!sessionId) return null;
        for (const bd of basesScript.activeBases) {
            if (bd?.data?.sessionId === sessionId) return basesScript.baseEntities?.[bd.data.id] ?? null;
        }
        return null;
    }

    function getLockBtn() {
        const base = getMyBase();
        if (!base) return null;
        const btn = base.findByName('LockdownButton');
        return btn?.script?.lockdownButton ?? null;
    }

    function triggerLock() {
        const btn = getLockBtn();
        if (!btn || typeof btn.onTriggerEnter !== 'function') return false;
        const player = getPlayer();
        if (!player) return false;
        const wasTrig = btn.hasTriggered;
        btn.onTriggerEnter(player);
        console.log('[ml] auto-lock: triggered (wasTriggered=%s)', wasTrig);
        return true;
    }

    let lastLockCheck = 0;

    // ── Anti-Disconnect ─────────────────────────────────────
    // Server kicks players whose position is inside a locked base.
    // Spoof outgoing position to last safe location when inside one.
    let lastSafePos = null;
    const BASE_RADIUS_SQ = 20 * 20;

    function isInsideLockedBase(x, z) {
        const basesScript = W.pc?.app?.root?.findByName('Bases')?.script?.petTycoonBasesManager;
        if (!basesScript?.activeBases) return false;
        const myId = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager?.room?.sessionId || W.pc?.sessionId;
        for (const entry of basesScript.activeBases) {
            if (!entry?.entity || entry.data?.sessionId === myId) continue;
            const lockBtn = entry.entity.findByName('LockdownButton')?.script?.lockdownButton;
            if (!lockBtn?.isLockdownActive || !(lockBtn.lockdownTimeLeft > 0)) continue;
            const bp = entry.entity.getPosition();
            const dx = x - bp.x, dz = z - bp.z;
            if (dx * dx + dz * dz < BASE_RADIUS_SQ) return true;
        }
        return false;
    }

    function hookAntiDisconnect() {
        const nm = W.pc?.app?.root?.findByName('NetworkManager')?.script?.networkManager;
        const room = nm?.room;
        if (!room || room.__mlSendHooked) return;
        room.__mlSendHooked = true;
        const origSend = room.send.bind(room);
        room.send = function (type, message) {
            if (type === 'p' && message) {
                if (isInsideLockedBase(message.x, message.z)) {
                    const safe = lastSafePos || (() => {
                        const b = getMyBase()?.getPosition();
                        return b ? { x: b.x, y: b.y + 1, z: b.z } : null;
                    })();
                    if (safe) return origSend(type, { x: safe.x, y: safe.y, z: safe.z, w: message.w });
                } else {
                    lastSafePos = { x: message.x, y: message.y, z: message.z };
                }
            }
            return origSend(type, message);
        };
        console.log('[ml] anti-disconnect: position spoof active');
    }

    // ── Anti-Death ──────────────────────────────────────────
    let antiDeathReady = false;

    function hookAntiDeath() {
        if (antiDeathReady) return;
        const app = W.pc?.app;
        if (!app) return;

        app.root.find(entity => {
            if (!entity.script?._scripts) return false;
            for (const inst of entity.script._scripts) {
                if (inst && typeof inst.deathYThreshold === 'number' && inst.deathYThreshold > -9999) {
                    inst.deathYThreshold = -99999;
                    console.log('[ml] anti-death: Y threshold → -99999 on %s', entity.name);
                }
            }
            return false;
        });

        const origFire = app.fire;
        app.fire = function (event, ...args) {
            if (event === 'DeathScreen:Trigger') return this;
            if (event === 'PlayerController:GotHit' && featAntiKnockback) return this;
            return origFire.call(this, event, ...args);
        };

        antiDeathReady = true;
        console.log('[ml] anti-death: ready');
    }

    // ── Anti-Knockback ──────────────────────────────────────
    let antiKnockbackReady = false;

    function hookAntiKnockback() {
        if (antiKnockbackReady || !featAntiKnockback) return;
        const player = getPlayer();
        const pc = getPC(player);
        if (!pc) return;
        pc.shockedTime = 0;
        pc.knockbackDecay = 1.0;
        antiKnockbackReady = true;
    }

    // ── Noclip ──────────────────────────────────────────────
    let noclipHooked = false;

    function hookNoclip() {
        if (noclipHooked) return;
        const player = getPlayer();
        const kcc = getKcc(player);
        if (!kcc || !kcc.update) return;
        const origUpdate = kcc.update.bind(kcc);
        kcc.update = function (dt) {
            if (!featNoclip) return origUpdate(dt);
            const pos = this.entity.getPosition();
            const preX = pos.x, preZ = pos.z;
            origUpdate(dt);
            const speed = this.speed || 7;
            const h = this._horizontal || 0, v = this._vertical || 0;
            const newPos = this.entity.getPosition();
            this.entity.setPosition(preX + h * speed * dt, newPos.y, preZ + v * speed * dt);
        };
        noclipHooked = true;
        console.log('[ml] noclip: KCC update hooked');
    }

    // ── Fast Sync ───────────────────────────────────────────
    let fastSyncReady = false;

    function hookFastSync() {
        if (fastSyncReady) return;
        const player = getPlayer();
        const pc = getPC(player);
        if (!pc) return;
        pc._origBroadcastInterval = pc.broadcastInterval;
        fastSyncReady = true;
    }

    createHUD();
    loadWaypoints();

    window.addEventListener('keydown', e => {
        const active = document.activeElement;
        if (active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA') return;
        if (kbListeningRow) return;

        if (e.code === KEYBINDS.cuddle) {
            e.preventDefault();
            if (featCuddle) $('ml-tp')?.click();
            return;
        }

        if (e.code === KEYBINDS.settings) {
            e.preventDefault();
            $('ml-cfg')?.click();
            return;
        }

        if (e.code === KEYBINDS.pets) {
            e.preventDefault();
            if (featPets) $('ml-pets-btn')?.click();
            return;
        }

        const player = getPlayer();
        const kcc    = getKcc(player);
        if (!kcc) return;

        if (cuddling && MOVE_KEYS.has(e.code)) {
            cuddling = false;
            cuddleTarget = null;
            console.log('[cuddle] cancelled');
        }

        const numpadMatch = e.code.match(/^Numpad(\d)$/);
        if (numpadMatch && featWaypoints) {
            const idx = parseInt(numpadMatch[1]);
            if (e.ctrlKey) {
                e.preventDefault();
                e.stopPropagation();
                slots[idx] = player.getPosition().clone();
                saveWaypoints();
                flash('ml-slots');
                console.log(`[slot ${idx}] saved`, slots[idx]);
                return;
            } else if (slots[idx]) {
                e.preventDefault();
                e.stopPropagation();
                backPos = player.getPosition().clone();
                teleport(player, slots[idx]);
                saveWaypoints();
                flash('ml-slots');
                console.log(`[slot ${idx}] teleported to`, slots[idx]);
                return;
            }
        }

        if (e.code === KEYBINDS.fly) {
            e.stopPropagation();
            if (!featFly) return;
            if (flyActive) {
                flyUp = true;
            } else {
                flyOn(kcc);
            }
            return;
        }

        if (e.code === KEYBINDS.flyDown && flyActive) { flyDown = true; return; }

        if (e.key === 'Shift' && !sprinting && featSprint) { sprinting = true; return; }

        if (e.code === KEYBINDS.setHome) {
            e.preventDefault();
            if (!featWaypoints) return;
            homePos = player.getPosition().clone();
            saveWaypoints();
            flash('ml-home');
            return;
        }

        if (e.code === KEYBINDS.home) {
            e.preventDefault();
            if (!featWaypoints) return;
            if (homePos) {
                backPos = player.getPosition().clone();
                teleport(player, homePos);
                saveWaypoints();
                flash('ml-go');
            }
            return;
        }

        if (e.code === KEYBINDS.back) {
            e.preventDefault();
            if (!featWaypoints) return;
            if (backPos) {
                const cur = player.getPosition().clone(); teleport(player, backPos); backPos = cur;
                saveWaypoints();
                flash('ml-back');
            }
        }
    }, true);

    window.addEventListener('keyup', e => {
        if (e.code === KEYBINDS.fly)     flyUp   = false;
        if (e.code === KEYBINDS.flyDown) flyDown = false;
        if (e.key === 'Shift' && sprinting) { sprinting = false; }
    }, true);

    const hud = { fly: $('ml-fly'), spr: $('ml-spr'), home: $('ml-home'), go: $('ml-go'), back: $('ml-back'), slots: $('ml-slots'), lock: $('ml-lock'), tp: $('ml-tp'), petsBtn: $('ml-pets-btn'), cfg: $('ml-cfg'), help: $('ml-help') };
    const panels = { plist: $('ml-plist'), settings: $('ml-settings'), dialog: $('ml-dialog'), pets: $('ml-pets') };

    setInterval(() => {
        hookPetSpawn();
        hookAntiDisconnect();
        hookAntiDeath();
        hookAntiKnockback();
        hookNoclip();
        hookFastSync();
        const player = getPlayer();
        if (!player) return;
        const kcc = getKcc(player);
        if (!kcc) return;

        const now = Date.now();
        const dt  = Math.min((now - prevTick) / 1000, 0.1);
        prevTick  = now;

        const pc = getPC(player);
        if (pc?.isDied) { pc.isDied = false; }

        if (fastSyncReady && pc) {
            pc.broadcastInterval = featFastSync ? Math.round(pc._origBroadcastInterval / 4) : pc._origBroadcastInterval;
        }
        if (sprinting) {
            const kb = W.pc?.app?.keyboard;
            if (kb && !kb.isPressed(W.pc.KEY_SHIFT)) { sprinting = false; }
        }
        if (sprinting) {
            const sprintMin = SPEED_DEFAULT * 3;
            if (sprintSpeed < sprintMin) sprintSpeed = sprintMin;
            if (accelEnabled) sprintSpeed = Math.min(sprintSpeed + ACCEL * dt, SPEED_CAP);
            setSpeed(pc, kcc, sprintSpeed);
            if (pc?.isSlowedSpeed) pc.isSlowedSpeed = false;
        } else if (sprintSpeed !== SPEED_DEFAULT) {
            sprintSpeed = SPEED_DEFAULT;
            setSpeed(pc, kcc, SPEED_DEFAULT);
        }

        if (hud.fly)  { hud.fly.classList.toggle('on', flyActive); hud.fly.classList.toggle('disabled', !featFly); }
        if (hud.spr)  { hud.spr.classList.toggle('on', sprinting); hud.spr.classList.toggle('disabled', !featSprint); }
        if (hud.home) hud.home.classList.toggle('disabled', !featWaypoints);
        if (hud.go)   hud.go.classList.toggle('disabled', !featWaypoints);
        if (hud.back) hud.back.classList.toggle('disabled', !featWaypoints);
        if (hud.slots) { const n = slots.filter(Boolean).length; hud.slots.textContent = `SLOTS ${n}/10`; hud.slots.classList.toggle('disabled', !featWaypoints); }
        if (hud.tp)   { hud.tp.classList.toggle('disabled', !featCuddle); hud.tp.classList.toggle('on', !!panels.plist?.classList.contains('open')); }
        if (hud.petsBtn) { hud.petsBtn.classList.toggle('disabled', !featPets); hud.petsBtn.classList.toggle('on', !!panels.pets?.classList.contains('open')); }
        if (hud.cfg)  hud.cfg.classList.toggle('on', !!panels.settings?.classList.contains('open'));
        if (hud.help) hud.help.classList.toggle('on', !!panels.dialog?.classList.contains('open'));

        // Auto-lock base: check every ~100ms
        if (featAutoLock && now - lastLockCheck > 100) {
            lastLockCheck = now;
            const lockBtn = getLockBtn();
            if (lockBtn) {
                const timeLeft = lockBtn.lockdownTimeLeft || 0;
                const isActive = lockBtn.isLockdownActive || false;
                if (hud.lock) {
                    hud.lock.classList.toggle('on', isActive && timeLeft > 0);
                    hud.lock.textContent = (isActive && timeLeft > 0) ? `\ud83d\udd12 ${Math.ceil(timeLeft)}s` : '\ud83d\udd13 UNLOCKED';
                }
                if (!isActive || timeLeft <= 0) triggerLock();
            } else if (hud.lock) {
                hud.lock.textContent = '\ud83d\udd12 LOCK';
                hud.lock.classList.remove('on');
            }
        }
        if (hud.lock) hud.lock.classList.toggle('disabled', !featAutoLock);

        if (cuddling && cuddleTarget && featCuddleFollow) {
            const tPos = cuddleTarget.getPosition?.();
            if (tPos) {
                kcc.gravity = 0;
                kcc._velY = 0;
                teleport(player, vec3(tPos));
            } else {
                cuddling = false;
                cuddleTarget = null;
                console.log('[cuddle] target lost');
            }
        }

        if (!flyActive) return;

        if (kcc._grounded) { flyOff(kcc, false); return; }

        if (kcc.gravity !== 0) kcc.gravity = 0;

        flyVelY = kcc._velY;

        if (flyUp) {
            if (flyVelY < 0) {
                flyVelY = 0;
            } else if (accelEnabled) {
                if (flyVelY < FLY_MIN_SPEED) flyVelY = FLY_MIN_SPEED;
                flyVelY = Math.min(flyVelY + ACCEL * dt, SPEED_CAP);
            } else {
                flyVelY = SPEED_CAP;
            }
        } else if (flyDown) {
            if (flyVelY > 0) {
                flyVelY = 0;
            } else if (accelEnabled) {
                if (flyVelY > -FLY_MIN_SPEED) flyVelY = -FLY_MIN_SPEED;
                flyVelY = Math.max(flyVelY - ACCEL * dt, -SPEED_CAP);
            } else {
                flyVelY = -SPEED_CAP;
            }
        } else {
            flyVelY = 0;
        }

        kcc._velY = flyVelY;
    }, 16);

})();