Torn Networth Scouter

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например 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();

})();