Greasy Fork is available in English.

Nitro Type Post Race Analysis NT

Post Race Analysis

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greatest.deepsurf.us/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.greatest.deepsurf.us/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. })();