You can pick your friends, but you can't pick your friend's pockets.
// ==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 = " [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: " + 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();
}
})();