Torn FF in chat

Shows FFScouter FF values next to chat names

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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

})();