Cyclist Ring (Enhanced)

Plays an alert and highlights when the Cyclist pickpocket target is available. Alerts are on by default and can be toggled.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Cyclist Ring (Enhanced)
// @namespace    microbes.torn.ring.enhanced
// @version      1.1
// @description  Plays an alert and highlights when the Cyclist pickpocket target is available. Alerts are on by default and can be toggled.
// @author       Microbes (Enhanced 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 ---
    // Set to true to have alerts enabled by default when the page loads, false to have them off.
    const ALERTS_ENABLED_BY_DEFAULT = true;
    const ALERT_SOUND_URL = 'https://audio.jukehost.co.uk/gxd2HB9RibSHhr13OiW6ROCaaRbD8103';
    const HIGHLIGHT_COLOR = "#00ff00"; // Green highlight for cyclist

    // --- State Management ---
    let isAlertsEnabled = ALERTS_ENABLED_BY_DEFAULT;

    // --- Core Functions ---

    /**
     * Checks the API response to see if the "Cyclist" target is available.
     * @param {Array} crimes - The array of crime objects from the API.
     * @returns {boolean} - True if the cyclist is available, false otherwise.
     */
    function isCyclistAvailable(crimes) {
        if (!crimes || !Array.isArray(crimes)) return false;
        for (const crime of crimes) {
            if (crime.title === "Cyclist" && crime.available === true) {
                return true;
            }
        }
        return false;
    }

    /**
     * Plays the alert sound.
     */
    function playAlertSound() {
        const audio = new Audio(ALERT_SOUND_URL);
        audio.play().catch(error => console.error("[Cyclist Ring] Audio playback failed:", error));
    }

    /**
     * Highlights all available cyclist targets on the page.
     */
    function highlightCyclists() {
        // The original logic to find the cyclist icon is clever.
        // It checks the background-position-y of the icon sprite. '0px' is the cyclist.
        $('.CircularProgressbar').nextAll().each(function() {
            if ($(this).css('background-position-y') === '0px') {
                // Traverse up to the main container for the target and apply the highlight
                $(this).parent().parent().parent().parent().css("background-color", HIGHLIGHT_COLOR);
            }
        });
    }

    /**
     * Sets up the main logic to intercept network requests and check for the cyclist.
     */
    function initializeAlerts() {
        interceptFetch("torn.com", "/page.php?sid=crimesData", (response) => {
            // Only run the check if alerts are currently enabled by the user
            if (!isAlertsEnabled) {
                return;
            }

            const crimes = response?.DB?.crimesByType;
            if (isCyclistAvailable(crimes)) {
                playAlertSound();
                highlightCyclists();
            }
        });
    }

    /**
     * Creates the toggle button and adds it to the page.
     */
    function setupInterface() {
        // *** FIX 1: Check if the button already exists to prevent duplicates ***
        if (document.getElementById('cyclist-alert-controls')) {
            return;
        }

        const controlsContainer = `
            <div id="cyclist-alert-controls" style="margin-bottom: 10px;">
                <a id="cyclist-toggle-btn" class="torn-btn"></a>
            </div>
        `;
        $('.pickpocketing-root').prepend(controlsContainer);

        const toggleButton = $('#cyclist-toggle-btn');

        // Function to update the button's appearance based on the current state
        function updateButtonState() {
            if (isAlertsEnabled) {
                toggleButton.text('Cyclist Alerts: ON').css({ 'background': '#4CAF50', 'color': 'white' });
            } else {
                toggleButton.text('Cyclist Alerts: OFF').css({ 'background': '', 'color': '' });
            }
        }

        // *** FIX 2: Make the button a toggle instead of disappearing ***
        toggleButton.on('click', () => {
            isAlertsEnabled = !isAlertsEnabled; // Flip the state
            updateButtonState();

            // Play a sound when enabling as confirmation
            if (isAlertsEnabled) {
                playAlertSound();
            }
        });

        // Set the initial state of the button when it's first created
        updateButtonState();
    }


    // --- Utility Functions (kept inside the script to avoid global conflicts) ---

    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) {
            const [resource, config] = args;
            const requestUrl = typeof resource === 'string' ? resource : resource.url;

            return originalFetch.apply(this, args).then(response => {
                if (response.url.includes(url) && response.url.includes(q)) {
                    const clone = response.clone();
                    clone.json()
                        .then(json => callback(json, response.url))
                        .catch(error => console.error("[Cyclist Ring][InterceptFetch] Error parsing JSON:", error));
                }
                return response;
            }).catch(error => {
                console.error("[Cyclist Ring][InterceptFetch] Error with fetch:", error);
                // Still reject the promise to not break the chain
                return Promise.reject(error);
            });
        };
    }

    // --- Script Entry Point ---

    // Wait for the crimes container to exist before doing anything
    waitForElementToExist('.pickpocketing-root').then(() => {
        console.log("[Cyclist Ring] Pickpocketing container found. Initializing script.");
        setupInterface();
    });

    // *** FIX 3: Enable by default by initializing the alerts immediately ***
    // This runs once when the script is loaded, independent of the UI.
    initializeAlerts();

})();