Cyclist Ring (Enhanced)

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();