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.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Advertisement:

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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);
    }

})();