Stable racing tracker with pagination + TCT + names + toggle
// ==UserScript==
// @name Torn Racing Tracker (FINAL FIXED)
// @namespace torn-racing
// @version 6.0
// @description Stable racing tracker with pagination + TCT + names + toggle
// @match https://www.torn.com/page.php?sid=racing*
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
const API_KEY = 'API HERE';
const STORAGE_KEY = 'tornRaceTrackerFinalV6';
const NAME_CACHE_KEY = 'tornRaceNamesV6';
const SIZE_KEY = 'tornRaceSizeV6';
const MAX_RACES = 200;
// ---------------- SAFE STORAGE ----------------
function load(key, fallback) {
try {
const v = GM_getValue(key);
return v ? JSON.parse(v) : fallback;
} catch {
return fallback;
}
}
function save(key, value) {
try {
GM_setValue(key, JSON.stringify(value));
} catch (e) {
console.error("Save failed:", e);
}
}
// ---------------- TIME HELPERS ----------------
function dateKey(ts) {
return new Date(ts * 1000).toISOString().slice(0, 10);
}
function todayKey() {
return new Date().toISOString().slice(0, 10);
}
function yesterdayKey() {
const d = new Date();
d.setUTCDate(d.getUTCDate() - 1);
return d.toISOString().slice(0, 10);
}
function timeOnly(ts) {
return new Date(ts * 1000).toISOString().slice(11, 16);
}
// ---------------- API ----------------
async function api(url) {
const r = await fetch(url);
return await r.json();
}
async function getMyId() {
const j = await api(`https://api.torn.com/v2/user?selections=basic&key=${API_KEY}`);
return j?.profile?.id;
}
async function getName(id, cache) {
if (cache[id]) return cache[id];
try {
const j = await api(`https://api.torn.com/v2/user/${id}/basic?key=${API_KEY}`);
const name = j?.profile?.name || `#${id}`;
cache[id] = name;
save(NAME_CACHE_KEY, cache);
return name;
} catch {
return `#${id}`;
}
}
// ---------------- PAGINATION (IMPORTANT FIX) ----------------
async function getRaces() {
let all = [];
let seen = new Set();
let url =
`https://api.torn.com/v2/user/races?sort=desc&limit=20&key=${API_KEY}`;
while (url && all.length < MAX_RACES) {
const data = await api(url);
if (!data.races?.length) break;
for (const r of data.races) {
if (seen.has(r.id)) continue;
seen.add(r.id);
all.push(r);
}
let prev = data._metadata?.links?.prev;
if (!prev) break;
if (!prev.includes('key=')) {
prev += (prev.includes('?') ? '&' : '?') + `key=${API_KEY}`;
}
url = prev;
}
return all.slice(0, MAX_RACES);
}
// ---------------- CORE LOGIC ----------------
function emptyStats() {
return {
wins: 0,
losses: 0,
beaten: {},
lostTo: {}
};
}
function add(map, name, time) {
if (!map[name]) map[name] = [];
map[name].push(time);
}
async function rebuild() {
const myId = await getMyId();
const nameCache = load(NAME_CACHE_KEY, {});
const races = await getRaces();
const stats = {
today: emptyStats(),
yesterday: emptyStats()
};
const t = todayKey();
const y = yesterdayKey();
for (const r of races) {
const day = dateKey(r.schedule.end);
if (day !== t && day !== y) continue;
const me = r.results.find(x => x.driver_id === myId);
if (!me) continue;
const bucket = day === t ? stats.today : stats.yesterday;
const time = timeOnly(r.schedule.end);
if (me.position === 1) {
bucket.wins++;
for (const opp of r.results) {
if (opp.driver_id === myId) continue;
const name = await getName(opp.driver_id, nameCache);
add(bucket.beaten, name, time);
}
} else {
bucket.losses++;
const winner = r.results.find(x => x.position === 1);
if (winner) {
const name = await getName(winner.driver_id, nameCache);
add(bucket.lostTo, name, time);
}
}
}
render(stats);
}
// ---------------- UI ----------------
function render(stats) {
let el = document.getElementById('racingTrackerFinal');
if (!el) {
el = document.createElement('div');
el.id = 'racingTrackerFinal';
document.body.appendChild(el);
}
const size = load(SIZE_KEY, 'small');
el.style.cssText = `
position:fixed;
top:120px;
right:15px;
width:${size === 'large' ? '420px' : '260px'};
background:#111;
color:#fff;
padding:10px;
font-size:12px;
border:1px solid #444;
border-radius:8px;
z-index:999999;
max-height:${size === 'large' ? '800px' : '220px'};
overflow:auto;
font-family:Arial;
`;
const fmt = (obj) =>
Object.entries(obj)
.map(([n, t]) => `${n} (${t.length}) → ${t.join(', ')}`)
.join('<br>') || 'None';
el.innerHTML = `
<div style="display:flex;justify-content:space-between;">
<b>🏁 Racing Tracker</b>
<button id="toggleSize">
${size === 'large' ? 'Small' : 'Large'}
</button>
</div>
<div style="font-size:10px;color:#aaa;">
TCT: ${new Date().toUTCString()}
</div>
<hr>
<b>TODAY</b><br>
Wins: ${stats.today.wins}<br>
Losses: ${stats.today.losses}<br><br>
${size === 'large' ? `
<b>Beaten</b><br>${fmt(stats.today.beaten)}<br><br>
<b>Lost To</b><br>${fmt(stats.today.lostTo)}
<hr>
<b>YESTERDAY</b><br>
Wins: ${stats.yesterday.wins}<br>
Losses: ${stats.yesterday.losses}<br><br>
<b>Beaten</b><br>${fmt(stats.yesterday.beaten)}<br><br>
<b>Lost To</b><br>${fmt(stats.yesterday.lostTo)}
` : ''}
`;
document.getElementById('toggleSize').onclick = () => {
save(SIZE_KEY, size === 'large' ? 'small' : 'large');
rebuild();
};
}
// ---------------- START ----------------
rebuild();
setInterval(() => {
rebuild().catch(console.error);
}, 60000);
})();