Torn Networth Scouter

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();

})();