您需要先安装一个扩展,例如 篡改猴、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();
- }
- });
- })();