Torn Pickpocketing Helper

Combines Cyclist alerts and difficulty color-coding for the pickpocketing crime. All features are toggleable.

目前為 2025-06-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Torn Pickpocketing Helper
// @namespace    torn.pickpocketing.helper
// @version      4.0 (Complete Rebuild)
// @description  Combines Cyclist alerts and difficulty color-coding for the pickpocketing crime. All features are toggleable.
// @author       Microbes & Korbrm (Rebuilt by eaksquad)
// @match        https://www.torn.com/loader.php?sid=crimes*
// @match        https://www.torn.com/crimes.php*
// @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;

    /**
     * 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; }

        // This selector is confirmed to work on the live page as of this version.
        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;

            const originalText = titleElement.textContent.trim().split(' (')[0];
            container.style.backgroundColor = '';
            container.style.borderLeft = '';
            titleElement.style.color = '';
            titleElement.textContent = originalText;

            if (isDifficultyColorsEnabled) {
                const category = Object.keys(markGroups).find(cat => markGroups[cat].includes(originalText));
                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})`;
                    }
                }
            }

            if (isCyclistAlertsEnabled && isCyclistCurrentlyAvailable) {
                const iconElement = container.querySelector('div[class*="icon_"]');
                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() {
        // *** THE PRIMARY FIX: Listen to the CORRECT URL that Torn now uses. ***
        interceptFetch("crimes.php", "step=pickpocketing", (response) => {
            console.log("[PPHelper] Intercepted pickpocketing data.", response);

            const crimes = response?.crimes?.pickpocketing?.marks;
            const skill = response?.crimes?.pickpocketing?.skill;

            if (!crimes || typeof skill === 'undefined') {
                console.error("[PPHelper] Crime data or skill not found in the new API response structure.", response);
                return;
            }

            isCyclistCurrentlyAvailable = crimes.some(c => c.title === "Cyclist" && c.available === true);

            if (isCyclistAlertsEnabled && isCyclistCurrentlyAvailable) {
                playAlertSound();
            }

            setTimeout(() => applyAllStyling(skill), 150);
        });
    }

    function setupInterface() {
        // Use a MutationObserver to ensure the UI is added reliably, even in Torn PDA.
        const observer = new MutationObserver((mutations, obs) => {
            const pickpocketingRoot = document.querySelector('.pickpocketing-root');
            if (pickpocketingRoot && !document.getElementById('pp-helper-controls')) {
                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>`;
                $(pickpocketingRoot).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': '' });
                }

                function forceRefresh() {
                    // This is the most reliable way to trigger a refresh of the targets
                    const refreshButton = document.querySelector('div[class*="refresh-icon_"]');
                    if (refreshButton) refreshButton.click();
                }

                cyclistBtn.on('click', () => { isCyclistAlertsEnabled = !isCyclistAlertsEnabled; updateButtons(); forceRefresh(); });
                colorsBtn.on('click', () => { isDifficultyColorsEnabled = !isDifficultyColorsEnabled; updateButtons(); forceRefresh(); });

                updateButtons();
                // We can stop observing now that the UI is in place.
                // obs.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- Utility Functions ---
    function playAlertSound() { const audio = new Audio(SETTINGS.cyclistAlerts.soundUrl); audio.play().catch(error => console.error("[PPHelper] Audio failed:", error)); }
    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 ---
    console.log("[PPHelper] Initializing Pickpocketing Helper v4.0");
    setupInterface();
    initializeCrimeObserver();
})();

QingJ © 2025

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