您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Input exact player names. When logging is ON, the script waits until each tracked player's race row (exact match) shows a finish time ("secs") then logs a merged summary with sequential numbering. It computes cumulative averages (tiers from rounded averages) and updates a cumulative score (e.g., "1-0" if player A wins). The UI shows total races & current score, allows deletion, and exports formatted results. Data persists.
当前为
// ==UserScript== // @name NitroType Tier Test and Duel Logger // @namespace http://tampermonkey.net/ // @version 1.15.1 // @description Input exact player names. When logging is ON, the script waits until each tracked player's race row (exact match) shows a finish time ("secs") then logs a merged summary with sequential numbering. It computes cumulative averages (tiers from rounded averages) and updates a cumulative score (e.g., "1-0" if player A wins). The UI shows total races & current score, allows deletion, and exports formatted results. Data persists. // @license MIT // @match *://www.nitrotype.com/* // @grant none // ==/UserScript== (function() { 'use strict'; // --- Global Variables --- let selectedPlayers = JSON.parse(localStorage.getItem("nitroSelectedPlayers") || "[]"); const RESULTS_KEY = "nitroTierTests"; const LOGGING_STATE_KEY = "nitroLoggingActive"; const CUM_STATS_KEY = "nitroCumulativeStats"; // For cumulative stats (sums & counts) const SCORE_KEY = "nitroScore"; // For cumulative score let observer = null; let isLogging = false; // ======================== // Tier Calculation Helper (with rounding) // ======================== function getTier(avgWpm) { let rounded = Math.round(avgWpm); if (rounded >= 175) return "HT1"; else if (rounded >= 160) return "LT1"; else if (rounded >= 145) return "HT2"; else if (rounded >= 130) return "LT2"; else if (rounded >= 115) return "HT3"; else if (rounded >= 100) return "LT3"; else if (rounded >= 85) return "HT4"; else if (rounded >= 70) return "LT4"; else if (rounded >= 50) return "HT5"; else return "LT5"; } // ======================== // Cumulative Score & Stats Helpers // ======================== function getCumulativeScore() { return JSON.parse(localStorage.getItem(SCORE_KEY) || '{"A":0,"B":0}'); } function saveCumulativeScore(scoreObj) { localStorage.setItem(SCORE_KEY, JSON.stringify(scoreObj)); } function getCumulativeStats() { return JSON.parse(localStorage.getItem(CUM_STATS_KEY) || "{}"); } function saveCumulativeStats(stats) { localStorage.setItem(CUM_STATS_KEY, JSON.stringify(stats)); } // Updates cumulative stats by adding the data from a new race's entries. function updateCumulativeStats(newRaceEntries) { let stats = getCumulativeStats(); newRaceEntries.forEach(entry => { const name = entry.username; const wpm = parseFloat(entry.wpm); const acc = parseFloat(entry.accuracy); if (!stats[name]) { stats[name] = { sumWpm: 0, sumAcc: 0, count: 0 }; } stats[name].sumWpm += isNaN(wpm) ? 0 : wpm; stats[name].sumAcc += isNaN(acc) ? 0 : acc; stats[name].count += 1; }); saveCumulativeStats(stats); } function getCumulativeAverages() { let stats = getCumulativeStats(); let averages = {}; selectedPlayers.forEach(name => { if (stats[name] && stats[name].count > 0) { averages[name] = { avgWpm: (stats[name].sumWpm / stats[name].count).toFixed(2), avgAcc: (stats[name].sumAcc / stats[name].count).toFixed(2) }; } else { averages[name] = { avgWpm: "N/A", avgAcc: "N/A" }; } }); return averages; } // ======================== // Storage Helpers for Race Results // ======================== function getStoredResults() { return JSON.parse(localStorage.getItem(RESULTS_KEY) || "[]"); } function saveStoredResults(results) { localStorage.setItem(RESULTS_KEY, JSON.stringify(results)); } // ======================== // Helper: Extract Exact Player Name from a Row // ======================== function getPlayerNameFromRow(row) { const container = row.querySelector('.raceResults-playerName .player-name--container'); if (!container) return ""; const span = container.querySelector('span.type-ellip'); if (span) return span.textContent.trim(); const anchor = container.querySelector('a'); if (anchor) return anchor.textContent.replace(/[\[\]]/g, '').trim(); return container.textContent.trim(); } // ======================== // UI Creation // ======================== function createUI() { if (document.getElementById('nt-tier-test-logger-ui')) return; const container = document.createElement('div'); container.id = 'nt-tier-test-logger-ui'; container.style.position = 'fixed'; container.style.top = '10px'; container.style.right = '10px'; container.style.zIndex = '10000'; container.style.backgroundColor = '#fff'; container.style.border = '1px solid #ccc'; container.style.padding = '10px'; container.style.fontFamily = 'Arial, sans-serif'; container.style.fontSize = '14px'; container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)'; container.style.maxWidth = '250px'; container.style.maxHeight = '80vh'; container.style.overflowY = 'auto'; // The header now includes race count and current score. container.innerHTML = ` <div style="margin-bottom: 8px; font-weight: bold;">Tier Test Logger</div> <!-- Player Management Section --> <div> <input type="text" id="playerNameInput" placeholder="Enter exact player name" style="width: 150px; padding: 5px;" /> <button id="addPlayerButton" style="padding: 5px;">Add</button> <button id="clearPlayersBtn" style="padding: 5px;">Clear Players</button> <div id="playersList" style="margin-top: 8px; border: 1px solid #eee; padding: 5px; max-height: 100px; overflow-y: auto;"></div> </div> <hr style="margin: 8px 0;"> <!-- Logging Controls Section --> <div> <button id="toggleLoggingBtn" style="padding: 5px;">Logging: OFF</button> <button id="saveResultsBtn" style="padding: 5px;">Save Results</button> <button id="clearResultsBtn" style="padding: 5px;">Clear Results</button> </div> <hr style="margin: 8px 0;"> <!-- Race Results Header --> <div style="font-weight: bold; margin-bottom: 5px;"> Race Results (<span id="raceCount">0</span> Total) | Current Score: <span id="currentScore">0-0</span> </div> <!-- Race Results Display Section --> <div id="raceResultsUI" style="border: 1px solid #eee; padding: 5px; max-height: 200px; overflow-y: auto;"></div> `; document.body.appendChild(container); document.getElementById('addPlayerButton').addEventListener('click', () => { const input = document.getElementById('playerNameInput'); let player = input.value.trim(); if (player && !selectedPlayers.includes(player)) { selectedPlayers.push(player); localStorage.setItem("nitroSelectedPlayers", JSON.stringify(selectedPlayers)); updatePlayersList(); } input.value = ''; }); document.getElementById('clearPlayersBtn').addEventListener('click', () => { selectedPlayers = []; localStorage.setItem("nitroSelectedPlayers", JSON.stringify(selectedPlayers)); updatePlayersList(); }); document.getElementById('toggleLoggingBtn').addEventListener('click', toggleLogging); document.getElementById('saveResultsBtn').addEventListener('click', showResultsInNewTab); document.getElementById('clearResultsBtn').addEventListener('click', clearResults); updatePlayersList(); updateRaceResultsUI(); } function updatePlayersList() { const listDiv = document.getElementById('playersList'); listDiv.innerHTML = ''; if (selectedPlayers.length === 0) { listDiv.textContent = 'No players added.'; return; } const ul = document.createElement('ul'); selectedPlayers.forEach((player, index) => { const li = document.createElement('li'); li.style.fontSize = '12px'; li.textContent = player + " "; const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.style.fontSize = '10px'; removeBtn.style.marginLeft = '5px'; removeBtn.addEventListener('click', () => { selectedPlayers.splice(index, 1); localStorage.setItem("nitroSelectedPlayers", JSON.stringify(selectedPlayers)); updatePlayersList(); }); li.appendChild(removeBtn); ul.appendChild(li); }); listDiv.appendChild(ul); } function updateRaceResultsUI() { const container = document.getElementById('raceResultsUI'); const storedResults = getStoredResults(); container.innerHTML = ''; // Update race count and current score in the header. document.getElementById('raceCount').textContent = storedResults.length; let cumulativeScore = getCumulativeScore(); document.getElementById('currentScore').textContent = `${cumulativeScore.A || 0}-${cumulativeScore.B || 0}`; if (storedResults.length === 0) { container.textContent = 'No results yet.'; return; } const ul = document.createElement('ul'); storedResults.forEach((result, index) => { const li = document.createElement('li'); li.style.fontSize = '12px'; li.style.marginBottom = '3px'; li.innerHTML = `Race ${result.raceNumber}: ${result.details}. Cumulative Averages: ${result.cumAverages}. Score: ${result.score} `; const delBtn = document.createElement('button'); delBtn.textContent = "Delete"; delBtn.style.fontSize = '10px'; delBtn.style.marginLeft = '5px'; delBtn.style.backgroundColor = "red"; delBtn.style.color = "white"; delBtn.style.border = "none"; delBtn.style.padding = "2px 5px"; delBtn.addEventListener('click', () => deleteRace(index)); li.appendChild(delBtn); ul.appendChild(li); }); container.appendChild(ul); } // ======================== // Recalculate Cumulative Stats and Score // ======================== function recalcCumulative() { let results = getStoredResults(); let newStats = {}; // Rebuild cumulative stats from remaining races let newScore = { A: 0, B: 0 }; // Recalculate cumulative score results.forEach(race => { if (race.rawEntries && race.rawEntries.length > 0) { // Rebuild cumulative stats for each player in this race race.rawEntries.forEach(entry => { const username = entry.username; const wpm = parseFloat(entry.wpm); const acc = parseFloat(entry.accuracy); if (!newStats[username]) { newStats[username] = { sumWpm: 0, sumAcc: 0, count: 0 }; } newStats[username].sumWpm += isNaN(wpm) ? 0 : wpm; newStats[username].sumAcc += isNaN(acc) ? 0 : acc; newStats[username].count += 1; }); // Update score by checking which player won this race let winningEntry = race.rawEntries.find(entry => entry.winnerIndicator); if (winningEntry) { if (selectedPlayers[0] && winningEntry.username === selectedPlayers[0].trim()) { newScore.A += 1; } else if (selectedPlayers[1] && winningEntry.username === selectedPlayers[1].trim()) { newScore.B += 1; } } } }); saveCumulativeStats(newStats); saveCumulativeScore(newScore); } // ======================== // Delete Race Function (Updated) // ======================== function deleteRace(index) { let results = getStoredResults(); if (index < 0 || index >= results.length) return; results.splice(index, 1); // Reassign race numbers after deletion results.forEach((res, i) => { res.raceNumber = i + 1; }); saveStoredResults(results); // Recalculate cumulative stats and score based on the remaining races recalcCumulative(); updateRaceResultsUI(); console.log(`Deleted race #${index + 1} and recalculated cumulative stats and score.`); } // ======================== // Process Race Results // ======================== function processRaceResults() { const raceContainer = document.querySelector('.gridTable.gridTable--raceResults'); if (!raceContainer) return; if (raceContainer.dataset.processed === "true") return; const rows = Array.from(raceContainer.querySelectorAll('.gridTable-row')); let trackedRows = rows.filter(row => { const name = getPlayerNameFromRow(row); if (!selectedPlayers.includes(name)) return false; const statsContainer = row.querySelector('.split.split--flag.split--reverse .list.list--inline'); if (!statsContainer) return false; const listItems = statsContainer.querySelectorAll('.list-item'); return listItems.length >= 3 && listItems[2].textContent.includes("secs"); }); let expectedCount = rows.filter(row => selectedPlayers.includes(getPlayerNameFromRow(row))).length; if (expectedCount > 1 && trackedRows.length < expectedCount) return; raceContainer.dataset.processed = "true"; let resultsArray = getStoredResults(); let raceNumber = resultsArray.length > 0 ? (resultsArray[resultsArray.length - 1].raceNumber + 1) : 1; let newEntries = trackedRows.map(row => { const username = getPlayerNameFromRow(row); const statsContainer = row.querySelector('.split.split--flag.split--reverse .list.list--inline'); const listItems = statsContainer.querySelectorAll('.list-item'); const wpm = listItems[0].textContent.trim().split(" ")[0]; const acc = listItems[1].textContent.trim().toLowerCase().includes("n/a") ? "N/A" : listItems[1].textContent.trim().split("%")[0]; return { username: username, wpm: parseFloat(wpm), accuracy: parseFloat(acc), winnerIndicator: "" }; }); newEntries.sort((a, b) => selectedPlayers.indexOf(a.username) - selectedPlayers.indexOf(b.username)); let winningEntry = null; if (newEntries.length >= 2) { let maxWpm = -Infinity; newEntries.forEach(entry => { if (entry.wpm > maxWpm) { maxWpm = entry.wpm; winningEntry = entry; } }); if (winningEntry) { if (selectedPlayers[0] && winningEntry.username === selectedPlayers[0].trim()) { winningEntry.winnerIndicator = "A"; } else if (selectedPlayers[1] && winningEntry.username === selectedPlayers[1].trim()) { winningEntry.winnerIndicator = "B"; } } } updateCumulativeStats(newEntries); const cumAverages = getCumulativeAverages(); let cumAvgStr = selectedPlayers.map(name => { if (cumAverages[name] && cumAverages[name].avgWpm !== "N/A") { let avgWpm = parseFloat(cumAverages[name].avgWpm); let tier = getTier(avgWpm); return `${name}: ${cumAverages[name].avgWpm} WPM, ${cumAverages[name].avgAcc}% Acc, Tier: ${tier}`; } else { return `${name}: N/A`; } }).join("; "); let cumulativeScore = getCumulativeScore(); if (newEntries.length >= 2 && winningEntry) { if (winningEntry.username === selectedPlayers[0].trim()) { cumulativeScore.A = (cumulativeScore.A || 0) + 1; } else if (selectedPlayers[1] && winningEntry.username === selectedPlayers[1].trim()) { cumulativeScore.B = (cumulativeScore.B || 0) + 1; } saveCumulativeScore(cumulativeScore); } let scoreStr = `${cumulativeScore.A || 0}-${cumulativeScore.B || 0}`; let detailsStr = newEntries.map(entry => { let s = `${entry.username}: ${entry.wpm} WPM, ${entry.accuracy}%`; if (entry.winnerIndicator) { s += ` (Winner: ${entry.winnerIndicator})`; } return s; }).join("; "); let summary = { raceNumber: raceNumber, details: detailsStr, cumAverages: cumAvgStr, score: scoreStr, rawEntries: newEntries }; resultsArray.push(summary); saveStoredResults(resultsArray); updateRaceResultsUI(); console.log(`Processed Race #${raceNumber}:`, summary); } // ======================== // Logging Control Functions // ======================== function startLogging() { processRaceResults(); observer = new MutationObserver(mutationsList => { processRaceResults(); }); observer.observe(document.body, { childList: true, subtree: true }); isLogging = true; document.getElementById('toggleLoggingBtn').textContent = "Logging: ON"; localStorage.setItem(LOGGING_STATE_KEY, "true"); console.log("Logging turned ON (results persist across pages)."); } function stopLogging() { if (observer) { observer.disconnect(); observer = null; } isLogging = false; document.getElementById('toggleLoggingBtn').textContent = "Logging: OFF"; localStorage.setItem(LOGGING_STATE_KEY, "false"); console.log("Logging turned OFF (stored results are preserved)."); } function toggleLogging() { if (isLogging) { stopLogging(); } else { startLogging(); } } // ======================== // Save & Clear Functions // ======================== function showResultsInNewTab() { let results = getStoredResults(); if (results.length === 0) { alert("No results to show."); return; } let ntUsernames = selectedPlayers.join(" / "); let prevRanks = selectedPlayers.map(() => "N/A").join(" / "); let cumAvgs = getCumulativeAverages(); let ranksAttained = selectedPlayers.map(name => { if (cumAvgs[name] && cumAvgs[name].avgWpm !== "N/A") { return getTier(parseFloat(cumAvgs[name].avgWpm)); } else { return "N/A"; } }).join(" / "); let avScores = selectedPlayers.map(name => { if (cumAvgs[name] && cumAvgs[name].avgWpm !== "N/A") { return cumAvgs[name].avgWpm; } else { return "N/A"; } }).join(" vs "); let lastRace = results[results.length - 1]; let winnerName = "N/A"; if (lastRace.rawEntries) { for (let entry of lastRace.rawEntries) { if (entry.winnerIndicator) { winnerName = entry.username; break; } } } let resultLine = `${winnerName} wins ${lastRace.score}`; let scoresSection = results.map(race => { if (race.rawEntries && race.rawEntries.length >= 2) { let first = race.rawEntries[0]; let second = race.rawEntries[1]; let winIndicator = ""; for (let entry of race.rawEntries) { if (entry.winnerIndicator) { winIndicator = entry.winnerIndicator; break; } } return `${first.wpm} (${first.accuracy}%) vs ${second.wpm} (${second.accuracy}%) — ${winIndicator}`; } else if (race.rawEntries && race.rawEntries.length === 1) { let only = race.rawEntries[0]; return `${only.wpm} (${only.accuracy}%)`; } else { return race.details; } }).join('\n'); let finalOutput = `NT usernames: ${ntUsernames} Previous Ranks: ${prevRanks} Ranks Attained: ${ranksAttained} Av Scores: ${avScores} Result: ${resultLine} Scores: ${scoresSection}`; let newTab = window.open('', '_blank'); if (!newTab) { alert("Could not open new tab. Please allow pop-ups for NitroType."); return; } newTab.document.write('<html><head><title>Race Results</title></head><body>'); newTab.document.write('<pre style="white-space: pre-wrap;">' + finalOutput + '</pre>'); newTab.document.write('</body></html>'); newTab.document.close(); } function clearResults() { saveStoredResults([]); updateRaceResultsUI(); saveCumulativeStats({}); saveCumulativeScore({A: 0, B: 0}); console.log("Cleared stored race results, cumulative stats, and cumulative score."); } // ======================== // Initialization // ======================== window.addEventListener('load', () => { createUI(); if (localStorage.getItem(LOGGING_STATE_KEY) === "true") { startLogging(); } }); })();