Torn Pickpocketing Helper (Definitive Build - Fixed v2)

A definitive, working merger of Cyclist Ring and Pickpocketing Colors using direct DOM observation.

当前为 2025-06-23 提交的版本,查看 最新版本

// ==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.");

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址