Torn Racing Telemetry

Track racing data and show acceleration status

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();