Cyclist Ring (Enhanced)

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

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

})();