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