Torn Pickpocketing Optimizer

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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