Torn Racing Telemetry

Enhances Torn Racing with real-time telemetry, race stats history, accurate race information, and detailed logs.

当前为 2025-04-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         Torn Racing Telemetry
// @namespace    https://www.torn.com/profiles.php?XID=2782979
// @version      2.5.1
// @description  Enhances Torn Racing with real-time telemetry, race stats history, accurate race information, and detailed 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;

    // ===== CORE CONFIG & STATE MANAGEMENT =====
    const Config = {
        defaults: {
            displayMode: 'speed',
            colorCode: true,
            animateChanges: true,
            speedUnit: 'mph',
            periodicCheckInterval: 1000, // Hardcoded to 1 second (1000ms)
            minUpdateInterval: 500,
            language: 'en',
            apiKey: ''
        },
        
        data: {}, // Will hold current configuration
        
        // Load config with safe JSON parsing
        load() {
            this.data = Object.assign({}, this.defaults, (() => {
                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 {};
                    }
                }
                return {};
            })());
            return this.data;
        },
        
        // Save config
        save() {
            GM_setValue('racingTelemetryConfig', JSON.stringify(this.data));
        },
        
        // Get config value
        get(key) {
            return this.data[key];
        },
        
        // Set config value and save
        set(key, value) {
            this.data[key] = value;
            this.save();
        }
    };

    // Application state
    const State = {
        previousMetrics: {},
        trackInfo: { total: 0, laps: 0, length: 0 },
        racingStats: null,
        statsHistory: [],
        raceLog: [],
        currentCar: null,
        preciseSkill: null,
        carClass: null,
        currentRaceId: null,
        raceStarted: false,
        periodicCheckIntervalId: null,
        telemetryVisible: true,
        observers: [],
        lastUpdateTimes: {},
        
        // Initialize state from storage
        init() {
            // Load stats history
            let savedStatsHistory = GM_getValue('statsHistory', []);
            if (!Array.isArray(savedStatsHistory)) savedStatsHistory = [];
            this.statsHistory = savedStatsHistory.filter(entry => entry && entry.timestamp && entry.skill).slice(0, 50);
            
            // Load race log
            const savedRaces = GM_getValue('raceLog', []);
            this.raceLog = (Array.isArray(savedRaces) ? savedRaces.filter(r => r?.id).slice(0, 50) : []);
            
            return this;
        },
        
        // Reset race state
        resetRace() {
            this.previousMetrics = {};
            this.trackInfo = { total: 0, laps: 0, length: 0 };
            this.raceStarted = false;
            clearInterval(this.periodicCheckIntervalId);
            this.periodicCheckIntervalId = null;
            this.observers.forEach(obs => obs.disconnect());
            this.observers = [];
            this.lastUpdateTimes = {};
        },
        
        // Save state item to storage
        save(key) {
            if (key === 'statsHistory') {
                GM_setValue('statsHistory', this.statsHistory);
            } else if (key === 'raceLog') {
                GM_setValue('raceLog', this.raceLog);
            } else if (key === 'racingStats') {
                GM_setValue('racingStats', JSON.stringify(this.racingStats));
            }
        }
    };

    // ===== UTILITY FUNCTIONS =====
    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;
            const parts = timeString.split(':');
            let minutes = 0;
            let seconds = 0;

            if (parts.length === 2) { // MM:SS.SS or MM:SS.HH format
                minutes = parseInt(parts[0], 10) || 0;
                seconds = parseFloat(parts[1]) || 0; // Parse seconds with decimal part
            } else if (parts.length === 3) { // Optional HH:MM:SS.SS format in future?
                // If we ever encounter HH:MM:SS.SS, handle it here. For now assume MM:SS.SS is standard.
                console.warn("Time string with 3 parts encountered, assuming HH:MM:SS.SS and taking last two parts as MM:SS.SS:", timeString);
                minutes = parseInt(parts[1], 10) || 0;
                seconds = parseFloat(parts[2]) || 0;
            } else {
                console.error("Unexpected time string format:", timeString);
                return 0; // Or handle error as needed
            }

            return (minutes * 60) + seconds; // Total seconds
        },
        
        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);
        },
        
        async fetchWithRetry(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 this.fetchWithRetry(url, retries - 1);
                throw e;
            }
        },
        
        formatTimestamp(timestamp) {
            let date;
            
            // Handle different timestamp formats
            if (typeof timestamp === 'number') {
                // If timestamp is a small number (seconds instead of milliseconds)
                if (timestamp < 10000000000) {
                    date = new Date(timestamp * 1000);
                } else {
                    date = new Date(timestamp);
                }
            } else if (typeof timestamp === 'string') {
                // Try to parse string timestamp
                if (timestamp.match(/^\d+$/)) {
                    // If it's all digits, treat as a number
                    const num = parseInt(timestamp, 10);
                    if (num < 10000000000) {
                        date = new Date(num * 1000);
                    } else {
                        date = new Date(num);
                    }
                } else {
                    // Otherwise just parse as date string
                    date = new Date(timestamp);
                }
            } else {
                return 'N/A';
            }

            if (isNaN(date) || date.getFullYear() < 2000) {
                return 'Invalid Date';
            }

            // Format as a more readable date/time string
            const options = { 
                year: 'numeric', 
                month: 'short', 
                day: 'numeric',
                hour: '2-digit',
                minute: '2-digit',
                hour12: true
            };
            
            return date.toLocaleDateString(undefined, options);
        },
        
        formatChange(oldVal, newVal) {
            const change = newVal - oldVal;
            if (change > 0) {
                return `<span style="color: #4CAF50;">${oldVal} → ${newVal} (+${change})</span>`;
            } else if (change < 0) {
                return `<span style="color: #f44336;">${oldVal} → ${newVal} (${change})</span>`;
            } else {
                return `${oldVal} → ${newVal} (no change)`;
            }
        },
        
        // Get color for stat value
        getStatColor(value) {
            if (value >= 80) return '#4CAF50'; // Green for high values
            if (value >= 60) return '#8BC34A'; // Light green
            if (value >= 40) return '#FFC107'; // Yellow/amber
            if (value >= 20) return '#FF9800'; // Orange
            return '#F44336'; // Red for low values
        }
    };

    // ===== DATA MANAGEMENT =====
    const DataManager = {
        async fetchRacingStats() {
            if (!Config.get('apiKey')?.trim()) return;
            try {
                const data = await Utils.fetchWithRetry(
                    `https://api.torn.com/v2/user/personalstats?cat=racing&key=${Config.get('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);
                State.save('statsHistory');
            }
            State.racingStats = { racing: newStats };
            State.save('racingStats');
            
            // Update UI if needed
            const statsPanelContent = document.querySelector('.stats-history-popup .popup-content');
            if (statsPanelContent) {
                statsPanelContent.innerHTML = UI.generateStatsContent();
            }
        },
        
        async fetchRaces() {
            if (!Config.get('apiKey')?.trim()) return;
            try {
                const data = await Utils.fetchWithRetry(
                    `https://api.torn.com/v2/user/races?key=${Config.get('apiKey')}&limit=10&sort=DESC&cat=official`
                );
                // First process all races to store the basic information
                if (data.races) {
                    data.races.forEach(race => this.processRace(race));
                    State.save('raceLog');
                }
                
                // Update the panel if it's open
                const racesPanelContent = document.querySelector('.recent-races-popup .popup-content');
                if (racesPanelContent) {
                    racesPanelContent.innerHTML = UI.generateRacesContent();
                }
            } catch (e) {
                console.error('Race fetch failed:', e);
                Utils.displayError(`Race data error: ${e.message}`);
            }
        },
        
        processRace(race) {
            const existingRaceIndex = State.raceLog.findIndex(r => r.id === race.id);
            
            if (existingRaceIndex !== -1) {
                // Update existing race with any new information
                const existingRace = State.raceLog[existingRaceIndex];
                // Preserve track information if we already have it
                const trackName = existingRace.trackName || null;
                const trackLength = existingRace.trackLength || null;
                const trackDescription = existingRace.trackDescription || null;
                
                // Update the race with new data from API but keep our track info
                State.raceLog[existingRaceIndex] = { 
                    ...race, 
                    fetchedAt: Date.now(),
                    trackName, 
                    trackLength, 
                    trackDescription
                };
            } else if (race?.id) {
                // Add new race
                State.raceLog.unshift({ 
                    ...race, 
                    fetchedAt: Date.now(),
                    trackName: null,
                    trackLength: null,
                    trackDescription: null
                });
                State.raceLog = State.raceLog.slice(0, 50);
            }
        },
        
        updateRaceWithTrackInfo(raceId, trackInfo) {
            if (!raceId || !trackInfo) return;
            
            console.log('Updating race with track info:', raceId, trackInfo);
            
            const index = State.raceLog.findIndex(r => r.id === raceId);
            if (index !== -1) {
                State.raceLog[index].trackName = trackInfo.name || State.raceLog[index].trackName;
                State.raceLog[index].trackLength = trackInfo.length || State.raceLog[index].trackLength;
                State.raceLog[index].trackDescription = trackInfo.description || State.raceLog[index].trackDescription;
                
                // Update storage
                State.save('raceLog');
                
                // Update UI if panel is open
                const racesPanelContent = document.querySelector('.recent-races-popup .popup-content');
                if (racesPanelContent) {
                    racesPanelContent.innerHTML = UI.generateRacesContent();
                }
                
                return true;
            }
            
            return false;
        }
    };

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

        // Helper to interpolate between two colors.
        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.
        getTelemetryColor(acceleration) {
            const grey = [136, 136, 136];
            if (!Config.get('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 this.interpolateColor(grey, green, factor);
            } else if (acceleration < 0) {
                return this.interpolateColor(grey, red, factor);
            } else {
                return `rgb(${grey[0]}, ${grey[1]}, ${grey[2]})`;
            }
        },

        // Animate telemetry text using an easeInOut function over a dynamic duration.
        animateTelemetry(element, fromSpeed, toSpeed, fromAcc, toAcc, duration, displayMode, speedUnit, extraText) {
            let startTime = null;
            const easeFunction = this.easeInOutQuad;
            const getColor = this.getTelemetryColor.bind(this);
            
            function step(timestamp) {
                if (!startTime) startTime = timestamp;
                let linearProgress = (timestamp - startTime) / duration;
                if (linearProgress > 1) linearProgress = 1;
                let progress = easeFunction(linearProgress);
                let currentSpeed = fromSpeed + (toSpeed - fromSpeed) * progress;
                let currentAcc = fromAcc + (toAcc - fromAcc) * progress;
                element._currentSpeed = currentSpeed;
                element._currentAcc = currentAcc;
                let color = Config.get('colorCode') ? getColor(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;
                }
            }
            
            if (element._telemetryAnimationFrame) {
                cancelAnimationFrame(element._telemetryAnimationFrame);
            }
            element._telemetryAnimationFrame = requestAnimationFrame(step);
        },

        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.get('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 };
        },

        updateDriverDisplay(driverElement, percentageText, progressPercentage) {
            try {
                const driverId = driverElement?.id;
                if (!driverId) return;
                
                const now = Date.now();
                if (now - (State.lastUpdateTimes[driverId] || 0) < Config.get('minUpdateInterval')) return;
                State.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') {
                    statusText.textContent = '🛑 NOT STARTED';
                    statusText.style.color = 'rgb(136, 136, 136)';
                    return;
                }

                // Check if ANY driver has started reporting time
                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')) {
                    statusText.textContent = '🛑 NOT STARTED';
                    statusText.style.color = 'rgb(136, 136, 136)';
                    return;
                }

                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.get('speedUnit')));
                    statusText.textContent = `🏁 ${avgSpeedFormatted} ${Config.get('speedUnit')}`;
                    statusText.style.color = 'rgb(136, 136, 136)';
                } else {
                    const metrics = this.calculateDriverMetrics(driverId, progressPercentage, now);
                    const targetSpeed = Math.round(Utils.convertSpeed(metrics.speed, Config.get('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.get('animateChanges')) {
                        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;
                        
                        this.animateTelemetry(
                            statusText, fromSpeed, toSpeed, fromAcc, toAcc, 
                            duration, Config.get('displayMode'), Config.get('speedUnit'), extraText
                        );
                        
                        State.previousMetrics[driverId].lastDisplayedSpeed = toSpeed;
                        State.previousMetrics[driverId].lastDisplayedAcceleration = toAcc;
                    } else {
                        let text;
                        if (Config.get('displayMode') === 'speed') {
                            text = `${targetSpeedFormatted} ${Config.get('speedUnit')}`;
                        } else if (Config.get('displayMode') === 'acceleration') {
                            text = `${metrics.acceleration.toFixed(1)} g`;
                        } else {
                            text = `${targetSpeedFormatted} ${Config.get('speedUnit')} | ${metrics.acceleration.toFixed(1)} g`;
                        }
                        text += extraText;
                        statusText.textContent = text;
                        statusText.style.color = Config.get('colorCode') ? 
                            this.getTelemetryColor(metrics.acceleration) : 'rgb(136, 136, 136)';
                    }
                }
            } catch (e) {
                console.error('Driver display update failed:', e);
            }
        }
    };

    // ===== UI MANAGEMENT =====
    const UI = {
        setupStyles() {
            GM_addStyle(`
                :root {
                    --primary-color: #4CAF50;
                    --background-dark: #1a1a1a;
                    --background-light: #2a2a2a;
                    --text-color: #e0e0e0;
                    --border-color: #404040;
                    --accent-color: #64B5F6;
                    --highlight-color: #FFD54F;
                    --positive-color: #4CAF50;
                    --negative-color: #f44336;
                }
                body.telemetry-hidden #telemetryButtonContainer {
                    margin-bottom: 0;
                }
                .telemetry-button-container {
                    display: flex;
                    width: 100%;
                    margin: 0 0px 0px;
                    border-radius: 6px;
                    overflow: hidden;
                    background: var(--background-dark);
                    border: 1px solid var(--border-color);
                }
                .telemetry-button {
                    flex-grow: 1;
                    background: transparent;
                    color: var(--text-color);
                    border: none;
                    padding: 12px 15px;
                    text-align: center;
                    cursor: pointer;
                    transition: background-color 0.2s ease, color 0.2s ease;
                    font-size: 15px;
                }
                .telemetry-button:hover {
                    background-color: var(--background-light);
                    color: var(--accent-color);
                }
                .telemetry-button:active {
                    background-color: var(--accent-color);
                    color: var(--background-dark);
                }
                .telemetry-popup-container {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background-color: rgba(0, 0, 0, 0.6);
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    z-index: 1000;
                    backdrop-filter: blur(5px);
                }
                .telemetry-popup {
                    background: var(--background-dark);
                    border-radius: 10px;
                    padding: 20px;
                    color: var(--text-color);
                    font-family: 'Arial', sans-serif;
                    box-shadow: 0 8px 16px rgba(0,0,0,0.6);
                    width: 90%;
                    max-width: 700px;
                    max-height: 95%;
                    overflow-y: auto;
                    position: relative;
                    border: 1px solid var(--border-color);
                }
                .popup-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 15px;
                    padding-bottom: 8px;
                    border-bottom: 1px solid var(--border-color);
                }
                .popup-title {
                    font-size: 1.4em;
                    font-weight: bold;
                    color: var(--primary-color);
                }
                .close-button {
                    background: var(--background-light);
                    border: none;
                    border-radius: 6px;
                    color: var(--text-color);
                    padding: 8px 16px;
                    cursor: pointer;
                    transition: background-color 0.2s ease, color 0.2s ease;
                    font-size: 14px;
                }
                .close-button:hover {
                    background-color: var(--accent-color);
                    color: var(--background-dark);
                }
                .popup-content {
                    padding: 15px 0;
                    border-radius: 8px;
                    max-height: 70vh;
                    overflow-y: auto;
                }
                #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.get('minUpdateInterval') / 1000}s ease, opacity 0.3s ease;
                }
                .telemetry-hidden .driver-status { display: none; }
                
                .settings-header, .history-header, .races-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    background: var(--background-light);
                    padding: 10px 15px;
                    border-radius: 8px 8px 0 0;
                }
                .settings-title, .history-title, .races-title {
                    font-size: 1.1em;
                    font-weight: bold;
                    color: var(--primary-color);
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }
                .toggle-btn, .reset-btn, .toggle-telemetry-btn, .copy-btn {
                    background: var(--background-light);
                    border: none;
                    border-radius: 6px;
                    color: var(--text-color);
                    padding: 8px 16px;
                    cursor: pointer;
                    transition: background-color 0.2s ease, color 0.2s ease;
                    font-size: 14px;
                    margin-top: 5px;
                }
                .toggle-btn:hover, .reset-btn:hover, .toggle-telemetry-btn:hover, .copy-btn:hover {
                    background-color: var(--accent-color);
                    color: var(--background-dark);
                }
                .reset-system-btn {
                    background: #f44336;
                    border: none;
                    border-radius: 6px;
                    color: #fff;
                    padding: 8px 16px;
                    cursor: pointer;
                    transition: all 0.2s;
                    font-size: 14px;
                }
                .reset-system-btn:hover {
                     background-color: #d32f2f;
                 }
                .settings-content, .history-content, .races-content {
                    display: flex;
                    flex-direction: column;
                    gap: 15px;
                    padding: 15px;
                    background: var(--background-dark);
                    border-radius: 0 0 8px 8px;
                }
                .setting-group {
                    display: flex;
                    flex-direction: column;
                    gap: 10px;
                    padding: 10px 15px;
                    background: var(--background-light);
                    border-radius: 8px;
                    border: 1px solid var(--border-color);
                }
                .setting-item {
                    display: flex;
                    flex-direction: column;
                    align-items: stretch;
                    justify-content: flex-start;
                    padding: 12px;
                    background: var(--background-dark);
                    border-radius: 6px;
                }
                .setting-item > span {
                    margin-bottom: 8px;
                    font-weight: normal;
                    color: var(--text-color);
                }
                .switch {
                    position: relative;
                    display: inline-block;
                    width: 45px;
                    height: 24px;
                }
                .switch input {
                    opacity: 0;
                    width: 0;
                    height: 0;
                }
                .slider {
                    position: absolute;
                    cursor: pointer;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: #4d4d4d;
                    transition: .3s;
                    border-radius: 12px;
                }
                .slider:before {
                    position: absolute;
                    content: "";
                    height: 20px;
                    width: 20px;
                    left: 3px;
                    bottom: 2px;
                    background: #f4f4f4;
                    transition: .3s;
                    border-radius: 50%;
                }
                input:checked + .slider {
                    background: var(--primary-color);
                }
                input:checked + .slider:before {
                    transform: translateX(21px);
                }
                .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;
                    margin: 5px 0;
                }
                .radio-item label {
                    text-align: left;
                    display: block;
                    width: 100%;
                }
                .radio-item:first-child {
                    margin-top: 0;
                }
                .api-key-input {
                    width: 100%;
                    padding: 10px;
                    border-radius: 6px;
                    border: 1px solid var(--border-color);
                    background: var(--background-light);
                    color: var(--text-color);
                    font-size: 14px;
                }
                .api-key-input:focus {
                    outline: none;
                    border-color: var(--accent-color);
                    box-shadow: 0 0 5px rgba(100, 181, 246, 0.5);
                }
                .history-entry, .race-entry {
                    padding: 15px;
                    background: var(--background-light);
                    border-radius: 6px;
                    margin-bottom: 8px;
                    font-size: 14px;
                    line-height: 1.5;
                    border: 1px solid var(--border-color);
                }
                .history-entry:last-child, .race-entry:last-child {
                    margin-bottom: 0;
                }
                .current-stats {
                    padding: 15px;
                    background: var(--background-light);
                    border-radius: 6px;
                    margin-bottom: 15px;
                    border: 1px solid var(--border-color);
                }
                .current-stats h3 {
                    color: var(--primary-color);
                    margin-top: 0;
                    margin-bottom: 10px;
                    font-size: 1.2em;
                }
                .current-stats p {
                    margin: 5px 0;
                }
                .setting-dropdown {
                    width: 100%;
                    padding: 10px;
                    border-radius: 6px;
                    border: 1px solid var(--border-color);
                    background: var(--background-light);
                    color: var(--text-color);
                    font-size: 14px;
                    appearance: none;
                    -webkit-appearance: none;
                    -moz-appearance: none;
                    background-image: url('data:image/svg+xml;utf8,<svg fill="white" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
                    background-repeat: no-repeat;
                    background-position-x: 98%;
                    background-position-y: 12px;
                    padding-right: 30px;
                }
                .setting-dropdown::-ms-expand {
                    display: none;
                }
                .setting-dropdown:focus {
                    outline: none;
                    border-color: var(--accent-color);
                    box-shadow: 0 0 5px rgba(100, 181, 246, 0.5);
                }
                
                /* Enhanced styles for the racing panels */
                .race-entry {
                    display: grid;
                    grid-template-columns: 1fr 1fr;
                    gap: 10px;
                }
                .race-entry .race-header {
                    grid-column: 1 / -1;
                    border-bottom: 1px solid var(--border-color);
                    padding-bottom: 8px;
                    margin-bottom: 8px;
                }
                .race-entry .race-title {
                    font-weight: bold;
                    font-size: 1.1em;
                }
                .race-entry .race-title a {
                    color: var(--primary-color);
                    text-decoration: none;
                    transition: color 0.2s ease;
                }
                .race-entry .race-title a:hover {
                    text-decoration: underline;
                    color: var(--accent-color);
                }
                .race-entry .race-id {
                    color: #999;
                    font-size: 0.9em;
                }
                .race-entry .race-info {
                    display: flex;
                    flex-direction: column;
                    gap: 6px;
                }
                .race-entry .race-detail {
                    display: flex;
                    align-items: center;
                }
                .race-entry .race-detail-label {
                    font-weight: bold;
                    width: 100px;
                    color: var(--text-color);
                }
                .race-entry .race-detail-value {
                    flex: 1;
                }
                .race-entry .race-status {
                    padding: 2px 8px;
                    border-radius: 3px;
                    display: inline-block;
                    text-transform: capitalize;
                }
                .race-entry .race-status.finished {
                    background-color: var(--primary-color);
                    color: white;
                }
                
                /* Stats history enhancements */
                .history-entry {
                    display: grid;
                    grid-template-columns: 1fr 1fr;
                    gap: 10px;
                }
                .history-entry .history-time {
                    grid-column: 1 / -1;
                    font-weight: bold;
                    border-bottom: 1px solid var(--border-color);
                    padding-bottom: 5px;
                    margin-bottom: 5px;
                    color: var(--accent-color);
                }
                .history-entry .stat-change {
                    display: flex;
                    flex-direction: column;
                    margin-bottom: 5px;
                }
                .history-entry .stat-label {
                    font-weight: bold;
                    color: var(--text-color);
                }
                .history-entry .stat-value {
                    font-family: monospace;
                }
                .history-entry .stat-value .increase {
                    color: var(--positive-color);
                }
                .history-entry .stat-value .decrease {
                    color: var(--negative-color);
                }
                
                /* Current stats section in stats history */
                .current-stats {
                    display: grid;
                    grid-template-columns: 1fr 1fr;
                    gap: 15px;
                }
                .current-stats h3 {
                    grid-column: 1 / -1;
                    text-align: center;
                    border-bottom: 1px solid var(--border-color);
                    padding-bottom: 10px;
                }
                .current-stats .stat-item {
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    background: var(--background-dark);
                    padding: 10px;
                    border-radius: 5px;
                }
                .current-stats .stat-label {
                    font-size: 0.9em;
                    color: var(--text-color);
                    margin-bottom: 5px;
                }
                .current-stats .stat-value {
                    font-size: 1.4em;
                    font-weight: bold;
                    color: var(--highlight-color);
                }
                
                /* Mobile styles */
                @media (max-width: 480px) {
                    .telemetry-button-container {
                        flex-direction: column;
                        margin: 0 5px 10px;
                        border-radius: 8px;
                    }
                    .telemetry-button {
                        border-radius: 0;
                    }
                    .telemetry-button:not(:last-child) {
                        border-bottom: 1px solid var(--border-color);
                    }
                    .telemetry-popup {
                        width: 95%;
                        margin: 10px;
                        border-radius: 8px;
                        padding: 15px;
                    }
                    .popup-content {
                        max-height: 80vh;
                        padding: 10px 0;
                    }
                    #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;
                    }
                    .settings-title, .history-title, .races-title {
                        font-size: 14px;
                    }
                }
            `);
        },
        
        // Helper to create a popup container
        createPopupContainer(title, contentGenerator, popupClass) {
            const container = document.createElement('div');
            container.className = 'telemetry-popup-container ' + popupClass;
            container.innerHTML = `
                <div class="telemetry-popup">
                    <div class="popup-header">
                        <span class="popup-title">${title}</span>
                        <button class="close-button">Close</button>
                    </div>
                    <div class="popup-content">
                        ${contentGenerator ? contentGenerator() : ''}
                    </div>
                </div>
            `;
            container.querySelector('.close-button').addEventListener('click', () => {
                container.remove();
            });
            return container;
        },
        
        // Create Settings Popup
        createSettingsPopup() {
            const popupContainer = this.createPopupContainer("RACING TELEMETRY SETTINGS", this.generateSettingsContent, 'settings-popup');
            const popupContent = popupContainer.querySelector('.popup-content');
            this.updateSettingsUI(popupContent);

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

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

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

            const copyButton = popupContent.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 = popupContent.querySelector('.toggle-telemetry-btn');
            if (telemetryToggleButton) {
                telemetryToggleButton.addEventListener('click', () => {
                    State.telemetryVisible = !State.telemetryVisible;
                    document.body.classList.toggle('telemetry-hidden', !State.telemetryVisible);
                    telemetryToggleButton.textContent = State.telemetryVisible ? 'Hide Telemetry' : 'Show Telemetry';
                });
            }

            return popupContainer;
        },
        
        // Create Stats Popup
        createStatsPopup() {
            const popupContainer = this.createPopupContainer("PERSONAL STATS HISTORY", this.generateStatsContent, 'stats-history-popup');
            const popupContent = popupContainer.querySelector('.popup-content');
            popupContent.innerHTML = this.generateStatsContent(); // Initial content
            return popupContainer;
        },
        
        // Create Races Popup
        createRacesPopup() {
             const popupContainer = this.createPopupContainer("RECENT RACES (LAST 10)", this.generateRacesContent, 'recent-races-popup');
             const popupContent = popupContainer.querySelector('.popup-content');
             popupContent.innerHTML = this.generateRacesContent(); // Initial content
             return popupContainer;
        },
        
        // Update the settings UI with the current config
        updateSettingsUI(popupContent) {
            popupContent.querySelectorAll('input, select').forEach(el => {
                if (el.type === 'checkbox') el.checked = Config.get(el.dataset.setting);
                else if (el.tagName === 'SELECT') el.value = Config.get(el.dataset.setting);
                else if (el.type === 'radio') el.checked = Config.get(el.name) === el.value;
                else if (el.type === 'number') el.value = Config.get(el.dataset.setting);
                else if (el.classList.contains('api-key-input')) el.value = Config.get(el.dataset.setting);
            });
        },
        
        // Generate Settings Content
        generateSettingsContent() {
            return `
                <div class="settings-content">
                    <div class="setting-group">
                        <div class="setting-item" title="Select data to display">
                            <span>Display Mode</span>
                            <select class="setting-dropdown" data-setting="displayMode">
                                <option value="speed" ${Config.get('displayMode') === 'speed' ? 'selected' : ''}>Speed</option>
                                <option value="acceleration" ${Config.get('displayMode') === 'acceleration' ? 'selected' : ''}>Acceleration</option>
                                <option value="both" ${Config.get('displayMode') === 'both' ? 'selected' : ''}>Both</option>
                            </select>
                        </div>
                        <div class="setting-item" title="Color-code acceleration status">
                            <span>Color Coding</span>
                            <label class="switch"><input type="checkbox" ${Config.get('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.get('animateChanges') ? 'checked' : ''} data-setting="animateChanges"><span class="slider"></span></label>
                        </div>
                        <div class="setting-item" title="Speed unit preference">
                            <span>Speed Unit</span>
                            <select class="setting-dropdown" data-setting="speedUnit">
                                <option value="mph" ${Config.get('speedUnit') === 'mph' ? 'selected' : ''}>mph</option>
                                <option value="kmh" ${Config.get('speedUnit') === 'kmh' ? 'selected' : ''}>km/h</option>
                            </select>
                        </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.get('apiKey')}" data-setting="apiKey" placeholder="Enter API Key">
                        </div>
                    </div>
                    <div class="setting-group">
                        <button class="reset-system-btn">Reset System</button>
                        <button class="copy-btn">Copy HTML</button>
                        <button class="toggle-telemetry-btn">${State.telemetryVisible ? 'Hide Telemetry' : 'Show Telemetry'}</button>
                    </div>
                </div>
            `;
        },
        
        // Generate Stats Content
        generateStatsContent() {
            let content = '';
            
            // Current racing stats section
            if (State.racingStats?.racing || State.preciseSkill !== null) {
                const currentRacingStats = State.racingStats?.racing || {};
                
                // Determine which skill value to show (prefer the more precise one from the HTML)
                const skillValue = State.preciseSkill !== null 
                    ? State.preciseSkill 
                    : (currentRacingStats.skill || '0');
                
                // Add class if available
                const classDisplay = State.carClass 
                    ? `<span style="background: #333; padding: 2px 6px; border-radius: 4px; margin-left: 8px;">Class ${State.carClass}</span>` 
                    : '';
                
                content += `
                    <div class="current-stats">
                        <h3>Current Racing Stats</h3>
                        <div class="stat-item">
                            <div class="stat-label">Racing Skill</div>
                            <div class="stat-value">${skillValue}${classDisplay}</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-label">Racing Points</div>
                            <div class="stat-value">${currentRacingStats.points?.toLocaleString() || '0'}</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-label">Races Entered</div>
                            <div class="stat-value">${currentRacingStats.races?.entered?.toLocaleString() || '0'}</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-label">Races Won</div>
                            <div class="stat-value">${currentRacingStats.races?.won?.toLocaleString() || '0'}</div>
                        </div>
                    </div>
                `;
                
                // Add current car information if available
                if (State.currentCar) {
                    content += `
                        <div class="current-stats">
                            <h3>Current Car</h3>
                            <div class="stat-item" style="grid-column: 1 / -1;">
                                <div class="stat-label">Car</div>
                                <div class="stat-value">${State.currentCar.name || 'Unknown'} (${State.currentCar.type || 'Unknown'})</div>
                            </div>`;
                            
                    // Add car stats if available
                    if (State.currentCar.stats) {
                        const stats = State.currentCar.stats;
                        content += `
                            <div class="stat-item">
                                <div class="stat-label">Top Speed</div>
                                <div class="stat-value progress-container">
                                    <div class="progress-bar" style="width: ${stats['top speed'] || 0}%; background-color: ${Utils.getStatColor(stats['top speed'])}"></div>
                                    <span>${Math.round(stats['top speed'] || 0)}%</span>
                                </div>
                            </div>
                            <div class="stat-item">
                                <div class="stat-label">Acceleration</div>
                                <div class="stat-value progress-container">
                                    <div class="progress-bar" style="width: ${stats['acceleration'] || 0}%; background-color: ${Utils.getStatColor(stats['acceleration'])}"></div>
                                    <span>${Math.round(stats['acceleration'] || 0)}%</span>
                                </div>
                            </div>
                            <div class="stat-item">
                                <div class="stat-label">Handling</div>
                                <div class="stat-value progress-container">
                                    <div class="progress-bar" style="width: ${stats['handling'] || 0}%; background-color: ${Utils.getStatColor(stats['handling'])}"></div>
                                    <span>${Math.round(stats['handling'] || 0)}%</span>
                                </div>
                            </div>
                            <div class="stat-item">
                                <div class="stat-label">Braking</div>
                                <div class="stat-value progress-container">
                                    <div class="progress-bar" style="width: ${stats['braking'] || 0}%; background-color: ${Utils.getStatColor(stats['braking'])}"></div>
                                    <span>${Math.round(stats['braking'] || 0)}%</span>
                                </div>
                            </div>
                        `;
                    }
                    
                    content += `</div>`;
                }
            }

            // Stats history entries
            content += State.statsHistory.map(entry => {
                return `
                    <div class="history-entry">
                        <div class="history-time">
                            ${Utils.formatTimestamp(entry.timestamp)}
                        </div>
                        <div class="stat-change">
                            <div class="stat-label">Racing Skill</div>
                            <div class="stat-value">${Utils.formatChange(entry.skill.old, entry.skill.new)}</div>
                        </div>
                        <div class="stat-change">
                            <div class="stat-label">Racing Points</div>
                            <div class="stat-value">${Utils.formatChange(entry.points.old, entry.points.new)}</div>
                        </div>
                        <div class="stat-change">
                            <div class="stat-label">Races Entered</div>
                            <div class="stat-value">${Utils.formatChange(entry.racesEntered.old, entry.racesEntered.new)}</div>
                        </div>
                        <div class="stat-change">
                            <div class="stat-label">Races Won</div>
                            <div class="stat-value">${Utils.formatChange(entry.racesWon.old, entry.racesWon.new)}</div>
                        </div>
                    </div>
                `;
            }).join('');

            return content || '<div class="history-entry">No stats history available</div>';
        },
        
        // Generate Races Content
        generateRacesContent() {
            // Sort races by start time (newest first)
            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;
            });

            // Check if we're in a race currently
            let currentRaceContent = '';
            if (State.currentRaceId && State.trackInfo.name) {
                const statusHTML = `<span class="race-status active">Active</span>`;
                
                currentRaceContent = `
                    <div class="race-entry current-race">
                        <div class="race-header">
                            <div class="race-title">
                                <a href="https://www.torn.com/loader.php?sid=racing&tab=log&raceID=${State.currentRaceId}" target="_self">
                                Current Race: ${State.trackInfo.name || 'Unknown Track'}</a>
                            </div>
                            <div class="race-id">ID: ${State.currentRaceId}</div>
                        </div>
                        
                        <div class="race-info">
                            <div class="race-detail">
                                <div class="race-detail-label">Track:</div>
                                <div class="race-detail-value">${State.trackInfo.name || 'Unknown'} (${State.trackInfo.length}mi)</div>
                            </div>
                            <div class="race-detail">
                                <div class="race-detail-label">Status:</div>
                                <div class="race-detail-value">${statusHTML}</div>
                            </div>
                            <div class="race-detail">
                                <div class="race-detail-label">Laps:</div>
                                <div class="race-detail-value">${State.trackInfo.laps || '0'}</div>
                            </div>
                        </div>
                        
                        <div class="race-info">
                            <div class="race-detail">
                                <div class="race-detail-label">Description:</div>
                                <div class="race-detail-value" style="font-style: italic; font-size: 0.9em;">${State.trackInfo.description || 'No description available'}</div>
                            </div>
                        </div>
                    </div>
                `;
                
                // Store this track info for our current race in the raceLog as well
                const currentRaceIndex = State.raceLog.findIndex(r => r.id === State.currentRaceId);
                if (currentRaceIndex !== -1) {
                    State.raceLog[currentRaceIndex].trackName = State.trackInfo.name;
                    State.raceLog[currentRaceIndex].trackLength = State.trackInfo.length;
                    State.raceLog[currentRaceIndex].trackDescription = State.trackInfo.description;
                    State.raceLog[currentRaceIndex].trackInfoFetched = true;
                }
            }

            const pastRacesContent = State.raceLog.map(race => {
                // Skip the current race as we've already rendered it
                if (race.id === State.currentRaceId && State.trackInfo.name) {
                    return '';
                }
                
                // Format the race status with appropriate styling
                const statusClass = race.status ? race.status.toLowerCase() : '';
                const statusHTML = `<span class="race-status ${statusClass}">${race.status || 'Unknown'}</span>`;
                
                // Get your position if available
                let positionInfo = '';
                if (race.user_data?.position) {
                    const positionClass = race.user_data.position <= 3 ? 'highlight-position' : '';
                    positionInfo = `<span class="race-position ${positionClass}">Position: ${race.user_data.position}/${race.participants?.maximum || 0}</span>`;
                }
                
                // Prepare track information - use trackName if available, otherwise fall back to track_id
                const trackInfo = race.trackName 
                    ? `${race.trackName}${race.trackLength ? ` (${race.trackLength}mi)` : ''}`
                    : `ID ${race.track_id || 'Unknown'}`;
                
                // Title might be a generic "Off race by XYZ", try to improve it if we have track info
                let title = race.title || 'Untitled Race';
                if (race.trackName && title.startsWith('Off race by')) {
                    title = `${race.trackName} - ${title}`;
                }
                
                let descriptionHTML = '';
                if (race.trackDescription) {
                    descriptionHTML = `
                    <div class="race-info">
                        <div class="race-detail">
                            <div class="race-detail-label">Description:</div>
                            <div class="race-detail-value" style="font-style: italic; font-size: 0.9em;">${race.trackDescription}</div>
                        </div>
                    </div>`;
                }
                
                return `
                    <div class="race-entry${race.status === 'active' ? ' current-race' : ''}">
                        <div class="race-header">
                            <div class="race-title">
                                <a href="https://www.torn.com/loader.php?sid=racing&tab=log&raceID=${race.id}" target="_self">${title}</a>
                            </div>
                            <div class="race-id">ID: ${race.id || 'Unknown'} ${positionInfo}</div>
                        </div>
                        
                        <div class="race-info">
                            <div class="race-detail">
                                <div class="race-detail-label">Track:</div>
                                <div class="race-detail-value">${trackInfo}</div>
                            </div>
                            <div class="race-detail">
                                <div class="race-detail-label">Status:</div>
                                <div class="race-detail-value">${statusHTML}</div>
                            </div>
                            <div class="race-detail">
                                <div class="race-detail-label">Laps:</div>
                                <div class="race-detail-value">${race.laps || '0'}</div>
                            </div>
                        </div>
                        
                        <div class="race-info">
                            <div class="race-detail">
                                <div class="race-detail-label">Participants:</div>
                                <div class="race-detail-value">${race.participants?.current || '0'}/${race.participants?.maximum || '0'}</div>
                            </div>
                            <div class="race-detail">
                                <div class="race-detail-label">Start Time:</div>
                                <div class="race-detail-value">${race.schedule?.start ? Utils.formatTimestamp(race.schedule.start) : 'N/A'}</div>
                            </div>
                        </div>
                        ${descriptionHTML}
                    </div>
                `;
            }).join('');
            
            return currentRaceContent + pastRacesContent || '<div class="race-entry">No recent races found</div>';
        },
        
        // Initialize the user interface
        initializeUI(container) {
            const buttonContainer = document.createElement('div');
            buttonContainer.id = 'telemetryButtonContainer';
            buttonContainer.className = 'telemetry-button-container';

            const settingsButton = document.createElement('div');
            settingsButton.className = 'telemetry-button';
            settingsButton.textContent = 'Settings';
            settingsButton.addEventListener('click', () => {
                document.body.appendChild(UI.createSettingsPopup());
            });

            const statsButton = document.createElement('div');
            statsButton.className = 'telemetry-button';
            statsButton.textContent = 'Stats History';
            statsButton.addEventListener('click', () => {
                document.body.appendChild(UI.createStatsPopup());
                DataManager.fetchRacingStats(); // Refresh data when popup is opened
            });

            const racesButton = document.createElement('div');
            racesButton.className = 'telemetry-button';
            racesButton.textContent = 'Recent Races';
            racesButton.addEventListener('click', () => {
                document.body.appendChild(UI.createRacesPopup());
                DataManager.fetchRaces(); // Refresh data when popup is opened
            });

            buttonContainer.appendChild(settingsButton);
            buttonContainer.appendChild(statsButton);
            buttonContainer.appendChild(racesButton);

            container.prepend(buttonContainer);
        }
    };

    // ===== RACE MANAGER =====
    const RaceManager = {
        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*)/);
                
                // Extract the track name
                let trackName = '';
                if (trackHeader.textContent) {
                    const headerText = trackHeader.textContent.trim();
                    // The track name is typically before the first dash
                    const dashIndex = headerText.indexOf('-');
                    if (dashIndex > 0) {
                        trackName = headerText.substring(0, dashIndex).trim();
                    }
                }
                
                State.trackInfo = {
                    name: trackName,
                    laps: lapsMatch ? parseInt(lapsMatch[1]) : 5,
                    length: lengthMatch ? parseFloat(lengthMatch[1]) : 3.4,
                    description: infoElement?.dataset.desc || '',
                    get total() { return this.laps * this.length; }
                };
                
                // Check if we're viewing a race log by examining the URL
                const raceLogMatch = window.location.href.match(/raceID=(\d+)/);
                if (raceLogMatch && raceLogMatch[1]) {
                    const raceLogId = raceLogMatch[1];
                    
                    // Look for track info in current page
                    const raceTitle = document.querySelector('h4')?.textContent || '';
                    const trackSection = document.querySelector('.track-section');
                    if (trackSection) {
                        const nameElement = trackSection.querySelector('b');
                        const descElement = trackSection.querySelector('i');
                        const tracklengthElement = trackSection.querySelectorAll('li')[1]?.querySelector('span');
                        const lapsElement = trackSection.querySelectorAll('li')[2]?.querySelector('span');
                        
                        if (nameElement) {
                            const trackInfo = {
                                id: raceLogId,
                                name: nameElement.textContent.trim(),
                                description: descElement ? descElement.textContent.trim() : '',
                                length: tracklengthElement ? parseFloat(tracklengthElement.textContent.match(/(\d+\.?\d*)/)?.[1] || '0') : 0,
                                laps: lapsElement ? parseInt(lapsElement.textContent.trim() || '0') : 0
                            };
                            
                            // Save this track info to our race log
                            DataManager.updateRaceWithTrackInfo(raceLogId, trackInfo);
                        }
                    }
                }
            } catch (e) {
                console.error('Error updating track info:', e);
                State.trackInfo = { name: '', laps: 5, length: 3.4, total: 17, description: '' };
            }
            
            // Also update player profile info while we're here
            this.extractPlayerInfo();
        },
        
        extractPlayerInfo() {
            try {
                // Extract precise racing skill
                const skillElement = document.querySelector('.banner .skill');
                if (skillElement && skillElement.textContent) {
                    State.preciseSkill = parseFloat(skillElement.textContent.trim());
                }
                
                // Extract car class
                const classElement = document.querySelector('.banner .class-letter');
                if (classElement && classElement.textContent) {
                    State.carClass = classElement.textContent.trim();
                }
                
                // Extract current car info
                const carModelElements = document.querySelectorAll('.model-wrap .model p');
                if (carModelElements.length >= 2) {
                    State.currentCar = {
                        name: carModelElements[0].textContent.trim(),
                        type: carModelElements[1].textContent.trim()
                    };
                }
                
                // Extract car stats
                const carStats = {};
                const statElements = document.querySelectorAll('.properties-wrap li');
                statElements.forEach(statElement => {
                    const titleElement = statElement.querySelector('.title');
                    if (titleElement) {
                        const statName = titleElement.textContent.trim();
                        const progressElement = statElement.querySelector('.progressbar');
                        if (progressElement) {
                            const style = progressElement.getAttribute('style');
                            if (style) {
                                const widthMatch = style.match(/width:\s*(\d+\.?\d*)%/);
                                if (widthMatch && widthMatch[1]) {
                                    carStats[statName.toLowerCase()] = parseFloat(widthMatch[1]);
                                }
                            }
                        }
                    }
                });
                
                if (Object.keys(carStats).length > 0) {
                    State.currentCar = State.currentCar || {};
                    State.currentCar.stats = carStats;
                }
                
                // Try to extract current race ID
                const driverElement = document.querySelector('#leaderBoard li[data-id]');
                if (driverElement) {
                    const dataId = driverElement.getAttribute('data-id');
                    if (dataId) {
                        const raceIdMatch = dataId.match(/^(\d+)-/);
                        if (raceIdMatch && raceIdMatch[1]) {
                            State.currentRaceId = raceIdMatch[1];
                        }
                    }
                }
            } catch (e) {
                console.error('Error extracting player info:', e);
            }
        },
        
        setupPeriodicCheck() {
            if (State.periodicCheckIntervalId) return;
            State.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);
                        Telemetry.updateDriverDisplay(driverEl, text, progress);
                    });
                } catch (e) {
                    console.error('Periodic check error:', e);
                }
            }, Config.get('periodicCheckInterval'));
        },
        
        observeDrivers() {
            State.observers.forEach(obs => obs.disconnect());
            State.observers = [];
            const drivers = document.querySelectorAll('#leaderBoard li[id^="lbr-"]');
            drivers.forEach(driverEl => {
                const timeEl = driverEl.querySelector('.time');
                if (!timeEl) return;
                Telemetry.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) {
                            Telemetry.updateDriverDisplay(driverEl, text, progress);
                        }
                    } catch (e) {
                        console.error('Mutation observer error:', e);
                    }
                });
                observer.observe(timeEl, { childList: true, subtree: true, characterData: true });
                State.observers.push(observer);
            });
        },
        
        initializeLeaderboard() {
            const leaderboard = document.getElementById('leaderBoard');
            if (!leaderboard) return;
            if (leaderboard.children.length) {
                this.observeDrivers();
                this.setupPeriodicCheck();
            } else {
                new MutationObserver((_, obs) => {
                    if (leaderboard.children.length) {
                        this.observeDrivers();
                        this.setupPeriodicCheck();
                        obs.disconnect();
                    }
                }).observe(leaderboard, { childList: true });
            }
        },
        
        detectRaceLogPage() {
            try {
                const raceLogMatch = window.location.href.match(/raceID=(\d+)/);
                if (!raceLogMatch || !raceLogMatch[1]) return;
                
                // We're on a race log page
                const raceId = raceLogMatch[1];
                console.log('Detected race log page for race ID:', raceId);
                
                // Extract race info from the page
                const raceTitle = document.querySelector('h4')?.textContent?.trim() || '';
                const trackSection = document.querySelector('.track-section');
                
                if (trackSection) {
                    const nameElement = trackSection.querySelector('b');
                    const descElement = trackSection.querySelector('i');
                    const tracklengthElement = trackSection.querySelectorAll('li')[1]?.querySelector('span');
                    const lapsElement = trackSection.querySelectorAll('li')[2]?.querySelector('span');
                    
                    if (nameElement) {
                        const trackInfo = {
                            name: nameElement.textContent.trim(),
                            description: descElement ? descElement.textContent.trim() : '',
                            length: tracklengthElement ? parseFloat(tracklengthElement.textContent.match(/(\d+\.?\d*)/)?.[1] || '0') : 0,
                            laps: lapsElement ? parseInt(lapsElement.textContent.trim() || '0') : 0
                        };
                        
                        // First check if we already have this race in our log
                        const existingRace = State.raceLog.find(r => r.id === raceId);
                        if (!existingRace) {
                            // We don't have this race yet, add it with the track info
                            const newRace = {
                                id: raceId,
                                title: raceTitle,
                                trackName: trackInfo.name,
                                trackLength: trackInfo.length,
                                trackDescription: trackInfo.description,
                                laps: trackInfo.laps,
                                fetchedAt: Date.now()
                            };
                            
                            State.raceLog.unshift(newRace);
                            State.raceLog = State.raceLog.slice(0, 50);
                            State.save('raceLog');
                        } else {
                            // Update the existing race with track info
                            DataManager.updateRaceWithTrackInfo(raceId, trackInfo);
                        }
                    }
                }
            } catch (e) {
                console.error('Error detecting race log page:', e);
            }
        }
    };

    // ===== MAIN APPLICATION =====
    const App = {
        initialize() {
            try {
                // Initialize configs and state
                Config.load();
                State.init();
                UI.setupStyles();
                
                const container = document.querySelector('.cont-black');
                if (!container) throw new Error('Container not found');
                
                const urlRaceId = window.location.href.match(/sid=racing.*?(?=&|$)/)?.[0] || 'default';
                if (State.currentRaceId !== urlRaceId) {
                    State.resetRace();
                    State.currentRaceId = urlRaceId;
                }
                
                if (container.querySelector('#telemetryButtonContainer')) {
                    RaceManager.updateTrackInfo();
                    RaceManager.initializeLeaderboard();
                    return;
                }

                UI.initializeUI(container);
                RaceManager.updateTrackInfo();
                RaceManager.initializeLeaderboard();
                
                // Don't fetch API data right away - defer it slightly to allow the page to render first
                setTimeout(() => {
                    DataManager.fetchRacingStats().then(() => {
                        DataManager.fetchRaces();
                    });
                }, 1000);
                
                // Check if we're on a race log page
                RaceManager.detectRaceLogPage();
            } catch (e) {
                console.error('Initialization failed:', e);
            }
        },
    };

    // Setup observers for page changes
    const racingUpdatesObserver = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.id === 'racingupdates') {
                    App.initialize();
                }
            });
            mutation.removedNodes.forEach(node => {
                if (node.nodeType === 1 && node.id === 'racingupdates') {
                    State.resetRace();
                }
            });
        });
    });

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址