Torn FF in chat

Shows FFScouter FF values next to chat names

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

Advertisement:

// ==UserScript==
// @name         Torn FF in chat
// @namespace    https://torn.com/
// @version      1
// @description  Shows FFScouter FF values next to chat names
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      ffscouter.com
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// ==/UserScript==

(function() {
    'use strict';

    GM_registerMenuCommand("Set FFScouter API Key", () => {

        const current = GM_getValue("ff_api_key", "");

        const key = prompt(
            "Enter your FFScouter API key:",
            current
        );

        if (key !== null) {
            GM_setValue("ff_api_key", key.trim());
            API_KEY = key.trim();
            console.log("FFScouter API key updated");
        }
    });

    let API_KEY = GM_getValue("ff_api_key", "");

    const CACHE_TIME = 60 * 60 * 1000; // 1 hour

    const ffCache = new Map();
    const pending = new Set();
    const userElements = new Map();

    // ---------------------------
    // CACHE HELPERS
    // ---------------------------

    function getCachedFF(xid) {
        const entry = ffCache.get(xid);
        if (!entry) return null;

        const now = Date.now();

        if (now - entry.time > CACHE_TIME) {
            ffCache.delete(xid);
            pending.add(xid); // re-fetch expired
            return null;
        }

        return entry.ff;
    }

    function saveCache() {
        const obj = {};
        for (const [k, v] of ffCache.entries()) {
            obj[k] = v;
        }
        localStorage.setItem("ff_cache", JSON.stringify(obj));
    }

    function loadCache() {
        try {
            const raw = localStorage.getItem("ff_cache");
            if (!raw) return;

            const data = JSON.parse(raw);
            const now = Date.now();

            for (const [k, v] of Object.entries(data)) {
                if (now - v.time < CACHE_TIME) {
                    ffCache.set(k, v);
                }
            }

        } catch (e) {
            console.error("Cache load failed", e);
        }
    }

    loadCache();

    // ---------------------------
    // UI
    // ---------------------------

    function getFFColor(ff) {

        if (ff == null) return "#666";

        const min = 1;
        const max = 5;

        let t = (ff - min) / (max - min);
        t = Math.max(0, Math.min(1, t));

        // darker, more readable hue range:
        // 230 (blue) → 120 (dark green) → 45 (amber) → 0 (red)
        const hue = 230 - (t * 230);

        return `hsl(${hue}, 70%, 35%)`;
    }

    function applyFF(el, ff) {

        if (el.dataset.ffApplied) return;

        const originalName = el.textContent;

        el.textContent = `[${ff.toFixed(2)}] ${originalName}`;

        el.style.backgroundColor = getFFColor(ff);
        el.style.borderRadius = '6px';
        el.style.padding = '0px 4px';
        el.style.fontWeight = '600';
        el.style.fontSize = '11px';
        el.style.lineHeight = '1.1';
        el.style.display = 'inline-block';

        el.title = `Fair Fight: ${ff.toFixed(2)}`;

        el.dataset.ffApplied = '1';
    }

    function updatePlayer(xid, ff) {

        const elements = userElements.get(xid);
        if (!elements) return;

        elements.forEach(el => applyFF(el, ff));
    }

    // ---------------------------
    // API
    // ---------------------------

    function fetchFF(ids) {

        if (!API_KEY) return;

        GM_xmlhttpRequest({
            method: 'GET',
            url:
                `https://ffscouter.com/api/v1/get-stats` +
                `?key=${API_KEY}` +
                `&targets=${ids.join(',')}`,

            onload: function(response) {

                try {

                    const data = JSON.parse(response.responseText);

                    for (const player of data) {

                        const xid = String(player.player_id);
                        const ff = player.fair_fight;

                        ffCache.set(xid, {
                            ff: ff,
                            time: Date.now()
                        });

                        updatePlayer(xid, ff);
                    }

                    saveCache();

                } catch (e) {
                    console.error('FF parse error', e);
                }
            },

            onerror: function(err) {
                console.error('FF request failed', err);
            }
        });
    }

    // ---------------------------
    // CHAT SCAN
    // ---------------------------

    function scanChat() {

        const users = document.querySelectorAll(
            '.senderContainer___qpYVw a.sender___Y2urd'
        );

        users.forEach(el => {

            const href = el.getAttribute('href');
            const match = href?.match(/XID=(\d+)/);
            if (!match) return;

            const xid = match[1];

            if (!userElements.has(xid)) {
                userElements.set(xid, []);
            }

            const list = userElements.get(xid);

            if (!list.includes(el)) {
                list.push(el);
            }

            const cached = getCachedFF(xid);

            if (cached !== null) {
                updatePlayer(xid, cached);
            } else {
                pending.add(xid);
            }
        });
    }

    setInterval(scanChat, 2000);

    setInterval(() => {

        if (!pending.size) return;

        const ids = [...pending].slice(0, 200);
        ids.forEach(id => pending.delete(id));

        fetchFF(ids);

    }, 3000);

    console.log('FF Highlighter Loaded');

})();