Ortenskung crime bot - fixed (1.8.1)

Automates crimes (neighborhood + car robbery). Clean GUI and fixed loop.

当前为 2025-09-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         Ortenskung crime bot - fixed (1.8.1)
// @namespace    http://tampermonkey.net/
// @version      1.8.1
// @description  Automates crimes (neighborhood + car robbery). Clean GUI and fixed loop.
// @match        https://www.ortenskung.com/en/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // ---------------- KEEP-ALIVE (optional) ----------------
    try {
        setInterval(() => { /* heartbeat */ }, 30000);
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        const osc = audioCtx.createOscillator();
        osc.frequency.value = 0.0001; // near-silent (avoid 0 to reduce some errors)
        osc.connect(audioCtx.destination);
        osc.start();
    } catch (e) { /* audio context may be blocked, ignore */ }

    // ---------------- State ----------------
    let running = false;
    let loopTimer = null;
    let selectedCrimeId = 1;
    let autoClose = true;

    // If the UI already exists (script re-run), remove it before creating new one
    const existing = document.getElementById('__ortens_crime_bot_panel');
    if (existing) existing.remove();

    // ---------------- Create GUI ----------------
    const panel = document.createElement('div');
    panel.id = '__ortens_crime_bot_panel';
    Object.assign(panel.style, {
        position: 'fixed',
        top: '60px',
        right: '40px',
        width: '320px',
        background: '#131313',
        color: '#eaeaea',
        borderRadius: '10px',
        boxShadow: '0 6px 18px rgba(0,0,0,0.6)',
        fontFamily: 'Inter, Roboto, sans-serif',
        zIndex: 999999,
        userSelect: 'none',
        padding: '0',
        overflow: 'hidden'
    });

    panel.innerHTML = `
        <div id="__header" style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:#0f0f0f;border-bottom:1px solid rgba(255,255,255,0.04);cursor:move">
            <div style="font-weight:600">Crime Bot</div>
            <div style="display:flex;gap:6px;align-items:center">
                <button id="__minBtn" title="Minimize" style="background:transparent;border:none;color:#aaa;cursor:pointer;font-size:14px">—</button>
                <button id="__closeBtn" title="Close" style="background:transparent;border:none;color:#aaa;cursor:pointer;font-size:14px">✕</button>
            </div>
        </div>
        <div id="__body" style="padding:10px;display:block">
            <div style="display:flex;gap:8px;margin-bottom:8px">
                <button class="__tab" data-tab="main" style="flex:1;padding:6px;border-radius:6px;border:0;background:#1b1b1b;color:#fff;cursor:pointer">Main</button>
                <button class="__tab" data-tab="settings" style="flex:1;padding:6px;border-radius:6px;border:0;background:#121212;color:#888;cursor:pointer">Settings</button>
            </div>

            <div id="__tab_main" class="__tabpanel">
                <label style="font-size:13px">Crime Type</label>
                <select id="__crimeType" style="width:100%;padding:6px;margin:6px 0 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);background:#0b0b0b;color:#fff">
                    <option value="neighborhood">Neighborhood</option>
                    <option value="car">Car Robbery</option>
                </select>

                <label style="font-size:13px">Crime ID</label>
                <select id="__crimeId" style="width:100%;padding:6px;margin:6px 0 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);background:#0b0b0b;color:#fff">
                    ${Array.from({length:30}, (_,i)=>i+1).map(n=>`<option value="${n}">${n}</option>`).join('')}
                </select>

                <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
                    <label style="font-size:13px;display:flex;align-items:center;gap:6px">
                        <input type="checkbox" id="__autoClose" checked /> <span style="font-size:13px">Close dialogs</span>
                    </label>
                </div>

                <button id="__startBtn" style="width:100%;padding:8px;border-radius:8px;border:0;background:linear-gradient(180deg,#2fa84f,#208f3a);color:#fff;font-weight:600;cursor:pointer">Start</button>
            </div>

            <div id="__tab_settings" class="__tabpanel" style="display:none">
                <p style="font-size:13px;color:#bbb;margin:0 0 8px">Settings</p>
                <p style="font-size:12px;color:#999;margin:0 0 8px">Loop delay (ms)</p>
                <input id="__loopDelay" type="number" value="4000" style="width:100%;padding:6px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);background:#0b0b0b;color:#fff" />
                <p style="font-size:12px;color:#999;margin:10px 0 0">If something breaks, stop and inspect the selectors in the script.</p>
            </div>
        </div>
        <div id="__footer" style="font-size:11px;padding:6px 10px;background:#0f0f0f;border-top:1px solid rgba(255,255,255,0.03);color:#9a9a9a">
            Status: <span id="__status">Idle</span>
        </div>
    `;

    document.body.appendChild(panel);

    // ---------------- UI helpers ----------------
    const header = panel.querySelector('#__header');
    const body = panel.querySelector('#__body');
    const closeBtn = panel.querySelector('#__closeBtn');
    const minBtn = panel.querySelector('#__minBtn');
    const tabs = panel.querySelectorAll('.__tab');
    const tabMain = panel.querySelector('#__tab_main');
    const tabSettings = panel.querySelector('#__tab_settings');
    const startBtn = panel.querySelector('#__startBtn');
    const crimeIdEl = panel.querySelector('#__crimeId');
    const crimeTypeEl = panel.querySelector('#__crimeType');
    const autoCloseEl = panel.querySelector('#__autoClose');
    const statusEl = panel.querySelector('#__status');
    const loopDelayEl = panel.querySelector('#__loopDelay');

    // Dragging (pointer-friendly)
    (function makeDraggable(el) {
        let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0;
        header.addEventListener('pointerdown', (ev) => {
            dragging = true;
            sx = ev.clientX; sy = ev.clientY;
            const rect = el.getBoundingClientRect();
            ox = rect.left; oy = rect.top;
            header.setPointerCapture(ev.pointerId);
        });
        document.addEventListener('pointermove', (ev) => {
            if (!dragging) return;
            const dx = ev.clientX - sx;
            const dy = ev.clientY - sy;
            el.style.left = (ox + dx) + 'px';
            el.style.top = (oy + dy) + 'px';
            el.style.right = 'auto';
            el.style.position = 'fixed';
        });
        document.addEventListener('pointerup', (ev) => {
            dragging = false;
        });
    })(panel);

    // Tabs
    tabs.forEach(t => t.addEventListener('click', () => {
        tabs.forEach(x => x.style.background = '#121212');
        t.style.background = '#1b1b1b';
        const tab = t.dataset.tab;
        if (tab === 'main') {
            tabMain.style.display = 'block';
            tabSettings.style.display = 'none';
        } else {
            tabMain.style.display = 'none';
            tabSettings.style.display = 'block';
        }
    }));

    // Minimize / close
// --- Replace the existing minBtn.addEventListener('click', ...) handler with this block ---
minBtn.addEventListener('click', () => {
    // Use a data attribute to track minimized state (reliable across style changes)
    const isMinimized = panel.getAttribute('data-minimized') === 'true';

    if (!isMinimized) {
        // Minimize: hide body & footer, shrink panel for a compact header-only look
        body.style.display = 'none';
        footer.style.display = 'none';

        // Keep header visible and reduce width so it looks tidy when minimized
        panel.style.width = '160px';
        panel.style.height = 'auto';
        panel.setAttribute('data-minimized', 'true');

        // Update button visuals & tooltip for clarity
        minBtn.textContent = '▢'; // restore icon
        minBtn.title = 'Restore';
    } else {
        // Restore: show body & footer and return panel sizing
        body.style.display = 'block';
        footer.style.display = 'block';

        panel.style.width = '320px'; // original width used in your script
        panel.style.height = 'auto';
        panel.setAttribute('data-minimized', 'false');

        minBtn.textContent = '—'; // minimize icon
        minBtn.title = 'Minimize';
    }
});


    // ---------------- Core automation ----------------

    function setStatus(text) {
        statusEl.textContent = text;
    }

    function stopLoop() {
        running = false;
        if (loopTimer) {
            clearTimeout(loopTimer);
            loopTimer = null;
        }
        startBtn.textContent = 'Start';
        setStatus('Stopped');
    }

    async function startLoop() {
        if (running) return;
        running = true;
        startBtn.textContent = 'Stop';
        setStatus('Running');
        // sync UI values
        selectedCrimeId = parseInt(crimeIdEl.value, 10);
        autoClose = autoCloseEl.checked;
        await performCrime(); // start immediately
    }

    startBtn.addEventListener('click', () => {
        if (!running) startLoop();
        else stopLoop();
    });

    // Utility: safe query with timeout (optional)
    function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

    // Attempt to robustly find and click elements. The exact selectors depend on the game's DOM;
    // this function uses plausible selectors and falls back gracefully.
    async function performCrime() {
        if (!running) return;

        try {
            const delay = Math.max(50, parseInt(loopDelayEl.value, 10) || 4000);

            // 0) Check timers area if available (best-effort)
            const timerEl = document.querySelector('#my_timers > div.val') || document.querySelector('.timers .val');
            if (timerEl) {
                const txt = timerEl.textContent.trim();
                const m = txt.match(/(\d+)\s*\/\s*(\d+)/);
                if (m) {
                    const used = parseInt(m[1], 10), total = parseInt(m[2], 10);
                    const avail = total - used;
                    if (avail <= 0) {
                        setStatus('No timers left — waiting');
                        loopTimer = setTimeout(performCrime, 5000);
                        return;
                    }
                }
            }

            const mode = (crimeTypeEl.value || 'neighborhood');

            // 1) Ensure relevant dialog is open
            if (mode === 'neighborhood') {
                // Try to open crimes dialog if not present
                if (!document.querySelector('#go_crimes_dialog') && document.querySelector('#go_crimes')) {
                    document.querySelector('#go_crimes').click();
                    await sleep(300);
                }
                // Switch to Neighborhood tab/button if needed
                const nbBtn = Array.from(document.querySelectorAll('.crimes_button, .crimes_button_small, .crimes-tab, .crimes_button'))
                    .find(el => /neighborhood/i.test(el.textContent || el.innerText || ''));
                if (nbBtn && !document.querySelector('#neighborhood_crimes')) {
                    try { nbBtn.click(); } catch (e) {}
                    await sleep(250);
                }
            } else if (mode === 'car') {
                // Try to open cars dialog
                if (!document.querySelector('#dialog_cars') && document.querySelector('#go_cars')) {
                    document.querySelector('#go_cars').click();
                    await sleep(300);
                } else {
                    // sometimes the button text is 'Cars' or 'Car Robbery' or '#go_rob_car'
                    const carOpener = Array.from(document.querySelectorAll('a,button'))
                        .find(el => /(car|cars|vehicle|rob)/i.test(el.textContent || el.getAttribute('title') || ''));
                    if (carOpener && !document.querySelector('#dialog_cars')) {
                        try { carOpener.click(); } catch (e) {}
                        await sleep(300);
                    }
                }
            }

            // 2) Find commit/click button for the selected crime
            let commitBtn = null;

            if (mode === 'neighborhood') {
                // Buttons often have onclick including "'id':'N'" or "id: N"
                const btns = Array.from(document.querySelectorAll('.crime_button_small, .crime_button, a, button'))
                    .filter(el => el.getAttribute && (el.getAttribute('onclick') || '').toString().length > 0);
                commitBtn = btns.find(el => {
                    const on = (el.getAttribute('onclick') || '');
                    return new RegExp(`['"]?id['"]?\\s*[:=]\\s*['"]?${selectedCrimeId}['"]?`).test(on);
                }) || btns.find(el => (el.getAttribute('onclick') || '').includes(`'id':'${selectedCrimeId}'`));
                // fallback: match by visible label containing the id (rare)
                if (!commitBtn) {
                    const byText = Array.from(document.querySelectorAll('.crime_button_small, .crime_button'))
                        .find(el => (el.textContent || '').includes(String(selectedCrimeId)));
                    if (byText) commitBtn = byText;
                }
                // final fallback: first available small crime button
                if (!commitBtn) {
                    commitBtn = document.querySelector('.crime_button_small, .crime_button');
                }
            } else if (mode === 'car') {
                // Car robbery buttons may live under #dialog_cars or have onclick with 'robbing:perform' or 'robbing'
                commitBtn = Array.from(document.querySelectorAll('#dialog_cars .crime_button_small, #dialog_cars a, #dialog_cars button, .car_list a, .car_list button'))
                    .find(el => {
                        const on = el.getAttribute('onclick') || '';
                        return /robbing:perform|robbing|car|vehicle/i.test(on + ' ' + (el.textContent || ''));
                    });
                if (!commitBtn) {
                    // broader search
                    commitBtn = Array.from(document.querySelectorAll('a,button'))
                        .find(el => {
                            const on = (el.getAttribute('onclick') || '') + ' ' + (el.textContent || '');
                            return /robbing:perform|rob car|rob car|car robbery|rob car/i.test(on);
                        });
                }
                // last fallback: any clickable inside dialog_cars
                if (!commitBtn) {
                    commitBtn = document.querySelector('#dialog_cars .crime_button_small') || document.querySelector('#dialog_cars a') || document.querySelector('#dialog_cars button');
                }
            }

            if (!commitBtn) {
                setStatus('No crime button found — retrying');
                loopTimer = setTimeout(performCrime, 5000);
                return;
            }

            // Click commit
            try {
                commitBtn.scrollIntoView({block:'center', behavior:'auto'});
            } catch (e) {}
            try {
                commitBtn.click();
                setStatus(`Clicked ${mode} button`);
            } catch (e) {
                try { commitBtn.dispatchEvent(new MouseEvent('click', {bubbles:true,cancelable:true})); setStatus('Dispatched click'); }
                catch (err) { console.error('click failed', err); setStatus('Click failed'); }
            }

            // 3) After clicking, attempt to close dialogs / notifications
            setTimeout(() => {
                try {
                    if (typeof close_dialog === 'function') {
                        // best-effort closes
                        try { close_dialog('social'); } catch {}
                        try { close_dialog('levelup'); } catch {}
                        if (autoClose) try { close_dialog('crimes'); } catch {}
                        if (autoClose) try { close_dialog('cars'); } catch {}
                    }
                } catch (err) { /* ignore */ }

                // click primary notification button if present
                const notif = document.querySelector('.button.first, .ui-button.primary, .ui-dialog .button');
                if (notif) {
                    try { notif.click(); } catch (e) {}
                }
            }, 800);

            // 4) Schedule next run
            loopTimer = setTimeout(performCrime, Math.max(500, delay));
        } catch (err) {
            console.error('performCrime error', err);
            setStatus('Error — retrying');
            loopTimer = setTimeout(performCrime, 5000);
        }
    }

    // Expose stop for debugging from console
    window.__ortensCrimeBot = {
        stop: stopLoop,
        start: startLoop,
        status: () => ({ running, selectedCrimeId, autoClose })
    };

    // set initial selectedCrimeId
    selectedCrimeId = parseInt(crimeIdEl.value, 10);

})();