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