// ==UserScript==
// @name Torn Pickpocketing Helper
// @namespace torn.pickpocketing.helper
// @version 3.0
// @description Combines Cyclist alerts and difficulty color-coding for the pickpocketing crime. All features are toggleable.
// @author Microbes & Korbrm (Merged & Rebuilt by eaksquad)
// @match https://www.torn.com/loader.php?sid=crimes*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const SETTINGS = {
cyclistAlerts: {
enabled: true,
soundUrl: 'https://audio.jukehost.co.uk/gxd2HB9RibSHhr13OiW6ROCaaRbD8103',
highlightColor: '#00ff00',
},
difficultyColors: {
enabled: true,
showCategoryText: true,
}
};
// --- Data Definitions ---
const markGroups = {
"Safe": ["Drunk man", "Drunk woman", "Homeless person", "Junkie", "Elderly man", "Elderly woman"],
"Moderately Unsafe": ["Classy lady", "Laborer", "Postal worker", "Young man", "Young woman", "Student"],
"Unsafe": ["Rich kid", "Sex worker", "Thug"],
"Risky": ["Jogger", "Businessman", "Businesswoman", "Gang member", "Mobster"],
"Dangerous": ["Cyclist"],
"Very Dangerous": ["Police officer"],
};
const categoryColorMap = { "Safe": "#37b24d", "Moderately Unsafe": "#74b816", "Unsafe": "#f59f00", "Risky": "#f76707", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" };
const skillTiers = {
tier1: { "Safe": "#37b24d", "Moderately Unsafe": "#f76707", "Unsafe": "#f03e3e", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier2: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#f76707", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier3: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#f76707", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier4: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#f76707", "Very Dangerous": "#7048e8" },
tier5: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#37b24d", "Very Dangerous": "#7048e8" },
};
// --- State Management ---
let isCyclistAlertsEnabled = SETTINGS.cyclistAlerts.enabled;
let isDifficultyColorsEnabled = SETTINGS.difficultyColors.enabled;
let isCyclistCurrentlyAvailable = false;
// --- Core Logic ---
/**
* The master function to style all targets. It's called after the API confirms an update.
* @param {number} skill - The user's current pickpocketing skill.
*/
function applyAllStyling(skill) {
let activeTierColors;
if (skill < 10) { activeTierColors = skillTiers.tier1; }
else if (skill < 35) { activeTierColors = skillTiers.tier2; }
else if (skill < 65) { activeTierColors = skillTiers.tier3; }
else if (skill < 80) { activeTierColors = skillTiers.tier4; }
else { activeTierColors = skillTiers.tier5; }
const targetContainers = document.querySelectorAll('div[class*="crimes-pickpocketing-container"] div[class*="item-container_"]');
targetContainers.forEach(container => {
const titleElement = container.querySelector('div[class*="title_"]');
if (!titleElement) return;
// Reset styles and text before applying new ones
container.style.backgroundColor = '';
container.style.borderLeft = '';
titleElement.style.color = '';
const originalText = titleElement.textContent.trim().split(' (')[0];
titleElement.textContent = originalText;
// --- Difficulty Colors Logic (from original Colors script) ---
if (isDifficultyColorsEnabled) {
let category = '';
for (const cat in markGroups) {
if (markGroups[cat].includes(originalText)) {
category = cat;
break;
}
}
if (category) {
titleElement.style.color = categoryColorMap[category];
container.style.borderLeft = `3px solid ${activeTierColors[category]}`;
if (SETTINGS.difficultyColors.showCategoryText && window.innerWidth > 400) {
titleElement.textContent = `${originalText} (${category})`;
}
}
}
// --- Cyclist Highlight Logic (from original Cyclist script) ---
// This runs after and will override the difficulty colors if the cyclist is available.
if (isCyclistAlertsEnabled && isCyclistCurrentlyAvailable) {
const iconElement = container.querySelector('div[class*="icon_"]');
// This is the key: identify the cyclist by its unique icon style, not by name.
if (iconElement && $(iconElement).css('background-position-y') === '0px') {
container.style.backgroundColor = SETTINGS.cyclistAlerts.highlightColor;
}
}
});
}
/**
* Sets up the main logic to intercept network requests.
*/
function initializeCrimeObserver() {
interceptFetch("torn.com", "/page.php?sid=crimesData", (response) => {
// Resilient API path check: works whether Torn uses "DB" or not.
const crimesData = response.crimesByType ? response : response.DB;
if (!crimesData || !crimesData.crimesByType || typeof crimesData.pickpocketing?.skill === 'undefined') {
console.error("[PPHelper] Crime data or skill not found in API response.", response);
return;
}
const crimes = crimesData.crimesByType;
const skill = crimesData.pickpocketing.skill;
isCyclistCurrentlyAvailable = crimes.some(c => c.title === "Cyclist" && c.available === true);
if (isCyclistAlertsEnabled && isCyclistCurrentlyAvailable) {
playAlertSound();
}
// Use a timeout to ensure the DOM has rendered before we try to style it.
setTimeout(() => applyAllStyling(skill), 150);
});
}
function setupInterface() {
if (document.getElementById('pp-helper-controls')) return;
const controlsContainer = `<div id="pp-helper-controls" style="margin-bottom: 10px; display: flex; gap: 10px; flex-wrap: wrap;"><a id="cyclist-toggle-btn" class="torn-btn"></a><a id="colors-toggle-btn" class="torn-btn"></a></div>`;
$('.pickpocketing-root').prepend(controlsContainer);
const cyclistBtn = $('#cyclist-toggle-btn');
const colorsBtn = $('#colors-toggle-btn');
function updateButtons() {
cyclistBtn.text(`Cyclist Alerts: ${isCyclistAlertsEnabled ? 'ON' : 'OFF'}`).css(isCyclistAlertsEnabled ? { 'background': '#4CAF50', 'color': 'white' } : { 'background': '', 'color': '' });
colorsBtn.text(`Difficulty Colors: ${isDifficultyColorsEnabled ? 'ON' : 'OFF'}`).css(isDifficultyColorsEnabled ? { 'background': '#4CAF50', 'color': 'white' } : { 'background': '', 'color': '' });
}
cyclistBtn.on('click', () => {
isCyclistAlertsEnabled = !isCyclistAlertsEnabled;
updateButtons();
if (isCyclistAlertsEnabled && isCyclistCurrentlyAvailable) playAlertSound();
// Trigger a re-style to apply/remove the highlight immediately
document.querySelector('div[class*="crimes-pickpocketing-container"]').click();
});
colorsBtn.on('click', () => {
isDifficultyColorsEnabled = !isDifficultyColorsEnabled;
updateButtons();
// Trigger a re-style to apply/remove colors immediately
document.querySelector('div[class*="crimes-pickpocketing-container"]').click();
});
updateButtons();
}
// --- Utility Functions ---
function playAlertSound() { const audio = new Audio(SETTINGS.cyclistAlerts.soundUrl); audio.play().catch(error => console.error("[PPHelper] Audio failed:", error)); }
function waitForElementToExist(selector) { return new Promise(resolve => { if (document.querySelector(selector)) return resolve(document.querySelector(selector)); const observer = new MutationObserver(() => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { subtree: true, childList: true }); }); }
function interceptFetch(url, q, callback) { const originalFetch = window.fetch; window.fetch = function(...args) { return originalFetch.apply(this, args).then(response => { if (response.url.includes(url) && response.url.includes(q)) { response.clone().json().then(json => callback(json)).catch(error => console.error("[PPHelper] Intercept failed:", error)); } return response; }); }; }
// --- Script Entry Point ---
waitForElementToExist('.pickpocketing-root').then(() => {
console.log("[PPHelper] Initializing Pickpocketing Helper v3.0");
setupInterface();
});
initializeCrimeObserver();
})();