Torn Pickpocketing Optimizer

You can pick your friends, but you can't pick your friend's pockets.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Torn Pickpocketing Optimizer
// @namespace    http://tampermonkey.net/
// @version      35.0
// @description  You can pick your friends, but you can't pick your friend's pockets.
// @author       Kia-Kaha
// @match        https://www.torn.com/loader.php?sid=crimes*
// @match        https://www.torn.com/page.php?sid=crimes*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ========================================================================
    // 1. DATA MATRICES
    // ========================================================================
    const SPRITE_MAP = {
        "-170": { name: "Phone",       mult: 0.7, type: "safe" },
        "-102": { name: "Music",       mult: 0.7, type: "safe" },
        "-34":  { name: "Daydreaming", mult: 0.7, type: "safe" },
        "-340": { name: "Begging",     mult: 0.7, type: "safe" },
        "-272": { name: "Impaired",    mult: 0.5, type: "safe" },
        "-238": { name: "Loitering",   mult: 1.0, type: "neutral" },
        "-306": { name: "Walking",     mult: 1.0, type: "risky" },
        "-136": { name: "Walking",     mult: 1.0, type: "risky" },
        "-204": { name: "Running",     mult: 2.5, type: "danger" },
        "0":    { name: "Cycling",     mult: 2.5, type: "danger" },
        "-68":  { name: "Reading?",    mult: 0.7, type: "unknown" }
    };

    const BASE_DIFFICULTY = {
        "drunk man": 10, "drunk woman": 10,
        "homeless person": 15, "junkie": 15,
        "elderly man": 25, "elderly woman": 25,
        "student": 40, "young man": 40, "young woman": 40,
        "laborer": 50, "postal worker": 50,
        "classy lady": 70, "businessman": 70, "businesswoman": 70,
        "jogger": 75, "rich kid": 80,
        "thug": 85, "gang member": 85, "sex worker": 85,
        "mobster": 110, "police officer": 110, "cyclist": 120
    };

    const BODY_MULTIPLIERS = {
        "skinny": 0.85, "scrawny": 0.85,
        "average": 1.0,
        "athletic": 1.1, "muscular": 1.2, "beefy": 1.25,
        "obese": 1.0
    };

    const safeSetValue = (key, value) => {
        if (typeof GM_setValue === 'function') GM_setValue(key, value);
        else window.localStorage.setItem('tp_' + key, value);
    };

    const safeGetValue = (key, def) => {
        if (typeof GM_getValue === 'function') return GM_getValue(key, def);
        const val = window.localStorage.getItem('tp_' + key);
        return val !== null ? val : def;
    };

    // ========================================================================
    // 2. CRIME SKILL DETECTION
    // ========================================================================
    const getCrimeSkill = () => {
        const pref = safeGetValue('manual_level_pref', 'auto');
        if (pref.indexOf('auto') === -1) {
            let val = pref;
            if (val.indexOf('_') !== -1) val = val.split('_')[0];
            return parseInt(val);
        }
        try {
            const masteryNode = document.querySelector('div[class*="crime-mastery"]');
            if (masteryNode) {
                 const match = masteryNode.innerText.match(/Level\s*(\d{1,3})/i);
                 if (match) return parseInt(match[1]);
            }
            const mobileSkillNode = document.querySelector('button[aria-label^="Skill:"]');
            if (mobileSkillNode) {
                 const text = mobileSkillNode.getAttribute('aria-label');
                 const match = text.match(/Skill:\s*(\d+(\.\d+)?)/);
                 if (match) return Math.floor(parseFloat(match[1]));
            }
            const progressNode = document.querySelector('div[aria-label*="Crime skill:"]');
            if (progressNode) {
                 const text = progressNode.getAttribute('aria-label');
                 const match = text.match(/Crime skill:\s*(\d+)/);
                 if (match) return parseInt(match[1]);
            }
        } catch (e) {
            console.log("TP Optimizer: Auto-detect failed", e);
        }
        return 1;
    };

    // ========================================================================
    // 3. TARGET ANALYSIS
    // ========================================================================
    const analyzeTarget = (row) => {
        let text = (row.textContent || "").toLowerCase();
        let spriteID = null;
        let detectedStatus = null;

        const activityDiv = row.querySelector('div[class*="activity"]');
        if (activityDiv) {
            const iconDiv = activityDiv.querySelector('div[style*="background-position"]');
            if (iconDiv) {
                const style = iconDiv.getAttribute('style');
                const match = style.match(/background-position-y:\s*(-?\d+)px/);
                if (match) spriteID = match[1];
                else if (style.indexOf('background-position-y: 0px') !== -1 || style.indexOf('background-position-y:0px') !== -1) spriteID = "0";
            }
        } else {
             const iconWrap = row.querySelector('span[class*="iconWrap"]');
             if (iconWrap) spriteID = "-170";
        }

        if (spriteID && SPRITE_MAP[spriteID]) {
            detectedStatus = SPRITE_MAP[spriteID];
        } else if (spriteID) {
            detectedStatus = { name: "Unknown", mult: 1.0, type: "unknown" };
        } else {
            detectedStatus = { name: "Moving", mult: 1.0, type: "risky" };
        }

        return { text, spriteID, detectedStatus };
    };

    const calculateHeuristic = (row, playerCS) => {
        const { text, spriteID, detectedStatus } = analyzeTarget(row);

        let baseDiff = 50;
        for (const [name, val] of Object.entries(BASE_DIFFICULTY)) {
            if (text.indexOf(name) !== -1) { baseDiff = val; break; }
        }

        let bodyMult = 1.0;
        let bodyType = "";
        for (const [type, val] of Object.entries(BODY_MULTIPLIERS)) {
            if (text.indexOf(type) !== -1) {
                bodyMult = val;
                bodyType = type;
                break;
            }
        }

        const modifiedDifficulty = baseDiff * detectedStatus.mult * bodyMult;
        const volatilityPenalty = playerCS > 75 ? (playerCS - 75) * 0.5 : 0;
        let safetyMargin = (playerCS * 1.2) - modifiedDifficulty - volatilityPenalty;

        if (detectedStatus.mult >= 2.0) safetyMargin = -999;

        return {
            text: text,
            diff: modifiedDifficulty,
            status: detectedStatus.name,
            type: detectedStatus.type,
            safetyMargin: safetyMargin,
            spriteID: spriteID,
            bodyType: bodyType
        };
    };

    // ========================================================================
    // 4. MAIN LOOP
    // ========================================================================
    const processTargets = () => {
        const url = window.location.href;
        if (url.indexOf('pickpocketing') === -1 && window.location.hash.indexOf('pickpocketing') === -1) {
            const panel = document.getElementById('tp-control-panel');
            if (panel) panel.style.display = 'none';
            return;
        }

        createInterface();

        const rows = document.querySelectorAll('li[class*="crime-option"], div[class*="crime-option"]');
        if (rows.length === 0) return;

        let maxScore = -9999;
        const processedList = [];
        const currentCS = getCrimeSkill();
        const rawPref = safeGetValue('manual_level_pref', 'auto');
        const isSkinnyMode = rawPref.indexOf('skinny') !== -1;
        const isProfitMode = (currentCS >= 100) || rawPref.indexOf('profit') !== -1 || rawPref === '100';

        const select = document.querySelector('#tp-control-panel select');
        if (select && rawPref.indexOf('auto') !== -1) {
            const option = select.options[select.selectedIndex];
            if (option && !option.innerText.includes('Lvl')) {
                Array.from(select.options).forEach(o => o.innerText = o.innerText.split(' (Lvl')[0]);
                option.innerText = option.innerText.split(' (Lvl')[0] + ' (Lvl ' + currentCS + ')';
            }
        }

        rows.forEach(row => {
            const btn = row.querySelector('button');

            // --- CONTEXT AWARE DEAD CHECK ---
            // We check the PARENT containers (virtual-item) because the "Success" message
            // is often a sibling to the row, not a child.
            const parentContext = row.closest('.virtual-item') || row.closest('li') || row;
            const fullText = (parentContext.innerText || "").toLowerCase();

            const isDead =
                !btn ||
                btn.disabled ||
                btn.classList.contains('disabled') ||
                btn.getAttribute('aria-disabled') === 'true' ||
                row.classList.contains('expired') ||
                fullText.indexOf('success') !== -1 ||
                fullText.indexOf('failure') !== -1 ||
                fullText.indexOf('hospitalized') !== -1;

            if (isDead) {
                // Instantly strip styles
                row.style.background = "";
                row.style.borderLeft = "";
                row.style.opacity = "0.5";
                if (btn) btn.style.boxShadow = "";

                const reasonEl = row.querySelector('.tp-reason');
                if (reasonEl) reasonEl.innerHTML = "";

                return; // SKIP processing
            }

            // Reset alive targets
            row.style.opacity = "1";

            const data = calculateHeuristic(row, currentCS);

            if (isSkinnyMode && data.text.indexOf('skinny') === -1 && data.text.indexOf('scrawny') === -1) {
                row.style.opacity = '0.3'; return;
            }
            if (!isProfitMode && (data.text.indexOf('police') !== -1 || data.text.indexOf('mobster') !== -1)) {
                row.style.opacity = '0.3'; return;
            }

            processedList.push({ row, btn, data });
            if (data.safetyMargin > maxScore) maxScore = data.safetyMargin;
        });

        processedList.forEach(item => {
            const { row, btn, data } = item;

            row.style.background = "";
            row.style.borderLeft = "";
            btn.style.boxShadow = "";

            const isSafest = (data.safetyMargin === maxScore && data.safetyMargin > 0);
            const isDanger = (data.safetyMargin < -20) || (data.type === 'danger');

            const cGreen = "#32cd32";
            const cGold = "#FFD700";

            if (isSafest) {
                const color = isProfitMode ? cGold : cGreen;
                row.style.background = "linear-gradient(90deg, " + color + "22 0%, rgba(0,0,0,0) 100%)";
                row.style.borderLeft = "4px solid " + color;
                btn.style.boxShadow = "0 0 5px " + color;
            } else if (isDanger) {
                row.style.opacity = '0.5';
            }

            injectStatusText(row, data, isSafest, isDanger);
        });
    };

    const injectStatusText = (row, data, isBest, isDanger) => {
        let container = row.querySelector('div[class*="title"]');
        if (!container) {
            const allDivs = row.querySelectorAll('div');
            if (allDivs.length > 2) container = allDivs[1];
            else container = row;
        }

        let reasonEl = container.querySelector('.tp-reason');
        if (!reasonEl) {
            reasonEl = document.createElement('div');
            reasonEl.className = 'tp-reason';
            reasonEl.style.fontSize = "10px";
            reasonEl.style.fontWeight = "bold";
            reasonEl.style.marginTop = "2px";
            container.appendChild(reasonEl);
        }

        let idDisplay = "";
        let idColor = "#555";
        if (data.spriteID && data.type === 'unknown') {
            idDisplay = " [ID:" + data.spriteID + "]";
            idColor = "#D000FF";
        }

        let text = "";
        let color = "#888";
        const marginVal = data.safetyMargin.toFixed(0);
        const sign = data.safetyMargin > 0 ? "+" : "";
        const bodyTag = (data.bodyType === 'skinny' || data.bodyType === 'scrawny') ? " [Skinny]" : "";

        if (isDanger) {
            text = "[DANGER] " + data.status + idDisplay;
            color = "#ff4444";
        } else if (isBest) {
            text = "[BEST] [Margin: " + sign + marginVal + "] " + data.status + bodyTag + idDisplay;
            color = "#32cd32";
        } else if (data.safetyMargin > 0) {
            text = "[SAFE] [Margin: " + sign + marginVal + "] " + data.status + bodyTag + idDisplay;
            color = "#aaa";
        } else {
            text = "[RISKY] [Margin: " + sign + marginVal + "] " + data.status + bodyTag + idDisplay;
            color = "#d2691e";
        }

        reasonEl.innerHTML = '<span style="color:' + color + '">' + text + '</span> <span style="color:' + idColor + '; font-size:9px;">' + idDisplay + '</span>';
    };

    const createInterface = () => {
        let panel = document.getElementById('tp-control-panel');
        if (panel) { panel.style.display = 'block'; return; }

        panel = document.createElement('div');
        panel.id = 'tp-control-panel';
        Object.assign(panel.style, {
            position: 'fixed', top: '50px', right: '10px', zIndex: '999999', textAlign: 'right'
        });

        const select = document.createElement('select');
        Object.assign(select.style, {
            background: 'rgba(0,0,0,0.9)', color: '#fff', border: '1px solid #444',
            borderRadius: '4px', fontSize: '11px', padding: '4px'
        });

        let opts = [
            {v:'auto', t:'🕵️ [Auto] Max Success'},
            {v:'auto_skinny', t:'👙 [Auto] Skinny Hunt'},
            {v:'auto_profit', t:'🤑 [Auto] Profit/XP'},
            {v:'1', t:'Manual: Lvl 1-24'},
            {v:'25', t:'Manual: Lvl 25-59'},
            {v:'60', t:'Manual: Lvl 60-99'}
        ];

        opts.forEach(o => {
            const el = document.createElement('option');
            el.value = o.v; el.innerText = o.t;
            select.appendChild(el);
        });

        select.value = safeGetValue('manual_level_pref', 'auto');
        select.addEventListener('change', (e) => {
            safeSetValue('manual_level_pref', e.target.value);
            processTargets();
        });

        panel.appendChild(select);
        document.body.appendChild(panel);
    };

    setInterval(processTargets, 750);
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', processTargets);
    } else {
        processTargets();
    }
})();