Torn Networth Scouter

Fetches networth, formats decimals conditionally, applies high-visibility color grading, and fixes popup spam

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Torn Networth Scouter
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Fetches networth, formats decimals conditionally, applies high-visibility color grading, and fixes popup spam
// @author       Domegid
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- Prevent running in iframes (Fix for Torn PDA multi-prompt bug) ---
    if (window.top !== window.self) return;

    // --- Configuration & State ---
    const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
    const API_DELAY = 700; // 700ms delay to stay under 100 calls/min limit
    const fetchQueue = [];
    let isFetching = false;

    // Retrieve or prompt for API Key
    let apiKey = GM_getValue('torn_nw_apikey', '');
    if (!apiKey) {
        apiKey = prompt("Torn Networth Scouter:\nPlease enter your Torn API key (requires 'Public' access for profiles):");
        if (apiKey) {
            GM_setValue('torn_nw_apikey', apiKey.trim());
        } else {
            console.warn("Networth Scouter paused: No API key provided.");
        }
    }

    // Load Cache (v2 uses raw numbers)
    let nwCache = GM_getValue('torn_nw_cache_v2', {});

    // --- Utility Functions ---
    function getCachedNW(userId) {
        const entry = nwCache[userId];
        if (entry) {
            const isExpired = (Date.now() - entry.timestamp > CACHE_TTL);
            return { nw: entry.nw, expired: isExpired };
        }
        return null;
    }

    function setCachedNW(userId, rawNw) {
        nwCache[userId] = { nw: rawNw, timestamp: Date.now() };
        GM_setValue('torn_nw_cache_v2', nwCache);
    }

    function formatNW(num) {
        if (num >= 1e9) {
            // Billions or more: Show 1 decimal place
            return (num / 1e9).toFixed(1) + 'B';
        } else if (num >= 1e6) {
            // Millions: No decimal place, rounded
            return Math.round(num / 1e6) + 'M';
        } else if (num >= 1e3) {
            // Thousands: No decimal place, rounded
            return Math.round(num / 1e3) + 'K';
        }
        return Math.round(num).toString();
    }

    function getColorForNW(num) {
        // Red grading for under 500M
        if (num < 100_000_000) return '#ff3333'; // < 100M: Vivid Red
        if (num < 500_000_000) return '#ff6666'; // < 500M: Soft Red
        
        // Yellow grading for 500M to 2B
        if (num < 1_000_000_000) return '#ffea00'; // 500M - 1B: Bright Yellow
        if (num < 2_000_000_000) return '#ccff00'; // 1B - 2B: Electric Lime/Yellow-Green
        
        // Light/Neon Green grading for 2B and above
        if (num < 4_000_000_000) return '#39ff14'; // 2B - 4B: Fluorescent Neon Green
        return '#00ff00'; // 4B+: Pure Bright Lime Green
    }

    // --- API Queue Worker ---
    async function processQueue() {
        if (isFetching || fetchQueue.length === 0 || !apiKey) return;
        isFetching = true;

        while (fetchQueue.length > 0) {
            const item = fetchQueue.shift();
            let rawNw = null;
            
            const cachedData = getCachedNW(item.userId);
            if (cachedData && !cachedData.expired) {
                 rawNw = cachedData.nw;
            }
            
            if (rawNw === null) {
                try {
                    const response = await fetch(`https://api.torn.com/user/${item.userId}?selections=personalstats&key=${apiKey}`);
                    const data = await response.json();
                    
                    if (data.error) {
                        console.error("NW Scouter API Error:", data.error.error);
                        rawNw = -1; // API Error
                    } else if (data.personalstats && data.personalstats.networth !== undefined) {
                        rawNw = data.personalstats.networth;
                        setCachedNW(item.userId, rawNw);
                    } else {
                        rawNw = -2; // Hidden stats or N/A
                        setCachedNW(item.userId, rawNw); 
                    }
                } catch (error) {
                    console.error("NW Scouter Fetch Error:", error);
                    rawNw = -1;
                }
                
                await new Promise(resolve => setTimeout(resolve, API_DELAY));
            }
            
            if (rawNw !== null) {
                // Reset opacity in case it was dimmed for background fetching
                item.badge.style.opacity = '1';
                
                if (rawNw === -1) {
                    item.badge.innerText = `[Err]`;
                    item.badge.style.color = '#ff3333';
                } else if (rawNw === -2) {
                    item.badge.innerText = `[N/A]`;
                    item.badge.style.color = '#aaaaaa';
                } else {
                    item.badge.innerText = `[$${formatNW(rawNw)}]`;
                    item.badge.style.color = getColorForNW(rawNw);
                }
            }
        }
        isFetching = false;
    }

    // --- UI Injection ---
    function injectBadge(linkElement, initialText, color) {
        const badge = document.createElement("span");
        badge.className = "nw-badge";
        badge.innerText = `[${initialText}]`;

        const honorBar = linkElement.querySelector('.honor-text-wrap');

        if (honorBar) {
            badge.style.cssText = `
                position: absolute; 
                right: 10%; 
                top: 50%; 
                transform: translateY(-50%); 
                color: ${color}; 
                font-weight: bold; 
                font-size: 11px; 
                z-index: 10; 
                text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
            `;
            honorBar.style.position = 'relative'; 
            honorBar.appendChild(badge);
        } else {
            badge.style.cssText = `color: ${color}; margin-left: 6px; font-weight: bold; font-size: 11px; white-space: nowrap; display: inline-flex; align-items: center;`;
            linkElement.insertAdjacentElement('afterend', badge);
        }

        return badge; 
    }

    // --- DOM Observer ---
    const observer = new MutationObserver(() => {
        const profileLinks = document.querySelectorAll('a[href*="profiles.php?XID="]:not(.nw-scouter-processed)');
        
        profileLinks.forEach(link => {
            link.classList.add('nw-scouter-processed');

            // Skip top search dropdown
            if (link.closest('#header-root')) return;

            const linkText = link.innerText.trim();

            // Skip the action links inside the mini-profile popup
            if (linkText === "View profile" || linkText === "New tab") return;

            if (link.querySelector('.honor-text') || linkText.length > 0) {
                
                const idMatch = link.href.match(/XID=(\d+)/);
                if (!idMatch) return;
                const userId = idMatch[1];

                const cachedData = getCachedNW(userId);

                if (cachedData !== null) {
                    const rawNw = cachedData.nw;
                    let badge;
                    
                    if (rawNw === -1) {
                        badge = injectBadge(link, "Err", "#ff3333");
                    } else if (rawNw === -2) {
                        badge = injectBadge(link, "N/A", "#aaaaaa");
                    } else {
                        // Add an asterisk if we are showing expired data
                        const displayText = cachedData.expired ? `$${formatNW(rawNw)}*` : `$${formatNW(rawNw)}`;
                        badge = injectBadge(link, displayText, getColorForNW(rawNw));
                        
                        // Dim slightly while fetching
                        if (cachedData.expired) {
                            badge.style.opacity = '0.6';
                        }
                    }

                    // Queue for background refresh if expired
                    if (cachedData.expired) {
                        fetchQueue.push({ userId, badge });
                        processQueue();
                    }
                } else {
                    const badge = injectBadge(link, "...", "#cccccc");
                    fetchQueue.push({ userId, badge });
                    processQueue();
                }
            }
        });
    });

    function initObserver() {
        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: true });
        } else {
            setTimeout(initObserver, 50); 
        }
    }

    initObserver();

})();