Torn Networth Scouter

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();