Torn Racing Tracker (FINAL FIXED)

Stable racing tracker with pagination + TCT + names + toggle

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();