Nitro Type - Race Result Enhancements

Shows NT Season Points earned and Skipped Characters (Nitros) Used on Race Result screen.

  1. // ==UserScript==
  2. // @name Nitro Type - Race Result Enhancements
  3. // @version 0.4.4
  4. // @description Shows NT Season Points earned and Skipped Characters (Nitros) Used on Race Result screen.
  5. // @author Toonidy
  6. // @match *://*.nitrotype.com/race
  7. // @match *://*.nitrotype.com/race/*
  8. // @icon https://i.ibb.co/YRs06pc/toonidy-userscript.png
  9. // @require https://greasyfork.org/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
  10. // @grant none
  11. // @license MIT
  12. // @namespace https://greasyfork.org/users/858426
  13. // ==/UserScript==
  14.  
  15. /* global createLogger findReact */
  16.  
  17. const logging = createLogger("Nitro Type Race Result Enhancements")
  18.  
  19. /////////////
  20. // Utils //
  21. /////////////
  22.  
  23. /** Calculate User's Race score. */
  24. const getUserRaceResult = (user) => {
  25. const { typed, nitros, skipped, startStamp, completeStamp, errors } = user.progress
  26.  
  27. let endStamp = completeStamp || Date.now()
  28.  
  29. const wpm = Math.round((typed - skipped) / 5 / ((endStamp - startStamp) / 6e4)),
  30. accuracy = ((1 - errors / (typed - skipped)) * 100).toFixed(2),
  31. points = Math.round((100 + wpm / 2) * (1 - errors / (typed - skipped)))
  32.  
  33. return { accuracy, points, wpm, nitros, skipped }
  34. }
  35.  
  36. /** Sort Handler to sort by rank position. */
  37. const sortRacersHandler = (e, t) => {
  38. // Source: https://www.nitrotype.com/dist/site/js/ra.js
  39. return e.disqualified && !t.disqualified
  40. ? 1
  41. : (t.disqualified && !e.disqualified) || (e.progress.completeStamp && !t.progress.completeStamp)
  42. ? -1
  43. : t.progress.completeStamp && !e.progress.completeStamp
  44. ? 1
  45. : e.progress.completeStamp && t.progress.completeStamp
  46. ? e.progress.completeStamp < t.progress.completeStamp
  47. ? -1
  48. : 1
  49. : e.progress.percentageFinished === t.progress.percentageFinished
  50. ? 0
  51. : e.progress.percentageFinished > t.progress.percentageFinished
  52. ? -1
  53. : 1
  54. }
  55.  
  56. ///////////////////
  57. // Racing Page //
  58. ///////////////////
  59.  
  60. const raceContainer = document.getElementById("raceContainer"),
  61. raceObj = raceContainer ? findReact(raceContainer) : null
  62. if (!raceContainer || !raceObj) {
  63. logging.error("Init")("Could not find the race track")
  64. return
  65. }
  66.  
  67. const server = raceObj.server
  68.  
  69. /** Mutation obverser to track whether results screen showed up. */
  70. const resultObserver = new MutationObserver((mutations, observer) => {
  71. for (const mutation of mutations) {
  72. for (const newNode of mutation.addedNodes) {
  73. if (newNode.classList?.contains("race-results")) {
  74. observer.disconnect()
  75.  
  76. // Setup New Racer Stats Container
  77. const racers = raceObj.state.racers.slice().sort(sortRacersHandler)
  78.  
  79. const dummyCell = document.createElement("div")
  80. dummyCell.classList.add("split-cell")
  81.  
  82. let racerRankNewNodes = []
  83.  
  84. const racerRankNodes = newNode.querySelectorAll(".gridTable-row")
  85. racerRankNodes.forEach((node, i) => {
  86. const r = racers[i]
  87.  
  88. const listRow = node.querySelector(".gridTable-cell:nth-of-type(2) .split"),
  89. statRow = listRow.querySelector(".split-cell:nth-of-type(2)")
  90.  
  91. // Add in the new stat fields
  92. const { points, skipped } = getUserRaceResult(r),
  93. accuracyNode = statRow.querySelector(".list .list-item:nth-of-type(2)"),
  94. suffixClass = accuracyNode?.querySelector("span")?.className || "tc-ts"
  95.  
  96. const newStatRow = document.createElement("div")
  97. newStatRow.className = `${listRow.className} new-stat-row`
  98. newStatRow.append(dummyCell.cloneNode(), statRow)
  99. listRow.append(dummyCell.cloneNode())
  100.  
  101. listRow.after(newStatRow)
  102.  
  103. const skippedNode = document.createElement("div")
  104. skippedNode.classList.add("list-item", "skipped")
  105. skippedNode.innerHTML = `${r.robot ? "N/A" : skipped} <span class="${suffixClass}">Skipped</span>`
  106.  
  107. const pointsNode = document.createElement("div")
  108. pointsNode.classList.add("list-item", "points")
  109. pointsNode.innerHTML = `${r.robot ? "N/A" : points} <span class="${suffixClass}">Points</span>`
  110.  
  111. racerRankNewNodes[i] = [skippedNode, pointsNode]
  112.  
  113. if (!accuracyNode) {
  114. if (!node.classList.contains("is-wampus") && !r.disqualified) {
  115. logging.warn(`Race Result")("Unable to setup new stats on row ${i}`)
  116. }
  117. return
  118. }
  119. accuracyNode.after(skippedNode, pointsNode)
  120. })
  121.  
  122. /* Track new progress updates */
  123. server.on("update", (e) => {
  124. const racers = raceObj.state.racers.slice().sort(sortRacersHandler)
  125. racerRankNodes.forEach((node, i) => {
  126. const r = racers[i],
  127. { points, skipped } = getUserRaceResult(r),
  128. [skippedNode, pointsNode] = racerRankNewNodes[i],
  129. accuracyNode = node.querySelector(".new-stat-row .list .list-item:nth-of-type(2)")
  130.  
  131. if (r.disqualified || node.classList.contains("is-wampus")) {
  132. skippedNode.remove()
  133. pointsNode.remove()
  134. return
  135. }
  136.  
  137. skippedNode.childNodes[0].textContent = `${r.robot ? "N/A" : skipped} `
  138. pointsNode.childNodes[0].textContent = `${r.robot ? "N/A" : points} `
  139.  
  140. if (!accuracyNode) {
  141. logging.warn(`Race Result")("Unable to insert new stats back into row ${i}`)
  142. return
  143. }
  144. accuracyNode.after(skippedNode, pointsNode)
  145. })
  146. })
  147. return
  148. }
  149. }
  150. }
  151. })
  152.  
  153. resultObserver.observe(raceContainer, { childList: true, subtree: true })
  154.  
  155. logging.info("Init")("Race Result listener has been setup")