NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name PokeIdle Market Auto-Buy
// @name:pt-BR PokeIdle Market Auto-Buy
// @namespace https://openuserjs.org/users/azzo
// @version 0.2.0
// @description Opens the pokeidle.io Community Market by itself, keeps searching all the time and buys listings that match your rules (pokémon or item, name, level, power, IV, shiny, quality, min/max price, gold or gems). Starts in simulation mode, with daily spending limits and a re-check before every click.
// @description:pt-BR Abre sozinho o Mercado da Comunidade do pokeidle.io, pesquisa o tempo todo e compra as listagens que batem com as suas regras (pokémon ou item, nome, nível, poder, IV, shiny, qualidade, preço mínimo e máximo, ouro ou gemas). Começa em modo simulação, com limites diários de gasto e conferência antes de cada clique.
// @license MIT
// @copyright 2026, azzo (https://openuserjs.org/users/azzo)
// @match https://pokeidle.io/*
// @run-at document-idle
// @noframes
// @grant none
// ==/UserScript==
// ==OpenUserJS==
// @author azzo
// ==/OpenUserJS==
(function () {
'use strict';
// Motor do Market Auto-Buy: funções puras (sem DOM), testáveis em Node.
const E = (function () {
const flat = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
const strip = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
function parseNum(s) {
const m = String(s == null ? '' : s).replace(/\s+/g, '').match(/(\d[\d.,]*)([kmbt])?/i);
if (!m) return NaN;
let t = m[1];
const suf = (m[2] || '').toLowerCase();
const dot = t.lastIndexOf('.'), com = t.lastIndexOf(',');
if (dot >= 0 && com >= 0) { const d = Math.max(dot, com); t = t.slice(0, d).replace(/[.,]/g, '') + '.' + t.slice(d + 1); }
else if (dot >= 0 || com >= 0) {
const parts = t.split(dot >= 0 ? '.' : ',');
t = parts.length > 2 || (parts[1].length === 3 && !suf) ? parts.join('') : parts[0] + '.' + parts[1];
}
return parseFloat(t) * ({ k: 1e3, m: 1e6, b: 1e9, t: 1e12 }[suf] || 1);
}
// Qualquer sinal de dinheiro real / loja / carteira: a linha é ignorada por segurança.
const DENY = /\$|r\$|us\$|\busd|usdt|\bpix\b|cart[aã]o|cr[eé]dito|d[eé]bito|credit|diamantes?|diamonds?|pacote|\bpack\b|\bloja\b|\bshop\b|recarga|\bsaque\b|withdraw|dep[oó]sito|deposit|solana|wallet|carteira/i;
const UI = /^(comprar|buy|purchase|vender|sell|cancelar|cancel|ver|detalhes|favoritar|vendedor|seller|pre[cç]o|price|gemas?|gems?|ouro|gold|coins?|moedas?|nv|lv|iv|xp|hp|qtd|quantidade|shiny|em campo|equipe|total|unidade|item|pok[eé]mon)$/i;
const NUM = '(\\d[\\d.,]*\\s?[kmbt]?)';
const CURW = '(gemas?|gems?|💎|ouro|gold|coins?|moedas?|🪙)';
const curOf = (t) => (/gem|💎/i.test(t) ? 'gems' : 'gold');
// Texto de uma linha do mercado -> {name, kind, level, power, iv, shiny, quality, qty, price, currency, seller, ambiguous}
function parseListing(text) {
const raw = String(text == null ? '' : text);
const lines = raw.split(/\n+/).map((l) => l.trim()).filter(Boolean);
const f = flat(raw);
const L = { raw: strip(f), name: '', kind: 'item', level: NaN, power: NaN, iv: NaN, shiny: false, quality: NaN, qty: 1, price: NaN, currency: '', seller: '', ambiguous: '' };
const g = (re) => { const m = f.match(re); return m ? parseFloat(String(m[1]).replace(',', '.')) : NaN; };
L.level = g(/(?:nv|lv|n[ií]vel)\.?\s*(\d{1,4})\b/i);
L.power = g(/\bP\s?([1-5])\b/);
L.iv = g(/\bIV\s*(\d+)/i);
L.quality = g(/(?:qualidade|quality)\D{0,8}(\d+(?:[.,]\d+)?)/i);
const q = f.match(/(?:^|\s)[x×]\s?(\d{1,4})\b/i);
if (q) L.qty = +q[1];
L.kind = Number.isFinite(L.level) ? 'pokemon' : 'item';
L.shiny = L.kind === 'pokemon' && /shiny|✨|★|brilhante/i.test(f);
const sl = lines.map((l) => l.match(/^(?:vendedor|seller|por|by)\b\s*:?\s*(.{2,32})$/i)).find(Boolean);
if (sl) L.seller = sl[1].trim();
// preço: "1.500 Gemas" ou "Ouro: 2,5M"; mais de um valor diferente = ambíguo
const seen = new Map();
const add = (v, c) => seen.set(v + '|' + c, { v, c });
const reNC = new RegExp(NUM + '\\s*' + CURW + '(?![\\p{L}])', 'giu'), reCN = new RegExp('(?<![\\p{L}])' + CURW + '\\s*[:=]?\\s*' + NUM, 'giu');
lines.forEach((ln, i) => { // por linha: o número de um campo não gruda na moeda de outro
for (const m of ln.matchAll(reNC)) add(parseNum(m[1]), curOf(m[2]));
for (const m of ln.matchAll(reCN)) add(parseNum(m[2]), curOf(m[1]));
const nx = lines[i + 1];
if (nx) { // número numa linha e a moeda na seguinte (ou o contrário)
if (new RegExp('^' + NUM + '$', 'i').test(ln) && new RegExp('^' + CURW + '[::]?$', 'iu').test(nx)) add(parseNum(ln), curOf(nx));
if (new RegExp('^' + CURW + '[::]?$', 'iu').test(ln) && new RegExp('^' + NUM + '$', 'i').test(nx)) add(parseNum(nx), curOf(ln));
}
});
if (DENY.test(f)) L.ambiguous = 'termo de pagamento/loja na linha (ignorada por segurança)';
else if (seen.size === 0) L.ambiguous = 'sem preço em ouro/gemas';
else if (seen.size > 1) L.ambiguous = 'mais de um preço na linha';
else if (/gemas?|gems?|💎/i.test(f) && /ouro|gold|coins?|moedas?|🪙/i.test(f)) L.ambiguous = 'duas moedas na mesma linha';
else { const p = [...seen.values()][0]; if (p.v > 0) { L.price = p.v; L.currency = p.c; } else L.ambiguous = 'preço inválido'; }
// nome: primeira linha que parece nome (corta em "Nv", "P2", "IV", números)
for (const line of lines) {
if (new RegExp(CURW, 'iu').test(line) && /\d/.test(line)) continue;
if (/^(?:vendedor|seller|por|by)\b/i.test(line)) continue;
let c = line.split(/\s+(?:nv\b|lv\b|n[ií]vel\b|p\s?[1-5]\b|iv\b|[x×]\s?\d|\d)/i)[0].replace(/[::]+$/, '').trim();
if (L.kind === 'pokemon') c = c.replace(/shiny|✨|★|brilhante/gi, '').trim();
if (c.length < 3 || c.length > 32 || !/^[\p{L}][\p{L}\p{N}'’.\- ♀♂]*$/u.test(c) || UI.test(c) || (c.match(/\p{L}/gu) || []).length < 3) continue;
L.name = c; break;
}
if (!L.ambiguous && !L.name) L.ambiguous = 'sem nome legível na linha';
return L;
}
const terms = (s) => String(s == null ? '' : s).split(',').map((t) => strip(t).trim()).filter(Boolean);
const no = (why) => ({ ok: false, why });
// A linha atende a regra? (qualquer dado desconhecido num filtro "mínimo" reprova: na dúvida, não compra)
function matchRule(r, L) {
if (!r.on) return no('regra desligada');
if (L.ambiguous) return no(L.ambiguous);
if (r.kind === 'pokemon' && L.kind !== 'pokemon') return no('não é pokémon');
if (r.kind === 'item' && L.kind !== 'item') return no('não é item');
const names = terms(r.names);
const nm = strip(L.name);
if (names.length && !names.some((t) => nm.includes(t))) return no('nome não confere');
if (L.currency !== r.currency) return no('moeda diferente');
if (!(r.max > 0)) return no('regra sem preço máximo');
if (L.price > r.max) return no('acima do preço máximo');
if (r.min > 0 && L.price < r.min) return no('abaixo do preço mínimo');
if (L.kind === 'pokemon') {
if (r.lvMin > 0 && !(L.level >= r.lvMin)) return no('nível abaixo do mínimo');
if (r.lvMax > 0 && !(L.level <= r.lvMax)) return no('nível acima do máximo');
if (r.powerMin > 0 && !(L.power >= r.powerMin)) return no('poder abaixo do mínimo ou desconhecido');
if (r.ivMin > 0 && !(L.iv >= r.ivMin)) return no('IV abaixo do mínimo ou desconhecido');
if (r.qualMin > 0 && !(L.quality >= r.qualMin)) return no('qualidade abaixo do mínimo ou desconhecida');
if (r.shiny === 'only' && !L.shiny) return no('não é shiny');
if (r.shiny === 'no' && L.shiny) return no('é shiny');
}
const must = terms(r.must), forbid = terms(r.forbid);
if (must.some((t) => !L.raw.includes(t))) return no('falta texto obrigatório');
if (forbid.some((t) => L.raw.includes(t))) return no('tem texto proibido');
return { ok: true, why: 'ok' };
}
// Limites de segurança (por dia). Limite 0 = não compra naquela moeda.
function checkLimits(cfg, day, L, r) {
const cap = L.currency === 'gems' ? cfg.capGems : cfg.capGold;
if (!(cap > 0)) return no('limite diário de ' + (L.currency === 'gems' ? 'gemas' : 'ouro') + ' está em 0');
if ((day[L.currency] || 0) + L.price > cap) return no('estouraria o limite diário');
if (cfg.capBuysDay > 0 && (day.buys || 0) >= cfg.capBuysDay) return no('limite de compras do dia atingido');
if (r.maxBuys > 0 && (r.bought || 0) >= r.maxBuys) return no('limite de compras da regra atingido');
const blocked = terms(cfg.blocked);
if (blocked.some((t) => L.raw.includes(t))) return no('vendedor/texto bloqueado');
return { ok: true, why: 'ok' };
}
// O diálogo de confirmação bate com a listagem? (preço e moeda iguais, sem termos de pagamento)
function dialogMatches(text, L) {
const f = flat(text);
if (DENY.test(f)) return false;
const nums = [...f.matchAll(new RegExp(NUM, 'g'))].map((m) => parseNum(m[1]));
if (!nums.some((n) => Math.abs(n - L.price) < 1e-6)) return false;
const wantsGems = L.currency === 'gems';
if (wantsGems && !/gem|💎/i.test(f)) return false;
if (!wantsGems && !/ouro|gold|coin|moeda|🪙/i.test(f)) return false;
return !L.name || strip(f).includes(strip(L.name));
}
const api = { parseNum, parseListing, matchRule, checkLimits, dialogMatches, DENY, strip, terms };
return api;
})();
const VERSION = '0.2.0', KEY = 'pmab_v1';
const BUYTXT = /^(comprar|buy|purchase)(?:\s+(?:agora|now))?$/i;
const CONFIRM = /^(confirmar(?: compra)?|confirm|sim|yes|ok|finalizar)$/i;
const CANCEL = /^(cancelar|cancel|n[aã]o|no|voltar|fechar|close|×|x)$/i;
const NAV = /^[^\p{L}]*(?:mercado(?: da comunidade)?|community market|market)[^\p{L}]*$/iu;
const SEARCHW = /^[^\p{L}]*(?:buscar|pesquisar|procurar|search|filtrar|aplicar(?: filtros?)?|🔍)[^\p{L}]*$/iu;
const REFRESHW = /^[^\p{L}]*(?:atualizar|refresh|recarregar|reload|🔄|↻|⟳)[^\p{L}]*$/iu;
const DEFAULT_CFG = { sim: true, autoConfirm: false, autoOpen: true, autoSearch: true, searchSec: 5, searchByName: false, minGapSec: 4, maxPerMin: 3, capGems: 0, capGold: 0, capBuysDay: 5, blocked: '', beep: true };
const MKT = /mercado|market|comunidade|community|rmt/i;
const today = () => { const d = new Date(Date.now()); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };
const newRule = () => ({ on: true, label: 'Nova regra', kind: 'any', names: '', currency: 'gems', min: 0, max: 0, lvMin: 0, lvMax: 0, powerMin: 0, ivMin: 0, qualMin: 0, shiny: 'any', must: '', forbid: '', maxBuys: 0, bought: 0, open: true });
let S = { v: VERSION, rules: [newRule()], cfg: Object.assign({}, DEFAULT_CFG), day: { date: today(), gems: 0, gold: 0, buys: 0 }, log: [], ui: { tab: 'rules', collapsed: false } };
// estado só desta aba: NUNCA persistido (depois de recarregar a página, volta sempre parado e em simulação)
const SES = { armed: false, ack: false, busy: false, looping: false, lastBuy: 0, buyTimes: [], rows: [], seen: new WeakMap(), simSeen: new WeakMap(), cool: new WeakMap(),
panel: null, lastOpen: 0, openFails: 0, navWarned: false, lastSearch: 0, searches: 0, searchWarned: false, nameIdx: 0, obs: null, obsEl: null, qt: 0, lastLoopAt: 0, loops: 0 };
let TX = new Map(), storageOk = true, msg = '', diagText = '';
// ---------- util ----------
const flat = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const fmt = (n) => !Number.isFinite(n) ? '–' : n >= 1e9 ? (n / 1e9).toFixed(2) + 'B' : n >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : n >= 1e4 ? (n / 1e3).toFixed(1) + 'k' : String(Math.round(n * 100) / 100);
const load = () => { try { return JSON.parse(localStorage.getItem(KEY)); } catch (e) { return null; } };
const save = () => { try { S.log = S.log.slice(-120); localStorage.setItem(KEY, JSON.stringify(S)); storageOk = true; } catch (e) { storageOk = false; } };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitFor(fn, ms) { const t0 = Date.now(); for (;;) { const v = fn(); if (v) return v; if (Date.now() - t0 > ms) return null; await sleep(100); } }
const SKIP = /^(SCRIPT|STYLE|NOSCRIPT|SVG|PATH|LINK|META|BR|HR)$/i;
function leafText(el) {
const out = [];
(function w(n) {
for (const c of n.childNodes) {
if (c.nodeType === 3) { const t = c.nodeValue.trim(); if (t) out.push(t); }
else if (c.nodeType === 1 && c.tagName === 'IMG') { const a = (c.getAttribute('alt') || '').trim(); if (a) out.push(a); }
else if (c.nodeType === 1 && !SKIP.test(c.tagName)) w(c);
}
})(el);
return out.join('\n');
}
const tx = (el) => { let v = TX.get(el); if (v === undefined) { v = leafText(el); TX.set(el, v); } return v; };
const vis = (el) => el.getClientRects().length > 0;
const disabled = (b) => b.disabled || b.getAttribute('aria-disabled') === 'true' || /\bdisabled\b/.test(b.getAttribute('class') || '');
function addLog(kind, text) { S.log.push({ t: Date.now(), kind, text }); save(); }
function beep() {
if (!S.cfg.beep) return;
try { const A = window.AudioContext || window.webkitAudioContext, c = new A(), o = c.createOscillator(), g = c.createGain(); o.connect(g); g.connect(c.destination); o.frequency.value = 880; g.gain.value = 0.06; o.start(); o.stop(c.currentTime + 0.18); } catch (e) {}
}
function rollDay() { if (S.day.date !== today()) S.day = { date: today(), gems: 0, gold: 0, buys: 0 }; }
const cname = (c) => (c === 'gems' ? 'gemas' : 'ouro');
const desc = (L) => (L.name || '?') + (L.kind === 'pokemon' ? ' Nv ' + L.level + (Number.isFinite(L.power) ? ' P' + L.power : '') + (Number.isFinite(L.iv) ? ' IV ' + L.iv : '') + (L.shiny ? ' ✨' : '') : '') + ' por ' + fmt(L.price) + ' ' + cname(L.currency);
// ---------- leitura do mercado (sem seletores: ancora no botão "Comprar") ----------
function buyButtons() {
return [...document.querySelectorAll('button,[role="button"],a,input[type="button"],input[type="submit"],[class*="btn" i],[class*="button" i]')].filter((b) => {
if (host.contains(b)) return false;
const t = flat(b.tagName === 'INPUT' ? b.value : leafText(b));
return t.length <= 22 && BUYTXT.test(t) && vis(b);
});
}
const SHOPW = /loja|shop|store|diamante|diamond|pacote|\bpack\b|recarga|\bpix\b/i;
// A linha está numa tela de mercado? Só contam os TÍTULOS acima dela (nunca o texto da própria linha);
// o título mais próximo decide: "Mercado/Market" libera, "Loja/Diamantes/Pacote" bloqueia, ambos ou nenhum = não compra.
function inMarket(el, btns) {
for (let c = el, p = el.parentElement; p && c !== document.body; c = p, p = p.parentElement) {
const heads = [];
for (const sb of p.children) { if (sb === c) break; if (!btns.some((b) => sb.contains(b))) heads.push(flat(tx(sb)).slice(0, 80)); }
for (const h of heads.reverse()) { const m = MKT.test(h), sh = SHOPW.test(h); if (m && !sh) return true; if (sh && !m) return false; }
}
return false;
}
function findRows() {
const btns = buyButtons(), rows = [], single = btns.length === 1; // com uma só listagem não dá para comparar irmãos
SES.btns = btns;
for (const b of btns) {
let best = null;
for (let p = b.parentElement, i = 0; p && i < 8 && p !== document.body; p = p.parentElement, i++) {
if (btns.filter((x) => p.contains(x)).length !== 1 || flat(tx(p)).length > 260) break;
// sozinha na tela: a linha termina antes de entrar o resto do painel (campos, títulos, outros botões)
if (single && [...p.querySelectorAll('input,select,textarea,h1,h2,h3,h4,h5,h6,button,[role="button"],a')].some((x) => x !== b && !b.contains(x) && !x.contains(b) && vis(x) && !host.contains(x))) break;
best = p;
}
if (best && !rows.some((r) => r.el === best)) rows.push({ el: best, btn: b });
}
return rows;
}
function analyze(r) {
const L = E.parseListing(tx(r.el));
r.market = inMarket(r.el, SES.btns);
if (!L.ambiguous && !r.market) L.ambiguous = 'fora do mercado (a tela não parece o mercado)';
r.L = L; r.rule = null; r.why = L.ambiguous || 'nenhuma regra serve';
if (L.ambiguous) return r;
if (disabled(r.btn)) { r.why = 'botão desativado'; return r; }
for (const rule of S.rules) {
const m = E.matchRule(rule, L);
if (!m.ok) { if (r.why === 'nenhuma regra serve') r.why = '"' + rule.label + '": ' + m.why; continue; }
const lim = E.checkLimits(S.cfg, S.day, L, rule);
if (!lim.ok) { r.why = '"' + rule.label + '": ' + lim.why; r.blockedBy = lim.why; continue; }
r.rule = rule; r.why = 'apta (regra "' + rule.label + '")'; break;
}
r.eligible = !!r.rule;
return r;
}
const sigOf = (r) => desc(r.L) + '|' + r.L.seller;
const isNew = (map, r) => { const k = sigOf(r); if (map.get(r.btn) === k) return false; map.set(r.btn, k); return true; };
const coolHit = (r) => { const c = SES.cool.get(r.btn); return !!c && c.sig === sigOf(r) && Date.now() - c.t < 20e3; };
// ---------- compra ----------
function findDialog(btn, known) {
const els = [...document.querySelectorAll('[role="dialog"],[role="alertdialog"],[class*="modal" i],[class*="dialog" i],[class*="popup" i],[class*="confirm" i],[class*="overlay" i]')].filter((e) => vis(e) && !host.contains(e) && !e.contains(btn));
for (const e of els.reverse()) {
const bs = [...e.querySelectorAll('button,[role="button"],[class*="btn" i],[class*="button" i]')].filter(vis);
const confirm = bs.find((b) => { const t = flat(leafText(b)); return CONFIRM.test(t) || (BUYTXT.test(t) && !known.has(b)); });
if (confirm) return { el: e, confirm, cancel: bs.find((b) => CANCEL.test(flat(leafText(b)))) };
}
return null;
}
function registerPurchase(rule, L) {
rollDay(); S.day[L.currency] = (S.day[L.currency] || 0) + L.price; S.day.buys += 1; rule.bought = (rule.bought || 0) + 1;
addLog('COMPRA', desc(L));
}
async function doBuy(c) {
TX = new Map();
if (!c.el.isConnected || !vis(c.btn) || disabled(c.btn)) return addLog('CANCELADO', 'o botão sumiu ou foi desativado');
const L2 = E.parseListing(tx(c.el));
const same = L2.price === c.L.price && L2.currency === c.L.currency && L2.name === c.L.name;
const m = E.matchRule(c.rule, L2), lim = E.checkLimits(S.cfg, S.day, L2, c.rule);
if (!same || !m.ok || !lim.ok) return addLog('CANCELADO', 'a linha mudou antes do clique (' + (!same ? 'preço/nome diferente' : m.ok ? lim.why : m.why) + ')');
addLog('COMPRANDO', desc(L2));
SES.cool.set(c.btn, { t: Date.now(), sig: sigOf(c) });
const known = new Set(buyButtons());
c.btn.click();
SES.buyTimes.push(Date.now());
const dlg = await waitFor(() => findDialog(c.btn, known), 2500);
if (dlg) {
const dtext = flat(leafText(dlg.el));
if (!E.dialogMatches(dtext, L2)) {
if (dlg.cancel) dlg.cancel.click(); else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
return addLog('CANCELADO', 'o diálogo de confirmação não bate com a listagem: "' + dtext.slice(0, 90) + '"');
}
if (!S.cfg.autoConfirm) { beep(); return addLog('AGUARDANDO VOCÊ', 'confirme no jogo: ' + desc(L2)); }
dlg.confirm.click();
await sleep(900);
} else await sleep(1200); // sem diálogo: o clique pode ter comprado direto (conta como gasto, por segurança)
registerPurchase(c.rule, L2);
}
async function attempt(c) {
const now = Date.now();
if (now - SES.lastBuy < S.cfg.minGapSec * 1000) return;
SES.buyTimes = SES.buyTimes.filter((t) => now - t < 60e3);
if (SES.buyTimes.length >= S.cfg.maxPerMin) return;
if (S.cfg.sim) { if (isNew(SES.simSeen, c)) { addLog('SIMULAÇÃO', 'compraria ' + desc(c.L) + ' (regra "' + c.rule.label + '")'); } return; }
if (!SES.ack) return;
SES.busy = true;
try { await doBuy(c); } catch (e) { addLog('ERRO', String(e && e.message || e)); SES.armed = false; renderCtl(); }
finally { SES.busy = false; SES.lastBuy = Date.now(); }
}
// ---------- navegação automática: abrir o mercado e pesquisar sozinho ----------
const label = (b) => flat(b.tagName === 'INPUT' ? b.value : leafText(b)) || (b.getAttribute('aria-label') || b.getAttribute('title') || '').trim();
const clickables = (root) => [...root.querySelectorAll('button,[role="button"],a,input[type="button"],input[type="submit"],[class*="btn" i],[class*="button" i]')].filter((b) => vis(b) && !host.contains(b) && !disabled(b));
// botão de pesquisa do painel: "Buscar/Pesquisar/…" tem prioridade sobre "Atualizar"; se houver mais de um candidato, não clica
function findSearchBtn(panel) {
const bs = clickables(panel).filter((b) => !BUYTXT.test(label(b)));
for (const re of [SEARCHW, REFRESHW]) { const f = bs.filter((b) => re.test(label(b))); if (f.length === 1) return { btn: f[0], n: 1 }; if (f.length > 1) return { btn: null, n: f.length }; }
return { btn: null, n: 0 };
}
function findInput(panel) {
const ins = [...panel.querySelectorAll('input[type="text"],input[type="search"],input:not([type]),textarea')].filter((x) => vis(x) && !host.contains(x) && !x.disabled && !x.readOnly);
if (ins.length === 1) return ins[0];
const hint = ins.filter((x) => /buscar|pesquisar|procurar|search|nome|name|filtro|filter/i.test((x.placeholder || '') + ' ' + (x.getAttribute('aria-label') || '') + ' ' + (x.name || '')));
return hint.length === 1 ? hint[0] : null;
}
function locatePanel() { // lista vazia: acha o painel pelo título "Mercado" (não pelo item do menu)
for (const e of document.querySelectorAll('h1,h2,h3,h4,h5,[class*="title" i],[class*="heading" i]')) {
if (host.contains(e) || !vis(e) || e.children.length > 2 || !NAV.test(flat(e.textContent))) continue;
for (let p = e.parentElement, i = 0; p && i < 6; p = p.parentElement, i++) if (findSearchBtn(p).n === 1 || findInput(p)) return p;
}
return null;
}
function ensurePanel(rows) {
if (SES.panel && SES.panel.isConnected && vis(SES.panel)) return SES.panel;
const mk = rows.filter((r) => r.market);
let p = null;
if (mk.length) { for (let a = mk[0].el.parentElement, i = 0; a && i < 10 && !p; a = a.parentElement, i++) if (findSearchBtn(a).n === 1 || findInput(a)) p = a; if (!p) p = mk[0].el.parentElement; }
if (!p) p = locatePanel();
SES.panel = p;
return p;
}
function findNav() { // o item "Mercado" do menu: texto exato, o menor elemento, nunca título nem algo dentro do próprio painel
const c = [...document.querySelectorAll('button,[role="button"],[role="tab"],a,li,div,span,[class*="tab" i],[class*="menu" i],[class*="nav" i]')].filter((e) =>
!host.contains(e) && e.children.length <= 3 && e.textContent.length <= 40 && NAV.test(flat(e.textContent)) && vis(e) && !/^H[1-6]$/.test(e.tagName) && !(SES.panel && SES.panel.contains(e)));
const inner = c.filter((e) => !c.some((o) => o !== e && e.contains(o)));
const isBtn = (e) => /^(BUTTON|A)$/.test(e.tagName) || e.getAttribute('role') === 'tab' || e.getAttribute('role') === 'button' || getComputedStyle(e).cursor === 'pointer';
return inner.find(isBtn) || inner[0] || null;
}
async function maybeOpen(rows) {
if (ensurePanel(rows)) { SES.openFails = 0; return true; }
if (!S.cfg.autoOpen) return false;
const now = Date.now();
if (now - SES.lastOpen < 6000 || (SES.openFails >= 3 && now - SES.lastOpen < 60e3)) return false;
SES.lastOpen = now;
const nav = findNav();
SES.openFails++;
if (!nav) { if (!SES.navWarned) { SES.navWarned = true; addLog('AVISO', 'não achei o item "Mercado" do menu para abrir sozinho: abra o mercado uma vez à mão'); } return false; }
addLog('ABRINDO', 'cliquei em "' + flat(nav.textContent) + '" no menu');
nav.click();
const p = await waitFor(() => { TX = new Map(); return ensurePanel(findRows().map(analyze)); }, 2500);
if (p) { SES.openFails = 0; SES.navWarned = false; }
return !!p;
}
function setSearchText(panel) { // opcional: digita o nome da regra no campo de busca do mercado (uma regra por vez)
const inp = findInput(panel);
if (!inp) return;
const terms = []; let blank = false;
S.rules.filter((r) => r.on).forEach((r) => { const t = E.terms(r.names); if (t.length) terms.push(...t); else blank = true; });
if (blank) terms.push('');
if (!terms.length) return;
const v = terms[SES.nameIdx++ % terms.length];
const setter = Object.getOwnPropertyDescriptor(inp.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype, 'value').set;
setter.call(inp, v);
inp.dispatchEvent(new Event('input', { bubbles: true })); inp.dispatchEvent(new Event('change', { bubbles: true }));
}
function maybeSearch(panel) {
if (!S.cfg.autoSearch || !panel) return;
const now = Date.now();
if (now - SES.lastSearch < Math.max(3, S.cfg.searchSec) * 1000) return;
SES.lastSearch = now;
if (S.cfg.searchByName) setSearchText(panel);
const f = findSearchBtn(panel);
if (f.n === 1) { f.btn.click(); SES.searches++; setTimeout(quick, 700); }
else if (!SES.searchWarned) { SES.searchWarned = true; addLog('AVISO', f.n ? 'há mais de um botão de pesquisar/atualizar no mercado: não cliquei em nenhum' : 'não achei o botão de pesquisar/atualizar do mercado'); }
}
// reage na hora quando a lista do mercado muda (não espera o próximo ciclo)
function quick() { // nunca descarta o aviso de mudança: se for cedo demais (ou ocupado), reprograma
if (!SES.armed) return;
const wait = SES.looping || SES.busy ? 300 : Math.max(0, 300 - (Date.now() - SES.lastLoopAt));
if (wait > 0) { clearTimeout(SES.qt); SES.qt = setTimeout(quick, wait + 20); return; }
loop();
}
function observe(panel) {
if (!panel || SES.obsEl === panel) return;
if (SES.obs) SES.obs.disconnect();
SES.obsEl = panel;
SES.obs = new MutationObserver(() => { clearTimeout(SES.qt); SES.qt = setTimeout(quick, 150); });
SES.obs.observe(panel, { childList: true, subtree: true });
}
async function loop() {
if (SES.busy || SES.looping) return;
SES.looping = true; SES.lastLoopAt = Date.now(); SES.loops++;
try {
TX = new Map(); rollDay();
let rows = [];
try { rows = findRows().map(analyze); } catch (e) { if (!SES.errShown) { SES.errShown = true; addLog('ERRO', 'leitura do mercado: ' + e.message); } }
SES.rows = rows;
for (const r of rows) if (r.eligible && isNew(SES.seen, r)) { addLog('ENCONTRADO', desc(r.L) + ' (regra "' + r.rule.label + '")'); beep(); }
if (SES.armed) {
const cand = rows.filter((r) => r.eligible && !coolHit(r)).sort((a, b) => a.L.price - b.L.price)[0];
if (cand) await attempt(cand); // comprar vem antes de pesquisar de novo
const open = await maybeOpen(rows);
if (open) { maybeSearch(SES.panel); observe(SES.panel); }
}
} finally { SES.looping = false; }
renderLive(); if (S.ui.tab === 'market' || S.ui.tab === 'log') renderTab();
}
// ---------- diagnóstico ----------
async function copyText(s) {
try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(s); return true; } } catch (e) {}
try { const ta = document.createElement('textarea'); ta.value = s; ta.style.cssText = 'position:fixed;top:-1000px;opacity:0'; document.documentElement.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); ta.remove(); return !!ok; } catch (e) { return false; }
}
async function diag() {
const s = JSON.stringify({ v: VERSION, cfg: S.cfg, rules: S.rules.map((r) => ({ label: r.label, names: r.names, kind: r.kind, currency: r.currency, min: r.min, max: r.max })), day: S.day,
rows: SES.rows.slice(0, 8).map((r) => ({ text: flat(tx(r.el)).slice(0, 160), parsed: { name: r.L.name, kind: r.L.kind, level: r.L.level, power: r.L.power, iv: r.L.iv, shiny: r.L.shiny, price: r.L.price, currency: r.L.currency, seller: r.L.seller }, status: r.why })),
nav: { panel: SES.panel ? flat(tx(SES.panel)).slice(0, 100) : null, searchBtn: SES.panel ? (() => { const f = findSearchBtn(SES.panel); return f.n === 1 ? label(f.btn) : f.n + ' candidatos'; })() : null, input: SES.panel ? !!findInput(SES.panel) : null, navItem: (() => { const n = findNav(); return n ? flat(n.textContent) : null; })(), searches: SES.searches, openFails: SES.openFails },
log: S.log.slice(-12) }, null, 1);
diagText = '';
if (await copyText(s)) msg = 'Diagnóstico copiado: cole na conversa com o Claude.'; else diagText = s;
renderLive(); renderTab();
}
// ---------- HUD ----------
const host = document.createElement('div');
host.id = 'pmab-host';
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = '<style>' +
':host{all:initial}*{box-sizing:border-box}#p{position:fixed;right:12px;bottom:12px;z-index:2147483645;width:380px;max-height:86vh;overflow:auto;background:#101a17;color:#e8f3ee;font:12px/1.45 system-ui;border:1px solid #2b4a3f;border-radius:12px;box-shadow:0 6px 24px #000a}' +
'.hd{display:flex;align-items:center;gap:6px;padding:7px 10px;background:linear-gradient(90deg,#34d399,#10b981);color:#04281c;border-radius:11px 11px 0 0;cursor:pointer}.hd b{font-size:13px;color:#04281c}.hd .ver{opacity:.7;font-size:10px}.hd .car{margin-left:auto}' +
'.bd{padding:8px 10px}.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:4px 0}label.c{display:flex;gap:6px;align-items:flex-start;margin:3px 0}' +
'button{background:#183229;color:#e8f3ee;border:1px solid #2f6b55;border-radius:6px;padding:3px 8px;font:11px system-ui;cursor:pointer}button:hover{background:#21443a}' +
'button.big{font-size:13px;font-weight:700;padding:6px 14px}button.go{background:#0f5c3f;border-color:#34d399}button.stop{background:#7a1f1f;border-color:#ff6b6b}' +
'.badge{display:inline-block;border-radius:999px;padding:1px 9px;font-weight:700;font-size:11px}.b0{background:#26352f;color:#b6c9c0}.b1{background:#3b3a14;color:#ffe27a}.b2{background:#0f5c3f;color:#9bf0c8}' +
'.card{background:#17261f;border:1px solid #2b4a3f;border-radius:8px;padding:6px 8px;margin:6px 0}.card.ruleon{border-color:#34d399}.dim{color:#8fa89d}.warn{color:#ffd78a}.bad{color:#ff8a8a}.ok{color:#7be495}b{color:#fff}' +
'.tabs{display:flex;gap:4px;margin:8px 0 4px}.tabs button.on{background:#0f5c3f;border-color:#34d399}' +
'.f{display:flex;flex-direction:column;gap:1px;margin:3px 0;flex:1 1 45%}.f span{color:#8fa89d;font-size:10px;text-transform:uppercase;letter-spacing:.03em}' +
'input[type=text],input[type=number],select{width:100%;background:#0c1512;color:#e8f3ee;border:1px solid #2f6b55;border-radius:5px;padding:3px 5px;font:12px system-ui}' +
'.grid{display:flex;flex-wrap:wrap;gap:6px}table{width:100%;border-collapse:collapse;margin:4px 0;font-size:11px}td,th{padding:2px 3px;text-align:left;vertical-align:top}th{color:#8fa89d;font-weight:500}' +
'.lg{font-size:11px;border-bottom:1px solid #1e332a;padding:2px 0}.k{font-weight:700}.k-COMPRA{color:#7be495}.k-ENCONTRADO{color:#9bd8ff}.k-CANCELADO,.k-ERRO{color:#ff8a8a}.k-SIMULAÇÃO{color:#ffe27a}.k-AVISO{color:#ffd78a}' +
'</style><div id="p"><div class="hd" data-a="collapse"><b>Market Auto-Buy</b><span class="ver">v' + VERSION + '</span><span class="car" id="car">▼</span></div><div class="bd" id="bd"><div id="ctl"></div><div id="live"></div><div id="tabs"></div><div id="tab"></div></div></div>';
const $r = (id) => root.getElementById(id);
function renderCtl() {
const c = S.cfg;
$r('ctl').innerHTML = '<div class="row"><button class="big ' + (SES.armed ? 'stop' : 'go') + '" data-a="arm">' + (SES.armed ? '⏹ PARAR' : '▶ ARMAR') + '</button>' +
'<span class="badge ' + (!SES.armed ? 'b0' : c.sim ? 'b1' : 'b2') + '">' + (!SES.armed ? 'PARADO · só monitorando' : c.sim ? 'SIMULANDO · pesquisa, mas não compra' : 'COMPRANDO DE VERDADE') + '</span></div>' +
'<label class="c"><input type="checkbox" data-f="cfg.sim" ' + (c.sim ? 'checked' : '') + '><span><b>Modo simulação</b> (recomendado até você ver que as linhas foram lidas certo): só avisa o que compraria.</span></label>' +
(c.sim ? '' : '<label class="c warn"><input type="checkbox" data-a="ack" ' + (SES.ack ? 'checked' : '') + '><span>Entendo que compras reais gastam ouro/gemas (gemas valem dinheiro) e que não dá para desfazer.</span></label>');
}
function renderLive() {
const rows = SES.rows, ok = rows.filter((r) => r.eligible).length, c = S.cfg, d = S.day;
const warns = [];
const navTxt = SES.panel && SES.panel.isConnected && vis(SES.panel) ? 'mercado aberto ✓' : SES.armed && c.autoOpen ? 'abrindo o mercado…' : 'mercado fechado';
if (!(c.capGems > 0) && !(c.capGold > 0)) warns.push('Os limites diários de gemas e de ouro estão em 0, então nada será comprado. Defina na aba Config.');
if (!S.rules.some((r) => r.on && r.max > 0)) warns.push('Nenhuma regra ligada tem preço máximo, então nenhuma compra pode acontecer.');
if (!rows.length) warns.push(SES.armed && c.autoOpen ? 'Ainda sem listagens na tela: estou tentando abrir o mercado e pesquisar sozinho.' : 'Não vejo listagens com botão "Comprar". Clique em ARMAR (ele abre o mercado e pesquisa sozinho) ou abra a tela do mercado.');
if (c.autoSearch && c.searchSec < 5) warns.push('Pesquisar com menos de 5 s de intervalo pode ser tratado como excesso de requisições pelo servidor do jogo.');
$r('live').innerHTML = '<div class="card"><div>Linhas lidas: <b>' + rows.length + '</b> · aptas a comprar: <b class="' + (ok ? 'ok' : '') + '">' + ok + '</b></div>' +
'<div class="dim">' + navTxt + (c.autoSearch ? ' · pesquisas feitas: ' + SES.searches + (SES.lastSearch ? ' · última há ' + Math.round((Date.now() - SES.lastSearch) / 1000) + ' s' : '') : '') + '</div>' +
'<div class="dim">Hoje: gemas ' + fmt(d.gems) + ' / ' + fmt(c.capGems) + ' · ouro ' + fmt(d.gold) + ' / ' + fmt(c.capGold) + ' · compras ' + d.buys + ' / ' + (c.capBuysDay || '∞') + '</div></div>' +
warns.map((w) => '<div class="warn">⚠ ' + esc(w) + '</div>').join('') + (msg ? '<div class="ok">' + esc(msg) + '</div>' : '') + (!storageOk ? '<div class="bad">Não consegui salvar as configurações neste navegador.</div>' : '');
$r('car').textContent = S.ui.collapsed ? '▲' : '▼';
$r('bd').style.display = S.ui.collapsed ? 'none' : '';
}
const fld = (ri, f, label, type, ph) => '<label class="f"><span>' + label + '</span><input data-r="' + ri + '" data-f="' + f + '" data-t="' + type + '" type="' + (type === 'num' ? 'number' : 'text') + '" ' + (type === 'num' ? 'min="0" step="any"' : '') + ' value="' + esc(type === 'num' && !S.rules[ri][f] ? '' : S.rules[ri][f]) + '" placeholder="' + esc(ph || '') + '"></label>';
const sel = (ri, f, label, opts, rr) => '<label class="f"><span>' + label + '</span><select data-r="' + ri + '" data-f="' + f + '" ' + (rr ? 'data-rr="1"' : '') + '>' + opts.map((o) => '<option value="' + o[0] + '"' + (S.rules[ri][f] === o[0] ? ' selected' : '') + '>' + o[1] + '</option>').join('') + '</select></label>';
function ruleCard(r, i) {
const sum = (r.kind === 'pokemon' ? 'Pokémon' : r.kind === 'item' ? 'Item' : 'Qualquer') + ' · ' + (r.names ? esc(r.names) : 'qualquer nome') + ' · ' + (r.min ? fmt(r.min) + '–' : 'até ') + (r.max ? fmt(r.max) : '⚠ sem máximo') + ' ' + cname(r.currency);
let h = '<div class="card ' + (r.on ? 'ruleon' : '') + '"><div class="row"><label class="c" style="margin:0"><input type="checkbox" data-r="' + i + '" data-f="on" ' + (r.on ? 'checked' : '') + '><b>' + esc(r.label) + '</b></label><button data-a="open:' + i + '">' + (r.open ? 'Fechar' : 'Editar') + '</button><button data-a="del:' + i + '">Excluir</button></div><div class="dim">' + sum + (r.bought ? ' · comprou ' + r.bought : '') + '</div>';
if (r.open) {
h += '<div class="grid">' + fld(i, 'label', 'Nome da regra', 'str') + sel(i, 'kind', 'O que comprar', [['any', 'Qualquer coisa'], ['pokemon', 'Só pokémon'], ['item', 'Só item']], true) + '</div>' +
fld(i, 'names', 'Nome(s) (separe por vírgula; vazio = qualquer)', 'str', 'ex.: charizard, garchomp') +
'<div class="grid">' + sel(i, 'currency', 'Pagar em', [['gems', 'Gemas'], ['gold', 'Ouro']]) + fld(i, 'min', 'Preço mínimo', 'num', '0') + fld(i, 'max', 'Preço MÁXIMO (obrigatório)', 'num', 'ex.: 1500') + '</div>';
if (r.kind !== 'item') h += '<div class="dim" style="margin-top:4px">Filtros de pokémon (0 = sem filtro; se o dado não aparece na linha, não compra):</div><div class="grid">' + fld(i, 'lvMin', 'Nível mín.', 'num') + fld(i, 'lvMax', 'Nível máx.', 'num') + fld(i, 'powerMin', 'Poder mín. (1–5)', 'num') + fld(i, 'ivMin', 'IV mín.', 'num') + fld(i, 'qualMin', 'Qualidade mín.', 'num') + sel(i, 'shiny', 'Shiny', [['any', 'Tanto faz'], ['only', 'Só shiny'], ['no', 'Sem shiny']]) + '</div>';
h += '<div class="grid">' + fld(i, 'must', 'Texto obrigatório (vírgula)', 'str', 'ex.: perfeito') + fld(i, 'forbid', 'Texto proibido (vírgula)', 'str') + fld(i, 'maxBuys', 'Máx. de compras desta regra (0 = sem limite)', 'num') + '</div>';
}
return h + '</div>';
}
function renderTab() {
const tab = S.ui.tab, c = S.cfg;
$r('tabs').innerHTML = '<div class="tabs">' + [['rules', 'Regras'], ['config', 'Config'], ['market', 'Mercado'], ['log', 'Registro']].map((t) => '<button data-a="tab:' + t[0] + '" class="' + (tab === t[0] ? 'on' : '') + '">' + t[1] + '</button>').join('') + '</div>';
let h = '';
if (tab === 'rules') h = S.rules.map(ruleCard).join('') + '<button data-a="add">+ Nova regra</button><div class="dim">As regras são testadas de cima para baixo. Entre as linhas aptas, a mais barata é comprada primeiro.</div>';
else if (tab === 'config') {
const cf = (f, label, ph) => '<label class="f"><span>' + label + '</span><input data-f="cfg.' + f + '" data-t="num" type="number" min="0" step="any" value="' + (c[f] || '') + '" placeholder="' + (ph || '0') + '"></label>';
h = '<div class="card"><b>Limites de segurança (por dia)</b><div class="dim">Com 0, o script NÃO compra naquela moeda.</div><div class="grid">' + cf('capGems', 'Máx. de gemas por dia') + cf('capGold', 'Máx. de ouro por dia') + cf('capBuysDay', 'Máx. de compras por dia', '5') + '</div></div>' +
'<div class="card"><b>Abrir e pesquisar sozinho (só com o script ARMADO)</b>' +
'<label class="c"><input type="checkbox" data-f="cfg.autoOpen" ' + (c.autoOpen ? 'checked' : '') + '><span>Abrir o mercado sozinho (clica no item "Mercado" do menu quando ele não está na tela)</span></label>' +
'<label class="c"><input type="checkbox" data-f="cfg.autoSearch" ' + (c.autoSearch ? 'checked' : '') + '><span>Pesquisar sozinho o tempo todo (clica em "Buscar/Atualizar" do mercado)</span></label>' +
(c.autoSearch ? '<div class="grid">' + cf('searchSec', 'A cada quantos segundos (mín. 3; recomendo 5 ou mais)', '5') + '</div>' +
'<label class="c"><input type="checkbox" data-f="cfg.searchByName" ' + (c.searchByName ? 'checked' : '') + '><span>Digitar o nome das regras no campo de busca, uma regra por vez (útil se a lista do mercado é grande ou tem páginas)</span></label>' : '') +
'<div class="dim">Isso só navega e pesquisa: comprar continua dependendo de armar, da simulação e dos limites.</div></div>' +
'<div class="card"><b>Ritmo das compras</b><div class="grid">' + cf('minGapSec', 'Segundos entre compras', '4') + cf('maxPerMin', 'Máx. de compras por minuto', '3') + '</div></div>' +
'<div class="card"><b>Confirmação e avisos</b><label class="c"><input type="checkbox" data-f="cfg.autoConfirm" ' + (c.autoConfirm ? 'checked' : '') + '><span>Confirmar sozinho o diálogo de compra do jogo (desligado: você clica em confirmar). Só confirma se o diálogo trouxer o mesmo preço, moeda e nome.</span></label>' +
'<label class="c"><input type="checkbox" data-f="cfg.beep" ' + (c.beep ? 'checked' : '') + '><span>Tocar um bip quando achar algo</span></label></div>' +
'<div class="card"><b>Não comprar de</b><label class="f"><span>Vendedores/textos bloqueados (vírgula)</span><input data-f="cfg.blocked" data-t="str" type="text" value="' + esc(c.blocked) + '" placeholder="ex.: minhaconta2, amigo"></label><div class="dim">Ponha aqui as suas outras contas: os termos do jogo proíbem combinar resultados do mercado para transferir valor entre contas.</div></div>' +
'<div><button data-a="resetday">Zerar contadores de hoje</button></div>';
} else if (tab === 'market') {
h = SES.rows.length ? '<table><tr><th>Item / pokémon</th><th>Preço</th><th>Situação</th></tr>' + SES.rows.slice(0, 14).map((r) => '<tr><td>' + esc(r.L.name || '?') + (r.L.kind === 'pokemon' ? '<div class="dim">Nv ' + r.L.level + (Number.isFinite(r.L.power) ? ' P' + r.L.power : '') + (Number.isFinite(r.L.iv) ? ' IV ' + r.L.iv : '') + (r.L.shiny ? ' ✨' : '') + '</div>' : '') + '</td><td>' + (Number.isFinite(r.L.price) ? fmt(r.L.price) + ' ' + cname(r.L.currency) : '–') + '</td><td class="' + (r.eligible ? 'ok' : 'dim') + '">' + esc(r.why) + '</td></tr>').join('') + '</table>' : '<div class="dim">Nenhuma linha com botão "Comprar" na tela.</div>';
h += '<div><button data-a="diag">Diagnóstico</button></div>';
} else {
h = (S.log.length ? S.log.slice(-40).reverse().map((l) => '<div class="lg"><span class="k k-' + esc(l.kind) + '">' + esc(l.kind) + '</span> <span class="dim">' + new Date(l.t).toLocaleTimeString() + '</span> ' + esc(l.text) + '</div>').join('') : '<div class="dim">Sem eventos ainda.</div>') + '<div style="margin-top:6px"><button data-a="clearlog">Limpar registro</button></div>';
}
if (diagText) h += '<textarea data-a="selall" readonly rows="6" style="width:100%;font:10px monospace;background:#0c1512;color:#e8f3ee">' + esc(diagText) + '</textarea><div class="dim">Não consegui copiar sozinho: clique, Ctrl+A, Ctrl+C e cole na conversa.</div><button data-a="closediag">Fechar</button>';
$r('tab').innerHTML = h;
}
const setPath = (o, path, v) => { const ks = path.split('.'); let t = o; ks.slice(0, -1).forEach((k) => { t = t[k]; }); t[ks[ks.length - 1]] = v; };
function apply(t) {
const f = t.dataset.f; if (!f) return;
let v = t.type === 'checkbox' ? t.checked : t.value;
if (t.dataset.t === 'num') v = Math.max(0, parseFloat(String(v).replace(',', '.')) || 0);
if (t.dataset.r !== undefined) S.rules[+t.dataset.r][f] = v; else setPath(S, f, v);
if (f === 'cfg.sim') { SES.ack = false; if (v) msg = ''; }
if (f === 'cfg.autoSearch' || f === 'cfg.autoOpen' || f === 'cfg.searchByName') { SES.searchWarned = false; SES.navWarned = false; SES.openFails = 0; }
save();
if (f === 'cfg.sim') renderCtl();
if (t.dataset.rr || f === 'cfg.autoSearch' || f === 'on') renderTab();
renderLive();
}
root.addEventListener('input', (e) => { if (e.target.type !== 'checkbox' && e.target.tagName !== 'SELECT') apply(e.target); });
root.addEventListener('change', (e) => {
const t = e.target;
if (t.dataset && t.dataset.a === 'ack') { SES.ack = t.checked; renderCtl(); return; }
apply(t);
});
['keydown', 'keyup', 'keypress'].forEach((k) => root.addEventListener(k, (e) => e.stopPropagation())); // o jogo não pode ler o que você digita aqui
root.addEventListener('click', (e) => {
const a = e.target.closest('[data-a]');
if (!a) return;
const k = a.dataset.a;
if (k === 'collapse') { S.ui.collapsed = !S.ui.collapsed; save(); renderLive(); }
else if (k === 'arm') {
if (SES.armed) { SES.armed = false; addLog('PARADO', 'você parou'); }
else if (!S.cfg.sim && !SES.ack) { msg = 'Para comprar de verdade, marque o aviso de que você entende que gasta ouro/gemas.'; }
else { SES.armed = true; SES.lastBuy = Date.now(); msg = ''; addLog('ARMADO', S.cfg.sim ? 'modo simulação' : 'COMPRAS REAIS'); }
renderCtl(); renderLive();
}
else if (k.startsWith('tab:')) { S.ui.tab = k.slice(4); save(); renderTab(); }
else if (k === 'add') { S.rules.push(newRule()); save(); renderTab(); renderLive(); }
else if (k.startsWith('del:')) { S.rules.splice(+k.slice(4), 1); save(); renderTab(); renderLive(); }
else if (k.startsWith('open:')) { const r = S.rules[+k.slice(5)]; r.open = !r.open; save(); renderTab(); }
else if (k === 'resetday') { S.day = { date: today(), gems: 0, gold: 0, buys: 0 }; S.rules.forEach((r) => { r.bought = 0; }); save(); renderLive(); renderTab(); }
else if (k === 'diag') diag();
else if (k === 'selall') a.select();
else if (k === 'closediag') { diagText = ''; renderTab(); }
else if (k === 'clearlog') { S.log = []; save(); renderTab(); }
});
const saved = load();
if (saved && saved.cfg) { S = Object.assign(S, saved); S.cfg = Object.assign({}, DEFAULT_CFG, saved.cfg); delete S.cfg.refresh; delete S.cfg.refreshSec; S.v = VERSION; S.ui = Object.assign({ tab: 'rules', collapsed: false }, saved.ui); }
S.cfg.sim = true; // depois de recarregar, volta sempre ao modo simulação (e parado)
rollDay();
document.documentElement.appendChild(host);
try { if (localStorage.getItem('pmab_debug')) window.__pmab = { loop, quick, SES, get S() { return S; }, stopTimer: () => clearInterval(timerId) }; } catch (e) {}
renderCtl(); renderLive(); renderTab();
const timerId = setInterval(loop, 1500);
loop();
})();