Torn Racing Tracker (FINAL FIXED)

Stable racing tracker with pagination + TCT + names + toggle

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();