Replaces the "IN HOSPITAL" message on the attack page with a live countdown timer
// ==UserScript==
// @name Torn - Hospital Timer (Native UI)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Replaces the "IN HOSPITAL" message on the attack page with a live countdown timer
// @license MIT
// @author JayMike
// @match https://www.torn.com/loader.php?sid=attack*
// @match https://www.torn.com/loader2.php?sid=attack*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect api.torn.com
// ==/UserScript==
(function () {
'use strict';
// ─── CONFIG ───────────────────────────────────────────────────────────────
const API_KEY = GM_getValue('torn_api_key', '');
const REFRESH_EVERY = 15; // seconds between API re-sync polls
// ──────────────────────────────────────────────────────────────────────────
if (!API_KEY) {
promptForApiKey();
return;
}
const params = new URLSearchParams(window.location.search);
const targetId = params.get('user2ID');
if (!targetId) return;
let countdownInterval = null;
let pollInterval = null;
let injected = false;
// ─── FIND THE HOSPITAL MESSAGE BOX ───────────────────────────────────────
function findHospitalBox() {
const allDivs = document.querySelectorAll('div');
for (const div of allDivs) {
if (
div.innerText &&
div.innerText.includes('CURRENTLY IN HOSPITAL') &&
div.children.length === 0
) {
return div;
}
}
return null;
}
function waitForHospitalBox(callback, timeout = 15000) {
const start = Date.now();
const check = setInterval(() => {
const box = findHospitalBox();
if (box) {
clearInterval(check);
callback(box);
} else if (Date.now() - start > timeout) {
clearInterval(check);
callback(null);
}
}, 300);
}
// ─── INJECT LIVE TIMER INTO THE HOSPITAL BOX ─────────────────────────────
function injectTimer(box, untilTimestamp) {
box.innerHTML = '';
box.style.display = 'flex';
box.style.flexDirection = 'column';
box.style.alignItems = 'center';
box.style.justifyContent = 'center';
box.style.gap = '4px';
const topText = document.createElement('div');
topText.textContent = 'THIS PERSON IS CURRENTLY IN HOSPITAL';
topText.style.cssText = 'color:#e8581c;font-weight:bold;font-size:13px;text-align:center;line-height:1.3;';
const timerEl = document.createElement('div');
timerEl.id = 'hosp-live-countdown';
timerEl.style.cssText = 'color:#ffffff;font-size:26px;font-weight:bold;font-family:monospace;letter-spacing:3px;margin-top:6px;text-shadow:0 0 8px rgba(232,88,28,0.8);';
const labelEl = document.createElement('div');
labelEl.id = 'hosp-live-label';
labelEl.textContent = 'until out of hospital';
labelEl.style.cssText = 'color:#aaa;font-size:10px;';
box.appendChild(topText);
box.appendChild(timerEl);
box.appendChild(labelEl);
injected = true;
startCountdown(untilTimestamp);
}
function startCountdown(untilTimestamp) {
clearInterval(countdownInterval);
function tick() {
const el = document.getElementById('hosp-live-countdown');
const labelEl = document.getElementById('hosp-live-label');
if (!el) return;
const now = Math.floor(Date.now() / 1000);
const diff = untilTimestamp - now;
if (diff <= 0) {
el.textContent = 'OUT NOW!';
el.style.color = '#2ecc71';
el.style.textShadow = '0 0 8px rgba(46,204,113,0.8)';
if (labelEl) labelEl.textContent = 'ready to attack';
clearInterval(countdownInterval);
setTimeout(fetchAndUpdate, 2000);
return;
}
const h = Math.floor(diff / 3600);
const m = Math.floor((diff % 3600) / 60);
const s = diff % 60;
el.textContent = `${pad(h)}:${pad(m)}:${pad(s)}`;
}
tick();
countdownInterval = setInterval(tick, 1000);
}
// ─── API FETCH ─────────────────────────────────────────────────────────────
function fetchAndUpdate() {
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.torn.com/user/${targetId}?selections=profile&key=${API_KEY}&comment=HospTimerNative`,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.error) return;
const status = data.status || {};
const until = status.until || 0;
if (status.state === 'Hospital') {
if (!injected) {
const box = findHospitalBox();
if (box) injectTimer(box, until);
} else {
// Re-sync countdown with fresh API timestamp
startCountdown(until);
}
}
} catch (e) {}
},
});
}
// ─── MAIN ──────────────────────────────────────────────────────────────────
waitForHospitalBox((box) => {
if (!box) return; // Target not in hospital
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.torn.com/user/${targetId}?selections=profile&key=${API_KEY}&comment=HospTimerNative`,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.error) return;
const until = (data.status && data.status.until) || 0;
injectTimer(box, until);
pollInterval = setInterval(fetchAndUpdate, REFRESH_EVERY * 1000);
} catch (e) {}
},
});
});
// ─── HELPERS ───────────────────────────────────────────────────────────────
function pad(n) {
return String(n).padStart(2, '0');
}
function promptForApiKey() {
const key = window.prompt(
'Torn Hospital Timer\n\nEnter your Torn API key (read-only "user" access).\nYou only need to do this once.',
''
);
if (key && key.trim().length > 5) {
GM_setValue('torn_api_key', key.trim());
window.location.reload();
}
}
window.addEventListener('beforeunload', () => {
clearInterval(countdownInterval);
clearInterval(pollInterval);
});
})();