Nitro Type Post Race Analysis NT

Post Race Analysis

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/520085/1499398/Nitro%20Type%20Post%20Race%20Analysis%20NT.js

  1. // ==UserScript==
  2. // @name Nitro Type Post Race Analysis
  3. // @version 3.0
  4. // @description Post Race Analysis
  5. // @author TensorFlow - Dvorak
  6. // @match *://*.nitrotype.com/race
  7. // @match *://*.nitrotype.com/race/*
  8. // @grant none
  9. // @require https://update.gf.qytechs.cn/scripts/501960/1418069/findReact.js
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. const raceData = {};
  15. let chartInstance = null;
  16.  
  17. const loadChartJS = () => {
  18. const script = document.createElement("script");
  19. script.src = "https://cdn.jsdelivr.net/npm/chart.js";
  20. document.head.appendChild(script);
  21. };
  22.  
  23. loadChartJS();
  24.  
  25. const generateColorFromID = (id, index) => {
  26. const primaryColors = [
  27. "hsl(0, 70%, 50%)",
  28. "hsl(120, 70%, 50%)",
  29. "hsl(240, 70%, 50%)",
  30. "hsl(60, 70%, 50%)",
  31. "hsl(300, 70%, 50%)",
  32. ];
  33. return primaryColors[index % primaryColors.length];
  34. };
  35.  
  36. const ensureDrawerContainer = () => {
  37. let drawerContainer = document.getElementById("drawerContainer");
  38. if (!drawerContainer) {
  39. drawerContainer = document.createElement("div");
  40. drawerContainer.id = "drawerContainer";
  41. drawerContainer.style.position = "fixed";
  42. drawerContainer.style.left = "0";
  43. drawerContainer.style.bottom = "-50%";
  44. drawerContainer.style.width = "100%";
  45. drawerContainer.style.height = "50%";
  46. drawerContainer.style.backgroundColor = "#1E1E2F";
  47. drawerContainer.style.color = "#FFFFFF";
  48. drawerContainer.style.boxShadow = "0 -5px 15px rgba(0, 0, 0, 0.8)";
  49. drawerContainer.style.transition = "bottom 0.4s ease-in-out";
  50. drawerContainer.style.zIndex = "1000";
  51. drawerContainer.style.fontFamily = "Arial, sans-serif";
  52. drawerContainer.style.display = "flex";
  53. drawerContainer.style.flexDirection = "column";
  54.  
  55. drawerContainer.innerHTML = `
  56. <div style="background-color: #2E2E4F;">
  57. <button id="closeDrawer" style="position: absolute; right: 10px; top: 10px; background: none; border: none; color: #fff; font-size: 1.5rem; cursor: pointer;">&times;</button>
  58. </div>
  59. <div id="lessonContainer" style="padding: 10px; color: #FFFFFF; background-color: #2E2E4F; font-family: Arial, sans-serif; border: 1px solid #444; border-radius: 5px; overflow-y: auto; max-height: 20%;"></div>
  60. <div style="flex-grow: 1;">
  61. <canvas id="speedChart" style="background: #1e1e2f; display: block; width: 100%; height: 100%;"></canvas>
  62. </div>
  63. `;
  64. document.body.appendChild(drawerContainer);
  65.  
  66. const closeDrawer = document.getElementById("closeDrawer");
  67. closeDrawer.addEventListener("click", () => {
  68. drawerContainer.style.bottom = "-50%";
  69. });
  70.  
  71. const toggleButton = document.createElement("div");
  72. toggleButton.id = "toggleDrawer";
  73. toggleButton.style.position = "fixed";
  74. toggleButton.style.bottom = "20px";
  75. toggleButton.style.right = "20px";
  76. toggleButton.style.width = "50px";
  77. toggleButton.style.height = "50px";
  78. toggleButton.style.backgroundColor = "#2E2E4F";
  79. toggleButton.style.color = "#FFFFFF";
  80. toggleButton.style.borderRadius = "50%";
  81. toggleButton.style.display = "flex";
  82. toggleButton.style.alignItems = "center";
  83. toggleButton.style.justifyContent = "center";
  84. toggleButton.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)";
  85. toggleButton.style.cursor = "pointer";
  86. toggleButton.style.zIndex = "1001";
  87. toggleButton.innerText = "+";
  88. document.body.appendChild(toggleButton);
  89.  
  90. toggleButton.addEventListener("click", () => {
  91. if (drawerContainer.style.bottom === "0px") {
  92. drawerContainer.style.bottom = "-50%";
  93. } else {
  94. drawerContainer.style.bottom = "0";
  95. }
  96. });
  97.  
  98. document.addEventListener("click", (event) => {
  99. if (
  100. !drawerContainer.contains(event.target) &&
  101. event.target !== toggleButton &&
  102. drawerContainer.style.bottom === "0px"
  103. ) {
  104. drawerContainer.style.bottom = "-50%";
  105. }
  106. });
  107.  
  108. drawerContainer.addEventListener("click", (event) => {
  109. event.stopPropagation();
  110. });
  111. }
  112. };
  113.  
  114. const adjustCanvasSize = () => {
  115. const canvas = document.getElementById("speedChart");
  116. const container = document.getElementById("drawerContainer");
  117. const headerHeight = 50;
  118. const lessonHeight = document.getElementById("lessonContainer").offsetHeight;
  119.  
  120. const height = container.offsetHeight - headerHeight - lessonHeight;
  121. canvas.style.height = `${height}px`;
  122. canvas.style.width = "100%";
  123. };
  124.  
  125. const trackPlayerProgress = (player, baseTime) => {
  126. const { progress, profile } = player;
  127.  
  128. if (!progress || progress.left || progress.disqualified) {
  129. return;
  130. }
  131.  
  132. const typedCharacters = progress.typed || 0;
  133. const startStamp = progress.startStamp - baseTime;
  134. const currentStamp = Date.now() - baseTime;
  135.  
  136. if (!raceData[player.userID]) {
  137. raceData[player.userID] = {
  138. name: profile?.displayName || `Player ${player.userID}`,
  139. data: [],
  140. finished: false,
  141. finishTime: null,
  142. };
  143. }
  144.  
  145. if (raceData[player.userID].finished) {
  146. return;
  147. }
  148.  
  149. const raceTimeMs = progress.completeStamp
  150. ? progress.completeStamp - startStamp
  151. : currentStamp - startStamp;
  152.  
  153. if (progress.completeStamp && !raceData[player.userID].finished) {
  154. raceData[player.userID].finished = true;
  155. raceData[player.userID].finishTime = raceTimeMs;
  156. }
  157.  
  158. const wpm = typedCharacters / 5 / (raceTimeMs / 60000);
  159. const currentTime = (raceTimeMs / 1000).toFixed(2);
  160. const lastDataPoint = raceData[player.userID].data.at(-1);
  161.  
  162. if (!lastDataPoint || lastDataPoint.time !== currentTime) {
  163. raceData[player.userID].data.push({
  164. time: currentTime,
  165. wpm: parseFloat(wpm.toFixed(2)),
  166. });
  167. }
  168. };
  169.  
  170. const cleanData = () => {
  171. Object.keys(raceData).forEach((playerId) => {
  172. const player = raceData[playerId];
  173. if (!player.finished) return;
  174.  
  175. const finalPoint = player.data.at(-1);
  176. if (finalPoint && parseFloat(finalPoint.wpm) === 0) {
  177. player.data.pop();
  178. }
  179.  
  180. player.data = player.data.filter(
  181. (point) => parseFloat(point.time) < 10000
  182. );
  183. });
  184. };
  185.  
  186. const displayChart = (lessonText) => {
  187. cleanData();
  188. ensureDrawerContainer();
  189.  
  190. const drawerContainer = document.getElementById("drawerContainer");
  191. drawerContainer.style.bottom = "0";
  192.  
  193. const lessonContainer = document.getElementById("lessonContainer");
  194. lessonContainer.innerHTML = lessonText
  195. .split(" ")
  196. .map((word, index) => `<span id="word-${index}">${word}</span>`)
  197. .join(" ");
  198.  
  199. adjustCanvasSize();
  200.  
  201. const ctx = document.getElementById("speedChart").getContext("2d");
  202. if (chartInstance) {
  203. chartInstance.destroy();
  204. }
  205.  
  206. const datasets = Object.values(raceData).map((player) => ({
  207. label: player.name,
  208. data: player.data.map((point) => point.wpm),
  209. borderColor: generateColorFromID(player.name || player.userID),
  210. borderWidth: 3,
  211. fill: false,
  212. tension: 0.4,
  213. }));
  214.  
  215. const labels = Object.values(raceData)[0]?.data.map((point) => point.time) || [];
  216.  
  217. if (labels.length === 0 || datasets.length === 0) {
  218. console.error("No data to plot. Check race data collection.");
  219. return;
  220. }
  221.  
  222. chartInstance = new Chart(ctx, {
  223. type: "line",
  224. data: {
  225. labels,
  226. datasets,
  227. },
  228. options: {
  229. responsive: true,
  230. maintainAspectRatio: false,
  231. animation: {
  232. duration: 0,
  233. },
  234. plugins: {
  235. title: {
  236. display: true,
  237. text: "Race Performance (WPM)",
  238. color: "#FFFFFF",
  239. font: {
  240. size: 18,
  241. },
  242. },
  243. legend: {
  244. display: true,
  245. position: "top",
  246. labels: {
  247. color: "#FFFFFF",
  248. font: {
  249. size: 12,
  250. },
  251. },
  252. },
  253. tooltip: {
  254. callbacks: {
  255. label: (tooltipItem) => {
  256. const time = tooltipItem.label;
  257. const wordIndex = Math.floor(
  258. (time / labels[labels.length - 1]) *
  259. lessonText.split(" ").length
  260. );
  261. document
  262. .querySelectorAll("#lessonContainer span")
  263. .forEach((el) => (el.style.backgroundColor = ""));
  264. const highlightWord = document.getElementById(`word-${wordIndex}`);
  265. if (highlightWord) {
  266. highlightWord.style.backgroundColor = "#1a60ba";
  267. }
  268. return tooltipItem.raw;
  269. },
  270. },
  271. },
  272. },
  273. scales: {
  274. x: {
  275. title: { display: true, text: "Time (s)", color: "#FFFFFF" },
  276. ticks: { color: "#FFFFFF" },
  277. },
  278. y: {
  279. title: { display: true, text: "Words Per Minute (WPM)", color: "#FFFFFF" },
  280. ticks: { color: "#FFFFFF" },
  281. beginAtZero: true,
  282. },
  283. },
  284. },
  285. });
  286. };
  287.  
  288. const observeRace = () => {
  289. const raceContainer = document.getElementById("raceContainer");
  290. const reactObj = raceContainer ? findReact(raceContainer) : null;
  291.  
  292. if (!reactObj) {
  293. console.error("React object not found.");
  294. return;
  295. }
  296.  
  297. const server = reactObj.server;
  298. const baseTime = Date.now() - performance.now();
  299.  
  300. let lessonText = "";
  301.  
  302. server.on("status", (e) => {
  303. if (e.lesson) {
  304. lessonText = e.lesson;
  305. }
  306. });
  307.  
  308. server.on("update", (e) => {
  309. const racers = reactObj.state.racers;
  310. racers.forEach((player) => {
  311. trackPlayerProgress(player, baseTime);
  312. });
  313.  
  314. if (reactObj.state.raceStatus === "finished") {
  315. displayChart(lessonText);
  316. }
  317. });
  318. };
  319.  
  320. observeRace();
  321. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址