Shows FFScouter FF values next to chat names
// ==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');
})();