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      48.0
// @description  You can pick your friends, but you can't pick your friend's pockets.
// @author       Kia-Kaha (Updated with Math Fix)
// @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 
    // ========================================================================
    // A weighting factor to tune how much skill mitigates difficulty
    const W_SKILL = 0.8; 

    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 STATUS_WEIGHTS = {
        'sleeping': 0.3,
        'passed out': 0.4,
        'drunk': 0.5,
        'stumbling': 0.5,
        'distracted': 0.7,
        'on the phone': 0.7,
        'reading': 0.7,
        'listening': 0.7,
        'music': 0.7,
        'daydreaming': 0.7,
        'begging': 0.7,
        'loitering': 1.0,
        'walking': 1.0,
        'jogging': 1.5,
        'cycling': 1.8,
        'running': 2.5,
        'sprinting': 3.0
    };

    const SPRITE_FALLBACK = {
        "-170": 0.7, "-102": 0.7, "-34": 0.7, "-340": 0.7,
        "-272": 0.4, "-238": 1.0, "-306": 1.0, "-136": 1.0,
        "-204": 2.5, "0": 2.5
    };

    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 calculateHeuristic = (row, playerCS) => {
        let text = (row.textContent || "").toLowerCase();
        
        let baseDiff = 50; 
        for (const [name, val] of Object.entries(BASE_DIFFICULTY)) {
            if (text.indexOf(name) !== -1) { baseDiff = val; break; }
        }

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

        let statusMult = 1.0;
        let activityDiv = row.querySelector('div[class*="activity"]');
        let activityText = activityDiv ? activityDiv.textContent.toLowerCase() : text;
        
        let foundStatus = false;
        for (const [state, mult] of Object.entries(STATUS_WEIGHTS)) {
            if (activityText.indexOf(state) !== -1) {
                statusMult = mult;
                foundStatus = true;
                break;
            }
        }

        if (!foundStatus && 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/);
                let spriteID = match ? match[1] : (style.indexOf('0px') !== -1 ? "0" : null);
                if (spriteID && SPRITE_FALLBACK[spriteID]) {
                    statusMult = SPRITE_FALLBACK[spriteID];
                }
            }
        }

        // Apply dynamic weighting factor to player skill for accurate margin
        const finalDifficulty = baseDiff * statusMult * bodyMult;
        let safetyMargin = (playerCS * W_SKILL) - finalDifficulty;

        if (statusMult >= 2.0) safetyMargin = -999; 

        return {
            text: text,
            safetyMargin: safetyMargin,
            isDanger: safetyMargin < 0 || statusMult >= 2.0
        };
    };

    // ========================================================================
    // 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('.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('.commit-button');
            const parentContext = row.closest('.virtual-item') || row.closest('li') || row;
            const fullText = (parentContext.innerText || "").toLowerCase();
            const ariaLabel = btn ? (btn.getAttribute('aria-label') || "").toLowerCase() : "";

            const isPicked = ariaLabel.includes('already picked') || fullText.indexOf('success') !== -1;
            const isLost = ariaLabel.includes('lost') || row.classList.contains('expired') || fullText.indexOf('failure') !== -1 || fullText.indexOf('hospitalized') !== -1;

            if (isPicked || isLost) {
                // Dim the row completely and add a subtle background pattern
                row.style.background = "repeating-linear-gradient(45deg, rgba(0,0,0,0.02), rgba(0,0,0,0.02) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)";
                
                // Hide the actual game data without disrupting React's node structure
                let innerContent = row.querySelector('.crime-option-sections');
                if (innerContent) {
                    innerContent.style.opacity = '0';
                    innerContent.style.pointerEvents = 'none';
                }

                // Inject absolute positioned overlay
                let lostOverlay = row.querySelector('.tp-lost-overlay');
                if (!lostOverlay) {
                    lostOverlay = document.createElement('div');
                    lostOverlay.className = 'tp-lost-overlay';
                    lostOverlay.style.cssText = 'position: absolute; top: 0; left: 0; height: 100%; width: 100%; display: flex; align-items: center; justify-content: center; font-style: italic; font-size: 13px; font-weight: 500; letter-spacing: 0.5px; z-index: 10; pointer-events: none;';
                    row.appendChild(lostOverlay);
                }
                
                // Change display text based on state
                if (isPicked) {
                    lostOverlay.style.color = '#5cb85c'; // Success green tint
                    lostOverlay.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; opacity: 0.8;"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg> Pocket picked`;
                } else {
                    lostOverlay.style.color = '#999'; // Default faded gray
                    lostOverlay.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; opacity: 0.8;"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><line x1="17" y1="8" x2="23" y2="14"></line><line x1="23" y1="8" x2="17" y2="14"></line></svg> Target lost`;
                }

                return; 
            }

            // --- IMPORTANT: RESET ALIVE TARGETS ---
            row.style.background = "";
            row.style.opacity = "1";
            let innerContent = row.querySelector('.crime-option-sections');
            if (innerContent) {
                innerContent.style.opacity = '1';
                innerContent.style.pointerEvents = 'auto';
            }
            
            // Nuclear Cleanup: Destroy the node to prevent React flexbox artifacting
            let lostOverlay = row.querySelector('.tp-lost-overlay');
            if (lostOverlay) lostOverlay.remove();

            const data = calculateHeuristic(row, currentCS);

            if (isSkinnyMode && data.text.indexOf('skinny') === -1 && data.text.indexOf('scrawny') === -1) {
                row.style.opacity = '0.4'; return;
            }
            if (!isProfitMode && (data.text.indexOf('police') !== -1 || data.text.indexOf('mobster') !== -1)) {
                row.style.opacity = '0.4'; 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 = "";
            if(btn) btn.style.boxShadow = "";

            const isSafest = (data.safetyMargin === maxScore && data.safetyMargin > 0);
            const isDanger = data.isDanger;

            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;
                if(btn) 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('span[class*="physicalProps"]');
        
        if (!container) container = row.querySelector('div[class*="titleAndProps"]');
        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('span');
            reasonEl.className = 'tp-reason';
            reasonEl.style.fontSize = "10px";
            reasonEl.style.fontWeight = "bold";
            reasonEl.style.marginLeft = "6px";
            reasonEl.style.verticalAlign = "middle";
            reasonEl.style.whiteSpace = "nowrap"; 
            container.appendChild(reasonEl);
        }

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

        let color = "#888";
        const marginVal = Math.abs(data.safetyMargin).toFixed(0); 
        const sign = data.safetyMargin >= 0 ? "+" : "-";
        
        if (isDanger || data.safetyMargin < 0) {
            color = "#ff4444"; 
        } else if (isBest) {
            color = "#32cd32"; 
        } else if (data.safetyMargin >= 10) {
            color = "#aaa";    
        } else {
            color = "#d2691e"; 
        }

        const text = "[Safety:&nbsp;" + sign + marginVal + "]";
        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', display: 'flex', gap: '5px'
        });

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