Displays a movable timer for your virus
// ==UserScript==
// @name Virus Timer-PDA
// @namespace TornPDA
// @version 1.11
// @description Displays a movable timer for your virus
// @author Pholder [3623374]
// @match https://www.torn.com/*
// @grant none
// @license MIT
// @download https://greasyfork.org/en/scripts/574536-virus-timer-pda
// ==/UserScript==
(function() {
'use strict';
const apiKey = "###PDA-APIKEY###";
let virusUntil = 0;
let isFetching = false;
let lastKnownText = ""; // Memory for the last valid display
const styleSheet = document.createElement("style");
styleSheet.innerText = `
#pda-virus-badge {
position: fixed !important;
z-index: 2147483647 !important;
background: rgba(0, 0, 0, 0.95) !important;
color: #228B22 !important;
font-family: monospace !important;
font-weight: bold !important;
cursor: move !important;
pointer-events: auto !important;
user-select: none !important;
touch-action: none !important;
display: block !important;
box-shadow: 0 0 10px rgba(0,0,0,0.7);
border: 1px solid #228B22 !important;
}
@media screen and (min-width: 601px) { #pda-virus-badge { font-size: 13px !important; padding: 8px 12px !important; } }
@media screen and (max-width: 600px) { #pda-virus-badge { font-size: 11px !important; padding: 3px 8px !important; border-radius: 3px !important; } }
`;
document.head.appendChild(styleSheet);
function createBadge() {
if (document.getElementById('pda-virus-badge')) return;
const badge = document.createElement('div');
badge.id = 'pda-virus-badge';
badge.innerText = "Syncing...";
const savedPos = localStorage.getItem('pda_virus_pos');
if (savedPos) {
const pos = JSON.parse(savedPos);
badge.style.top = pos.top; badge.style.left = pos.left;
} else {
if (window.innerWidth <= 600) { badge.style.bottom = "80px"; badge.style.left = "10px"; }
else { badge.style.top = "0px"; badge.style.left = "0px"; }
}
(document.body || document.documentElement).appendChild(badge);
setupDragging(badge);
}
function setupDragging(el) {
let active = false, currentX, currentY, initialX, initialY;
const dragStart = (e) => {
const clientX = e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
const clientY = e.type === "touchstart" ? e.touches[0].clientY : e.clientY;
const rect = el.getBoundingClientRect();
initialX = clientX - rect.left; initialY = clientY - rect.top;
if (e.target === el) active = true;
};
const drag = (e) => {
if (active) {
e.preventDefault();
const clientX = e.type === "touchmove" ? e.touches[0].clientX : e.clientX;
const clientY = e.type === "touchmove" ? e.touches[0].clientY : e.clientY;
el.style.left = (clientX - initialX) + "px"; el.style.top = (clientY - initialY) + "px";
el.style.bottom = 'auto'; el.style.right = 'auto';
}
};
const dragEnd = () => {
if (active) {
localStorage.setItem('pda_virus_pos', JSON.stringify({ top: el.style.top, left: el.style.left }));
active = false;
}
};
el.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
el.addEventListener("touchstart", dragStart, {passive: false});
document.addEventListener("touchmove", drag, {passive: false});
document.addEventListener("touchend", dragEnd);
}
async function updateData() {
if (apiKey.includes("PDA-APIKEY") || isFetching) return;
isFetching = true;
try {
const res = await fetch(`https://api.torn.com/v2/user/virus?key=${apiKey}&st=${Date.now()}`);
const data = await res.json();
// Only update if we get a real timestamp back
if (data.virus && data.virus.until > 0) {
virusUntil = data.virus.until;
}
} catch (e) {}
isFetching = false;
}
function getScrapedTime() {
if (!window.location.href.includes('pc.php')) return null;
const bodyText = document.body.innerText;
const dayMatch = bodyText.match(/take (\d+) more day/i);
if (dayMatch) return dayMatch[1] + "d+";
const clockMatch = bodyText.match(/\d{1,2}:\d{2}:\d{2}/);
return clockMatch ? clockMatch[0] : null;
}
function render() {
const b = document.getElementById('pda-virus-badge');
if (!b) { createBadge(); return; }
const now = Math.floor(Date.now() / 1000);
const isMobile = window.innerWidth <= 600;
const icon = isMobile ? "" : "🦠 ";
let newText = "";
const scraped = getScrapedTime();
// 1. Priority: If we have an active API timer
if (virusUntil > now) {
const s = virusUntil - now;
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
newText = d > 0 ? `${icon}${d}d ${h}h ${m}m` : `${icon}${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
b.style.opacity = "1";
}
// 2. Secondary: Trust the page scraper if the API isn't ready
else if (scraped) {
newText = `${icon}~${scraped}`;
b.style.opacity = "1";
}
// 3. Fallback: Idle
else {
newText = isMobile ? "Idle" : "V: Idle";
b.style.opacity = "0.5";
}
// --- THE FIX: Only update if the text changed and isn't flickering to empty ---
if (newText !== lastKnownText) {
b.innerText = newText;
lastKnownText = newText;
}
}
createBadge();
updateData();
setInterval(updateData, 45000); // Slower API pull to reduce collision
setInterval(render, 1000);
setInterval(createBadge, 3000);
// When clicking "Begin", clear the cache to allow the scraper/API to refresh
document.addEventListener('click', (e) => {
if (window.location.href.includes('pc.php')) {
virusUntil = 0; // Reset API memory so scraper takes over immediately
setTimeout(updateData, 3000);
}
});
})();