Nitro Type Post Race Analysis

Post Race Analysis

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