Torn Racing Telemetry

Enhances Torn Racing with real-time telemetry, race stats history, and recent race logs.

目前為 2025-02-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Torn Racing Telemetry
// @namespace    https://www.torn.com/profiles.php?XID=2782979
// @version      2.4.1
// @description  Enhances Torn Racing with real-time telemetry, race stats history, and recent race logs.
// @match        https://www.torn.com/page.php?sid=racing*
// @match        https://www.torn.com/loader.php?sid=racing*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_connect
// @connect      api.torn.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.racingTelemetryScriptHasRun) return;
    window.racingTelemetryScriptHasRun = true;

    const defaultConfig = {
        displayMode: 'speed',
        colorCode: true,
        animateChanges: true,
        speedUnit: 'mph',
        periodicCheckInterval: 1000, // Hardcoded to 1 second (1000ms)
        minUpdateInterval: 500,
        language: 'en',
        apiKey: ''
    };

    // Load config and log it with JSON parse fix
    let config = Object.assign({}, defaultConfig, (() => {
        const savedConfig = GM_getValue('racingTelemetryConfig', null);
        if (savedConfig) {
            try {
                return JSON.parse(savedConfig);
            } catch (e) {
                console.error("Error parsing saved config, using default:", e);
                return {}; // Parsing failed, return empty object to avoid errors
            }
        }
        return {}; // No saved config found, return empty object to merge with defaultConfig
    })());

    let currentRaceId = null;
    let raceStarted = false;
    let periodicCheckIntervalId = null;
    let telemetryVisible = true;
    let observers = [];
    let lastUpdateTimes = {};

    const state = {
        previousMetrics: {},
        trackInfo: { total: 0, laps: 0, length: 0 },
        racingStats: null,
        statsHistory: [],
        raceLog: []
    };

    (function initStorage() {
        let savedStatsHistory = GM_getValue('statsHistory', []);
        if (!Array.isArray(savedStatsHistory)) savedStatsHistory = [];
        state.statsHistory = savedStatsHistory.filter(entry => entry && entry.timestamp && entry.skill).slice(0, 50);

        const savedRaces = GM_getValue('raceLog', []);
        state.raceLog = (Array.isArray(savedRaces) ? savedRaces.filter(r => r?.id).slice(0, 50) : []);
    })();

    const utils = {
        convertSpeed(speed, unit) {
            return unit === 'kmh' ? speed * 1.60934 : speed;
        },
        formatTime(seconds) {
            const minutes = Math.floor(seconds / 60);
            const secs = Math.floor(seconds % 60);
            return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        },
        parseTime(timeString) {
            if (!timeString || !timeString.includes(':')) return 0;
            return timeString.split(':').reduce((acc, val, idx) => acc + (parseFloat(val) * Math.pow(60, idx)), 0);
        },
        parseProgress(text) {
            const match = text.match(/(\d+\.?\d*)%/);
            return match ? parseFloat(match[1]) : 0;
        },
        displayError(message) {
            const errorDiv = document.createElement('div');
            errorDiv.style.cssText = 'position: fixed; bottom: 10px; right: 10px; background: #f44336; color: #fff; padding: 10px; border-radius: 5px;';
            errorDiv.textContent = message;
            document.body.appendChild(errorDiv);
            setTimeout(() => errorDiv.remove(), 5000);
        },
        fetchWithRetry: async (url, retries = 3) => {
            try {
                const response = await fetch(url);
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                return response.json();
            } catch (e) {
                if (retries > 0) return utils.fetchWithRetry(url, retries - 1);
                throw e;
            }
        },
        formatTimestamp: (timestamp) => {
            let date;
            if (typeof timestamp === 'number') {
                date = new Date(timestamp);
            } else if (typeof timestamp === 'string') {
                date = new Date(timestamp);
            } else {
                return 'N/A';
            }

            if (isNaN(date)) {
                console.error("Invalid date object created from timestamp:", timestamp);
                return 'Invalid Date';
            }

            return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
        }
    };

    const dataManager = {
        async fetchRacingStats() {
            if (!config.apiKey?.trim()) return;
            try {
                const data = await utils.fetchWithRetry(
                    `https://api.torn.com/v2/user/personalstats?cat=racing&key=${config.apiKey}`
                );
                if (data.error) throw new Error(data.error.error);
                this.processStats(data.personalstats.racing);
            } catch (e) {
                console.error('Stats fetch failed:', e);
                utils.displayError(`API Error: ${e.message}`);
            }
        },
        processStats(newStats) {
            const oldStats = state.racingStats?.racing;
            const changes = {
                timestamp: Date.now(),
                skill: { old: oldStats?.skill || 0, new: newStats.skill },
                points: { old: oldStats?.points || 0, new: newStats.points },
                racesEntered: { old: oldStats?.races?.entered || 0, new: newStats.races?.entered },
                racesWon: { old: oldStats?.races?.won || 0, new: newStats.races?.won }
            };

            if (!oldStats || JSON.stringify(changes) !== JSON.stringify(oldStats)) {
                state.statsHistory.unshift(changes);
                state.statsHistory = state.statsHistory.slice(0, 50);
                GM_setValue('statsHistory', state.statsHistory);
            }
            state.racingStats = { racing: newStats };
            GM_setValue('racingStats', JSON.stringify(state.racingStats));
            const statsPanelContent = document.querySelector('.stats-history .history-content');
            if (statsPanelContent) {
                statsPanelContent.innerHTML = generateStatsContent();
            }
        },
        async fetchRaces() {
            if (!config.apiKey?.trim()) return;
            try {
                const data = await utils.fetchWithRetry(
                    `https://api.torn.com/v2/user/races?key=${config.apiKey}&limit=10&sort=DESC&cat=official`
                );
                data.races?.forEach(race => this.processRace(race));
                GM_setValue('raceLog', state.raceLog);
                 const racesPanelContent = document.querySelector('.recent-races .races-content');
                 if (racesPanelContent) {
                     racesPanelContent.innerHTML = generateRacesContent();
                 }
            } catch (e) {
                console.error('Race fetch failed:', e);
                utils.displayError(`Race data error: ${e.message}`);
            }
        },
        processRace(race) {
            const exists = state.raceLog.some(r => r.id === race.id);
            if (!exists && race?.id) {
                state.raceLog.unshift({ ...race, fetchedAt: Date.now() });
                state.raceLog = state.raceLog.slice(0, 50);
            }
        }
    };


    GM_addStyle(`
        :root {
            --primary-color: #4CAF50;
            --background-dark: #1a1a1a;
            --background-light: #2a2a2a;
            --text-color: #e0e0e0;
            --border-color: #404040;
        }
        .telemetry-settings, .stats-history, .recent-races {
            background: var(--background-dark);
            border: 1px solid var(--border-color);
            border-radius: 8px;
            margin: 15px;
            padding: 15px;
            color: var(--text-color);
            font-family: 'Arial', sans-serif;
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            /* Removed max-height and overflow-y from panel styles */
        }
        #leaderBoard .driver-item .name {
            display: flex !important;
            align-items: center;
            min-width: 0;
            padding-right: 10px !important;
        }
        #leaderBoard .driver-item .name > span:first-child {
            flex: 1 1 auto;
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            padding-right: 8px;
        }
        .driver-status {
            flex: 0 0 auto;
            margin-left: auto;
            font-size: 11px;
            background: rgba(0,0,0,0.3);
            padding: 2px 6px;
            border-radius: 3px;
            white-space: nowrap;
            transition: color ${config.minUpdateInterval / 1000}s ease, opacity 0.3s ease;
        }
        .telemetry-hidden .driver-status { display: none; }
        @media (max-width: 480px) {
            #leaderBoard .driver-item .name {
                padding-right: 5px !important;
            }
            .driver-status {
                font-size: 10px;
                padding: 1px 4px;
                margin-left: 4px;
            }
            .telemetry-settings, .stats-history, .recent-races {
                margin: 8px;
                padding: 12px;
                /* Removed max-height and overflow-y from panel styles */
            }
            .settings-title, .history-title, .races-title {
                font-size: 14px;
            }
        }
        .settings-header, .history-header, .races-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: #252525;
            padding: 8px;
            border-radius: 6px;
        }
        .settings-title, .history-title, .races-title {
            font-size: 16px;
            font-weight: bold;
            color: var(--primary-color);
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .toggle-btn, .reset-btn, .toggle-telemetry-btn, .copy-btn { /* Keep existing styles for other buttons */
            background: #333;
            border: 1px solid var(--border-color);
            border-radius: 5px;
            color: #fff;
            padding: 6px 12px;
            cursor: pointer;
            transition: all 0.2s;
            font-size: 12px;
        }
        .reset-system-btn { /* New style for Reset System Button */
            background: #f44336; /* Red background */
            border: 1px solid var(--border-color); /* Inherit border */
            border-radius: 5px; /* Inherit border-radius */
            color: #fff; /* Inherit text color */
            padding: 6px 12px; /* Inherit padding */
            cursor: pointer; /* Inherit cursor */
            transition: all 0.2s; /* Inherit transition */
            font-size: 12px; /* Inherit font-size */
        }
        .settings-content, .history-content, .races-content {
            display: grid;
            gap: 12px;
            padding: 10px;
            background: #202020;
            border-radius: 6px;
            max-height: 300px; /* Set max height for content area */
            overflow-y: auto; /* Enable vertical scrolling */
        }
        .setting-group {
            display: grid;
            gap: 8px;
        }
        .setting-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px;
            background: var(--background-light);
            border-radius: 4px;
        }
        .switch {
            position: relative;
            width: 40px;
            height: 22px;
        }
        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }
        .slider {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: #404040;
            transition: .2s;
            border-radius: 11px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 18px;
            width: 18px;
            left: 2px;
            bottom: 2px;
            background: #fff;
            transition: .2s;
            border-radius: 50%;
        }
        input:checked + .slider {
            background: var(--primary-color);
        }
        input:checked + .slider:before {
            transform: translateX(18px);
        }
        .radio-group {
            display: flex;
            gap: 10px;
            padding: 8px;
            background: #252525;
            border-radius: 6px;
        }
        .radio-item {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 12px;
            background: #333;
            border-radius: 4px;
            cursor: pointer;
        }
        .api-key-input {
            width: 150px;
            padding: 5px;
            border-radius: 4px;
            border: 1px solid var(--border-color);
            background: var(--background-light);
            color: var(--text-color);
        }
        .history-entry, .race-entry {
            padding: 10px;
            background: var(--background-light);
            border-radius: 4px;
            white-space: pre-wrap;
        }
        .current-stats {
            padding: 10px;
            background: var(--background-light);
            border-radius: 4px;
            margin-bottom: 10px;
        }
    `);

    // Easing function: easeInOutQuad ramps up then down.
    function easeInOutQuad(t) {
        return t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
    }

    // Helper to interpolate between two colors.
    function interpolateColor(color1, color2, factor) {
        const result = color1.map((c, i) => Math.round(c + factor * (color2[i] - c)));
        return `rgb(${result[0]}, ${result[1]}, ${result[2]})`;
    }

    // Returns a color based on acceleration (in g's). If color coding is disabled, always return grey.
    function getTelemetryColor(acceleration) {
        const grey = [136, 136, 136];
        if (!config.colorCode) return `rgb(${grey[0]}, ${grey[1]}, ${grey[2]})`;
        const green = [76, 175, 80];
        const red = [244, 67, 54];
        const maxAcc = 1.0;
        let factor = Math.min(Math.abs(acceleration) / maxAcc, 1);
        if (acceleration > 0) {
            return interpolateColor(grey, green, factor);
        } else if (acceleration < 0) {
            return interpolateColor(grey, red, factor);
        } else {
            return `rgb(${grey[0]}, ${grey[1]}, ${grey[2]})`;
        }
    }

    // Animate telemetry text using an easeInOut function over a dynamic duration.
    function animateTelemetry(element, fromSpeed, toSpeed, fromAcc, toAcc, duration, displayMode, speedUnit, extraText) {
        let startTime = null;
        function step(timestamp) {
            if (!startTime) startTime = timestamp;
            let linearProgress = (timestamp - startTime) / duration;
            if (linearProgress > 1) linearProgress = 1;
            let progress = easeInOutQuad(linearProgress);
            let currentSpeed = fromSpeed + (toSpeed - fromSpeed) * progress;
            let currentAcc = fromAcc + (toAcc - fromAcc) * progress;
            element._currentSpeed = currentSpeed;
            element._currentAcc = currentAcc;
            let color = config.colorCode ? getTelemetryColor(currentAcc) : 'rgb(136, 136, 136)';
            let text;
            if (displayMode === 'speed') {
                text = `${Math.round(currentSpeed)} ${speedUnit}`;
            } else if (displayMode === 'acceleration') {
                text = `${currentAcc.toFixed(1)} g`;
            } else {
                text = `${Math.round(currentSpeed)} ${speedUnit} | ${currentAcc.toFixed(1)} g`;
            }
            text += extraText;
            element.textContent = text;
            element.style.color = color;
            if (linearProgress < 1) {
                element._telemetryAnimationFrame = requestAnimationFrame(step);
            } else {
                element._telemetryAnimationFrame = null;
            }
        }
        element._telemetryAnimationFrame = requestAnimationFrame(step);
    }

    // Function to update the settings UI with the current config
    function updateSettingsUI(panel) {
        panel.querySelectorAll('input, select').forEach(el => {
            if (el.type === 'checkbox') el.checked = config[el.dataset.setting];
            else if (el.type === 'radio') el.checked = config[el.name] === el.value;
            else if (el.type === 'number') el.value = config[el.dataset.setting];
            else if (el.classList.contains('api-key-input')) el.value = config[el.dataset.setting];
        });
    }

    // Create the telemetry settings panel.
    function createSettingsPanel() {
        const panel = document.createElement('div');
        panel.className = 'telemetry-settings';
        const isOpen = localStorage.getItem('telemetrySettingsOpen') === 'true';
        panel.innerHTML = `
            <div class="settings-header">
                <span class="settings-title">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="var(--primary-color)">
                        <path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.50.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l-.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64L19.43 13Z"/>
                    </svg>
                    RACING TELEMETRY
                </span>
                <button class="toggle-btn">${isOpen ? '▲' : '▼'}</button>
            </div>
            <div class="settings-content" style="display: ${isOpen ? 'grid' : 'none'}">
                <div class="setting-group">
                    <div class="setting-item" title="Select data to display">
                        <span>Display Mode</span>
                        <div class="radio-group">
                            <label class="radio-item"><input type="radio" name="displayMode" value="speed" ${config.displayMode === 'speed' ? 'checked' : ''}>Speed</label>
                            <label class="radio-item"><input type="radio" name="displayMode" value="acceleration" ${config.displayMode === 'acceleration' ? 'checked' : ''}>Acceleration</label>
                            <label class="radio-item"><input type="radio" name="displayMode" value="both" ${config.displayMode === 'both' ? 'checked' : ''}>Both</label>
                        </div>
                    </div>
                    <div class="setting-item" title="Color-code acceleration status">
                        <span>Color Coding</span>
                        <label class="switch"><input type="checkbox" ${config.colorCode ? 'checked' : ''} data-setting="colorCode"><span class="slider"></span></label>
                    </div>
                    <div class="setting-item" title="Enable smooth animations">
                        <span>Animations</span>
                        <label class="switch"><input type="checkbox" ${config.animateChanges ? 'checked' : ''} data-setting="animateChanges"><span class="slider"></span></label>
                    </div>
                    <div class="setting-item" title="Speed unit preference">
                        <span>Speed Unit</span>
                        <div class="radio-group">
                            <label class="radio-item"><input type="radio" name="speedUnit" value="mph" ${config.speedUnit === 'mph' ? 'checked' : ''}>mph</label>
                            <label class="radio-item"><input type="radio" name="speedUnit" value="kmh" ${config.speedUnit === 'kmh' ? 'checked' : ''}>km/h</label>
                        </div>
                    </div>
                </div>
                <div class="setting-group">
                    <div class="setting-item" title="Your Torn API key for fetching race data">
                        <span>API Key</span>
                        <input type="password" class="api-key-input" value="${config.apiKey}" data-setting="apiKey" placeholder="Enter API Key">
                    </div>
                </div>
                <button class="reset-system-btn">Reset System</button> <button class="copy-btn" style="margin-top: 10px;">Copy HTML</button>
                <button class="toggle-telemetry-btn" style="margin-top: 10px;">${telemetryVisible ? 'Hide Telemetry' : 'Show Telemetry'}</button>
            </div>
        `;

        const toggleButton = panel.querySelector('.toggle-btn');
        if (toggleButton) {
            toggleButton.addEventListener('click', () => {
                const content = panel.querySelector('.settings-content');
                const isVisible = content.style.display === 'grid';
                content.style.display = isVisible ? 'none' : 'grid';
                panel.querySelector('.toggle-btn').textContent = isVisible ? '▼' : '▲';
                localStorage.setItem('telemetrySettingsOpen', !isVisible);
            });
        }

        const resetButton = panel.querySelector('.reset-system-btn');
        if (resetButton) {
            resetButton.addEventListener('click', () => {
                const apiKey = config.apiKey;
                localStorage.clear();
                localStorage.setItem('racingTelemetryConfig', JSON.stringify({ apiKey: apiKey }));
                window.location.reload();
            });
        }

        const settingsInputs = panel.querySelectorAll('input, select');
        settingsInputs.forEach(el => {
            if (el) {
                el.addEventListener('change', () => {
                    if (el.type === 'checkbox') config[el.dataset.setting] = el.checked;
                    else if (el.type === 'radio') config[el.name] = el.value;
                    else if (el.type === 'number') config[el.dataset.setting] = parseInt(el.value, 10);
                    else if (el.classList.contains('api-key-input')) config[el.dataset.setting] = el.value;

                    GM_setValue('racingTelemetryConfig', JSON.stringify(config));

                    if (el.dataset.setting === 'apiKey') {
                        dataManager.fetchRacingStats().then(dataManager.fetchRaces.bind(dataManager));
                    }
                });
            }
        });

        const copyButton = panel.querySelector('.copy-btn');
        if (copyButton) {
            copyButton.addEventListener('click', () => {
                const container = document.getElementById('racingMainContainer');
                if (container) {
                    navigator.clipboard.writeText(container.outerHTML)
                        .then(() => alert('HTML copied to clipboard!'))
                        .catch(err => alert('Failed to copy HTML: ' + err));
                } else {
                    alert('racingMainContainer not found.');
                }
            });
        }

        const telemetryToggleButton = panel.querySelector('.toggle-telemetry-btn');
        if (telemetryToggleButton) {
            telemetryToggleButton.addEventListener('click', () => {
                telemetryVisible = !telemetryVisible;
                document.body.classList.toggle('telemetry-hidden', !telemetryVisible);
                telemetryToggleButton.textContent = telemetryVisible ? 'Hide Telemetry' : 'Show Telemetry';
            });
        }

        return panel;
    }

    // Create stats history panel
    function createStatsPanel() {
        const panel = document.createElement('div');
        panel.className = 'stats-history';
        const isOpen = localStorage.getItem('statsHistoryOpen') === 'true';

        panel.innerHTML = `
            <div class="history-header">
                <span class="history-title">PERSONAL STATS HISTORY</span>
                <button class="toggle-btn">${isOpen ? '▲' : '▼'}</button>
            </div>
            <div class="history-content" style="display: ${isOpen ? 'grid' : 'none'}">
                ${generateStatsContent()}
            </div>
        `;

        panel.querySelector('.toggle-btn').addEventListener('click', () => {
            const content = panel.querySelector('.history-content');
            const isVisible = content.style.display === 'grid';
            content.style.display = isVisible ? 'none' : 'grid';
            panel.querySelector('.toggle-btn').textContent = isVisible ? '▼' : '▲';
            localStorage.setItem('statsHistoryOpen', !isVisible);
        });

        return panel;
    }

    // Generate content for stats history panel
    function generateStatsContent() {
        let content = '';
        if (state.racingStats?.racing) {
            const currentRacingStats = state.racingStats.racing;
            content += `
                <div class="current-stats">
                    <h3>Current Racing Stats</h3>
                    <p>Skill: ${currentRacingStats.skill || 'N/A'}</p>
                    <p>Points: ${currentRacingStats.points || 'N/A'}</p>
                    <p>Races Entered: ${currentRacingStats.races?.entered || 'N/A'}</p>
                    <p>Races Won: ${currentRacingStats.races?.won || 'N/A'}</p>
                </div>
            `;
        }

        content += state.statsHistory.map(entry => {
            return `
                <div class="history-entry">
                    Time: ${utils.formatTimestamp(entry.timestamp)}
                    Skill: ${entry.skill.old} → ${entry.skill.new}
                    Points: ${entry.points.old} → ${entry.points.new}
                    Races Entered: ${entry.racesEntered.old} → ${entry.racesEntered.new}
                    Races Won: ${entry.racesWon.old} → ${entry.racesWon.new}
                </div>
            `;
        }).join('');

        return content || '<div class="history-entry">No stats history available</div>';
    }

    // Create recent races panel
    function createRacesPanel() {
        const panel = document.createElement('div');
        panel.className = 'recent-races';
        const isOpen = localStorage.getItem('recentRacesOpen') === 'true';

        panel.innerHTML = `
            <div class="races-header">
                <span class="races-title">RECENT RACES (LAST 10)</span>
                <button class="toggle-btn">${isOpen ? '▲' : '▼'}</button>
            </div>
            <div class="races-content" style="display: ${isOpen ? 'grid' : 'none'}">
                ${generateRacesContent()}
            </div>
        `;

        panel.querySelector('.toggle-btn').addEventListener('click', () => {
            const content = panel.querySelector('.races-content');
            const isVisible = content.style.display === 'grid';
            content.style.display = isVisible ? 'none' : 'grid';
            panel.querySelector('.toggle-btn').textContent = isVisible ? '▼' : '▲';
            localStorage.setItem('recentRacesOpen', !isVisible);
        });

        return panel;
    }

    // Generate content for recent races panel
    function generateRacesContent() {
        state.raceLog.sort((a, b) => {
            const startTimeA = a.schedule?.start ? new Date(a.schedule.start).getTime() : 0;
            const startTimeB = b.schedule?.start ? new Date(b.schedule.start).getTime() : 0;
            return startTimeB - startTimeA;
        });

        return state.raceLog.map(race => {
            return `
                <div class="race-entry">
                    ID: ${race.id || 'N/A'}
                    Title: ${race.title || 'N/A'}
                    Track ID: ${race.track_id || 'N/A'}
                    Status: ${race.status || 'N/A'}
                    Laps: ${race.laps || 'N/A'}
                    Participants: ${race.participants?.current || 0}/${race.participants?.maximum || 0}
                    Start Time: ${race.schedule?.start ? utils.formatTimestamp(race.schedule.start) : 'N/A'}
                </div>
            `;
        }).join('') || '<div class="race-entry">No recent races found</div>';
    }


    function calculateDriverMetrics(driverId, progressPercentage, timestamp) {
        const prev = state.previousMetrics[driverId] || {
            progress: progressPercentage,
            time: timestamp,
            instantaneousSpeed: 0,
            reportedSpeed: 0,
            acceleration: 0,
            lastDisplayedSpeed: 0,
            lastDisplayedAcceleration: 0,
            firstUpdate: true
        };
        let dt = (timestamp - prev.time) / 1000;
        const minDt = config.minUpdateInterval / 1000;
        const effectiveDt = dt < minDt ? minDt : dt;
        if (dt <= 0) {
            state.previousMetrics[driverId] = prev;
            return { speed: prev.reportedSpeed, acceleration: prev.acceleration, timeDelta: effectiveDt };
        }
        const distanceDelta = state.trackInfo.total * (progressPercentage - prev.progress) / 100;
        const currentInstantaneousSpeed = (distanceDelta / effectiveDt) * 3600;
        let averagedSpeed;
        if (prev.firstUpdate) {
            averagedSpeed = currentInstantaneousSpeed;
        } else {
            averagedSpeed = (prev.instantaneousSpeed + currentInstantaneousSpeed) / 2;
        }
        let acceleration;
        if (prev.firstUpdate) {
            acceleration = 0;
        } else {
            acceleration = ((averagedSpeed - prev.reportedSpeed) / effectiveDt) * 0.44704 / 9.81;
        }
        state.previousMetrics[driverId] = {
            progress: progressPercentage,
            time: timestamp,
            instantaneousSpeed: currentInstantaneousSpeed,
            reportedSpeed: averagedSpeed,
            acceleration: acceleration,
            lastDisplayedSpeed: prev.lastDisplayedSpeed || averagedSpeed,
            lastDisplayedAcceleration: prev.lastDisplayedAcceleration || acceleration,
            firstUpdate: false
        };
        return { speed: Math.abs(averagedSpeed), acceleration: acceleration, timeDelta: effectiveDt };
    }

    function updateDriverDisplay(driverElement, percentageText, progressPercentage) {
        try {
            const driverId = driverElement?.id;
            if (!driverId) return;
            const now = Date.now();
            if (now - (lastUpdateTimes[driverId] || 0) < config.minUpdateInterval) return;
            lastUpdateTimes[driverId] = now;
            const nameEl = driverElement.querySelector('.name');
            const timeEl = driverElement.querySelector('.time');
            const statusEl = driverElement.querySelector('.status-wrap div');
            if (!nameEl || !timeEl || !statusEl) return;
            let statusText = nameEl.querySelector('.driver-status') || document.createElement('span');
            statusText.className = 'driver-status';
            if (!statusText.parentElement) nameEl.appendChild(statusText);

            const infoSpot = document.getElementById('infoSpot');
            if (infoSpot && infoSpot.textContent.trim().toLowerCase() === 'race starting') { // More specific check
                statusText.textContent = '🛑 NOT STARTED';
                statusText.style.color = 'rgb(136, 136, 136)';
                return; // Early return if race is explicitly starting, thus NOT STARTED
            }


            // Check if ANY driver has started reporting time.  This is a more reliable start indicator.
            let raceHasBegun = false;
            const allDriverTimes = document.querySelectorAll('#leaderBoard li[id^="lbr-"] .time');
            for (const timeElement of allDriverTimes) {
                if (timeElement.textContent.trim() && timeElement.textContent.trim() !== '0%') {
                    raceHasBegun = true;
                    break;
                }
            }

            if (!raceHasBegun && !(infoSpot && infoSpot.textContent.trim().toLowerCase() === 'race finished')) { // Refine raceHasBegun check
                statusText.textContent = '🛑 NOT STARTED';
                statusText.style.color = 'rgb(136, 136, 136)';
                return; // Keep NOT STARTED if no driver has time and infoSpot isn't 'start' and NOT finished
            }


            const isFinished = ['finished', 'gold', 'silver', 'bronze'].some(cls => statusEl.classList.contains(cls));
            if (isFinished) {
                const finishTime = utils.parseTime(timeEl.textContent);
                const avgSpeed = finishTime > 0 ? (state.trackInfo.total / finishTime) * 3600 : 0;
                const avgSpeedFormatted = Math.round(utils.convertSpeed(avgSpeed, config.speedUnit));
                statusText.textContent = `🏁 ${avgSpeedFormatted} ${config.speedUnit}`; // Display average speed on finish
                statusText.style.color = 'rgb(136, 136, 136)';
            } else {
                const metrics = calculateDriverMetrics(driverId, progressPercentage, now);
                const targetSpeed = Math.round(utils.convertSpeed(metrics.speed, config.speedUnit));
                const targetSpeedFormatted = targetSpeed.toLocaleString(undefined, { maximumFractionalDigits: 0 });
                let extraText = "";
                if (driverElement.classList.contains('selected')) {
                    const pdLapEl = document.querySelector('#racingdetails .pd-lap');
                    if (pdLapEl) {
                        const [currentLap, totalLaps] = pdLapEl.textContent.split('/').map(Number);
                        const lapPercentage = 100 / totalLaps;
                        const progressInLap = (progressPercentage - (currentLap - 1) * lapPercentage) / lapPercentage * 100;
                        const remainingDistance = state.trackInfo.length * (1 - progressInLap / 100);
                        if (metrics.speed > 0) {
                            const estTime = (remainingDistance / metrics.speed) * 3600;
                            extraText = ` | Est. Lap: ${utils.formatTime(estTime)}`;
                        }
                    }
                }
                if (config.animateChanges) {
                    if (statusText._telemetryAnimationFrame) {
                        cancelAnimationFrame(statusText._telemetryAnimationFrame);
                        statusText._telemetryAnimationFrame = null;
                    }
                    let fromSpeed = (statusText._currentSpeed !== undefined) ? statusText._currentSpeed : state.previousMetrics[driverId].lastDisplayedSpeed;
                    let fromAcc = (statusText._currentAcc !== undefined) ? statusText._currentAcc : state.previousMetrics[driverId].lastDisplayedAcceleration;
                    let toSpeed = targetSpeed;
                    let toAcc = metrics.acceleration;
                    let duration = metrics.timeDelta * 1000;
                    animateTelemetry(statusText, fromSpeed, toSpeed, fromAcc, toAcc, duration, config.displayMode, config.speedUnit, extraText);
                    state.previousMetrics[driverId].lastDisplayedSpeed = toSpeed;
                    state.previousMetrics[driverId].lastDisplayedAcceleration = toAcc;
                } else {
                    let text;
                    if (config.displayMode === 'speed') {
                        text = `${targetSpeedFormatted} ${config.speedUnit}`;
                    } else if (config.displayMode === 'acceleration') {
                        text = `${metrics.acceleration.toFixed(1)} g`;
                    } else {
                        text = `${targetSpeedFormatted} ${config.speedUnit} | ${metrics.acceleration.toFixed(1)} g`;
                    }
                    text += extraText;
                    statusText.textContent = text;
                    statusText.style.color = config.colorCode ? getTelemetryColor(metrics.acceleration) : 'rgb(136, 136, 136)';
                }
            }
        } catch (e) {
            console.error('Driver display update failed:', e);
        }
    }


    function resetRaceState() {
        state.previousMetrics = {};
        state.trackInfo = { total: 0, laps: 0, length: 0 };
        raceStarted = false;
        clearInterval(periodicCheckIntervalId);
        periodicCheckIntervalId = null;
        observers.forEach(obs => obs.disconnect());
        observers = [];
        lastUpdateTimes = {};
        const container = document.querySelector('.cont-black');
        if (container) {
            const telemetryContainer = container.querySelector('#tornRacingTelemetryContainer');
            if (telemetryContainer) {
                telemetryContainer.remove();
            }
        }
    }

    function setupPeriodicCheck() {
        if (periodicCheckIntervalId) return;
        periodicCheckIntervalId = setInterval(() => {
            try {
                document.querySelectorAll('#leaderBoard li[id^="lbr-"]').forEach(driverEl => {
                    const timeEl = driverEl.querySelector('.time');
                    if (!timeEl) return;
                    const text = timeEl.textContent.trim();
                    const progress = utils.parseProgress(text);
                    updateDriverDisplay(driverEl, text, progress);
                });
            } catch (e) {
                console.error('Periodic check error:', e);
            }
        }, config.periodicCheckInterval); // Now using the hardcoded interval
    }

    function observeDrivers() {
        observers.forEach(obs => obs.disconnect());
        observers = [];
        const drivers = document.querySelectorAll('#leaderBoard li[id^="lbr-"]');
        drivers.forEach(driverEl => {
            const timeEl = driverEl.querySelector('.time');
            if (!timeEl) return;
            updateDriverDisplay(driverEl, timeEl.textContent || '0%', utils.parseProgress(timeEl.textContent || '0%'));
            const observer = new MutationObserver(() => {
                try {
                    const text = timeEl.textContent || '0%';
                    const progress = utils.parseProgress(text);
                    if (progress !== state.previousMetrics[driverEl.id]?.progress) {
                        updateDriverDisplay(driverEl, text, progress);
                    }
                } catch (e) {
                    console.error('Mutation observer error:', e);
                }
            });
            observer.observe(timeEl, { childList: true, subtree: true, characterData: true });
            observers.push(observer);
        });
    }

    function initializeLeaderboard() {
        const leaderboard = document.getElementById('leaderBoard');
        if (!leaderboard) return;
        if (leaderboard.children.length) {
            observeDrivers();
            setupPeriodicCheck();
        } else {
            new MutationObserver((_, obs) => {
                if (leaderboard.children.length) {
                    observeDrivers();
                    setupPeriodicCheck();
                    obs.disconnect();
                }
            }).observe(leaderboard, { childList: true });
        }
    }

    function updateTrackInfo() {
        try {
            const trackHeader = document.querySelector('.drivers-list .title-black');
            if (!trackHeader) throw new Error('Track header missing');
            const parentElement = trackHeader.parentElement;
            if (!parentElement) throw new Error('Track header parent missing');
            const infoElement = parentElement.querySelector('.track-info');
            const lapsMatch = trackHeader.textContent.match(/(\d+)\s+laps?/i);
            const lengthMatch = infoElement?.dataset.length?.match(/(\d+\.?\d*)/);
            state.trackInfo = {
                laps: lapsMatch ? parseInt(lapsMatch[1]) : 5,
                length: lengthMatch ? parseFloat(lengthMatch[1]) : 3.4,
                get total() { return this.laps * this.length; }
            };
        } catch (e) {
            state.trackInfo = { laps: 5, length: 3.4, total: 17 };
        }
    }

    function initializeUI(container) {
        const telemetryContainer = document.createElement('div');
        telemetryContainer.id = 'tornRacingTelemetryContainer';
        container.prepend(telemetryContainer);
        const settingsPanel = createSettingsPanel();
        telemetryContainer.append(
            settingsPanel,
            createStatsPanel(),
            createRacesPanel()
        );
        updateSettingsUI(settingsPanel);
    }


    function initialize() {
        try {
            const container = document.querySelector('.cont-black');
            if (!container) throw new Error('Container not found');
            const raceId = window.location.href.match(/sid=racing.*?(?=&|$)/)?.[0] || 'default';
            if (currentRaceId !== raceId) {
                resetRaceState();
                currentRaceId = raceId;
            }
            if (container.querySelector('#tornRacingTelemetryContainer')) {
                updateTrackInfo();
                initializeLeaderboard();
                return;
            }

            initializeUI(container);

            updateTrackInfo();
            initializeLeaderboard();
            dataManager.fetchRacingStats().then(dataManager.fetchRaces.bind(dataManager));
        } catch (e) {
            console.error('Initialization failed:', e);
        }
    }

    const racingUpdatesObserver = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.id === 'racingupdates') {
                    initialize();
                }
            });
            mutation.removedNodes.forEach(node => {
                if (node.nodeType === 1 && node.id === 'racingupdates') {
                    resetRaceState();
                }
            });
        });
    });

    racingUpdatesObserver.observe(document.body, { childList: true, subtree: true });
    document.readyState === 'complete' ? initialize() : window.addEventListener('load', initialize);
    window.addEventListener('popstate', initialize);
})();