Torn Racing Tracker (FINAL FIXED)

Stable racing tracker with pagination + TCT + names + toggle

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();