您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters and sorts races based on entry requirements and availability
// ==UserScript== // @name Torn: Find Good Race // @namespace torn.com // @version 0.11.1 // @description Filters and sorts races based on entry requirements and availability // @license MIT // @author YoYo // @match https://www.torn.com/page.php?sid=racing* // @grant none // ==/UserScript== (function () { 'use strict'; const BUTTON_ID = 'find-good-race-btn'; const SORT_BUTTON_ID = 'race-sort-toggle-btn'; const SORT_STORAGE_KEY = 'torn-race-sort-mode'; // 'timeAsc' | 'driversDesc' const SORT_MODES = ['timeAsc', 'driversDesc']; // persistent sort mode function getSortMode() { const saved = localStorage.getItem(SORT_STORAGE_KEY); return SORT_MODES.includes(saved) ? saved : 'timeAsc'; } function setSortMode(mode) { localStorage.setItem(SORT_STORAGE_KEY, mode); } function cycleSortMode() { const i = SORT_MODES.indexOf(getSortMode()); const next = SORT_MODES[(i + 1) % SORT_MODES.length]; setSortMode(next); } function modeLabel(mode = getSortMode()) { return mode === 'driversDesc' ? 'Sorting: Drivers ↓' : 'Sorting: Start Time ↑'; } let lastSortable = []; // cache of filtered races for quick re-sorting let lastUl = null; // cache of list element for re-render const waitForTargets = setInterval(() => { const raceList = document.querySelector('#racingAdditionalContainer > div.custom-events-wrap > div.cont-black.bottom-round > ul'); const controlDiv = document.querySelector('.cont-black.bottom-round .btn-wrap'); if (raceList && controlDiv && !document.getElementById(BUTTON_ID)) { clearInterval(waitForTargets); injectFilterAndSortButtons(controlDiv, raceList); } }, 300); function injectFilterAndSortButtons(targetContainer, raceListUl) { // --- Find Good Race button (original) --- const btn = document.createElement('button'); btn.id = BUTTON_ID; btn.textContent = 'Find Good Race'; btn.style.cssText = baseBtnCss(); // --- Sort toggle button --- const sortBtn = document.createElement('button'); sortBtn.id = SORT_BUTTON_ID; sortBtn.textContent = 'Sort'; sortBtn.style.cssText = baseBtnCss() + 'margin-left: 8px;'; sortBtn.title = modeLabel(); sortBtn.addEventListener('click', () => { cycleSortMode(); sortBtn.title = modeLabel(); if (lastSortable.length && lastUl) { sortAndRender(lastUl, lastSortable, getSortMode()); } }); btn.addEventListener('click', () => { const ul = raceListUl; lastUl = ul; const allLis = Array.from(ul.querySelectorAll(':scope > li')); const allowedCars = ['any car', 'any class a car']; // Build sortable records while filtering/hiding the rest const sortableLis = allLis.map(li => { // ❌ Password protected if (li.querySelector('.event-info .password.protected')) { li.style.display = 'none'; return null; } // ❌ Car restriction const carLi = li.querySelector('.event-info .car'); const carText = carLi?.textContent?.toLowerCase() || ''; const isAllowed = allowedCars.some(allowed => carText.includes(allowed)); if (!isAllowed) { li.style.display = 'none'; return null; } // ❌ Full or low-capacity; also capture current drivers for sorting const driverLi = li.querySelector('.body-container .drivers'); const match = driverLi?.textContent?.match(/(\d+)\s*\/\s*(\d+)/); let current = NaN, max = NaN; if (match) { current = parseInt(match[1], 10); max = parseInt(match[2], 10); if (current >= max || max < 50) { li.style.display = 'none'; return null; } } // ✅ Parse time const timeEl = li.querySelector('.body-container .startTime'); if (!timeEl) { li.style.display = 'none'; return null; } const rawText = (timeEl.textContent || '').trim(); // show visible; keep metrics for sorting li.style.display = ''; return { li, drivers: Number.isFinite(current) ? current : -1, timeText: rawText, seconds: parseTimeToSeconds(rawText) }; }).filter(Boolean); lastSortable = sortableLis; // Apply current sort mode sortAndRender(ul, sortableLis, getSortMode()); console.log(`✅ Done. ${sortableLis.length} races shown. (${modeLabel()})`); }); // Insert both buttons after the "START A CUSTOM RACE" button row targetContainer.parentElement.appendChild(btn); targetContainer.parentElement.appendChild(sortBtn); } function baseBtnCss() { return ` margin-top: 10px; height: 32px; padding: 0 12px; font-size: 14px; font-family: "Fjalla One", Arial, serif; text-transform: uppercase; border-radius: 5px; cursor: pointer; color: #EEE; background: linear-gradient(180deg, #111111 0%, #555555 25%, #333333 60%, #333333 78%, #111111 100%); border: 1px solid #111; `; } function sortAndRender(ul, items, mode) { if (!ul || !items?.length) return; if (mode === 'driversDesc') { items.sort((a, b) => { // more drivers first; tie-breaker earliest start time const d = (b.drivers ?? -1) - (a.drivers ?? -1); if (d !== 0) return d; return (a.seconds ?? Number.POSITIVE_INFINITY) - (b.seconds ?? Number.POSITIVE_INFINITY); }); console.log('🔃 Sorting good races by number of drivers (desc)...'); } else { // 'timeAsc' items.sort((a, b) => { // earliest start first; tie-breaker more drivers const t = (a.seconds ?? Number.POSITIVE_INFINITY) - (b.seconds ?? Number.POSITIVE_INFINITY); if (t !== 0) return t; return (b.drivers ?? -1) - (a.drivers ?? -1); }); console.log('🔃 Sorting good races by start time (asc)...'); } // Re-append in sorted order const frag = document.createDocumentFragment(); for (const obj of items) frag.appendChild(obj.li); ul.appendChild(frag); } function parseTimeToSeconds(text) { if (!text) return Number.POSITIVE_INFINITY; const lower = text.toLowerCase(); if (lower.includes('waiting')) return 999999; // Supports patterns like: "1h 23m", "45m", "2h" let total = 0; const hrMatch = text.match(/(\d+)\s*h/); const minMatch = text.match(/(\d+)\s*m/); if (hrMatch) total += parseInt(hrMatch[1], 10) * 3600; if (minMatch) total += parseInt(minMatch[1], 10) * 60; if (total === 0) return Number.POSITIVE_INFINITY; return total; } })();