SimpleMMO Auto Bot

Optimized auto stepper, job performer, and NPC attacker for SimpleMMO

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         SimpleMMO Auto Bot
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Optimized auto stepper, job performer, and NPC attacker for SimpleMMO
// @author       PakGembus
// @match        https://web.simple-mmo.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // === PREVENT DUPLICATE & EXCLUDE PATHS ===
    if (window.SMMO_BOT_LOADED) return;
    window.SMMO_BOT_LOADED = true;
    if (['/messages', '/chat', '/inbox', '/settings', '/help', '/support'].some(p => location.pathname.startsWith(p))) return;

    // === CONFIG ===
    const CFG = { MIN_DELAY: 500, MAX_DELAY: 2500, CHECK_MS: 300 };
    const K = { STATE: 'smmo_state', STATS: 'smmo_stats', POS: 'smmo_pos', LOCK: 'smmo_lock', COLLAPSED: 'smmo_collapsed' };

    // === STATE ===
    let S = { running: false, waiting: false, paused: false, locked: false, collapsed: true, autoStep: true, autoJob: true, autoAttack: true, autoCatch: true, autoGather: true, autoArena: true, fastMode: false };
    let stats = { steps: 0, attacks: 0, catches: 0, gathers: 0, jobs: 0, arenas: 0, startTime: null };
    let timer = null, worker = null, pending = null, captchaInt = null, titleInt = null;
    const origTitle = document.title;

    // === STORAGE ===
    const get = (k, d = null) => { try { return JSON.parse(localStorage.getItem(k)) || d; } catch { return d; } };
    const set = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch { } };

    function loadAll() {
        const s = get(K.STATE, {});
        Object.keys(S).forEach(k => { if (s[k] !== undefined) S[k] = s[k]; });
        const st = get(K.STATS, {});
        if (st.startTime && Date.now() - st.startTime < 3600000) Object.assign(stats, st);
        else stats.startTime = Date.now();
        S.locked = get(K.LOCK, false);
        S.collapsed = get(K.COLLAPSED, true);
    }

    const save = () => set(K.STATE, S);
    const saveStats = () => set(K.STATS, { ...stats, lastUpdate: Date.now() });

    // === UTILITY ===
    const getDelay = () => S.fastMode ? 1000 + Math.random() * 1000 : CFG.MIN_DELAY + Math.random() * CFG.MAX_DELAY + (Math.random() > 0.95 ? 5000 : 0);
    const $ = id => document.getElementById(id);

    // === BUTTON FINDER ===
    const btnCfg = {
        step: { includes: ['take a step'], selectors: ['button[id^="step_btn"]'] },
        attack: { exact: ['attack'], includes: ['attack', 'fight'], excludes: ['step', 'stats', 'generate'], selectors: ['#attackButton'] },
        leave: { exact: ['leave'] },
        catch: { exact: ['catch', 'collect', 'salvage', 'grab', 'take', 'harvest'], includes: ['salvage'] },
        gather: { includes: ['press here to gather', 'gather all', 'gather (energy'], excludes: ['close'] },
        close: { exact: ['press here to close', 'close'] },
        job: { includes: ['perform', 'work', 'collect'] },
        arenaGenerate: { includes: ['generate next opponent'], excludes: [] },
        arenaAttack: { exact: ['attack'], includes: ['attack'], excludes: ['step', 'stats', 'generate'] }
    };

    function findBtn(cfg) {
        const { exact = [], includes = [], excludes = [], selectors = [] } = cfg;
        for (const b of document.querySelectorAll('button, a.btn, a')) {
            if (b.disabled) continue;
            const t = (b.innerText || '').toLowerCase().trim();
            if (excludes.some(e => t.includes(e))) continue;
            if (exact.some(e => t === e) || includes.some(i => t.includes(i))) return b;
        }
        for (const s of selectors) { try { const b = document.querySelector(s); if (b && !b.disabled) return b; } catch { } }
        return null;
    }

    const getBtn = type => findBtn(btnCfg[type] || {});
    const isArenaPage = () => {
        // Cek URL path
        if (location.pathname.includes('/battle/arena') || location.pathname.includes('/arena/')) return true;
        // Cek apakah ada tombol Generate Next Opponent
        const txt = document.body.innerText.toLowerCase();
        if (txt.includes('generate next opponent')) return true;
        // Cek breadcrumb Battle Arena
        if (txt.includes('battle arena')) return true;
        return false;
    };
    const getLeaveBtn = () => location.pathname.includes('/npcs/') && !isArenaPage() && ['defeated', 'victory', 'you won', 'you lost', 'slain'].some(w => document.body.innerText.toLowerCase().includes(w)) ? getBtn('leave') : null;
    const getCloseBtn = () => ['remaining material', 'fishing level', 'mining level', 'woodcutting', 'foraging'].some(w => document.body.innerText.toLowerCase().includes(w)) ? getBtn('close') : null;
    const isBtnLoading = b => !b || b.disabled || (b.querySelector('img[src*="please-wait"], .spinner') && getComputedStyle(b.querySelector('img[src*="please-wait"], .spinner')).display !== 'none');
    const hasArenaEnergy = () => {
        const txt = document.body.innerText;
        const match = txt.match(/[⚡🔋]\s*(\d+)/); // mencari icon energy dengan angka
        if (match) return parseInt(match[1]) > 0;
        // Juga cek tombol generate masih ada dan tidak disabled
        const genBtn = getBtn('arenaGenerate');
        return genBtn && !genBtn.disabled;
    };
    const getArenaLeaveBtn = () => {
        if (!isArenaPage()) return null;
        // Cek apakah battle selesai (menang atau kalah)
        const txt = document.body.innerText.toLowerCase();
        if (['you won', 'you lost', 'victory', 'defeated', 'battle over'].some(w => txt.includes(w))) {
            return getBtn('leave');
        }
        return null;
    };

    function isGatherDone() {
        if (getBtn('gather')) return false;
        const txt = document.body.innerText.toLowerCase();
        if (!txt.includes('press here to close')) return false;
        const m = txt.match(/remaining material[:\s]*(\d+)/i);
        return m ? parseInt(m[1]) === 0 : true;
    }

    // === CAPTCHA ===
    const checkCaptcha = () => location.href.includes('/i-am-not-a-bot') || ['woah! hold up', "i'm a person", 'are you a bot', 'verify you are human'].some(w => document.body.innerText.toLowerCase().includes(w)) || !!document.querySelector('.g-recaptcha, iframe[src*="recaptcha"], .cf-turnstile');

    // === TIMER ===
    function createWorker() {
        try {
            const w = new Worker(URL.createObjectURL(new Blob([`self.onmessage=e=>{setTimeout(()=>self.postMessage('done'),e.data.ms)};`], { type: 'application/javascript' })));
            w.onmessage = () => { if (pending && S.running) { const fn = pending; pending = null; fn(); } };
            return w;
        } catch { return null; }
    }

    function setTimer(fn, ms) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => { if (S.running) fn(); }, ms);
        if (worker) { pending = fn; worker.postMessage({ ms }); }
    }

    // === NOTIFICATIONS ===
    function playBeep() {
        try {
            const ctx = new (window.AudioContext || window.webkitAudioContext)();
            [800, 1000, 1200].forEach((f, i) => setTimeout(() => {
                const o = ctx.createOscillator(), g = ctx.createGain();
                o.connect(g); g.connect(ctx.destination);
                o.frequency.value = f; g.gain.setValueAtTime(0.3, ctx.currentTime);
                g.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
                o.start(); o.stop(ctx.currentTime + 0.3);
            }, i * 150));
        } catch { }
    }

    function flashTitle(msg) {
        if (titleInt) return;
        let orig = true;
        titleInt = setInterval(() => { document.title = orig ? `⚠️ ${msg}` : origTitle; orig = !orig; }, 1000);
    }

    function stopFlash() { if (titleInt) { clearInterval(titleInt); titleInt = null; document.title = origTitle; } }

    function startCaptchaMon() {
        if (captchaInt) return;
        captchaInt = setInterval(() => {
            if (!checkCaptcha()) {
                clearInterval(captchaInt); captchaInt = null; stopFlash();
                S.paused = false; updateUI();
                setTimeout(() => { if (!S.running) startBot(); }, 2000);
            }
        }, 2000);
    }

    function notify(title, msg) {
        playBeep(); flashTitle('CAPTCHA!');
        if ('Notification' in window && Notification.permission === 'granted') {
            const n = new Notification(title, { body: msg, icon: 'https://web.simple-mmo.com/favicon.ico', requireInteraction: true });
            n.onclick = () => { window.focus(); n.close(); stopFlash(); };
            setTimeout(() => n.close(), 60000);
        }
    }

    // === UI ===
    function createUI() {
        const css = document.createElement('style');
        css.textContent = `
            @keyframes pulse{0%,100%{transform:scale(1);box-shadow:0 2px 10px rgba(0,255,136,0.4)}50%{transform:scale(1.1);box-shadow:0 4px 20px rgba(0,255,136,0.7)}}
            @keyframes alert{0%,100%{box-shadow:0 2px 10px rgba(255,100,100,0.4);border-color:#f66}50%{box-shadow:0 4px 20px rgba(255,100,100,0.8);border-color:#f33}}
            #smmo-dot{position:fixed;z-index:99999;width:44px;height:44px;border-radius:50%;background:#1a1a2e;border:2px solid #0df;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;transition:all .2s}
            #smmo-dot:hover{transform:scale(1.15)}
            #smmo-dot.on{animation:pulse 1.5s infinite;border-color:#0f8}
            #smmo-dot.warn{animation:alert 1s infinite}
            #smmo-ui{position:fixed;z-index:99999;background:#1a1a2e;color:#fff;padding:12px;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,.4);font:12px/1.4 'Segoe UI',sans-serif;min-width:180px;border:1px solid #333;display:none}
            #smmo-ui.on{box-shadow:0 4px 20px rgba(0,255,136,0.3);border-color:#0f8}
            .smmo-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;color:#0df;font-weight:bold}
            .smmo-btn{width:22px;height:22px;border:none;border-radius:4px;background:#333;color:#fff;cursor:pointer;font-size:11px;margin-left:4px}
            .smmo-btn:hover{background:#444}
            .smmo-toggles{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px;font-size:10px;margin:8px 0;padding:8px;background:#111;border-radius:6px}
            .smmo-toggles label{display:flex;align-items:center;gap:4px;cursor:pointer}
            .smmo-toggles input{margin:0}
            .smmo-main{width:100%;padding:8px;border:none;border-radius:6px;color:#fff;font-weight:bold;cursor:pointer;font-size:11px}
            .smmo-stats{font-size:9px;color:#888;margin-top:8px;text-align:center}
        `;
        document.head.appendChild(css);

        // Dot
        const dot = document.createElement('div');
        dot.id = 'smmo-dot';
        dot.innerHTML = '🎮';
        dot.title = 'SMMO Bot';
        dot.style.cssText = `left:20px;bottom:20px;`;
        dot.onclick = () => { S.collapsed = !S.collapsed; set(K.COLLAPSED, S.collapsed); updateUI(); };
        document.body.appendChild(dot);

        // Panel
        const ui = document.createElement('div');
        ui.id = 'smmo-ui';
        const pos = get(K.POS, { left: 20, top: window.innerHeight - 320 });
        ui.style.cssText = `left:${pos.left}px;top:${pos.top}px;`;
        ui.innerHTML = `
            <div class="smmo-hdr">
                <span>🎮 Bot</span>
                <div>
                    <button class="smmo-btn" id="lock-btn" title="Lock">🔓</button>
                    <button class="smmo-btn" id="close-btn" title="Minimize">●</button>
                </div>
            </div>
            <div id="status" style="text-align:center;padding:6px;background:#111;border-radius:4px;margin-bottom:8px">⏸️ Idle</div>
            <div class="smmo-toggles">
                <label><input type="checkbox" id="t-step" checked>🚶 Step</label>
                <label><input type="checkbox" id="t-attack" checked>⚔️ Attack</label>
                <label><input type="checkbox" id="t-job" checked>💼 Job</label>
                <label><input type="checkbox" id="t-catch" checked>🎣 Catch</label>
                <label><input type="checkbox" id="t-gather" checked>⛏️ Gather</label>
                <label><input type="checkbox" id="t-arena" checked>🏟️ Arena</label>
                <label><input type="checkbox" id="t-fast">⚡ Fast</label>
            </div>
            <button class="smmo-main" id="main-btn" style="background:linear-gradient(135deg,#0a6,#0ac)">▶ START</button>
            <div class="smmo-stats" id="stats"></div>
        `;
        document.body.appendChild(ui);

        // Events
        $('close-btn').onclick = () => { S.collapsed = true; set(K.COLLAPSED, true); updateUI(); };
        $('lock-btn').onclick = () => { S.locked = !S.locked; set(K.LOCK, S.locked); $('lock-btn').textContent = S.locked ? '🔒' : '🔓'; ui.style.cursor = S.locked ? 'default' : 'move'; };
        $('main-btn').onclick = () => S.running ? stopBot() : startBot();

        ['step', 'attack', 'job', 'catch', 'gather', 'arena', 'fast'].forEach(k => {
            const key = k === 'fast' ? 'fastMode' : 'auto' + k.charAt(0).toUpperCase() + k.slice(1);
            const cb = $('t-' + k);
            cb.checked = S[key];
            cb.onchange = () => { S[key] = cb.checked; save(); };
        });

        // Drag
        let drag = false, ox, oy;
        ui.onmousedown = e => {
            if (S.locked || ['BUTTON', 'INPUT'].includes(e.target.tagName)) return;
            drag = true; ox = e.clientX - ui.offsetLeft; oy = e.clientY - ui.offsetTop;
        };
        document.onmousemove = e => { if (drag && !S.locked) { ui.style.left = (e.clientX - ox) + 'px'; ui.style.top = (e.clientY - oy) + 'px'; } };
        document.onmouseup = () => { if (drag && !S.locked) set(K.POS, { left: parseInt(ui.style.left), top: parseInt(ui.style.top) }); drag = false; };

        updateUI();
    }

    function updateUI() {
        const dot = $('smmo-dot'), ui = $('smmo-ui'), btn = $('main-btn'), status = $('status');
        if (dot) {
            dot.style.display = S.collapsed ? 'flex' : 'none';
            dot.className = 'smmo-dot' + (S.paused ? ' warn' : S.running ? ' on' : '');
            dot.innerHTML = S.paused ? '⚠️' : '🎮';
        }
        if (ui) {
            ui.style.display = S.collapsed ? 'none' : 'block';
            ui.className = S.running ? 'on' : '';
        }
        if (btn) {
            if (S.paused) { btn.textContent = '⏸️ PAUSED'; btn.style.background = 'linear-gradient(135deg,#c60,#a40)'; }
            else if (S.running) { btn.textContent = '⏹ STOP'; btn.style.background = 'linear-gradient(135deg,#c33,#a22)'; }
            else { btn.textContent = '▶ START'; btn.style.background = 'linear-gradient(135deg,#0a6,#0ac)'; }
        }
        if (status && S.paused) { status.innerHTML = '⚠️ <b>CAPTCHA</b>'; status.style.color = '#f66'; }
        updateStats();
    }

    function updateStatus(txt, color = '#fff') {
        const s = $('status');
        if (s && !S.paused) { s.textContent = txt; s.style.color = color; }
    }

    function updateStats() {
        const s = $('stats');
        if (!s) return;
        const m = stats.startTime ? Math.floor((Date.now() - stats.startTime) / 60000) : 0;
        s.textContent = `🚶${stats.steps} ⚔️${stats.attacks} 🎣${stats.catches} ⛏️${stats.gathers} 💼${stats.jobs} 🏟️${stats.arenas} ⏱️${m < 60 ? m + 'm' : Math.floor(m / 60) + 'h' + (m % 60) + 'm'}`;
    }

    function inc(type) { stats[type]++; saveStats(); updateStats(); }

    // === ACTIONS ===
    const doAction = (type, stat, msg, color) => {
        const key = 'auto' + type.charAt(0).toUpperCase() + type.slice(1);
        if (type !== 'leave' && type !== 'close' && !S[key]) return false;
        const b = type === 'leave' ? getLeaveBtn() : type === 'close' ? getCloseBtn() : getBtn(type);
        if (!b || (type === 'step' && isBtnLoading(b))) return false;
        b.click();
        if (stat) inc(stat);
        updateStatus(msg, color);
        return true;
    };

    // === MAIN LOOP ===
    function loop() {
        if (!S.running) return;

        if (checkCaptcha()) {
            S.paused = true; S.running = false; S.waiting = false;
            updateUI(); notify('⚠️ CAPTCHA!', 'Solve manually'); startCaptchaMon();
            return;
        }

        const schedule = (fn, delay, next = CFG.CHECK_MS) => {
            if (S.waiting) return true;
            S.waiting = true;
            setTimer(() => { if (!S.running) return; fn(); S.waiting = false; setTimer(loop, next); }, delay);
            return true;
        };

        // Priority: Leave > Close > Arena > Attack > Catch > Gather > Job > Step
        if (getLeaveBtn()) { updateStatus('🚪 Leaving...', '#4c5'); schedule(() => doAction('leave'), 500 + Math.random() * 500); return; }
        if (getCloseBtn() && isGatherDone()) { updateStatus('🔙 Closing...', '#888'); schedule(() => doAction('close'), 500 + Math.random() * 500); return; }

        // Battle Arena Logic
        if (S.autoArena && isArenaPage()) {
            // Cek apakah ada tombol leave setelah battle selesai di arena
            const arenaLeave = getArenaLeaveBtn();
            if (arenaLeave) {
                updateStatus('🏟️ Battle done, next...', '#d4a');
                schedule(() => {
                    // Tidak leave, tapi cari lawan baru
                    const genBtn = getBtn('arenaGenerate');
                    if (genBtn && !genBtn.disabled) {
                        genBtn.click();
                        inc('arenas');
                        updateStatus('🏟️ Next opponent!', '#d4a');
                    }
                }, 1000 + Math.random() * 1500);
                return;
            }

            // Cek tombol attack di arena
            const arenaAttackBtn = getBtn('arenaAttack');
            if (arenaAttackBtn && !arenaAttackBtn.disabled) {
                const d = 1000 + Math.random() * 1500;
                updateStatus(`🏟️ Arena ${(d / 1000).toFixed(1)}s`, '#d4a');
                schedule(() => {
                    arenaAttackBtn.click();
                    updateStatus('🏟️ Attack!', '#d4a');
                }, d, 500);
                return;
            }

            // Cek tombol generate opponent
            const genBtn = getBtn('arenaGenerate');
            if (genBtn && !genBtn.disabled && hasArenaEnergy()) {
                const d = 1500 + Math.random() * 2000;
                updateStatus(`🏟️ Generate ${(d / 1000).toFixed(1)}s`, '#d4a');
                schedule(() => {
                    genBtn.click();
                    inc('arenas');
                    updateStatus('🏟️ Opponent found!', '#d4a');
                }, d, 800);
                return;
            }

            // Energy habis atau tidak ada tombol
            if (!genBtn || genBtn.disabled) {
                updateStatus('🏟️ No energy/cooldown', '#888');
            }
        }

        if (S.autoAttack && getBtn('attack') && !isArenaPage()) { const d = 1000 + Math.random() * 1500; updateStatus(`⚔️ ${(d / 1000).toFixed(1)}s`, '#f66'); schedule(() => doAction('attack', 'attacks', '⚔️ Attack!', '#f66'), d, 500); return; }
        if (S.autoCatch && getBtn('catch')) { updateStatus('🎣 Catching...', '#0bd'); schedule(() => doAction('catch', 'catches', '🎣 Caught!', '#0bd'), 500 + Math.random() * 1000); return; }
        if (S.autoGather && getBtn('gather')) { const d = 1000 + Math.random() * 1000; updateStatus(`⛏️ ${(d / 1000).toFixed(1)}s`, '#8c4'); schedule(() => doAction('gather', 'gathers', '⛏️ Gathered!', '#8c4'), d, 500); return; }
        if (S.autoJob && location.pathname.includes('/job')) doAction('job', 'jobs', '💼 Done!', '#0f0');

        // Step
        if (S.autoStep && !S.waiting) {
            const b = getBtn('step');
            if (b && !isBtnLoading(b)) {
                const d = getDelay();
                updateStatus(`🤔 ${(d / 1000).toFixed(1)}s`, '#fe3');
                schedule(() => doAction('step', 'steps', '✅ Stepped!', '#0f0'), d);
                return;
            }
            updateStatus(b ? '⏳ Cooldown' : '🔍 Looking...', '#888');
        }

        setTimer(loop, CFG.CHECK_MS);
    }

    // === BOT CONTROL ===
    function startBot() {
        S.running = true; S.waiting = false; S.paused = false;
        if (captchaInt) { clearInterval(captchaInt); captchaInt = null; }
        stopFlash();
        if (!worker) worker = createWorker();
        save(); updateUI();
        updateStatus('🚀 Running...', '#0f0');
        loop();
    }

    function stopBot() {
        S.running = false; S.waiting = false;
        if (timer) clearTimeout(timer); timer = null; pending = null;
        save(); updateUI();
        updateStatus('⏸️ Stopped', '#fff');
    }

    // === INIT ===
    function init() {
        loadAll();
        if ('Notification' in window && Notification.permission === 'default') Notification.requestPermission();
        const setup = () => {
            createUI();
            if (S.running) { updateStatus('🚀 Resumed!', '#0f0'); setTimeout(loop, 1000); }
        };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(setup, 500));
        else setTimeout(setup, 500);
    }

    init();
})();