Torn FF in chat

Shows FFScouter FF values next to chat names

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();