// ==UserScript==
// @name Torn Racing Telemetry
// @namespace https://www.torn.com/profiles.php?XID=2782979
// @version 1.7.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
</label>
<label>
<input type="checkbox" ${config.animateChanges ? 'checked' : ''} data-setting="animateChanges">
Animate Changes
</label>
<label>
Update Interval (ms):
<input type="number" value="${config.updateInterval}" min="100" max="2000" step="100" data-setting="updateInterval">
</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 = '[]';
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);
}
`
}));
// 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();
})();