Greasy Fork is available in English.
Painel de controle para Paper.io — velocidade, zoom, ESP, bots, arena, turbo e bot auto-play. Funciona em paperio.site, inclusive no modo Conflict.
// ==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 · Espaço pausa<br>
WASD dirige · <b style="color:${A};">B</b> liga/desliga bot<br>
F turbo · 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);
}
})();