Torn Utilities + More Legible Names (Nova Edition)

Combines Torn Utilities (Last Action + Faction Inactivity) with clearer, larger player name text on honor bars.

// ==UserScript==
// @name         Torn Utilities + More Legible Names (Nova Edition)
// @namespace    nova.torn.utilities.combo
// @version      3.1
// @description  Combines Torn Utilities (Last Action + Faction Inactivity) with clearer, larger player name text on honor bars.
// @author       Nova & GingerBeardMan & TheFoxMan
// @match        https://www.torn.com/*
// @grant        none
// @license      Apache 2.0 + GNU GPLv3
// ==/UserScript==

(function () {
    'use strict';

    //////////////////////////////////////////////////////////////////////
    // PART 1 — TORN UTILITIES: LAST ACTION + FACTION INACTIVITY
    //////////////////////////////////////////////////////////////////////

    const APIKEY_STORAGE = 'tornApiKey';
    const activityHighlights = [
        [1, 1, '#FFFFFF40'],
        [2, 4, '#ff990060'],
        [5, 6, '#FF000060'],
        [7, 999, '#cc00ff60']
    ];

    const { fetch: origFetch } = window;

    function isDarkMode() {
        return document.documentElement.classList.contains('dark-mode')
            || document.body.classList.contains('dark-mode')
            || window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    function convert(seconds) {
        const d = Math.floor(seconds / 86400);
        const h = Math.floor((seconds % 86400) / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        return [d && `${d}d`, h && `${h}h`, m && `${m}m`, `${s}s`].filter(Boolean).join(' ');
    }

    async function getApiKey() {
        let key = localStorage.getItem(APIKEY_STORAGE);
        if (!key) {
            key = prompt('Enter your Torn API key (Display or Faction permission only):');
            if (key) localStorage.setItem(APIKEY_STORAGE, key);
        }
        return key;
    }

    // Mini-profile monitor (always active)
    window.fetch = async (...args) => {
        const [url] = args;
        const response = await origFetch(...args);

        if (typeof url === 'string' && url.includes('page.php?sid=UserMiniProfile')) {
            response.clone().json().then(body => {
                const seconds = body?.user?.lastAction?.seconds;
                if (seconds !== undefined) insertMiniProfile(seconds);
            }).catch(console.error);
        }

        return response;
    };

    function insertMiniProfile(seconds) {
        const root = $('#profile-mini-root');
        const icons = $('.icons', root);
        if (icons.length > 0) {
            $('.laction', root).remove();
            const textColor = isDarkMode() ? '#ccc' : '#333';
            const text = convert(seconds);
            const html = `
                <p class='laction'
                   style='font-size:11px;
                          color:${textColor};
                          float:right;
                          margin:2px 5px 0 0;
                          font-family:"Verdana", "Arial", sans-serif;'>
                    Last Action: ${text}
                </p>`;
            icons.append(html);
        } else {
            setTimeout(insertMiniProfile, 300, seconds);
        }
    }

    function defineFindHelpers() {
        if (!document.find)
            Object.defineProperties(Document.prototype, {
                find: { value: document.querySelector, enumerable: false },
                findAll: { value: document.querySelectorAll, enumerable: false }
            });

        if (!Element.prototype.find)
            Object.defineProperties(Element.prototype, {
                find: { value: Element.prototype.querySelector, enumerable: false },
                findAll: { value: Element.prototype.querySelectorAll, enumerable: false }
            });
    }

    defineFindHelpers();

    function waitFor(sel, parent = document) {
        return new Promise(resolve => {
            const check = setInterval(() => {
                const el = parent.find(sel);
                if (el) {
                    clearInterval(check);
                    resolve(el);
                }
            }, 400);
        });
    }

    async function showFactionLastAction() {
        await waitFor('.faction-info-wrap .members-list .table-body');

        const key = await getApiKey();
        const factionID = document.find('#view-wars')?.parentElement?.getAttribute('href')?.match(/\d+/)?.[0];
        const apiUrl = `https://api.torn.com/faction/${factionID || ''}?selections=basic&key=${key}`;
        const data = await (await fetch(apiUrl)).json();

        document.findAll('.faction-info-wrap .members-list .table-body > li').forEach(row => {
            const profileID = parseInt(row.find("a[href*='profiles.php']").getAttribute("href").split("XID=")[1]);
            const member = data.members?.[profileID];
            if (!member) return;

            const rel = member.last_action.relative;
            const ts = member.last_action.timestamp;
            const days = Math.floor((Date.now() / 1000 - ts) / 86400);

            let color;
            activityHighlights.forEach(rule => {
                if (rule[0] <= days && days <= rule[1]) color = rule[2];
            });

            row.setAttribute('data-last-action', rel);
            if (color) row.style.backgroundColor = color;
        });
    }

    document.head.insertAdjacentHTML('beforeend', `
        <style>
            .faction-info-wrap .members-list .table-body > li::after {
                display: block;
                content: attr(data-last-action);
                font-size: 11px;
                color: ${isDarkMode() ? '#bbb' : '#444'};
                margin-left: 8px;
            }
        </style>`);

    function monitorFactionPage() {
        if (window.location.href.includes('factions.php')) {
            showFactionLastAction();
        }
    }

    let lastURL = location.href;
    new MutationObserver(() => {
        const currentURL = location.href;
        if (currentURL !== lastURL) {
            lastURL = currentURL;
            monitorFactionPage();
        }
    }).observe(document, { subtree: true, childList: true });

    setInterval(() => {
        if (document.querySelector('.faction-info-wrap .members-list .table-body')) {
            showFactionLastAction();
        }
    }, 15000);

    console.log('%c[Torn Utilities]', 'color:#4CAF50', 'Loaded');

    //////////////////////////////////////////////////////////////////////
    // PART 2 — TORN: MORE LEGIBLE PLAYER NAMES
    //////////////////////////////////////////////////////////////////////

    const fontLink = document.createElement('link');
    fontLink.href = 'https://fonts.googleapis.com/css2?family=Manrope:wght@700&display=swap';
    fontLink.rel = 'stylesheet';
    document.head.appendChild(fontLink);

    const style = document.createElement('style');
    style.textContent = `
        .custom-honor-text {
            font-family: 'Manrope', sans-serif !important;
            font-weight: 700 !important;
            font-size: 12px !important;
            color: white !important;
            text-transform: uppercase !important;
            letter-spacing: 0.5px !important;
            pointer-events: none !important;
            position: absolute !important;
            top: 50%;
            left: 0;
            transform: translateY(-50%);
            width: 100% !important;
            display: flex !important;
            align-items: center;
            justify-content: center;
            z-index: 10 !important;
        }
        .honor-text-svg { display: none !important; }
        .outline-black { text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000 !important; }
        .outline-blue { text-shadow: -1px -1px 0 #310AF5, 1px -1px 0 #310AF5, -1px 1px 0 #310AF5, 1px 1px 0 #310AF5 !important; }
        .outline-red { text-shadow: -1px -1px 0 #ff4d4d, 1px -1px 0 #ff4d4d, -1px 1px 0 #ff4d4d, 1px 1px 0 #ff4d4d !important; }
        .outline-green { text-shadow: -1px -1px 0 #3B9932, 1px -1px 0 #3B9932, -1px 1px 0 #3B9932, 1px 1px 0 #3B9932 !important; }
        .outline-orange { text-shadow: -1px -1px 0 #ff9c40, 1px -1px 0 #ff9c40, -1px 1px 0 #ff9c40, 1px 1px 0 #ff9c40 !important; }
        .outline-purple { text-shadow: -1px -1px 0 #c080ff, 1px -1px 0 #c080ff, -1px 1px 0 #c080ff, 1px 1px 0 #c080ff !important; }
    `;
    document.head.appendChild(style);

    function getOutlineClass(wrap) {
        if (wrap.classList.contains('admin')) return 'outline-red';
        if (wrap.classList.contains('officer')) return 'outline-green';
        if (wrap.classList.contains('moderator')) return 'outline-orange';
        if (wrap.classList.contains('helper')) return 'outline-purple';
        if (wrap.classList.contains('blue')) return 'outline-blue';
        return 'outline-black';
    }

    function replaceHonorText() {
        document.querySelectorAll('.honor-text-wrap').forEach(wrap => {
            const sprite = wrap.querySelector('.honor-text-svg');
            const existing = wrap.querySelector('.custom-honor-text');
            if (sprite) sprite.style.display = 'none';
            if (existing) return;
            const text = wrap.getAttribute('data-title') || wrap.getAttribute('aria-label') || wrap.innerText || '';
            const cleaned = text.trim().toUpperCase();
            if (!cleaned) return;
            const div = document.createElement('div');
            div.className = `custom-honor-text ${getOutlineClass(wrap)}`;
            div.textContent = cleaned;
            wrap.appendChild(div);
        });
    }

    replaceHonorText();
    new MutationObserver(replaceHonorText).observe(document.body, { childList: true, subtree: true });

    console.log('%c[Legible Names]', 'color:#1E90FF', 'Loaded');
})();