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