Torn Racing Tracker (FINAL FIXED)

Stable racing tracker with pagination + TCT + names + toggle

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

Advertisement:

// ==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);

})();