PaperPilot

Painel de controle para Paper.io — velocidade, zoom, ESP, bots, arena, turbo e bot auto-play. Funciona em paperio.site, inclusive no modo Conflict.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         PaperPilot
// @namespace    paperpilot.userscript
// @version      1.0.0
// @description  Painel de controle para Paper.io — velocidade, zoom, ESP, bots, arena, turbo e bot auto-play. Funciona em paperio.site, inclusive no modo Conflict.
// @author       André Tenorio
// @match        https://paperio.site/*
// @match        https://paper-io.com/*
// @match        *://*.paper.io/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==
// PaperPilot — baseado na VavaMenu (Vavadragons); recursos de teclado por Silas (MIT).

(function() {
    'use strict';

    const state = {
        api: null,
        zoom: 1.0,
        zoomSeeded: false,
        speedMult: 1.0,
        prevSpeedMult: 1.0,
        paused: false,
        bots: 15,
        arenaSize: 2000,
        espEnabled: false,
        canvas: null,
        ctx: null,
        _kbInstalled: false
    };

    // Passo da velocidade pelas setas e limites do multiplicador.
    const SPEED_STEP = 0.2;
    const SPEED_MIN = 0;
    const SPEED_MAX = 5;

    // Cor de destaque da interface.
    const ACCENT = '#FFD400';

    // Calibração do ESP: pixels de tela por unidade de mundo = zoom × ESP_CAL.
    // Medido ao vivo no /conflict/: a escala da câmera (transform do canvas) é
    // exatamente g.scale × 1.0616 (dois pontos: 2.0->2.123 e 4.0->4.246), e o
    // jogador renderiza no centro do canvas, que não usa devicePixelRatio.
    // Se em outra resolução/DPR as linhas ficarem curtas/longas, ajuste só este número.
    const ESP_CAL = 1.0616;
    const IS_CONFLICT = location.pathname.indexOf('conflict') !== -1;

    // NOTA: no /conflict/ os controles lidos na construção da partida (ARENA e
    // BOTS) NÃO têm efeito — a engine lê esses valores internamente antes de
    // qualquer ponto interceptável (verificado: raio da arena fica fixo). Eles
    // continuam funcionando no modo raiz. O que farma rápido no conflict é a
    // VELOCIDADE; por isso a tecla F = TURBO (velocidade máxima <-> normal).

    // --- 0. Captura do jogo ------------------------------------------------
    // Dois bundles diferentes rodam conforme o modo:
    //   /          -> app2.js -> expõe window.paperio2api (api.game)
    //   /conflict/ -> app.js  -> mantém o objeto do jogo 100% dentro de uma
    //                            closure; ele NUNCA vai para window/DOM/Preact.
    // Para o caso da closure, o único jeito é, em document-start (antes do
    // bundle rodar), armar um setter no Object.prototype em propriedades
    // internas da instância do jogo. A engine constrói o jogo com
    // `this.<prop> = ...` numa instância de classe, então o setter dispara
    // com `this` === objeto do jogo. Essas chaves são específicas da engine
    // (verificadas ao vivo) e dificilmente colidem com outras libs; mesmo
    // assim validamos por formato antes de usar.
    const captured = new Set();
    const TRAP_KEYS = ['spawner', 'skinManager', 'schemeManager', 'drawedBases', 'gameOverCallback', 'deathCallback', 'fakeMouse'];

    function installTraps() {
        TRAP_KEYS.forEach((key) => {
            try {
                if (Object.getOwnPropertyDescriptor(Object.prototype, key)) return;
                Object.defineProperty(Object.prototype, key, {
                    configurable: true,
                    get() { return undefined; },
                    set(v) {
                        try { captured.add(this); } catch (e) {}
                        // Vira propriedade própria normal: o jogo segue funcionando
                        // e o trap para de interferir nesse objeto.
                        Object.defineProperty(this, key, { value: v, writable: true, configurable: true, enumerable: true });
                    }
                });
            } catch (e) {}
        });
    }
    // Instala imediatamente (document-start), antes do app.js executar.
    installTraps();

    const looksLikeGame = (o) => {
        try { return !!(o && typeof o === 'object' && o.config && ('unitSpeed' in o.config)); } catch (e) { return false; }
    };

    function resolveGame() {
        // Modo raiz: app2.js publica um global.
        const rootHolder = window.paperio2api || window.paper2;
        if (rootHolder) {
            const g = rootHolder.game || rootHolder;
            if (looksLikeGame(g)) return g;
        }
        // Modo conflict: escolhe uma instância capturada que esteja viva
        // (descarta as inválidas/mortas).
        let fallback = null;
        for (const o of captured) {
            if (!looksLikeGame(o)) { captured.delete(o); continue; }
            try {
                if (o.stopped) { fallback = fallback || o; continue; }
            } catch (e) {}
            return o; // jogo vivo tem prioridade
        }
        return fallback;
    }

    // Hooks de ARENA e BOTS (chamados pelo watchdog). Funcionam no modo raiz.
    // No /conflict/ NÃO têm efeito: a engine lê arenaSize/botsCount internamente
    // na construção da partida, antes de qualquer ponto interceptável.
    function hookArenaBots(game) {
        try {
            if (!looksLikeGame(game)) return;
            const cfg = game.config;
            if (!cfg._arenaHooked) {
                Object.defineProperty(cfg, 'arenaSize', {
                    get: () => state.arenaSize,
                    set: () => {},
                    configurable: true
                });
                cfg._arenaHooked = true;
            }
            if (!cfg._botsHooked) {
                Object.defineProperty(cfg, 'botsCount', {
                    get: () => state.bots,
                    set: (v) => { state.bots = v; },
                    configurable: true
                });
                cfg._botsHooked = true;
            }
        } catch (e) {}
    }

    // --- Sincronização da UI ----------------------------------------------
    function syncZoomUI() {
        const zr = document.getElementById('zr'), zv = document.getElementById('zv');
        if (zr) { if (state.zoom > parseFloat(zr.max)) zr.max = Math.ceil(state.zoom); zr.value = state.zoom; }
        if (zv) zv.innerText = state.zoom.toFixed(1);
    }
    function syncSpeedUI() {
        const sr = document.getElementById('sr'), sv = document.getElementById('sv');
        if (sr) sr.value = state.speedMult;
        if (sv) sv.innerText = state.speedMult.toFixed(1);
    }
    // Aviso transitório na tela.
    function flashHint(msg) {
        if (!document.body) return;
        let h = document.getElementById('pp-hint');
        if (!h) {
            h = document.createElement('div');
            h.id = 'pp-hint';
            h.style = `position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:rgba(18,18,22,0.85);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid ${ACCENT}66;border-radius:8px;color:${ACCENT};font-family:'Segoe UI',system-ui,sans-serif;font-size:12px;font-weight:600;padding:9px 16px;z-index:100000;pointer-events:none;transition:opacity .4s;box-shadow:0 6px 20px rgba(0,0,0,0.4);`;
            document.body.appendChild(h);
        }
        h.textContent = msg;
        h.style.opacity = '1';
        clearTimeout(h._t);
        h._t = setTimeout(() => { h.style.opacity = '0'; }, 2800);
    }
    // Atualiza o ponto de status do bot no cabeçalho.
    function updateBotDot() {
        const dot = document.getElementById('pp-dot');
        if (dot) { dot.style.background = bot.on ? ACCENT : '#5a5a62'; dot.style.boxShadow = '0 0 7px ' + (bot.on ? ACCENT : 'transparent'); }
    }

    // --- Teclado (recursos do script do Silas, adaptados à captura) --------
    function installKeyboard() {
        if (state._kbInstalled) return;
        state._kbInstalled = true;
        const digitando = () => {
            const el = document.activeElement;
            return el && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
        };
        window.addEventListener('keydown', (e) => {
            if (!e.isTrusted) return; // ignora eventos sintéticos (os disparados pelo próprio bot)
            if (digitando()) return;  // não atrapalhar quando estiver digitando o nick
            switch (e.code) {
                case 'ArrowUp':
                    e.preventDefault();
                    state.paused = false;
                    state.speedMult = Math.min(SPEED_MAX, +(state.speedMult + SPEED_STEP).toFixed(2));
                    syncSpeedUI();
                    break;
                case 'ArrowDown':
                    e.preventDefault();
                    state.paused = false;
                    state.speedMult = Math.max(SPEED_MIN, +(state.speedMult - SPEED_STEP).toFixed(2));
                    syncSpeedUI();
                    break;
                case 'Space':
                    e.preventDefault();
                    if (state.paused) {
                        state.speedMult = state.prevSpeedMult;
                        state.paused = false;
                    } else {
                        state.prevSpeedMult = state.speedMult;
                        state.speedMult = 0; // congela o movimento
                        state.paused = true;
                    }
                    syncSpeedUI();
                    break;
                // WASD = direção nativa do jogo (não mexemos; o bot usa isso).
                // TURBO (farm): velocidade máxima <-> normal. Velocidade alta =
                // captura território rápido = score sobe rápido (funciona ao vivo).
                case 'KeyF':
                    e.preventDefault();
                    state.paused = false;
                    state.speedMult = (state.speedMult >= SPEED_MAX) ? 1.0 : SPEED_MAX;
                    syncSpeedUI();
                    flashHint(state.speedMult >= SPEED_MAX ? ('TURBO: velocidade ' + SPEED_MAX + 'x') : ('Velocidade ' + state.speedMult.toFixed(1) + 'x'));
                    break;
                // B = liga/desliga o bot (auto-play).
                case 'KeyB':
                    e.preventDefault();
                    botToggle();
                    break;
            }
        });
    }
    installKeyboard();

    // --- BOT (auto-play, EXPERIMENTAL) ------------------------------------
    // Pilota via WASD nativo do jogo (keydown sintético). Ao ligar, AUTO-CALIBRA
    // o mapeamento tecla->direção (pressiona cada tecla e mede pra onde o jogador
    // foi), depois farma fazendo retângulos por quadrante em volta da base,
    // voltando sempre ao centro (= captura) e crescendo o raio a cada volta.
    // NÃO é uma IA perfeita: pode morrer pra inimigos. Tecla B liga/desliga.
    const BOT_KEYS = ['KeyW', 'KeyD', 'KeyS', 'KeyA'];
    const bot = { on: false, timer: null, calib: {}, calIdx: 0, calStart: 0, calPos: null, center: null, r: 0, quad: 0, wp: 0, playerId: null };

    function botKey(code) {
        const kc = { KeyW: 87, KeyA: 65, KeyS: 83, KeyD: 68 }[code] || 0;
        const init = { code, key: code.slice(3).toLowerCase(), keyCode: kc, which: kc, bubbles: true };
        try { document.dispatchEvent(new KeyboardEvent('keydown', init)); window.dispatchEvent(new KeyboardEvent('keydown', init)); } catch (e) {}
    }
    const _d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
    const _bear = (f, t) => Math.atan2(t.y - f.y, t.x - f.x);
    const _adiff = (a, b) => { let d = a - b; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; return Math.abs(d); };
    function _centroid(poly) {
        try {
            let sx = 0, sy = 0, n = 0;
            for (const p of poly) { const x = (p && p.x != null) ? p.x : p[0], y = (p && p.y != null) ? p.y : p[1]; if (typeof x === 'number' && typeof y === 'number') { sx += x; sy += y; n++; } }
            return n ? { x: sx / n, y: sy / n } : null;
        } catch (e) { return null; }
    }
    function botReset() { bot.calib = {}; bot.calIdx = 0; bot.calStart = 0; bot.calPos = null; bot.center = null; bot.r = 0; bot.quad = 0; bot.wp = 0; bot.playerId = null; }
    function botToggle() {
        if (bot.on) {
            bot.on = false;
            if (bot.timer) { clearInterval(bot.timer); bot.timer = null; }
            flashHint('BOT desligado');
        } else {
            bot.on = true; botReset();
            bot.timer = setInterval(botTick, 110);
            flashHint('BOT ligado (calibrando…)');
        }
        updateBotDot();
    }
    function botTick() {
        const g = state.api;
        if (!g || !g.player || !g.player.position) return; // morto/menu: aguarda
        const pos = { x: g.player.position.x, y: g.player.position.y };

        // (Re)ancora ao nascer/renascer.
        if (g.player.id !== bot.playerId) {
            bot.playerId = g.player.id;
            bot.center = (g.player.base && _centroid(g.player.base.polygon)) || { x: pos.x, y: pos.y };
            bot.r = 120; bot.quad = 0; bot.wp = 0;
        }

        // 1) Calibração: aprende tecla -> direção real do movimento.
        if (bot.calIdx < BOT_KEYS.length) {
            const key = BOT_KEYS[bot.calIdx];
            if (!bot.calStart) { bot.calStart = Date.now(); bot.calPos = { x: pos.x, y: pos.y }; botKey(key); return; }
            if (Date.now() - bot.calStart > 320) {
                if (_d(pos, bot.calPos) > 2) bot.calib[key] = _bear(bot.calPos, pos);
                bot.calIdx++; bot.calStart = 0;
                if (bot.calIdx >= BOT_KEYS.length) flashHint('BOT farmando (' + Object.keys(bot.calib).length + '/4 direções)');
            }
            return;
        }
        if (!Object.keys(bot.calib).length) return; // calibração falhou (sem direções)

        // 2) Farm: retângulo por quadrante em volta da base; volta ao centro = captura.
        const c = bot.center;
        if (!bot.r) bot.r = 120;
        const cap = (g.border && g.border.radius) ? g.border.radius * 0.55 : 500;
        const sx = [1, 1, -1, -1][bot.quad % 4], sy = [-1, 1, 1, -1][bot.quad % 4];
        const wps = [
            { x: c.x + sx * bot.r, y: c.y },
            { x: c.x + sx * bot.r, y: c.y + sy * bot.r },
            { x: c.x, y: c.y + sy * bot.r },
            { x: c.x, y: c.y }
        ];
        const idx = bot.wp % 4;
        if (_d(pos, wps[idx]) < (idx === 3 ? 35 : 45)) {
            bot.wp++;
            if (bot.wp % 4 === 0) { bot.quad++; if (bot.quad % 4 === 0) bot.r = Math.min(bot.r + 60, cap); }
        }
        const target = wps[bot.wp % 4];
        const want = _bear(pos, target);
        let best = null, bd = 1e9;
        for (const k in bot.calib) { const dd = _adiff(bot.calib[k], want); if (dd < bd) { bd = dd; best = k; } }
        if (best) botKey(best);
    }

    // --- 1. Motor de vigilância (watchdog) ---------------------------------
    setInterval(() => {
        const game = resolveGame();
        if (!game || !game.config) return;

        state.api = game;
        const cfg = game.config;

        // Garante que o menu existe
        if (!document.getElementById("pp-menu") && document.body) {
            createMenu();
        }

        // Hook do Zoom (semeia uma vez a partir da escala natural do jogo,
        // pra o /conflict/ não abrir absurdamente afastado; depois o usuário controla).
        if (!game._zoomHooked) {
            try {
                if (!state.zoomSeeded) {
                    const cur = (typeof game.scale === 'number' && game.scale > 0) ? game.scale : state.zoom;
                    state.zoom = cur;
                    state.zoomSeeded = true;
                    syncZoomUI();
                }
                Object.defineProperty(game, 'scale', {
                    get: () => state.zoom,
                    set: () => {},
                    configurable: true
                });
                game._zoomHooked = true;
            } catch (e) {}
        }

        // Hook da Velocidade
        if (!cfg._speedHooked) {
            try {
                let baseSpeed = cfg.unitSpeed || 90;
                Object.defineProperty(cfg, 'unitSpeed', {
                    get: () => baseSpeed * state.speedMult,
                    set: (v) => { if (v > 0) baseSpeed = v; },
                    configurable: true
                });
                cfg._speedHooked = true;
            } catch (e) {}
        }

        // Hook de Bots + Arena (backup; o principal acontece na captura).
        hookArenaBots(game);
    }, 500);

    // --- 2. Sistema de ESP -------------------------------------------------
    function initESP() {
        if (state.canvas && document.body.contains(state.canvas)) return;
        state.canvas = document.createElement('canvas');
        Object.assign(state.canvas.style, {
            position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
            pointerEvents: 'none', zIndex: '9998'
        });
        document.body.appendChild(state.canvas);
        state.ctx = state.canvas.getContext('2d');

        const loop = () => {
            if (state.ctx) {
                state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
                if (state.espEnabled && state.api?.units && state.api.player?.position) {
                    state.canvas.width = window.innerWidth;
                    state.canvas.height = window.innerHeight;
                    renderLines();
                }
            }
            requestAnimationFrame(loop);
        };
        loop();
    }

    function renderLines() {
        const { player, units } = state.api;
        const centerX = window.innerWidth / 2;
        const centerY = window.innerHeight / 2;
        // Conflict: calibrado (escala real da câmera). Outros modos: fórmula original.
        const multiplier = IS_CONFLICT ? (state.zoom * ESP_CAL) : (35 / state.zoom);

        units.forEach(u => {
            if (u === player || u.death || !u.position) return;
            const dx = (u.position.x - player.position.x) * multiplier;
            const dy = (u.position.y - player.position.y) * multiplier;

            state.ctx.beginPath();
            state.ctx.moveTo(centerX, centerY);
            state.ctx.lineTo(centerX + dx, centerY + dy);
            state.ctx.strokeStyle = ACCENT;
            state.ctx.lineWidth = 1.4;
            state.ctx.stroke();
        });
    }

    // --- 3. Interface (UI) -------------------------------------------------
    function createMenu() {
        if (document.getElementById("pp-menu")) return;
        const A = ACCENT;
        const row = (label, valId, valTxt, inId, min, max, step, val, unit) => `
            <div style="margin-bottom:11px;">
              <div style="display:flex;justify-content:space-between;align-items:baseline;font-size:10px;letter-spacing:.4px;color:#b9b9c2;margin-bottom:5px;">
                <span>${label}</span>
                <span style="color:${A};font-weight:700;font-size:11px;"><span id="${valId}">${valTxt}</span>${unit || ''}</span>
              </div>
              <input type="range" id="${inId}" min="${min}" max="${max}" step="${step}" value="${val}" style="width:100%;accent-color:${A};height:4px;cursor:pointer;">
            </div>`;

        const box = document.createElement('div');
        box.id = "pp-menu";
        box.style.cssText = `position:fixed;top:46px;right:20px;width:212px;background:rgba(17,18,22,0.72);backdrop-filter:blur(11px);-webkit-backdrop-filter:blur(11px);border:1px solid ${A}40;border-radius:14px;color:#e9e9ee;font-family:'Segoe UI',Roboto,system-ui,sans-serif;z-index:99999;user-select:none;box-shadow:0 10px 34px rgba(0,0,0,0.5);overflow:hidden;`;
        box.innerHTML = `
            <div id="pp-head" style="display:flex;align-items:center;gap:8px;cursor:move;padding:11px 13px;background:linear-gradient(90deg,${A}1f,transparent);border-bottom:1px solid ${A}26;">
              <span id="pp-dot" style="width:9px;height:9px;border-radius:50%;background:#5a5a62;flex:0 0 auto;"></span>
              <span style="flex:1;font-weight:800;font-size:13px;letter-spacing:.5px;color:${A};">PaperPilot</span>
              <span id="pp-min" style="cursor:pointer;color:#9a9aa5;font-size:16px;line-height:1;padding:0 3px;">–</span>
            </div>
            <div id="pp-body" style="padding:13px;">
              ${row('ARENA', 'av', state.arenaSize, 'ar', 500, 10000, 100, state.arenaSize, '')}
              ${row('BOTS', 'bv', state.bots, 'br', 0, 300, 1, state.bots, '')}
              ${row('VELOCIDADE', 'sv', state.speedMult.toFixed(1), 'sr', SPEED_MIN, SPEED_MAX, 0.1, state.speedMult, 'x')}
              ${row('ZOOM', 'zv', state.zoom.toFixed(1), 'zr', 0.3, 5, 0.1, state.zoom, 'x')}
              <label style="display:flex;align-items:center;gap:8px;margin-top:6px;font-size:11px;color:#cfcfd6;cursor:pointer;">
                <input type="checkbox" id="et" ${state.espEnabled ? 'checked' : ''} style="accent-color:${A};width:14px;height:14px;cursor:pointer;"> ESP (traçados)
              </label>
              <div style="margin-top:13px;border-top:1px solid #ffffff14;padding-top:9px;font-size:9.5px;color:#8f8f99;line-height:1.75;">
                <b style="color:${A};letter-spacing:.5px;">ATALHOS</b><br>
                ↑/↓ velocidade &nbsp;·&nbsp; Espaço pausa<br>
                WASD dirige &nbsp;·&nbsp; <b style="color:${A};">B</b> liga/desliga bot<br>
                F turbo &nbsp;·&nbsp; roda do mouse: zoom
              </div>
            </div>
        `;
        document.body.appendChild(box);

        const ar = document.getElementById('ar'), br = document.getElementById('br'), sr = document.getElementById('sr'), zr = document.getElementById('zr'), et = document.getElementById('et');
        const av = document.getElementById('av'), bv = document.getElementById('bv'), sv = document.getElementById('sv'), zv = document.getElementById('zv');

        ar.oninput = () => { state.arenaSize = parseInt(ar.value); av.innerText = ar.value; };
        br.oninput = () => { state.bots = parseInt(br.value); bv.innerText = br.value; };
        sr.oninput = () => { state.speedMult = parseFloat(sr.value); state.paused = false; sv.innerText = parseFloat(sr.value).toFixed(1); };
        zr.oninput = () => { state.zoom = parseFloat(zr.value); zv.innerText = parseFloat(zr.value).toFixed(1); };
        et.onchange = () => { state.espEnabled = et.checked; if (state.espEnabled) initESP(); };

        // Sincroniza com valores semeados antes do menu existir + status do bot.
        syncZoomUI(); syncSpeedUI(); updateBotDot();

        // Minimizar / expandir
        const body = document.getElementById('pp-body'), minBtn = document.getElementById('pp-min');
        minBtn.onclick = (e) => { e.stopPropagation(); const hidden = body.style.display === 'none'; body.style.display = hidden ? 'block' : 'none'; minBtn.textContent = hidden ? '–' : '+'; };

        // Zoom pela roda do mouse
        window.addEventListener('wheel', (e) => {
            if (!state.api) return;
            state.zoom = Math.min(Math.max(state.zoom + (e.deltaY > 0 ? 0.1 : -0.1), 0.3), 5.0);
            const zvE = document.getElementById('zv'), zrE = document.getElementById('zr');
            if (zvE) zvE.innerText = state.zoom.toFixed(1);
            if (zrE) zrE.value = state.zoom;
        }, { passive: true });

        // Arrastar pelo cabeçalho
        let active = false, ox, oy;
        document.getElementById('pp-head').addEventListener('mousedown', (e) => { if (e.target.id === 'pp-min') return; active = true; ox = e.clientX - box.offsetLeft; oy = e.clientY - box.offsetTop; });
        document.addEventListener('mousemove', (e) => { if (active) { box.style.left = (e.clientX - ox) + 'px'; box.style.top = (e.clientY - oy) + 'px'; box.style.right = 'auto'; } });
        document.addEventListener('mouseup', () => active = false);
    }

})();