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