您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Chain timer with reliable updates, better UI, and sound alerts
// ==UserScript== // @name Torn Chain Timer - Enhanced // @namespace http://tampermonkey.net/ // @version 3.6 // @description Chain timer with reliable updates, better UI, and sound alerts // @author lilha [2630451] & KillerCleat [2842410] // @match https://www.torn.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // Configuration const config = { minSize: 20, maxSize: 200, defaultSize: 40, updateInterval: 250, criticalThreshold: 45, warningThreshold: 90, soundEnabled: GM_getValue('soundEnabled', true), fontSize: GM_getValue('fontSize', 40), boxPosition: GM_getValue('boxPosition', { left: 20, top: 20 }) }; // Hitting 1 Sound let beepAudio = new Audio('https://www.myinstants.com/media/sounds/hitting-1.mp3'); // Add base CSS GM_addStyle(` #chain-timer-container { position: fixed; left: ${config.boxPosition.left}px; top: ${config.boxPosition.top}px; padding: 10px; background: rgba(0, 0, 0, 0.8); color: white; font-weight: bold; border-radius: 5px; z-index: 9999; cursor: move; user-select: none; transition: all 0.3s ease; min-width: 120px; } #chain-timer { font-size: ${config.fontSize}px; text-align: center; margin-bottom: 5px; transition: font-size 0.2s ease; } .timer-controls { display: flex; gap: 5px; justify-content: center; align-items: center; flex-wrap: wrap; } .timer-btn { cursor: pointer; padding: 2px 5px; background: rgba(255, 255, 255, 0.2); border-radius: 3px; font-size: 12px; } .timer-btn:hover { background: rgba(255, 255, 255, 0.3); } #size-slider { width: 80px; margin: 0 5px; cursor: pointer; accent-color: #ffffff; } #chain-timer-container.warning { background: orange; } #chain-timer-container.critical { background: red; animation: flashScreen 0.5s infinite alternate; } @keyframes flashScreen { 0% { opacity: 1; } 100% { opacity: 0.3; } } .size-control { display: flex; align-items: center; background: rgba(255, 255, 255, 0.1); padding: 2px 5px; border-radius: 3px; } .size-label { font-size: 10px; opacity: 0.8; margin-right: 5px; } #chain-timer-container.fullscreen { position: fixed !important; left: 0 !important; top: 0 !important; width: 100vw !important; height: 100vh !important; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.95); z-index: 10000; padding: 20px; } #chain-timer-container.fullscreen #chain-timer { font-size: calc(min(120px, 15vh)) !important; } `); // Create timer container const timerContainer = document.createElement('div'); timerContainer.id = 'chain-timer-container'; timerContainer.innerHTML = ` <div id="chain-timer">--:--</div> <div class="timer-controls"> <div class="size-control"> <span class="size-label">Size</span> <input type="range" id="size-slider" min="${config.minSize}" max="${config.maxSize}" value="${config.fontSize}"> </div> <span class="timer-btn" id="timer-fullscreen">⛶</span> <span class="timer-btn" id="timer-minimize">_</span> <span class="timer-btn" id="toggle-sound">${config.soundEnabled ? '🔊' : '🔇'}</span> </div> `; document.body.appendChild(timerContainer); let lastTime = ''; let observer; let interval; let warningBeepPlayed = false; let criticalBeepPlayed = false; let isMinimized = false; let isFullscreen = false; function initObserver() { if (observer) observer.disconnect(); clearInterval(interval); // Try both old and new selectors const timerElement = document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV'); if (timerElement) { observer = new MutationObserver(() => updateTimer()); observer.observe(timerElement, { characterData: true, childList: true, subtree: true }); interval = setInterval(updateTimer, config.updateInterval); updateTimer(); } else { setTimeout(initObserver, 1000); document.getElementById('chain-timer').textContent = '--:--'; } } function updateTimer() { const timerElement = document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV'); const displayElement = document.getElementById('chain-timer'); if (timerElement) { const newTime = timerElement.textContent.trim(); if (!/^\d+:\d{2}$/.test(newTime)) return; if (newTime !== lastTime) { lastTime = newTime; displayElement.textContent = newTime; const [mins, secs] = newTime.split(':').map(Number); const totalSeconds = mins * 60 + secs; timerContainer.classList.remove('warning', 'critical'); if (totalSeconds <= config.criticalThreshold) { timerContainer.classList.add('critical'); if (config.soundEnabled && !criticalBeepPlayed) { beepAudio.play().catch(err => console.error("Beep failed to play:", err)); criticalBeepPlayed = true; } } else if (totalSeconds <= config.warningThreshold) { timerContainer.classList.add('warning'); if (config.soundEnabled && !warningBeepPlayed) { beepAudio.play().catch(err => console.error("Beep failed to play:", err)); warningBeepPlayed = true; } } else { warningBeepPlayed = false; criticalBeepPlayed = false; } } } else { displayElement.textContent = '--:--'; timerContainer.classList.remove('warning', 'critical'); } } // Size slider functionality const sizeSlider = document.getElementById('size-slider'); const timerDisplay = document.getElementById('chain-timer'); sizeSlider.addEventListener('input', (e) => { const newSize = parseInt(e.target.value); config.fontSize = newSize; GM_setValue('fontSize', newSize); if (!isFullscreen) { timerDisplay.style.fontSize = `${newSize}px`; } }); // Fullscreen toggle document.getElementById('timer-fullscreen').addEventListener('click', () => { isFullscreen = !isFullscreen; timerContainer.classList.toggle('fullscreen'); document.getElementById('timer-fullscreen').textContent = isFullscreen ? '⮌' : '⛶'; if (!isFullscreen) { // Only update font size when exiting fullscreen timerDisplay.style.fontSize = `${config.fontSize}px`; } }); // Minimize button document.getElementById('timer-minimize').addEventListener('click', () => { const controls = timerContainer.querySelector('.timer-controls'); isMinimized = !isMinimized; if (isMinimized) { controls.style.display = 'none'; document.getElementById('timer-minimize').textContent = '□'; } else { controls.style.display = 'flex'; document.getElementById('timer-minimize').textContent = '_'; } }); // Sound toggle document.getElementById('toggle-sound').addEventListener('click', () => { config.soundEnabled = !config.soundEnabled; GM_setValue('soundEnabled', config.soundEnabled); document.getElementById('toggle-sound').textContent = config.soundEnabled ? '🔊' : '🔇'; if (config.soundEnabled) { beepAudio.play().catch(err => console.error("Beep failed to play:", err)); } }); // Make the timer draggable timerContainer.onmousedown = function(event) { if (event.target.classList.contains('timer-btn') || event.target.id === 'size-slider' || isFullscreen) return; let shiftX = event.clientX - timerContainer.getBoundingClientRect().left; let shiftY = event.clientY - timerContainer.getBoundingClientRect().top; function moveAt(pageX, pageY) { config.boxPosition = { left: Math.max(0, Math.min(pageX - shiftX, window.innerWidth - timerContainer.offsetWidth)), top: Math.max(0, Math.min(pageY - shiftY, window.innerHeight - timerContainer.offsetHeight)) }; timerContainer.style.left = `${config.boxPosition.left}px`; timerContainer.style.top = `${config.boxPosition.top}px`; GM_setValue('boxPosition', config.boxPosition); } function onMouseMove(event) { moveAt(event.pageX, event.pageY); } document.addEventListener('mousemove', onMouseMove); document.onmouseup = function() { document.removeEventListener('mousemove', onMouseMove); document.onmouseup = null; }; }; timerContainer.ondragstart = () => false; // Initialize initObserver(); // Handle page navigation setInterval(() => { if (!document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV')) { initObserver(); } }, 3000); document.addEventListener('visibilitychange', () => { if (!document.hidden) initObserver(); }); // Handle SPA navigation const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); setTimeout(initObserver, 500); }; const originalReplaceState = history.replaceState; history.replaceState = function() { originalReplaceState.apply(this, arguments); setTimeout(initObserver, 500); }; })();