Virus Timer-PDA

Displays a movable timer for your virus

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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