Sidebar Countdown Alarm with Test Icon

Alarms when the sidebar chain countdown timer drops below 2 minutes (120s). Sound is a "pep-pep" tone. Test button is a small icon. Notifications focus the tab.

// ==UserScript==
// @name         Sidebar Countdown Alarm with Test Icon
// @license MIT
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Alarms when the sidebar chain countdown timer drops below 2 minutes (120s). Sound is a "pep-pep" tone. Test button is a small icon. Notifications focus the tab.
// @author       Gemini
// @match        https://www.torn.com/*
// @grant        GM_notification
// @grant        GM_openInTab
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    // Selector for the "Chain: 4324/5k 04:58" element's time component
    const TARGET_SELECTOR = '#sidebar > div:nth-child(1) > div > div.user-information___VBSOk > div > div.toggle-content___BJ9Q9 > div > div:nth-child(5) > a.chain-bar___vjdPL.bar-desktop___F8PEF > div.bar-stats___E_LqA > p.bar-timeleft___B9RGV';
    // Selector for the parent element where we will place the test button
    const PARENT_SELECTOR = '#sidebar > div:nth-child(1) > div > div.user-information___VBSOk > div > div.toggle-content___BJ9Q9 > div > div:nth-child(5) > a.chain-bar___vjdPL.bar-desktop___F8PEF > div.bar-stats___E_LqA';
    const ALARM_THRESHOLD_SECONDS = 100; // 2 minutes
    let alarmTriggered = false;

    // --- Core Alarm Function ---

    /**
     * Plays a distinct "pep-pep" sound (two short, high tones).
     */
    function playPepPepSound() {
        try {
            const AudioContext = window.AudioContext || window.webkitAudioContext;
            if (!AudioContext) return;

            const context = new AudioContext();

            // Function to create a single tone
            const createTone = (frequency, duration, delay) => {
                const oscillator = context.createOscillator();
                const gainNode = context.createGain();

                oscillator.type = 'sine';
                oscillator.frequency.setValueAtTime(frequency, context.currentTime + delay);
                gainNode.gain.setValueAtTime(1, context.currentTime + delay);

                oscillator.connect(gainNode);
                gainNode.connect(context.destination);

                oscillator.start(context.currentTime + delay);
                // Stop the sound after the duration
                oscillator.stop(context.currentTime + delay + duration);
            };

            // Pep 1: High pitch, short duration
            createTone(1000, 0.3, 0);

            // Pep 2: Slightly lower pitch, short duration, with a small delay
            createTone(800, 0.3, 0.2);

             // Pep 1: High pitch, short duration
            createTone(1000, 0.3, 0.2);

            // Pep 2: Slightly lower pitch, short duration, with a small delay
            createTone(800, 0.3, 0.2);

        } catch (e) {
            console.error("Could not play alarm sound:", e);
        }
    }

    /**
     * Triggers the alarm: plays sound and shows a notification.
     * @param {string} currentTime - The time string (e.g., '01:59').
     * @param {boolean} isTest - Flag to indicate if this is a test run.
     */
    function triggerAlarm(currentTime, isTest = false) {
        const title = isTest ? '🚨 TEST ALARM 🚨' : '🚨 CHAIN WARNING 🚨';
        const text = isTest ? `TEST SUCCESSFUL! Time: ${currentTime}` : `Time is critically low: ${currentTime}`;

        if (!isTest && alarmTriggered) return;
        if (!isTest) {
            alarmTriggered = true; // Prevents repeated alarms during a real chain drop
        }

        console.log(`[Countdown Alarm] ${isTest ? 'Test' : 'Alarm'} triggered. Time: ${currentTime}`);

        // 1. Audible Alarm (Sound)
        playPepPepSound();

        // 2. Visual Alarm (Notification)
        if (typeof GM_notification === 'function') {
            GM_notification({
                title: title,
                text: text,
                image: 'https://www.torn.com/favicon.ico',
                timeout: isTest ? 5000 : 10000,
                onclick: function() { // This will bring the tab to focus when clicked
                    window.focus();
                }
            });
        }
    }


    // --- Timer Monitoring Logic ---

    /**
     * Parses a 'MM:SS' time string into total seconds.
     */
    function parseTimeToSeconds(timeString) {
        const parts = timeString.split(':');
        if (parts.length === 2) {
            const minutes = parseInt(parts[0], 10);
            const seconds = parseInt(parts[1], 10);
            if (!isNaN(minutes) && !isNaN(seconds)) {
                return (minutes * 60) + seconds;
            }
        }
        return NaN;
    }

    /**
     * The main logic to check the timer value.
     */
    function checkTimer(element) {
        const timeString = element.textContent.trim();
        const secondsRemaining = parseTimeToSeconds(timeString);

        if (isNaN(secondsRemaining)) {
            return;
        }

        if (secondsRemaining > 0 && secondsRemaining <= ALARM_THRESHOLD_SECONDS) {
            triggerAlarm(timeString, false); // Trigger for a real countdown
        } else if (secondsRemaining > ALARM_THRESHOLD_SECONDS) {
            alarmTriggered = false; // Reset for new chains
        }
    }


    // --- Test Button UI Logic ---

    function createTestButton(parentContainer) {
        const testButton = document.createElement('button');
        testButton.textContent = '🔊'; // Only the speaker icon
        testButton.title = 'Test Alarm'; // Add a tooltip for clarity
        testButton.style.cssText = `
            background-color: #ff9900;
            color: #111;
            border: none;
            border-radius: 3px;
            padding: 0px 4px; /* Reduced padding for minimal height */
            margin-left: 5px; /* Reduced margin */
            cursor: pointer;
            font-size: 10px; /* Kept small, but readable */
            line-height: 1.2; /* Tighter line height */
            font-weight: bold;
            display: inline-block;
            vertical-align: middle;
            transition: background-color 0.1s;
        `;
        testButton.onmouseover = function() { this.style.backgroundColor = '#ffc04d'; };
        testButton.onmouseout = function() { this.style.backgroundColor = '#ff9900'; };
        testButton.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            triggerAlarm('01:59', true); // Run test alarm
        };

        // Append the button to the Chain stats container
        parentContainer.appendChild(testButton);
    }

    // --- Main Setup Function ---

    const findAndObserve = () => {
        const targetElement = document.querySelector(TARGET_SELECTOR);
        const parentContainer = document.querySelector(PARENT_SELECTOR);

        if (targetElement && parentContainer) {
            console.log("[Countdown Alarm] Target elements found. Initializing...");

            // 1. Create the Test Button
            createTestButton(parentContainer);

            // 2. Initial check
            checkTimer(targetElement);

            // 3. Set up the MutationObserver for the timer
            const observer = new MutationObserver((mutationsList, observer) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'characterData' || mutation.type === 'childList') {
                        checkTimer(targetElement);
                    }
                }
            });

            observer.observe(targetElement, {
                childList: true,
                subtree: true,
                characterData: true
            });

        } else {
            setTimeout(findAndObserve, 2000); // Keep trying to find elements
        }
    };

    // Start the process
    findAndObserve();
})();