SimpleMMO Auto Bot

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();