您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copy race data as CSV from racing log page
// ==UserScript== // @name Torn Race Data Button // @namespace underko.torn.scripts.racing // @version 1.0 // @author underko[3362751] // @description Copy race data as CSV from racing log page // @match https://www.torn.com/loader.php?sid=racing&tab=log&raceID=* // @grant GM.setClipboard // @require https://code.jquery.com/jquery-3.6.0.min.js // @license MIT // ==/UserScript== /* global $ */ (function () { 'use strict'; const observer = new MutationObserver(() => { if (document.querySelector('#racingdetails') && allRequiredDataPresent() && !document.querySelector('#copyRaceCsvBtn')) { addButton(); } }); observer.observe(document.body, { childList: true, subtree: true }); function allRequiredDataPresent() { const skillText = $('.skill').first().text().trim(); const hasSkill = /^([\d.]+).*Last gain:\s*([\d.]+)/.test(skillText); const hasPosition = $('#racingdetails .pd-position').length > 0; const hasLap = $('#racingdetails .pd-lap').length > 0; const hasTime = $('#racingdetails .pd-completion').length > 0; const titles = $('.properties .title').toArray().map(el => $(el).text().trim()); const hasTrack = titles.some(t => t.startsWith('Track:')); const hasType = titles.some(t => t.startsWith('Type:')); return hasSkill && hasPosition && hasLap && hasTime && hasTrack && hasType; } function addButton() { const button = $('<button id="copyRaceCsvBtn">') .text('Copy Race CSV') .css({ marginLeft: '10px', padding: '4px 8px', background: '#28a745', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', position: 'absolute' }) .click(copyRaceDataToClipboard); $('.content-title').first().prepend(button); } function formatCompletionTime(completionTime) { const parts = completionTime.split(':'); if (parts.length === 2) { // Format is MM:SS.ms → prepend "00:" return `00:${completionTime.padStart(8, '0')}`; } else if (parts.length === 3) { // Format is HH:MM:SS.ms → normalize with leading zeros const [h, m, s] = parts; const [sec, ms = '00'] = s.split('.'); return `${h.padStart(2, '0')}:${m.padStart(2, '0')}:${sec.padStart(2, '0')}.${ms.padEnd(2, '0')}`; } else { return '00:00:00.00'; // fallback for invalid formats } } function copyRaceDataToClipboard() { const skillText = $('.skill').first().text().trim(); const skillMatch = skillText.match(/^([\d.]+).*Last gain:\s*([\d.]+)/); const currentRS = skillMatch ? skillMatch[1] : ''; const rsGain = skillMatch ? skillMatch[2] : ''; const posText = $('#racingdetails .pd-position').text().trim(); const lapText = $('#racingdetails .pd-lap').text().trim(); const completionTime = $('#racingdetails .pd-completion').text().trim(); const [position, racers] = posText.split('/').map(x => x.trim()); const [_, totalLaps] = lapText.split('/').map(x => x.trim()); const timeFormatted = formatCompletionTime(completionTime); const titles = $('.properties .title').toArray().map(el => $(el).text().trim()); const typeText = titles.find(t => t.startsWith('Type:'))?.replace('Type:', '').trim() || ''; const trackText = titles.find(t => t.startsWith('Track:'))?.replace('Track:', '').trim() || ''; const dateTime = new Date().toISOString().slice(0, 19).replace('T', ' '); const csv = `${dateTime},${trackText},${totalLaps},${typeText},${racers},${position},${currentRS},${rsGain},${timeFormatted}`; GM.setClipboard(csv); console.log('Copied CSV:', csv); } })();