Torn FF in chat

Shows FFScouter FF values next to chat names

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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');

})();