Torn Racing Telemetry

Track racing data and show acceleration status

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

// ==UserScript==
// @name         Torn Racing Telemetry
// @namespace    https://www.torn.com/profiles.php?XID=2782979
// @version      1.7.3.3
// @description  Track racing data and show acceleration status
// @match        https://www.torn.com/page.php?sid=racing*
// @match        https://www.torn.com/loader.php?sid=racing*
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==
 
(() => {
    // Default settings
    const defaultConfig = {
        updateInterval: 1000,
        showSpeed: true,
        showAcceleration: false,
        colorCode: true,
        animateChanges: true
    };
 
    // Get or initialize settings
    let config = { ...defaultConfig, ...GM_getValue('racingTelemetryConfig', {}) };
 
    // Create settings panel
    const createSettingsPanel = () => {
        const panel = document.createElement('div');
        panel.className = 'telemetry-settings';
        panel.innerHTML = `
            <div class="settings-header">
                <span>Racing Telemetry Settings</span>
                <button class="toggle-settings">▼</button>
            </div>
            <div class="settings-content" style="display:none">
                <label>
                    <input type="radio" name="speedAcceleration" value="speed" ${config.showSpeed ? 'checked' : ''} data-setting="showSpeed">
                      Show Speed
                    </label>
                    <label>
                    <input type="radio" name="speedAcceleration" value="acceleration" ${config.showAcceleration ? 'checked' : ''} data-setting="showAcceleration">
                    Show Acceleration
                </label>
                <label>
                    <input type="checkbox" ${config.colorCode ? 'checked' : ''} data-setting="colorCode">
                    Color Code Status Text
                </label>
                <label>
                    <input type="checkbox" ${config.animateChanges ? 'checked' : ''} data-setting="animateChanges">
                    Animate Speed Changes
                </label>
                <label>
                    Update Interval (ms):
                    <input type="number" value="${config.updateInterval}" min="100" max="2000" step="100" data-setting="updateInterval">
                    <p>Higher = Smoother | Lower = Accurate</p>
                    <p>Short/Official Races: 500ms-2000ms</p>
                    <p>50-100 Lap Races: 2000ms-5000ms</p>
                    <p>This update setting will be removed shortly when I figure out how to make it automatic.</p>
                </label>
            </div>
        `;
 
        // Add event listeners
        const content = panel.querySelector('.settings-content');
        panel.querySelector('.toggle-settings').addEventListener('click', (e) => {
            content.style.display = content.style.display === 'none' ? 'block' : 'none';
            e.target.textContent = content.style.display === 'none' ? '▼' : '▲';
        });
 
        panel.querySelectorAll('input').forEach(input => {
            input.addEventListener('change', () => {
                const setting = input.dataset.setting;
                if (input.type === 'radio') {
                    config.speedAcceleration = input.value;
                    config.showSpeed = input.value === "speed";
                    config.showAcceleration = input.value === "acceleration";
                } else {
                    config[setting] = input.checked ? input.checked : Number(input.value);
                }
                GM_setValue('racingTelemetryConfig', config);
                if (setting === 'updateInterval') {
                    clearInterval(updateInterval);
                    updateInterval = setInterval(update, config.updateInterval);
                }
            });
        });
 
        return panel;
    };
 
    const prev = {};
 
    const getTrack = () => {
        const info = document.querySelector('.track-info');
        const len = info ? parseFloat(info.dataset.length.replace(/mi/, '')) : 3.4; // Convert miles to a number
        // Select the second .title-black element
        const titleBlack = document.querySelectorAll('.title-black')[1];
        const lapsMatch = titleBlack?.textContent.match(/(\d+)\s+laps?/);
        const laps = lapsMatch ? parseInt(lapsMatch[1]) : 5;
        //console.log(`Track Length: ${len} miles, Laps: ${laps}`);
        return { total: len * laps };
    };
 
    const parsePercentage = pctStr => {
        return parseFloat(pctStr.replace('%', '')) / 100;
    };
 
    const calcMetrics = (pctStr, id, now) => {
        const pct = parsePercentage(pctStr);
        //console.log(`ID: ${id}, PCT: ${pct}, Now: ${now}`);
        if (!prev[id]) {
            prev[id] = { pct, time: now, spd: 0, acc: 'calculating...' };
            //console.log(`Initialized prev[${id}]:`, prev[id]);
            return { spd: 0, acc: 'calculating...' };
        }
 
        const td = (now - prev[id].time) / 1000;
        //console.log(`Time Difference: ${td}`);
        if (td <= 0) {
            //console.log(`Time difference is zero or negative, skipping calculation for ID: ${id}`);
            return prev[id];
        }
 
        const track = getTrack();
        const distanceCovered = track.total * (pct - prev[id].pct);
        //console.log(`Distance Covered: ${distanceCovered}`);
        const spd = (distanceCovered / td) * 3600; // Speed in mph
        //console.log(`Speed: ${spd}`);
        const acc = Math.abs(spd - prev[id].spd) < 1 ? 'steady' :
                   spd > prev[id].spd + 1 ? 'accelerating' : 'braking';
        //console.log(`Acceleration: ${acc}`);
 
        prev[id] = { pct, time: now, spd, acc };
        //console.log(`Updated prev[${id}]:`, prev[id]);
        return { spd: Math.abs(spd), acc };
    };
 
    const updateStatus = (el, status, spd, fin = false, d = null) => {
        el.querySelector('.status-text')?.remove();
        if (!config.showSpeed && !config.showAcceleration) return;
 
        const stat = document.createElement('span');
        stat.className = 'status-text';
        stat.style.cssText = 'margin-left:10px;font-size:12px;';
 
        if (fin) {
            const finishTime = d.querySelector('.pd-laptime').textContent;
            const finishTimeSecs = parseTime(finishTime);
            const avgSpeed = (getTrack().total / finishTimeSecs) * 3600;
            stat.textContent = `[Finished - Avg: ${avgSpeed.toFixed(1)} mph]`;
            stat.style.color = config.colorCode ? 'green' : 'gray'; // Setting color for finished status
        } else if (status === 'not started') {
            stat.textContent = '[Not Started]';
            stat.style.color = config.colorCode ? 'gray' : 'gray'; // Setting color for not started status
        } else if (status === 'calculating...') {
            stat.textContent = 'calculating...';
            stat.style.color = config.colorCode ? 'gray' : 'gray'; // Setting color for calculating status
        } else {
            const speedText = config.showSpeed ? `${Math.abs(spd).toFixed(1)} mph` : '';
            const accText = config.showAcceleration ? status : '';
            stat.textContent = `[${speedText}${speedText && accText ? ' - ' : ''}${accText}]`;
 
            // Apply color coding based on the settings
            if (config.colorCode) {
                stat.style.color = status === 'accelerating' ? 'green' : status === 'braking' ? 'red' : 'gray';
            } else {
                stat.style.color = 'gray'; // Default color if color code is disabled
            }
        }
 
        el.appendChild(stat);
    };
 
    const animate = (el, start, end, dur) => {
        // Apply color coding based on the settings
        if (config.colorCode) {
            el.style.color = start<(end-5) ? 'green' : start>(end+5) ? 'red' : 'gray';
        } else {
            el.style.color = 'gray'; // Default color if color code is disabled
        }
        if (!config.animateChanges) {
            el.textContent = `[${end.toFixed(1)} mph]`;
            return;
        }
        const t0 = performance.now();
        const update = t => {
            const p = Math.min((t - t0) / dur, 1);
            el.textContent = `[${(start + (end - start) * p).toFixed(1)} mph]`;
            p < 1 && requestAnimationFrame(update);
        };
        requestAnimationFrame(update);
    };
 
    const parseTime = t => {
        const [m, s] = t.split(':').map(Number);
        return m * 60 + s;
    };
 
    const update = () => {
        document.querySelectorAll('#leaderBoard li[id^="lbr-"]').forEach(d => {
            const name = d.querySelector('.name');
            const time = d.querySelector('.time');
            if (!name || !time) return;
 
            const status = d.querySelector('.status-wrap div');
            const fin = ['finished', 'gold', 'silver', 'bronze'].some(c => status?.classList.contains(c));
 
            if (!time.textContent.trim()) {
                updateStatus(name, 'not started', 0);
            } else if (fin) {
                updateStatus(name, null, time.textContent, true, d);
            } else {
                const { spd, acc } = calcMetrics(time.textContent, d.id, Date.now());
                const statEl = name.querySelector('.status-text');
                if (statEl) {
                    if (config.showSpeed) {
                        const currentSpeed = parseFloat(statEl.textContent.replace(/[^\d.-]/g, ''));
                        if (!isNaN(currentSpeed)) {
                            animate(statEl, currentSpeed, spd, config.updateInterval);
                        } else {
                            //console.log(`Current speed is NaN, setting to ${spd.toFixed(1)} mph for ID: ${d.id}`);
                            statEl.textContent = `[${spd.toFixed(1)} mph]`;
                        }
                    } else if (config.showAcceleration) {
                        updateStatus(name, acc, spd); // Update with acceleration
                    }
                } else {
                    updateStatus(name, acc, spd); // Initial update
                }
            }
        });
    };
 
    // Add styles
    document.head.appendChild(Object.assign(document.createElement('style'), {
        textContent: `
            .telemetry-settings {
                background: #f4f4f4;
                border: 1px solid #ddd;
                border-radius: 4px;
                margin: 10px;
                padding: 5px;
                color: #000000;
                text-shadow: 0 0px 0px rgba(0, 0, 0, 0);
            }
            .settings-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 5px;
                cursor: pointer;
            }
            .settings-content {
                padding: 10px;
            }
            .settings-content label {
                display: block;
                margin: 5px 0;
            }
            .toggle-settings {
                background: none;
                border: none;
                cursor: pointer;
            }
            .status-text {
                display: inline-block;
                font-family: Arial,sans-serif;
                padding: 2px 5px;
                border-radius: 3px;
                background: rgba(0,0,0,.05);
            }
            .status-text {
                display: inline-block;
                font-family: Arial, sans-serif;
                padding: 2px 5px;
                border-radius: 3px;
                background: rgba(0, 0, 0, .05);
                transition: color 5s ease; /* Add transition for color changes */
            }
        `
    }));
 
    // Insert settings panel
    const container = document.querySelector('.cont-black');
    if (container) {
        container.insertBefore(createSettingsPanel(), container.firstChild);
    }
 
    // When the script loads
    const storedConfig = GM_getValue('racingTelemetryConfig', {});
    config.speedAcceleration = storedConfig.speedAcceleration || "speed"; // Default to "speed"
    config.showSpeed = config.speedAcceleration === "speed";
    config.showAcceleration = config.speedAcceleration === "acceleration";
 
    // Start update interval
    let updateInterval = setInterval(update, config.updateInterval);
    update();
})();