Torn Chain Watch Mobile

Displays chain timer below Chain Box in faction menu.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Chain Watch Mobile
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Displays chain timer below Chain Box in faction menu.
// @author       Codex234
// @match        https://www.torn.com/factions.php?step=your&type=1*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let currentAudio = null;
    let isTestActive = false;
    let testEndTime = 0;
    let lastStopTime = 0;

    const sounds = [
        { name: 'None', src: '' },
        { name: 'Train Bell', src: 'https://www.soundjay.com/transportation/sounds/train-crossing-bell-01.mp3' },
        { name: 'Smoke Alarm', src: 'https://www.soundjay.com/mechanical/sounds/smoke-detector-1.mp3' },
        { name: 'Car Alarm', src: 'https://www.soundjay.com/transportation/sounds/car-alarm-1.mp3' },
        { name: 'Air Raid', src: 'https://cdn.pixabay.com/audio/2021/08/04/audio_21be84d662.mp3' },
        { name: 'Dial Up', src: 'https://sfxcontent.s3.amazonaws.com/soundfx/EmergencyAlertSystemBeep.mp3' },
        { name: 'Electric', src: 'https://cdn.freesound.org/previews/361/361491_4930962-lq.mp3' },
        { name: 'Car Horn', src: 'https://cdn.freesound.org/previews/436/436587_1535323-lq.mp3' },
        { name: 'Klaxon', src: 'https://cdn.freesound.org/previews/32/32088_114160-lq.mp3' },
        { name: 'EAS Alarm', src: 'https://cdn.pixabay.com/audio/2022/03/15/audio_30d62f0685.mp3' }
    ];

    function loadSettings() {
        const defaultSettings = {
            threshold: 0,
            soundUrl: 'https://www.soundjay.com/transportation/sounds/train-crossing-bell-01.mp3',
            profileXid: '3471923',
            enableSound: true
        };
        const stored = JSON.parse(localStorage.getItem('chainWatchSettings') || '{}');
        return { ...defaultSettings, ...stored };
    }

    function saveSettings(newSettings) {
        const settings = { ...loadSettings(), ...newSettings };
        localStorage.setItem('chainWatchSettings', JSON.stringify(settings));
        return settings;
    }

    function parseTime(str) {
        if (str === 'N/A' || !str) return Infinity;
        const parts = str.trim().split(':');
        const nums = parts.map(Number);
        let total = 0;
        if (parts.length === 3) {
            const [h, m, s] = nums;
            total = (h || 0) * 3600 + (m || 0) * 60 + (s || 0);
        } else if (parts.length === 2) {
            const [m, s] = nums;
            total = (m || 0) * 60 + (s || 0);
        } else {
            return Infinity;
        }
        return total > 0 ? total : 0;
    }

    function playSound(isTest = false) {
        const settings = loadSettings();
        if (!settings.enableSound || !settings.soundUrl) return;

        if (!isTest) {
            const now = Date.now();
            if (now - lastStopTime < 10000) return;
        }

        if (currentAudio) {
            currentAudio.pause();
            currentAudio = null;
        }

        try {
            currentAudio = new Audio(settings.soundUrl);
            currentAudio.preload = 'auto';
            currentAudio.volume = 1.0;
            currentAudio.loop = isTest;

            currentAudio.addEventListener('ended', () => {
                currentAudio = null;
            });

            currentAudio.addEventListener('canplaythrough', () => {
                currentAudio.play().catch((e) => {
                    console.log('Audio play failed:', e);
                });
            });

            currentAudio.addEventListener('error', (e) => {
                console.log('Audio load error:', e);
            });

            currentAudio.load();

            if (isTest) {
                isTestActive = true;
                testEndTime = Date.now() + 5000;
                setTimeout(() => {
                    if (currentAudio) {
                        currentAudio.loop = false;
                        currentAudio.pause();
                        currentAudio.currentTime = 0;
                        currentAudio = null;
                    }
                    isTestActive = false;
                }, 5000);
            }
        } catch (e) {
            console.log('Audio creation error:', e);
        }
    }

    function stopSound() {
        if (currentAudio) {
            currentAudio.pause();
            currentAudio.currentTime = 0;
            currentAudio = null;
        }
        isTestActive = false;
        lastStopTime = Date.now();
    }

    function checkAlerts(timerValue) {
        const seconds = parseTime(timerValue);
        const settings = loadSettings();
        const thresholdSeconds = settings.threshold || 0;

        if (seconds > thresholdSeconds) {
            localStorage.removeItem('chainAlerted');
        }

        if (thresholdSeconds > 0 && seconds <= thresholdSeconds && seconds > 0 && !localStorage.getItem('chainAlerted')) {
            const now = Date.now();
            if (now - lastStopTime >= 10000) {
                localStorage.setItem('chainAlerted', 'true');
                playSound(false);
            }
        }
    }

    function verifySoundState(timerValue) {
        const seconds = parseTime(timerValue);
        const settings = loadSettings();
        const thresholdSeconds = settings.threshold || 0;
        const now = Date.now();
        const inTestPeriod = isTestActive && now < testEndTime;
        const shouldPlay = settings.enableSound && settings.soundUrl && (
            (thresholdSeconds > 0 && seconds <= thresholdSeconds && seconds > 0 && localStorage.getItem('chainAlerted')) ||
            (thresholdSeconds > 0 && seconds <= 0)
        );

        if (shouldPlay && !currentAudio) {
            if (now - lastStopTime >= 10000) {
                playSound(false);
            }
        } else if (!shouldPlay && currentAudio && !inTestPeriod) {
            stopSound();
        }
    }

    // Update profile links across displays
    function updateProfileLinks() {
        const settings = loadSettings();
        const href = `https://www.torn.com/loader.php?sid=attack&user2ID=${settings.profileXid}`;
        document.querySelectorAll('.profile-link').forEach(link => {
            link.href = href;
        });
    }
    let created = false;
    function createTimerDisplay(id, timerValue) {
        let timerDisplay = document.getElementById(id);
        const labelId = 'timer-label';
        const textId = 'timer-text';
        const currentSettings = loadSettings();
        if (!timerDisplay) {
            timerDisplay = document.createElement('div');
            timerDisplay.id = id;
            timerDisplay.style.cssText = `
                background: linear-gradient(90deg, #2c3e50, #3498db);
                color: white;
                padding: 15px;
                margin: 10px 0;
                border-radius: 8px;
                text-align: center;
                font-family: Arial, sans-serif;
                font-weight: bold;
                width: 100%;
                box-sizing: border-box;
                box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                display: flex;
                flex-direction: column;
                align-items: center;
                pointer-events: auto;
            `;

            // Set width to match chain-box
            const chainBox = document.querySelector('.chain-box');
            if (chainBox) {
                timerDisplay.style.width = chainBox.offsetWidth + 'px';
                timerDisplay.style.maxWidth = 'none';
            }

            // Label
            const labelSpan = document.createElement('span');
            labelSpan.id = labelId;
            labelSpan.textContent = 'Timer:';
            labelSpan.style.cssText = `
                font-size: 1.2rem;
                margin-bottom: 5px;
            `;
            timerDisplay.appendChild(labelSpan);

            // Timer text
            const textSpan = document.createElement('span');
            textSpan.id = textId;
            textSpan.textContent = `${timerValue}`;
            textSpan.style.cssText = `
                font-size: 2.5rem;
                width: 50%;
                margin-bottom: 10px;
            `;
            timerDisplay.appendChild(textSpan);

            // Profile button
            const button = document.createElement('a');
            button.className = 'profile-link';
            button.textContent = 'Attack';
            button.style.cssText = `
                display: inline-block;
                background-color: #007bff;
                color: white;
                padding: 10px;
                margin: 10px 0;
                border-radius: 5px;
                text-decoration: none;
                font-size: 1.1rem;
                width: 100%;
                max-width: 200px;
                text-align: center;
                box-sizing: border-box;
                pointer-events: auto;
            `;
            button.onmouseover = () => button.style.backgroundColor = '#0056b3';
            button.onmouseout = () => button.style.backgroundColor = '#007bff';
            timerDisplay.appendChild(button);

            // Settings panel
            const settingsPanel = document.createElement('div');
            settingsPanel.style.cssText = `
                width: 100%;
                padding: 10px;
                background: rgba(255,255,255,0.1);
                border-radius: 5px;
                margin-top: 10px;
                font-size: 0.9rem;
                display: flex;
                flex-direction: column;
                gap: 8px;
            `;

            // Threshold (minutes:seconds)
            const thresholdDiv = document.createElement('div');
            const min = Math.floor(currentSettings.threshold / 60);
            const sec = currentSettings.threshold % 60;
            thresholdDiv.innerHTML = `
                <label>Alarm: <input type="number" id="min-input" min="0" max="5" value="${min}" style="width: 40px; margin: 0 2px;"> : <input type="number" id="sec-input" min="0" max="59" value="${sec}" style="width: 40px; margin: 0 2px;"> (0:00 = off)</label>
            `;
            const minInput = thresholdDiv.querySelector('#min-input');
            const secInput = thresholdDiv.querySelector('#sec-input');
            const updateThreshold = () => {
                let m = parseInt(minInput.value) || 0;
                if (m > 5) {
                    m = 5;
                    minInput.value = m;
                }
                let s = parseInt(secInput.value) || 0;
                if (s > 59) {
                    s = 59;
                    secInput.value = s;
                }
                saveSettings({ threshold: m * 60 + s });
                verifySoundState(timerValue);
            };
            minInput.onchange = updateThreshold;
            secInput.onchange = updateThreshold;
            settingsPanel.appendChild(thresholdDiv);

            // Sound select
            const soundDiv = document.createElement('div');
            soundDiv.innerHTML = `<label>Sound: <select id="sound-select" style="width: 100%; box-sizing: border-box; margin-top: 2px;"></select></label>`;
            const select = soundDiv.querySelector('#sound-select');
            sounds.forEach(sound => {
                const opt = document.createElement('option');
                opt.value = sound.src;
                opt.textContent = sound.name;
                select.appendChild(opt);
            });
            select.value = currentSettings.soundUrl;
            select.onchange = (e) => {
                saveSettings({ soundUrl: e.target.value });
                verifySoundState(timerValue);
            };
            settingsPanel.appendChild(soundDiv);

            // Enable sound checkbox
            const enableSoundDiv = document.createElement('div');
            enableSoundDiv.innerHTML = `
                <label><input type="checkbox" id="enable-sound" ${currentSettings.enableSound ? 'checked' : ''} style="margin-right: 5px;"> Enable sound</label>
            `;
            enableSoundDiv.querySelector('#enable-sound').onchange = (e) => {
                saveSettings({ enableSound: e.target.checked });
                verifySoundState(timerValue);
            };
            settingsPanel.appendChild(enableSoundDiv);

            // Button row for test and stop
            const buttonRow = document.createElement('div');
            buttonRow.style.cssText = `
                display: flex;
                gap: 10px;
                justify-content: center;
            `;

            // Test sound button
            const testButton = document.createElement('button');
            testButton.textContent = 'Test Sound';
            testButton.style.cssText = `
                background: #28a745;
                color: white;
                border: none;
                padding: 5px 10px;
                border-radius: 3px;
                cursor: pointer;
                font-size: 0.8rem;
            `;
            testButton.onclick = () => playSound(true);
            buttonRow.appendChild(testButton);

            // Stop sound button
            const stopButton = document.createElement('button');
            stopButton.textContent = 'Stop Sound';
            stopButton.style.cssText = `
                background: #dc3545;
                color: white;
                border: none;
                padding: 5px 10px;
                border-radius: 3px;
                cursor: pointer;
                font-size: 0.8rem;
            `;
            stopButton.onmouseover = () => stopButton.style.backgroundColor = '#c82333';
            stopButton.onmouseout = () => stopButton.style.backgroundColor = '#dc3545';
            stopButton.onclick = stopSound;
            buttonRow.appendChild(stopButton);

            settingsPanel.appendChild(buttonRow);

            // Profile XID
            const xidDiv = document.createElement('div');
            xidDiv.innerHTML = `
                <label>Profile ID: <input type="text" id="xid-input" value="${currentSettings.profileXid}" style="width: 100px; margin: 0 5px;"></label>
            `;
            xidDiv.querySelector('#xid-input').onchange = (e) => {
                saveSettings({ profileXid: e.target.value });
                updateProfileLinks();
            };
            settingsPanel.appendChild(xidDiv);

            timerDisplay.appendChild(settingsPanel);
        } else {
            const textSpan = document.getElementById(textId);
            if (textSpan) {
                textSpan.textContent = `${timerValue}`;
            }
            const updatedSettings = loadSettings();
            const minInput = document.getElementById('min-input');
            const secInput = document.getElementById('sec-input');
            const soundSelect = document.getElementById('sound-select');
            const soundCb = document.getElementById('enable-sound');
            const xidInput = document.getElementById('xid-input');

            if (minInput && !minInput.matches(':focus')) {
                minInput.value = Math.floor(updatedSettings.threshold / 60);
            }
            if (secInput && !secInput.matches(':focus')) {
                secInput.value = updatedSettings.threshold % 60;
            }
            if (soundSelect && !soundSelect.matches(':focus')) {
                soundSelect.value = updatedSettings.soundUrl;
            }
            if (soundCb && !soundCb.matches(':focus')) {
                soundCb.checked = updatedSettings.enableSound;
            }
            if (xidInput && !xidInput.matches(':focus')) {
                xidInput.value = updatedSettings.profileXid;
            }
            // Update width if chain-box changes
            const chainBox = document.querySelector('.chain-box');
            if (chainBox) {
                timerDisplay.style.width = chainBox.offsetWidth + 'px';
            }
        }
        updateProfileLinks();
        return timerDisplay;
    }

    // Function to update timer display
    function updateTimerDisplay() {
        const timerElement = document.querySelector('.chain-box-timeleft');
        const timerValue = timerElement ? timerElement.textContent.trim() : 'N/A';

        checkAlerts(timerValue);
        verifySoundState(timerValue); // Double-check sound state after update

        // Update or create below faction message (settings)
        const factionMsg = document.querySelector('.f-msg.m-top10');
        if (factionMsg) {
            if (!created) {
                const timerDisplay = createTimerDisplay('timer_display', timerValue);
                if (factionMsg.nextSibling) {
                    factionMsg.parentNode.insertBefore(timerDisplay, factionMsg.nextSibling);
                } else {
                    factionMsg.parentNode.appendChild(timerDisplay);
                }
                created = true;
            } else {
                createTimerDisplay('timer_display', timerValue);
            }
        }
    }

    // Initial setup
    function waitForElement(selector, callback, maxAttempts = 30, interval = 500) {
        let attempts = 0;
        const check = () => {
            const element = document.querySelector(selector);
            if (element) {
                callback(element);
            } else if (attempts < maxAttempts) {
                attempts++;
                setTimeout(check, interval);
            }
        };
        check();
    }

    waitForElement('.chain-box-timeleft', (timerElement) => {
        updateTimerDisplay();

        // Observe changes to timer element
        const observer = new MutationObserver(updateTimerDisplay);
        observer.observe(timerElement, { childList: true, characterData: true, subtree: true });

        // Fallback periodic update
        setInterval(updateTimerDisplay, 1000);
    });
})();