Torn FF in chat

Shows FFScouter FF values next to chat names

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Advertisement:

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();