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