// ==UserScript==
// @name Torn Race Finder
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Makes it easier to find races that have the most drivers and start pretty soon
// @author defend [2683949]
// @match https://www.torn.com/page.php?sid=racing*
// @match https://www.torn.com/loader.php?sid=racing*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use-strict';
// --------------------------------------------------
// 1. --- STYLES ---
// --------------------------------------------------
GM_addStyle(`
/* Highlights */
.tcr-standard-highlight { background: rgba(100, 149, 237, 0.25) !important; box-shadow: inset 5px 0 0 0 rgba(100, 149, 237, 0.8) !important; }
.tcr-priority-highlight { background: rgba(60, 179, 113, 0.25) !important; box-shadow: inset 5px 0 0 0 rgba(46, 139, 87, 0.9) !important; border-radius: 5px; }
/* Dropdown UI styles */
.tcr-collapsible { cursor: pointer; user-select: none; }
.tcr-collapsible::after { content: ' \\25BE'; float: right; color: #777; margin-right: 8px;}
.tcr-collapsible.tcr-open::after { content: ' \\25B4'; }
.tcr-hidden { display: none; }
.tcr-settings-wrap { display: flex; flex-wrap: wrap; gap: 20px; padding: 20px 15px; }
.tcr-settings-section { display: flex; flex-direction: column; gap: 6px; }
.tcr-settings-section h4 { margin: 0 0 5px 0; padding-bottom: 5px; font-size: 14px; border-bottom: 1px solid #444; color: #ccc; }
.tcr-input-group { display: flex; align-items: center; gap: 6px; }
.tcr-input-group label { color: #ddd; }
.tcr-settings-section input[type="number"] { width: 50px; background-color: #333; border: 1px solid #555; color: white; padding: 3px; border-radius: 3px;}
.tcr-rank-text-tag { font-family: monospace; background-color: rgba(0, 0, 0, 0.5); color: #e0e0e0; padding: 2px 8px; border-radius: 4px; font-size: 11px; letter-spacing: 1px; }
.tcr-race-hidden { display: none !important; }
`);
// --------------------------------------------------
// 2. --- SETTINGS MANAGEMENT ---
// --------------------------------------------------
const DEFAULTS = { tracks: ['Speedway', 'Withdrawal', 'Docks'], laps: 100, cars: ['Any car', 'Any class A car'], hideUnmatched: false };
function getSettings() { return GM_getValue('tcrSettings', DEFAULTS); }
function saveSettings() {
const tracks = Array.from(document.querySelectorAll('.tcr-track-cb:checked')).map(cb => cb.value);
const laps = parseInt(document.getElementById('tcr-laps-input').value, 10);
const cars = Array.from(document.querySelectorAll('.tcr-car-cb:checked')).map(cb => cb.value);
const hideUnmatched = document.getElementById('tcr-hide-cb').checked;
GM_setValue('tcrSettings', { tracks, laps, cars, hideUnmatched });
findAndHighlightRaces();
}
// --------------------------------------------------
// 3. --- UI CREATION ---
// --------------------------------------------------
function createSettingsUI() {
const anchorElement = document.querySelector('#racingAdditionalContainer .start-race');
if (!anchorElement || document.querySelector('.tcr-settings-panel')) return;
const settings = getSettings();
const uiState = GM_getValue('tcrUiState', 'closed');
const allTracks = ["Uptown", "Withdrawal", "Docks", "Speedway", "Meltdown", "Two Islands", "Industrial", "Vector", "Mudpit", "Parkland", "Hammerhead", "Sewage", "Underdog", "Stone Park", "Convict", "Commerce"];
const allCarTypes = ['Any car', 'Any class A car', 'Any class B car', 'Any class C car', 'Any class D car', 'Any class E car'];
const panel = document.createElement('div');
panel.className = 'messages-race-wrap tcr-settings-panel';
panel.innerHTML = `
<div class="title-black top-round m-top10 tcr-collapsible ${uiState === 'open' ? 'tcr-open' : ''}">Race Finder Settings</div>
<div class="cont-black bottom-round ${uiState === 'closed' ? 'tcr-hidden' : ''}">
<div class="tcr-settings-wrap">
<div class="tcr-settings-section">
<h4>Tracks</h4>
<div style="columns: 2; gap: 20px;">${allTracks.map(track => `<div class="tcr-input-group"><input type="checkbox" class="tcr-track-cb" value="${track}" ${settings.tracks.includes(track) ? 'checked' : ''}><label>${track}</label></div>`).join('')}</div>
</div>
<div class="tcr-settings-section">
<h4>Options</h4>
<div class="tcr-input-group"><label for="tcr-laps-input">Laps:</label><input type="number" id="tcr-laps-input" value="${settings.laps}"></div><br>
${allCarTypes.map(car => `<div class="tcr-input-group"><input type="checkbox" class="tcr-car-cb" value="${car}" ${settings.cars.includes(car) ? 'checked' : ''}><label>${car}</label></div>`).join('')}
<br>
<hr style="width:100%; border-color: #444; margin-top: 10px;">
<div class="tcr-input-group">
<input type="checkbox" id="tcr-hide-cb" ${settings.hideUnmatched ? 'checked' : ''}>
<label for="tcr-hide-cb"><b>Hide Unmatched Races</b></label>
</div>
</div>
</div>
</div>`;
anchorElement.after(panel);
const titleElement = panel.querySelector('.tcr-collapsible');
titleElement.addEventListener('click', () => {
const content = titleElement.nextElementSibling;
const isHidden = content.classList.toggle('tcr-hidden');
titleElement.classList.toggle('tcr-open', !isHidden);
GM_setValue('tcrUiState', isHidden ? 'closed' : 'open');
});
panel.querySelectorAll('input').forEach(el => el.addEventListener('change', saveSettings));
}
// --------------------------------------------------
// 4. --- CORE LOGIC (with updated scoring formula) ---
// --------------------------------------------------
function findAndHighlightRaces() {
const SETTINGS = getSettings();
const raceListItems = document.querySelectorAll('.events-list > li');
if (!raceListItems.length) return;
let matchingRaces = [];
document.querySelectorAll('li.name[data-original-name]').forEach(el => {
el.innerHTML = el.getAttribute('data-original-name');
el.removeAttribute('data-original-name'); el.removeAttribute('title');
});
for (const raceLi of raceListItems) {
raceLi.classList.remove('tcr-standard-highlight', 'tcr-priority-highlight', 'tcr-race-hidden');
if (!raceLi.querySelector('.event-header')) continue;
let isMatch = false;
const driversText = raceLi.querySelector('.drivers')?.textContent || '0 / 0';
const driverParts = driversText.split('/');
const currentDrivers = parseInt((driverParts[0] || '').replace(/\D/g, ''), 10) || 0;
const maxDrivers = parseInt(driverParts[1] || '0', 10);
if (!(currentDrivers >= maxDrivers || maxDrivers <= 2 || raceLi.classList.contains('protected'))) {
const trackName = raceLi.querySelector('.track')?.childNodes[0]?.nodeValue?.trim() || '';
const laps = parseInt(raceLi.querySelector('.track .laps')?.textContent?.match(/\d+/)?.[0] || '0', 10);
const carText = raceLi.querySelector('.car .t-hide')?.textContent?.trim() || '';
let carRequirementMet = false;
for (const selectedCar of SETTINGS.cars) {
if (carText === selectedCar) { carRequirementMet = true; break; }
if (selectedCar.startsWith('Any class')) {
if (carText === `Any stock${selectedCar.substring(3)}`) { carRequirementMet = true; break; }
}
}
if (SETTINGS.tracks.includes(trackName) && laps === SETTINGS.laps && carRequirementMet) {
isMatch = true;
const timeInMinutes = parseTimeToMinutes(raceLi.querySelector('.startTime')?.textContent?.trim() || 'waiting');
// --- ** NEW DRIVER-FOCUSED SCORING FORMULA ** ---
const score = (timeInMinutes === Infinity) ? 0 : (currentDrivers * currentDrivers) / (timeInMinutes + 1);
matchingRaces.push({ element: raceLi, score });
}
}
if (SETTINGS.hideUnmatched && !isMatch) raceLi.classList.add('tcr-race-hidden');
}
if (!matchingRaces.length) return;
const sortedRaces = matchingRaces.sort((a, b) => b.score - a.score);
sortedRaces.forEach((race, index) => {
if (race.score <= 0) { race.element.classList.add('tcr-standard-highlight'); return; }
if (index === 0) { race.element.classList.add('tcr-priority-highlight'); } else { race.element.classList.add('tcr-standard-highlight'); }
if (index === 1 || index === 2) {
const nameElement = race.element.querySelector('li.name');
if (nameElement && !nameElement.hasAttribute('data-original-name')) {
const originalName = nameElement.innerHTML;
nameElement.setAttribute('data-original-name', originalName);
nameElement.setAttribute('title', `Original Name: ${originalName.replace(/<[^>]*>/g, '')}`);
const rankText = `---- ${index === 1 ? '2ND' : '3RD'} BEST ----`;
nameElement.innerHTML = `<span class="tcr-rank-text-tag">${rankText}</span>`;
}
}
});
}
// --------------------------------------------------
// 5. --- HELPERS & INITIALIZATION ---
// --------------------------------------------------
function parseTimeToMinutes(timeStr) {
if (!timeStr || timeStr.toLowerCase() === 'waiting') return Infinity;
let totalMinutes = 0;
const h = timeStr.match(/(\d+)\s*h/);
const m = timeStr.match(/(\d+)\s*m/);
if (h) totalMinutes += parseInt(h[1], 10) * 60;
if (m) totalMinutes += parseInt(m[1], 10);
return totalMinutes;
}
setInterval(() => {
createSettingsUI();
if (document.querySelector('.custom-events-wrap')) {
findAndHighlightRaces();
}
}, 500);
})();