Nitro Type Current Race Tracker w/ Leagues and NT Comps

Nitro Type Current Race Tracker w/ Leagues and NT Comps WIP

  1. // ==UserScript==
  2. // @name Nitro Type Current Race Tracker w/ Leagues and NT Comps
  3. // @version 1.8
  4. // @description Nitro Type Current Race Tracker w/ Leagues and NT Comps WIP
  5. // @author TensorFlow - Dvorak
  6. // @match *://*.nitrotype.com/race
  7. // @match *://*.nitrotype.com/race/*
  8. // @grant none
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.1/dexie.min.js#sha512-ybuxSW2YL5rQG/JjACOUKLiosgV80VUfJWs4dOpmSWZEGwdfdsy2ldvDSQ806dDXGmg9j/csNycIbqsrcqW6tQ==
  10. // @license MIT
  11. // @namespace https://greasyfork.org/users/1331131-tensorflow-dvorak
  12.  
  13. // ==/UserScript==
  14.  
  15. /* globals Dexie */
  16.  
  17. const findReact = (dom, traverseUp = 0) => {
  18. const key = Object.keys(dom).find((key) => key.startsWith("__reactFiber$"));
  19. const domFiber = dom[key];
  20. if (!domFiber) return null;
  21.  
  22. const getCompFiber = (fiber) => {
  23. let parentFiber = fiber?.return;
  24. while (parentFiber && typeof parentFiber.type === "string") {
  25. parentFiber = parentFiber.return;
  26. }
  27. return parentFiber;
  28. };
  29.  
  30. let compFiber = getCompFiber(domFiber);
  31. for (let i = 0; i < traverseUp && compFiber; i++) {
  32. compFiber = getCompFiber(compFiber);
  33. }
  34. return compFiber?.stateNode || null;
  35. };
  36.  
  37. const createLogger = (namespace) => {
  38. const logPrefix = (prefix = "") => {
  39. const formatMessage = `%c[${namespace}]${prefix ? `%c[${prefix}]` : ""}`;
  40. let args = [
  41. console,
  42. `${formatMessage}%c`,
  43. "background-color: #4285f4; color: #fff; font-weight: bold",
  44. ];
  45. if (prefix) {
  46. args = args.concat(
  47. "background-color: #4f505e; color: #fff; font-weight: bold"
  48. );
  49. }
  50. return args.concat("color: unset");
  51. };
  52.  
  53. const bindLog = (logFn, prefix) =>
  54. Function.prototype.bind.apply(logFn, logPrefix(prefix));
  55.  
  56. return {
  57. info: (prefix) => bindLog(console.info, prefix),
  58. warn: (prefix) => bindLog(console.warn, prefix),
  59. error: (prefix) => bindLog(console.error, prefix),
  60. log: (prefix) => bindLog(console.log, prefix),
  61. debug: (prefix) => bindLog(console.debug, prefix),
  62. };
  63. };
  64.  
  65. const logging = createLogger("Nitro Type Current Race Tracker");
  66.  
  67. // Config storage
  68. const db = new Dexie("CurrentRaceTracker");
  69. db.version(1).stores({
  70. users: "id, &username, team, displayName, status, league",
  71. });
  72. db.open().catch(function (e) {
  73. logging.error("Init")("Failed to open up the config database", e);
  74. });
  75.  
  76. //Race
  77. if (
  78. window.location.pathname === "/race" ||
  79. window.location.pathname.startsWith("/race/")
  80. ) {
  81. const raceContainer = document.getElementById("raceContainer");
  82. const raceObj = raceContainer ? findReact(raceContainer) : null;
  83. if (!raceContainer || !raceObj) {
  84. logging.error("Init")("Could not find the race container or race object");
  85. return;
  86. }
  87. if (!raceObj.props.user.loggedIn) {
  88. logging.error("Init")("Extractor is not available for Guest Racing");
  89. return;
  90. }
  91.  
  92. const displayedUserIDs = new Set();
  93.  
  94. const leagueTierText = {
  95. 0: "None",
  96. 1: "Learner",
  97. 2: "Novice",
  98. 3: "Rookie",
  99. 4: "Pro",
  100. 5: "Ace",
  101. 6: "Expert",
  102. 7: "Champion",
  103. 8: "Master",
  104. 9: "Epic",
  105. 10: "Legend",
  106. 11: "Tournament",
  107. 12: "Semi-Finals",
  108. 13: "Finals",
  109. };
  110.  
  111. function createLeagueInfoUI() {
  112. const leagueInfoContainer = document.createElement("div");
  113. leagueInfoContainer.id = "league-info-container";
  114. leagueInfoContainer.style.zIndex = "1000";
  115. leagueInfoContainer.style.backgroundColor = "rgba(0, 0, 0, 0.85)";
  116. leagueInfoContainer.style.color = "#fff";
  117. leagueInfoContainer.style.padding = "15px";
  118. leagueInfoContainer.style.borderRadius = "8px";
  119. leagueInfoContainer.style.fontFamily = "'Nunito', sans-serif";
  120. leagueInfoContainer.style.fontSize = "14px";
  121. leagueInfoContainer.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1)";
  122. leagueInfoContainer.style.width = "100%";
  123. leagueInfoContainer.style.overflowY = "auto";
  124. leagueInfoContainer.style.maxHeight = "70vh";
  125. leagueInfoContainer.innerHTML = `
  126. <h2 style="margin: 0 0 10px; font-size: 18px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">Racers Info</h2>
  127. <table id='league-info-table' style="width: 100%; border-collapse: collapse; text-align: left;">
  128. <thead>
  129. <tr style="border-bottom: 1px solid #444;">
  130. <th style="padding: 5px; color: #f4b400;">Name</th>
  131. <th style="padding: 5px; color: #e74c3c;">Level</th>
  132. <th style="padding: 5px; color: #0f9d58;">League</th>
  133. <th style="padding: 5px; color: #4285f4;">Session Races</th>
  134. <th style="padding: 5px; color: #ff5722;">Races This Week</th>
  135. </tr>
  136. </thead>
  137. <tbody id='league-info-list' style="color: #ddd;"></tbody>
  138. </table>
  139. `;
  140. const targetElement = document.querySelector("#raceContainer");
  141. if (targetElement) {
  142. targetElement.appendChild(leagueInfoContainer);
  143. } else {
  144. document.body.appendChild(leagueInfoContainer);
  145. }
  146. }
  147.  
  148. function fetchAdditionalData(username, listItem) {
  149. const proxyUrl = "https://api.allorigins.win/get?url=";
  150. const targetUrl = `https://www.ntcomps.com/racers/${username}`;
  151. fetch(`${proxyUrl}${encodeURIComponent(targetUrl)}`)
  152. .then((response) => response.json())
  153. .then((data) => {
  154. const parser = new DOMParser();
  155. const doc = parser.parseFromString(data.contents, "text/html");
  156. const foundIndicator = "<td>This week</td>";
  157.  
  158. if (!data.contents.includes(foundIndicator)) {
  159. listItem.querySelector(".races-this-week").textContent = "N/A";
  160. return;
  161. }
  162.  
  163. let racesCompleted = doc
  164. .querySelector("tr:nth-child(4) td:nth-child(2)")
  165. .textContent.trim();
  166. racesCompleted = racesCompleted.replace(/,/g, "");
  167. let racesCompletedInt = parseInt(racesCompleted, 10);
  168. if (isNaN(racesCompletedInt) || racesCompletedInt > 100000) {
  169. racesCompleted = "N/A";
  170. } else {
  171. racesCompleted = racesCompletedInt.toLocaleString();
  172. }
  173.  
  174. listItem.querySelector(".races-this-week").textContent = racesCompleted;
  175. })
  176. .catch((error) => {
  177. console.error("Error fetching additional data:", error);
  178. listItem.querySelector(".races-this-week").textContent = "N/A";
  179. });
  180. }
  181. function updateLeagueInfoUI(user) {
  182. const leagueInfoList = document.getElementById("league-info-list");
  183. if (!leagueInfoList || displayedUserIDs.has(user.userID)) return;
  184. displayedUserIDs.add(user.userID);
  185. const listItem = document.createElement("tr");
  186. const leagueText = leagueTierText[user.profile.leagueTier] || "Unknown";
  187. listItem.style.borderBottom = "1px solid #444";
  188. listItem.style.fontWeight = "bold";
  189. listItem.innerHTML = `
  190. <td style="padding: 5px; color: #f4b400;">${user.profile.displayName}</td>
  191. <td style="padding: 5px; color: #e74c3c;">${user.profile.level}</td>
  192. <td style="padding: 5px; color: #0f9d58;">${leagueText}</td>
  193. <td style="padding: 5px; color: #4285f4;">${user.profile.sessionRaces}</td>
  194. <td style="padding: 5px; color: #ff5722;" class="races-this-week">Loading...</td>
  195. `;
  196. leagueInfoList.appendChild(listItem);
  197. fetchAdditionalData(user.profile.userID, listItem);
  198. }
  199.  
  200. function logPlayerIDsAndLeagues() {
  201. const server = raceObj.server;
  202.  
  203. server.on("joined", (user) => {
  204. if (!user.robot) {
  205. updateLeagueInfoUI(user);
  206. }
  207. });
  208.  
  209. server.on("update", (e) => {
  210. if (e && e.racers) {
  211. e.racers.forEach((racer) => {
  212. if (!racer.robot && !displayedUserIDs.has(racer.userID)) {
  213. updateLeagueInfoUI(racer);
  214. }
  215. });
  216. }
  217. });
  218. }
  219.  
  220. function initializeRaceObj() {
  221. const raceContainer = document.getElementById("raceContainer");
  222.  
  223. if (raceContainer) {
  224. createLeagueInfoUI();
  225. logPlayerIDsAndLeagues();
  226.  
  227. const resultObserver = new MutationObserver(([mutation], observer) => {
  228. for (const node of mutation.addedNodes) {
  229. if (node.classList?.contains("race-results")) {
  230. observer.disconnect();
  231. logging.info("Update")("Race Results received");
  232.  
  233. const leagueInfoContainer = document.getElementById(
  234. "league-info-container"
  235. );
  236. const targetElement =
  237. document.querySelector("#raceContainer").parentElement;
  238. if (leagueInfoContainer && targetElement) {
  239. targetElement.appendChild(leagueInfoContainer);
  240. }
  241. break;
  242. }
  243. }
  244. });
  245. resultObserver.observe(raceContainer, { childList: true, subtree: true });
  246. } else {
  247. logging.error("Init")("Race container not found, retrying...");
  248. setTimeout(initializeRaceObj, 1000);
  249. }
  250. }
  251.  
  252. window.addEventListener("load", initializeRaceObj);
  253. }