Torn Networth Scouter

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();