// ==UserScript==
// @name Torn Pickpocketing Helper (Definitive Build - Fixed v2)
// @namespace torn.pickpocketing.helper.rebuilt.v12.2
// @version 12.2
// @description A definitive, working merger of Cyclist Ring and Pickpocketing Colors using direct DOM observation.
// @author Microbes & Korbrm (Rebuilt by AI Assistant)
// @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';
console.log("[PPHelper v12.2] Script loading.");
// --- Configuration ---
const SETTINGS = {
cyclistAlerts: {
enabled: true,
soundUrl: 'https://audio.jukehost.co.uk/gxd2HB9RibSHhr13OiW6ROCaaRbD8103',
highlightColor: '#00ff00',
highlightOpacity: '0.3'
},
difficultyColors: { enabled: true, showCategoryText: true },
debug: false // Set to true for debugging
};
// --- 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" }
};
// Create normalized lookup for better matching
const normalizedMarkGroups = {};
Object.keys(markGroups).forEach(category => {
normalizedMarkGroups[category] = markGroups[category].map(name => normalizeText(name));
});
// --- State Management ---
let isCyclistAlertsEnabled = SETTINGS.cyclistAlerts.enabled;
let isDifficultyColorsEnabled = SETTINGS.difficultyColors.enabled;
let wasCyclistVisibleLastRun = false;
let lastCyclistCheckTime = 0;
/**
* Normalize text for better matching
*/
function normalizeText(text) {
return text.toLowerCase()
.trim()
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.replace(/[^\w\s]/g, '') // Remove special characters except spaces
.trim();
}
/**
* Extract clean target name from element text
*/
function extractTargetName(text) {
if (!text) return '';
// Remove category text in parentheses
let cleanText = text.split(' (')[0].trim();
// Remove any other common suffixes or prefixes
cleanText = cleanText.replace(/^\s*[-•]\s*/, ''); // Remove bullet points
cleanText = cleanText.replace(/\s*\[.*?\]\s*/, ''); // Remove bracketed text
return cleanText.trim();
}
/**
* Find category for a target name with multiple matching strategies
*/
function findTargetCategory(targetName) {
if (!targetName) return null;
const normalizedTarget = normalizeText(targetName);
// Strategy 1: Exact normalized match
for (const [category, names] of Object.entries(normalizedMarkGroups)) {
if (names.includes(normalizedTarget)) {
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Exact match: "${targetName}" -> "${category}"`);
}
return category;
}
}
// Strategy 2: Partial match (contains)
for (const [category, originalNames] of Object.entries(markGroups)) {
for (const name of originalNames) {
const normalizedName = normalizeText(name);
if (normalizedTarget.includes(normalizedName) || normalizedName.includes(normalizedTarget)) {
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Partial match: "${targetName}" contains "${name}" -> "${category}"`);
}
return category;
}
}
}
// Strategy 3: Word-by-word match
const targetWords = normalizedTarget.split(' ');
for (const [category, originalNames] of Object.entries(markGroups)) {
for (const name of originalNames) {
const nameWords = normalizeText(name).split(' ');
const matchingWords = targetWords.filter(word => nameWords.includes(word));
if (matchingWords.length > 0 && matchingWords.length === nameWords.length) {
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Word match: "${targetName}" matches "${name}" -> "${category}"`);
}
return category;
}
}
}
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] No match found for: "${targetName}" (normalized: "${normalizedTarget}")`);
}
return null;
}
/**
* Enhanced cyclist detection with multiple fallback methods
*/
function findCyclistTargets() {
const cyclists = [];
// Method 1: Direct text search in title elements
const titleElements = document.querySelectorAll('div[class*="titleAndProps"] > div');
titleElements.forEach(titleElement => {
const text = extractTargetName(titleElement.textContent);
if (normalizeText(text) === normalizeText('Cyclist')) {
const container = titleElement.closest('div[class*="crimeOptionWrapper"]');
if (container) {
cyclists.push({ container, titleElement, text });
}
}
});
// Method 2: Search all text content for cyclist references
if (cyclists.length === 0) {
const allContainers = document.querySelectorAll('div[class*="crimeOptionWrapper"]');
allContainers.forEach(container => {
const allText = normalizeText(container.textContent);
if (allText.includes('cyclist')) {
const titleElement = container.querySelector('div[class*="titleAndProps"] > div');
if (titleElement) {
cyclists.push({ container, titleElement, text: extractTargetName(titleElement.textContent) });
}
}
});
}
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Found ${cyclists.length} cyclist targets`);
}
return cyclists;
}
/**
* Apply styling to a single target container
*/
function styleTarget(container, titleElement, targetName, category, activeTierColors) {
if (!category) return false;
// Apply difficulty colors
if (isDifficultyColorsEnabled) {
const categoryColor = categoryColorMap[category];
const tierColor = activeTierColors[category];
if (categoryColor && tierColor) {
titleElement.style.color = categoryColor;
container.style.borderLeft = `3px solid ${tierColor}`;
// Add category text if enabled and screen is wide enough
if (SETTINGS.difficultyColors.showCategoryText && window.innerWidth > 400) {
if (!titleElement.textContent.includes(`(${category})`)) {
titleElement.textContent = `${targetName} (${category})`;
}
}
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Styled "${targetName}" as "${category}" with colors ${categoryColor}/${tierColor}`);
}
return true;
}
}
return false;
}
/**
* Enhanced styling function with better error handling and timing
*/
function applyAllStyling() {
try {
const skillButton = document.querySelector('button[aria-label^="Skill:"]');
if (!skillButton) {
if (SETTINGS.debug) {
console.log("[PPHelper v12.2] No skill button found, retrying...");
}
return;
}
const skillText = skillButton.getAttribute('aria-label');
const currentSkill = parseFloat(skillText.replace('Skill: ', ''));
let activeTierColors;
if (currentSkill < 10) { activeTierColors = skillTiers.tier1; }
else if (currentSkill < 35) { activeTierColors = skillTiers.tier2; }
else if (currentSkill < 65) { activeTierColors = skillTiers.tier3; }
else if (currentSkill < 80) { activeTierColors = skillTiers.tier4; }
else { activeTierColors = skillTiers.tier5; }
const commitButtons = document.querySelectorAll('button[aria-label="Pickpocket, 5 nerve"]');
let isCyclistVisibleThisRun = false;
let styledCount = 0;
let totalTargets = 0;
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Current skill: ${currentSkill}, Found ${commitButtons.length} targets`);
}
// Process each target
commitButtons.forEach((button, index) => {
const container = button.closest('div[class*="crimeOptionWrapper"]');
if (!container) return;
const titleElement = container.querySelector('div[class*="titleAndProps"] > div');
if (!titleElement) return;
totalTargets++;
// Reset all styles first
container.style.backgroundColor = '';
container.style.borderLeft = '';
container.style.boxShadow = '';
container.classList.remove('cyclist-highlight');
titleElement.style.color = '';
titleElement.style.fontWeight = '';
titleElement.style.textShadow = '';
// Extract and clean the target name
const originalText = titleElement.textContent.trim();
const targetName = extractTargetName(originalText);
// Reset to clean text
titleElement.textContent = targetName;
if (SETTINGS.debug && index < 3) { // Only log first 3 for brevity
console.log(`[PPHelper v12.2] Processing target ${index + 1}: "${originalText}" -> "${targetName}"`);
}
// Find category and apply styling
const category = findTargetCategory(targetName);
if (category) {
const styled = styleTarget(container, titleElement, targetName, category, activeTierColors);
if (styled) styledCount++;
// Check for cyclist
if (normalizeText(targetName) === normalizeText('Cyclist')) {
isCyclistVisibleThisRun = true;
if (isCyclistAlertsEnabled) {
// Apply cyclist-specific highlighting
container.style.backgroundColor = SETTINGS.cyclistAlerts.highlightColor + SETTINGS.cyclistAlerts.highlightOpacity;
container.style.boxShadow = `0 0 15px ${SETTINGS.cyclistAlerts.highlightColor}`;
container.style.border = `2px solid ${SETTINGS.cyclistAlerts.highlightColor}`;
container.classList.add('cyclist-highlight');
titleElement.style.fontWeight = 'bold';
titleElement.style.textShadow = `0 0 5px ${SETTINGS.cyclistAlerts.highlightColor}`;
if (SETTINGS.debug) {
console.log("[PPHelper v12.2] Cyclist highlighted successfully");
}
}
}
} else if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] No category found for: "${targetName}"`);
}
});
if (SETTINGS.debug) {
console.log(`[PPHelper v12.2] Styled ${styledCount}/${totalTargets} targets. Cyclist visible: ${isCyclistVisibleThisRun}`);
}
// Sound alert logic with cooldown
const currentTime = Date.now();
if (isCyclistAlertsEnabled && isCyclistVisibleThisRun && !wasCyclistVisibleLastRun) {
if (currentTime - lastCyclistCheckTime > 2000) { // 2 second cooldown
console.log("[PPHelper v12.2] Cyclist has appeared! Playing sound.");
playAlertSound();
lastCyclistCheckTime = currentTime;
}
}
wasCyclistVisibleLastRun = isCyclistVisibleThisRun;
} catch (error) {
console.error("[PPHelper v12.2] Error in applyAllStyling:", error);
}
}
/**
* Enhanced observer with multiple trigger methods and better timing
*/
function initializeCrimeObserver() {
// Method 1: Intercept fetch requests
interceptFetch("torn.com", "/page.php?sid=crimesData", () => {
if (SETTINGS.debug) {
console.log("[PPHelper v12.2] Intercepted crimesData, triggering style update.");
}
// Multiple attempts with increasing delays
setTimeout(applyAllStyling, 100);
setTimeout(applyAllStyling, 300);
setTimeout(applyAllStyling, 600);
});
// Method 2: DOM mutation observer for dynamic content
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
if (node.querySelector && (
node.querySelector('div[class*="crimeOptionWrapper"]') ||
node.querySelector('button[aria-label="Pickpocket, 5 nerve"]') ||
normalizeText(node.textContent).includes('cyclist') ||
normalizeText(node.textContent).includes('police') ||
normalizeText(node.textContent).includes('student') ||
normalizeText(node.textContent).includes('business')
)) {
shouldUpdate = true;
}
}
});
}
});
if (shouldUpdate) {
if (SETTINGS.debug) {
console.log("[PPHelper v12.2] DOM mutation detected, updating styles.");
}
setTimeout(applyAllStyling, 100);
setTimeout(applyAllStyling, 300); // Second attempt for good measure
}
});
// Observe the entire crimes section
const crimesSection = document.querySelector('.pickpocketing-root') || document.body;
observer.observe(crimesSection, {
childList: true,
subtree: true,
characterData: true
});
// Method 3: Periodic check as fallback
setInterval(() => {
if (document.querySelector('button[aria-label="Pickpocket, 5 nerve"]')) {
applyAllStyling();
}
}, 10000); // Every 10 seconds
}
/**
* Enhanced interface setup
*/
function setupInterface() {
waitForElementToExist('.pickpocketing-root').then((pickpocketingRoot) => {
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>
<a id="test-sound-btn" class="torn-btn" style="background: #2196F3; color: white;">Test Sound</a>
<a id="debug-toggle-btn" class="torn-btn" style="background: #9C27B0; color: white;">Debug: ${SETTINGS.debug ? 'ON' : 'OFF'}</a>
</div>
`;
$(pickpocketingRoot).prepend(controlsContainer);
const cyclistBtn = $('#cyclist-toggle-btn');
const colorsBtn = $('#colors-toggle-btn');
const testSoundBtn = $('#test-sound-btn');
const debugBtn = $('#debug-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': '' });
debugBtn.text(`Debug: ${SETTINGS.debug ? 'ON' : 'OFF'}`);
}
function forceRefresh() {
const refreshButton = document.querySelector('div[class*="refresh-icon_"]');
if (refreshButton) {
refreshButton.click();
} else {
// Fallback: trigger styling directly with multiple attempts
setTimeout(applyAllStyling, 100);
setTimeout(applyAllStyling, 500);
setTimeout(applyAllStyling, 1000);
}
}
cyclistBtn.on('click', () => {
isCyclistAlertsEnabled = !isCyclistAlertsEnabled;
updateButtons();
forceRefresh();
});
colorsBtn.on('click', () => {
isDifficultyColorsEnabled = !isDifficultyColorsEnabled;
updateButtons();
forceRefresh();
});
testSoundBtn.on('click', () => {
console.log("[PPHelper v12.2] Testing sound manually.");
playAlertSound();
});
debugBtn.on('click', () => {
SETTINGS.debug = !SETTINGS.debug;
updateButtons();
console.log(`[PPHelper v12.2] Debug mode ${SETTINGS.debug ? 'enabled' : 'disabled'}`);
});
updateButtons();
// Initial styling application with multiple attempts
setTimeout(applyAllStyling, 500);
setTimeout(applyAllStyling, 1000);
setTimeout(applyAllStyling, 2000);
});
}
// --- Enhanced Utility Functions ---
function playAlertSound() {
try {
const audio = new Audio(SETTINGS.cyclistAlerts.soundUrl);
audio.volume = 0.7;
audio.play().catch(error => {
console.error("[PPHelper v12.2] Audio failed:", error);
// Fallback: try to create a simple beep
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5);
} catch (beepError) {
console.error("[PPHelper v12.2] Fallback beep also failed:", beepError);
}
});
} catch (error) {
console.error("[PPHelper v12.2] Sound creation 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 v12.2] Intercept JSON parsing failed:", error);
callback(null);
});
}
return response;
});
};
}
// --- Script Entry Point ---
console.log("[PPHelper v12.2] Initializing...");
// Wait for page to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setupInterface();
initializeCrimeObserver();
});
} else {
setupInterface();
initializeCrimeObserver();
}
// Add CSS for cyclist highlighting
const style = document.createElement('style');
style.textContent = `
.cyclist-highlight {
animation: cyclistPulse 2s infinite;
}
@keyframes cyclistPulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
`;
document.head.appendChild(style);
console.log("[PPHelper v12.2] Script fully loaded and initialized.");
})();