Torn Racing Tracker (FINAL FIXED)

Stable racing tracker with pagination + TCT + names + toggle

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Advertisement:

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();