Nitro Type - DPH configuration

Stats & Minimap & New Auto Reload & Alt. WPM / Countdown

  1. // ==UserScript==
  2. // @name Nitro Type - DPH configuration
  3. // @version 0.2.3
  4. // @description Stats & Minimap & New Auto Reload & Alt. WPM / Countdown
  5. // @author dphdmn / A lot of code by Toonidy is used
  6. // @match *://*.nitrotype.com/race
  7. // @match *://*.nitrotype.com/race/*
  8. // @icon https://static.wikia.nocookie.net/nitro-type/images/8/85/175_large_1.png/revision/latest?cb=20181229003942
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @namespace dphdmn
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js#sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.1/dexie.min.js#sha512-ybuxSW2YL5rQG/JjACOUKLiosgV80VUfJWs4dOpmSWZEGwdfdsy2ldvDSQ806dDXGmg9j/csNycIbqsrcqW6tQ==
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/interact.js/1.10.27/interact.min.js
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/pixi.js/6.5.4/browser/pixi.min.js
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. /* global Dexie moment NTGLOBALS PIXI interact */
  20.  
  21. const enableStats = GM_getValue('enableStats', true);
  22. //// GENERAL VISUAL OPTIONS ////
  23. const hideTrack = GM_getValue('hideTrack', true);
  24. const hideNotifications = GM_getValue('hideNotifications', true);
  25. const scrollPage = GM_getValue('scrollPage', false);
  26. const ENABLE_MINI_MAP = GM_getValue('ENABLE_MINI_MAP', true);
  27. const ENABLE_ALT_WPM_COUNTER = GM_getValue('ENABLE_ALT_WPM_COUNTER', true);
  28.  
  29. ////// AUTO RELOAD OPTIONS /////
  30. const greedyStatsReload = GM_getValue('greedyStatsReload', true);
  31. const greedyStatsReloadInt = GM_getValue('greedyStatsReloadInt', 50);
  32.  
  33. const reloadOnStats = GM_getValue('reloadOnStats', true);
  34.  
  35. //// BETTER STATS OPTIONS /////
  36. const RACES_OUTSIDE_CURRENT_TEAM = GM_getValue('RACES_OUTSIDE_CURRENT_TEAM', 0);
  37. const RACES_BEFORE_THIS_SEASON = GM_getValue('RACES_BEFORE_THIS_SEASON', 0);
  38.  
  39. const SEASON_RACES_EXTRA = GM_getValue('SEASON_RACES_EXTRA', 0);
  40. const TEAM_RACES_BUGGED = GM_getValue('TEAM_RACES_BUGGED', 0);
  41.  
  42. const config = {
  43. ///// ALT WPM COUNTER CONFIG //////
  44. targetWPM: GM_getValue('targetWPM', 79.5),
  45. indicateWPMWithin: GM_getValue('indicateWPMWithin', 2),
  46. timerRefreshIntervalMS: GM_getValue('timerRefreshIntervalMS', 25),
  47. dif: GM_getValue('dif', 0.8),
  48.  
  49. raceLatencyMS: 140,
  50.  
  51. ///// CUSTOM MINIMAP CONFIG ////// (hardcoded)
  52. colors: {
  53. me: 0xFF69B4,
  54. opponentPlayer: 0x00FFFF,
  55. opponentBot: 0xbbbbbb,
  56. opponentWampus: 0xFFA500,
  57. nitro: 0xef9e18,
  58. raceLane: 0x555555,
  59. startLine: 0x929292,
  60. finishLine: 0x929292
  61. },
  62. trackLocally: true,
  63. moveDestination: {
  64. enabled: true,
  65. alpha: 0.3,
  66. }
  67. };
  68.  
  69. // Create UI elements
  70. const createUI = () => {
  71. const container = document.createElement('div');
  72. container.style.position = 'fixed';
  73. container.style.bottom = '50px';
  74. container.style.left = '10px';
  75. container.style.background = 'rgba(20, 20, 20, 0.9)'; // Darker background
  76. container.style.color = 'cyan';
  77. container.style.padding = '10px';
  78. container.style.borderRadius = '8px'; // Slightly larger border radius for a modern look
  79. container.style.zIndex = '9999';
  80. container.style.display = 'none';
  81. container.style.width = '300px';
  82. container.style.maxHeight = '400px';
  83. container.style.overflowY = 'scroll';
  84.  
  85. // Apply custom scrollbar styles
  86. const style = document.createElement('style');
  87. style.textContent = `
  88. ::-webkit-scrollbar {
  89. width: 8px; /* Scrollbar width */
  90. }
  91.  
  92. ::-webkit-scrollbar-track {
  93. background: rgba(50, 50, 50, 0.6); /* Scrollbar track */
  94. border-radius: 5px; /* Rounded corners for the track */
  95. }
  96.  
  97. ::-webkit-scrollbar-thumb {
  98. background-color: #00cccc; /* Scrollbar color */
  99. border-radius: 5px; /* Rounded corners for the thumb */
  100. border: 2px solid rgba(20, 20, 20, 0.9); /* Border for thumb to match container background */
  101. }
  102.  
  103. ::-webkit-scrollbar-thumb:hover {
  104. background-color: #00e6e6; /* Lighter color on hover for a modern touch */
  105. }
  106. `;
  107. document.head.appendChild(style);
  108.  
  109. const title = document.createElement('h3');
  110. title.textContent = 'Configuration';
  111. title.style.margin = '0';
  112. title.style.color = '#ff007f';
  113. container.appendChild(title);
  114. const saveButton = document.createElement('button');
  115. saveButton.textContent = 'Save and Reload';
  116. saveButton.style.marginTop = '10px';
  117. saveButton.style.background = 'cyan';
  118. saveButton.style.color = 'black';
  119. saveButton.style.border = 'none';
  120. saveButton.style.padding = '5px';
  121. saveButton.style.cursor = 'pointer';
  122. saveButton.onclick = () => location.reload();
  123. container.appendChild(saveButton);
  124.  
  125. const addHeader = (labelText) => {
  126. const label = document.createElement('label');
  127. label.style.display = 'block';
  128. label.style.marginTop = '10px';
  129. label.style.color = '#ff007f';
  130. label.appendChild(document.createTextNode(' ' + labelText));
  131. container.appendChild(label);
  132. };
  133. const addCheckbox = (labelText, variableName, defaultValue) => {
  134. const label = document.createElement('label');
  135. label.style.display = 'block';
  136. label.style.marginTop = '10px';
  137. label.style.color = 'cyan';
  138.  
  139. const checkbox = document.createElement('input');
  140. checkbox.type = 'checkbox';
  141. checkbox.checked = GM_getValue(variableName, defaultValue);
  142. checkbox.onchange = () => GM_setValue(variableName, checkbox.checked);
  143.  
  144. label.appendChild(checkbox);
  145. label.appendChild(document.createTextNode(' ' + labelText));
  146. container.appendChild(label);
  147. };
  148.  
  149. const addNumberInput = (labelText, variableName, defaultValue) => {
  150. const label = document.createElement('label');
  151. label.style.display = 'block';
  152. label.style.marginTop = '10px';
  153. label.style.color = '#009c9a';
  154. label.style.fontSize = "14px";
  155. const input = document.createElement('input');
  156. input.type = 'number';
  157. input.value = GM_getValue(variableName, defaultValue);
  158. input.style.width = '100%';
  159. input.style.background = 'rgba(0, 0, 0, 0.6)';
  160. input.style.color = 'cyan';
  161. input.style.border = 'none';
  162. input.style.padding = '5px';
  163. input.style.marginTop = '5px';
  164. input.onchange = () => GM_setValue(variableName, parseFloat(input.value));
  165.  
  166. label.appendChild(document.createTextNode(labelText));
  167. label.appendChild(input);
  168. container.appendChild(label);
  169. };
  170.  
  171. // Add options to the UI
  172. addHeader("General options");
  173. addCheckbox('Hide Track', 'hideTrack', hideTrack);
  174. addCheckbox('Hide Notifications', 'hideNotifications', hideNotifications);
  175. addCheckbox('Enable Mini Map', 'ENABLE_MINI_MAP', ENABLE_MINI_MAP);
  176. addCheckbox('Auto Scroll Page', 'scrollPage', scrollPage);
  177. addHeader("Auto reload options");
  178. addCheckbox('Enable Auto Reload', 'reloadOnStats', reloadOnStats);
  179. addCheckbox('Enable FAST RELOAD', 'greedyStatsReload', greedyStatsReload);
  180. addNumberInput('FAST RELOAD - Check Interval', 'greedyStatsReloadInt', greedyStatsReloadInt);
  181. addHeader("Stats options");
  182. addCheckbox('Enable Stats', 'enableStats', enableStats);
  183. addNumberInput('Races Outside Current Team', 'RACES_OUTSIDE_CURRENT_TEAM', RACES_OUTSIDE_CURRENT_TEAM);
  184. addNumberInput('Races Before This Season', 'RACES_BEFORE_THIS_SEASON', RACES_BEFORE_THIS_SEASON);
  185. addNumberInput('Bugged season count (0 if no)', 'SEASON_RACES_EXTRA', SEASON_RACES_EXTRA);
  186. addNumberInput('Bugged team count (0 if no)', 'TEAM_RACES_BUGGED', TEAM_RACES_BUGGED);
  187. addHeader("Alt. WPM options");
  188. addCheckbox('Enable Alt. WPM / Countdown', 'ENABLE_ALT_WPM_COUNTER', ENABLE_ALT_WPM_COUNTER);
  189. addNumberInput('Target WPM (1 = No Sandbagging)', 'targetWPM', config.targetWPM);
  190. addNumberInput('Alt. WPM: Yellow when +X WPM', 'indicateWPMWithin', config.indicateWPMWithin);
  191. addNumberInput('Alt. WPM: Refresh int.', 'timerRefreshIntervalMS', config.timerRefreshIntervalMS);
  192. addNumberInput('Alt. WPM: +X WPM Delay', 'dif', config.dif);
  193.  
  194. document.body.appendChild(container);
  195.  
  196. const configureButton = document.createElement('button');
  197. configureButton.textContent = 'Configure';
  198. configureButton.style.position = 'fixed';
  199. configureButton.style.bottom = '10px';
  200. configureButton.style.left = '10px';
  201. configureButton.style.background = 'rgba(0, 0, 0, 0.8)';
  202. configureButton.style.color = 'cyan';
  203. configureButton.style.border = 'none';
  204. configureButton.style.padding = '5px';
  205. configureButton.style.cursor = 'pointer';
  206. configureButton.style.zIndex = '9999';
  207. configureButton.onclick = () => {
  208. container.style.display = container.style.display === 'none' ? 'block' : 'none';
  209. };
  210. document.body.appendChild(configureButton);
  211. };
  212.  
  213. createUI();
  214.  
  215.  
  216. /** Finds the React Component from given dom. */
  217. const findReact = (dom, traverseUp = 0) => {
  218. const key = Object.keys(dom).find((key) => key.startsWith("__reactFiber$"))
  219. const domFiber = dom[key]
  220. if (domFiber == null) return null
  221. const getCompFiber = (fiber) => {
  222. let parentFiber = fiber?.return
  223. while (typeof parentFiber?.type == "string") {
  224. parentFiber = parentFiber?.return
  225. }
  226. return parentFiber
  227. }
  228. let compFiber = getCompFiber(domFiber)
  229. for (let i = 0; i < traverseUp && compFiber; i++) {
  230. compFiber = getCompFiber(compFiber)
  231. }
  232. return compFiber?.stateNode
  233. }
  234.  
  235. var my_race_started = false;
  236. const TEAM_RACES_DIF = RACES_OUTSIDE_CURRENT_TEAM - TEAM_RACES_BUGGED;
  237. const CURRENT_SEASON_DIF = RACES_BEFORE_THIS_SEASON - SEASON_RACES_EXTRA;
  238.  
  239. if(hideTrack){
  240. const trackel = document.querySelector('.racev3-track')
  241. trackel.style.opacity = '0';
  242. trackel.style.marginTop = '-400px';
  243. }
  244. if (hideNotifications) {
  245. const style = document.createElement('style');
  246. style.textContent = `
  247. .growls {
  248. display: none !important; /* or visibility: hidden; */
  249. }
  250. `;
  251. document.head.appendChild(style);
  252. }
  253. /** Create a Console Logger with some prefixing. */
  254. const createLogger = (namespace) => {
  255. const logPrefix = (prefix = "") => {
  256. const formatMessage = `%c[${namespace}]${prefix ? `%c[${prefix}]` : ""}`
  257. let args = [console, `${formatMessage}%c`, "background-color: #D62F3A; color: #fff; font-weight: bold"]
  258. if (prefix) {
  259. args = args.concat("background-color: #4f505e; color: #fff; font-weight: bold")
  260. }
  261. return args.concat("color: unset")
  262. }
  263. return {
  264. info: (prefix) => Function.prototype.bind.apply(console.info, logPrefix(prefix)),
  265. warn: (prefix) => Function.prototype.bind.apply(console.warn, logPrefix(prefix)),
  266. error: (prefix) => Function.prototype.bind.apply(console.error, logPrefix(prefix)),
  267. log: (prefix) => Function.prototype.bind.apply(console.log, logPrefix(prefix)),
  268. debug: (prefix) => Function.prototype.bind.apply(console.debug, logPrefix(prefix)),
  269. }
  270. }
  271.  
  272. function logstats() {
  273. const raceContainer = document.getElementById("raceContainer"),
  274. canvasTrack = raceContainer?.querySelector("canvas"),
  275. raceObj = raceContainer ? findReact(raceContainer) : null;
  276. const currentUserID = raceObj.props.user.userID;
  277. const currentUserResult = raceObj.state.racers.find((r) => r.userID === currentUserID)
  278. if (!currentUserResult || !currentUserResult.progress || typeof currentUserResult.place === "undefined") {
  279. console.log("STATS LOGGER: Unable to find race results");
  280. return
  281. }
  282.  
  283. const {
  284. typed,
  285. skipped,
  286. startStamp,
  287. completeStamp,
  288. errors
  289. } = currentUserResult.progress,
  290. wpm = Math.round((typed - skipped) / 5 / ((completeStamp - startStamp) / 6e4)),
  291. time = ((completeStamp - startStamp) / 1e3).toFixed(2),
  292. acc = ((1 - errors / (typed - skipped)) * 100).toFixed(2),
  293. points = Math.round((100 + wpm / 2) * (1 - errors / (typed - skipped))),
  294. place = currentUserResult.place
  295.  
  296. console.log(`STATS LOGGER: ${place} | ${acc}% Acc | ${wpm} WPM | ${points} points | ${time} secs`)
  297. }
  298. const logging = createLogger("Nitro Type Racing Stats")
  299.  
  300. /* Config storage */
  301. const db = new Dexie("NTRacingStats")
  302. db.version(1).stores({
  303. backupStatData: "userID",
  304. })
  305. db.open().catch(function(e) {
  306. logging.error("Init")("Failed to open up the racing stat cache database", e)
  307. })
  308.  
  309. ////////////
  310. // Init //
  311. ////////////
  312.  
  313. const raceContainer = document.getElementById("raceContainer"),
  314. raceObj = raceContainer ? findReact(raceContainer) : null,
  315. server = raceObj?.server,
  316. currentUser = raceObj?.props.user
  317. if (!raceContainer || !raceObj) {
  318. logging.error("Init")("Could not find the race track")
  319. return
  320. }
  321. if (!currentUser?.loggedIn) {
  322. logging.error("Init")("Not available for Guest Racing")
  323. return
  324. }
  325.  
  326. raceContainer.addEventListener('click', (event) => {
  327. document.querySelector('.race-hiddenInput').click();
  328. });
  329. //////////////////
  330. // Components //
  331. //////////////////
  332.  
  333. /** Styles for the following components. */
  334. const style = document.createElement("style")
  335. style.appendChild(
  336. document.createTextNode(`
  337.  
  338. .racev3-track {
  339. margin-top: -30px;
  340. }
  341.  
  342. .header-bar--return-to-garage{
  343. display: none !important;
  344. }
  345.  
  346. .dropdown {
  347. display: none !important;
  348. }
  349.  
  350. .header-nav {
  351. display: none !important;
  352. }
  353. .logo-SVG {
  354. height: 50% !important;
  355. width: 50% important;
  356. }
  357. #raceContainer {
  358. margin-bottom: 0;
  359. }
  360. .nt-stats-root {
  361. text-shadow:
  362. 0.05em 0 black,
  363. 0 0.05em black,
  364. -0.05em 0 black,
  365. 0 -0.05em black,
  366. -0.05em -0.05em black,
  367. -0.05em 0.05em black,
  368. 0.05em -0.05em black,
  369. 0.05em 0.05em black;
  370. }
  371. .nt-stats-body {
  372. display: flex;
  373. justify-content: space-between;
  374. padding: 8px;
  375. background: linear-gradient(rgba(0, 0, 0, 0.66), rgba(0, 0, 0, 0.66)), fixed url(https://getwallpapers.com/wallpaper/full/1/3/a/171084.jpg);
  376. }
  377. .nt-stats-left-section {
  378. display: none;
  379. }
  380. .nt-stats-right-section {
  381. display: flex;
  382. flex-direction: column;
  383. row-gap: 8px;
  384. }
  385. .nt-stats-toolbar {
  386. display: none;
  387. justify-content: space-between;
  388. align-items: center;
  389. padding-left: 8px;
  390. color: rgba(255, 255, 255, 0.8);
  391. background-color: #03111a;
  392. font-size: 12px;
  393. }
  394. .nt-stats-toolbar-status {
  395. display: flex;
  396. }
  397. .nt-stats-toolbar-status .nt-stats-toolbar-status-item {
  398. padding: 0 8px;
  399. background-color: #0a2c42;
  400. }
  401. .nt-stats-toolbar-status .nt-stats-toolbar-status-item-alt {
  402. padding: 0 8px;
  403. background-color: #22465c;
  404. }
  405. .nt-stats-daily-challenges {
  406. width: 350px;
  407. }
  408. .nt-stats-daily-challenges .daily-challenge-progress--badge {
  409. z-index: 0;
  410. }
  411. .nt-stats-season-progress {
  412. display: none;
  413. padding: 8px;
  414. margin: 0 auto;
  415. border-radius: 8px;
  416. background-color: #3b3b3b;
  417. box-shadow: 0 28px 28px 0 rgb(2 2 2 / 5%), 0 17px 17px 0 rgb(2 2 2 / 20%), 0 8px 8px 0 rgb(2 2 2 / 15%);
  418. }
  419. .nt-stats-season-progress .season-progress-widget {
  420. width: 350px;
  421. }
  422. .nt-stats-season-progress .season-progress-widget--level-progress-bar {
  423. transition: width 0.3s ease;
  424. }
  425. .nt-stats-info {
  426. text-align: center;
  427. color: #eee;
  428. font-size: 14px;
  429. opacity: 80%
  430. }
  431. .nt-stats-metric-row {
  432. margin-bottom: 4px;
  433. }
  434. .nt-stats-metric-value, .nt-stats-metric-suffix {
  435. font-weight: 300;
  436. color: cyan;
  437. }
  438. .nt-stats-metric-value {
  439. color: rgb(0, 245, 245);
  440. }
  441. .nt-stats-right-section {
  442. flex-grow: 1;
  443. margin-left: 15px;
  444. }`)
  445. )
  446. document.head.appendChild(style)
  447.  
  448. /** Populates daily challenge data merges in the given progress. */
  449. const mergeDailyChallengeData = (progress) => {
  450. const {
  451. CHALLENGES,
  452. CHALLENGE_TYPES
  453. } = NTGLOBALS,
  454. now = Math.floor(Date.now() / 1000)
  455. return CHALLENGES.filter((c) => c.expiration > now)
  456. .slice(0, 3)
  457. .map((c, i) => {
  458. const userProgress = progress.find((p) => p.challengeID === c.challengeID),
  459. challengeType = CHALLENGE_TYPES[c.type],
  460. field = challengeType[1],
  461. title = challengeType[0].replace(/\$\{goal\}/, c.goal).replace(/\$\{field\}/, `${challengeType[1]}${c.goal !== 1 ? "s" : ""}`)
  462. return {
  463. ...c,
  464. title,
  465. field,
  466. goal: c.goal,
  467. progress: userProgress?.progress || 0,
  468. }
  469. })
  470. }
  471.  
  472. /** Grab NT Racing Stats from various sources. */
  473. const getStats = async () => {
  474. //await new Promise(resolve => setTimeout(resolve, 3000));
  475. let backupUserStats = null
  476. try {
  477. backupUserStats = await db.backupStatData.get(currentUser.userID)
  478. } catch (ex) {
  479. logging.warn("Update")("Unable to get backup stats", ex)
  480. }
  481. try {
  482. const persistStorageStats = JSON.parse(JSON.parse(localStorage.getItem("persist:nt")).user),
  483. user = !backupUserStats || typeof backupUserStats.lastConsecRace !== "number" || persistStorageStats.lastConsecRace >= backupUserStats.lastConsecRace ?
  484. persistStorageStats :
  485. backupUserStats,
  486. dailyChallenges = mergeDailyChallengeData(user.challenges)
  487. return {
  488. user,
  489. dailyChallenges
  490. }
  491. } catch (ex) {
  492. logging.error("Update")("Unable to get stats", ex)
  493. }
  494. return Promise.reject(new Error("Unable to get stats"))
  495. }
  496.  
  497. /** Grab Summary Stats. */
  498. const getSummaryStats = () => {
  499. const authToken = localStorage.getItem("player_token")
  500. return fetch("/api/v2/stats/summary", {
  501. headers: {
  502. Authorization: `Bearer ${authToken}`,
  503. },
  504. })
  505. .then((r) => r.json())
  506. .then((r) => {
  507. return {
  508. seasonBoard: r?.results?.racingStats?.find((b) => b.board === "season"),
  509. dailyBoard: r?.results?.racingStats?.find((b) => b.board === "daily"),
  510. }
  511. })
  512. .catch((err) => Promise.reject(err))
  513. }
  514.  
  515. /** Grab Stats from Team Data. */
  516. const getTeamStats = () => {
  517. if (!currentUser?.tag) {
  518. return Promise.reject(new Error("User is not in a team"))
  519. }
  520. const authToken = localStorage.getItem("player_token")
  521. return fetch(`/api/v2/teams/${currentUser.tag}`, {
  522. headers: {
  523. Authorization: `Bearer ${authToken}`,
  524. },
  525. })
  526. .then((r) => r.json())
  527. .then((r) => {
  528. return {
  529. leaderboard: r?.results?.leaderboard,
  530. motd: r?.results?.motd,
  531. info: r?.results?.info,
  532. stats: r?.results?.stats,
  533. member: r?.results?.members?.find((u) => u.userID === currentUser.userID),
  534. season: r?.results?.season?.find((u) => u.userID === currentUser.userID),
  535. }
  536. })
  537. .catch((err) => Promise.reject(err))
  538. }
  539.  
  540. /** Stat Manager widget (basically a footer with settings button). */
  541. const ToolbarWidget = ((user) => {
  542. const root = document.createElement("div")
  543. root.classList.add("nt-stats-toolbar")
  544. root.innerHTML = `
  545. <div>
  546. NOTE: Team Stats and Season Stats are cached.
  547. </div>
  548. <div class="nt-stats-toolbar-status">
  549. <div class="nt-stats-toolbar-status-item">
  550. <span class=" nt-cash-status as-nitro-cash--prefix">N/A</span>
  551. </div>
  552. <div class="nt-stats-toolbar-status-item-alt">
  553. 📦 Mystery Box: <span class="mystery-box-status">N/A</span>
  554. </div>
  555. </div>`
  556.  
  557. /** Mystery Box **/
  558. const rewardCountdown = user.rewardCountdown,
  559. mysteryBoxStatus = root.querySelector(".mystery-box-status")
  560.  
  561. let isDisabled = Date.now() < user.rewardCountdown * 1e3,
  562. timer = null
  563.  
  564. const syncCountdown = () => {
  565. isDisabled = Date.now() < user.rewardCountdown * 1e3
  566. if (!isDisabled) {
  567. if (timer) {
  568. clearInterval(timer)
  569. }
  570. mysteryBoxStatus.textContent = "Claim Now!"
  571. return
  572. }
  573. mysteryBoxStatus.textContent = moment(user.rewardCountdown * 1e3).fromNow(false)
  574. }
  575. syncCountdown()
  576. if (isDisabled) {
  577. timer = setInterval(syncCountdown, 6e3)
  578. }
  579.  
  580. /** NT Cash. */
  581. const amountNode = root.querySelector(".nt-cash-status")
  582.  
  583. return {
  584. root,
  585. updateStats: (user) => {
  586. if (typeof user?.money === "number") {
  587. amountNode.textContent = `$${user.money.toLocaleString()}`
  588. }
  589. },
  590. }
  591.  
  592. })(raceObj.props.user)
  593.  
  594. /** Daily Challenge widget. */
  595. const DailyChallengeWidget = (() => {
  596. const root = document.createElement("div")
  597. root.classList.add("nt-stats-daily-challenges", "profile-dailyChallenges", "card", "card--open", "card--d", "card--grit", "card--shadow-l")
  598. root.innerHTML = `
  599. <div class="daily-challenge-list--heading">
  600. <h4>Daily Challenges</h4>
  601. <div class="daily-challenge-list--arriving">
  602. <div class="daily-challenge-list--arriving-label">
  603. <svg class="icon icon-recent-time"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.1494.svg#icon-recent-time"></use></svg>
  604. New <span></span>
  605. </div>
  606. </div>
  607. </div>
  608. <div class="daily-challenge-list--challenges"></div>`
  609.  
  610. const dailyChallengesContainer = root.querySelector(".daily-challenge-list--challenges"),
  611. dailyChallengesExpiry = root.querySelector(".daily-challenge-list--arriving-label span")
  612.  
  613. const dailyChallengeItem = document.createElement("div")
  614. dailyChallengeItem.classList.add("raceResults--dailyChallenge")
  615. dailyChallengeItem.innerHTML = `
  616. <div class="daily-challenge-progress">
  617. <div class="daily-challenge-progress--info">
  618. <div class="daily-challenge-progress--requirements">
  619. <div class="daily-challenge-progress--name">
  620. <div style="height: 19px;">
  621. <div align="left" style="white-space: nowrap; pavgSpeedosition: absolute; transform: translate(0%, 0px) scale(1, 1); left: 0px;">
  622. </div>
  623. </div>
  624. </div>
  625. <div class="daily-challenge-progress--status"></div>
  626. </div>
  627. <div class="daily-challenge-progress--progress">
  628. <div class="daily-challenge-progress--progress-bar-container">
  629. <div class="daily-challenge-progress--progress-bar" style="width: 40%"></div>
  630. <div class="daily-challenge-progress--progress-bar--earned" style="width: 40%"></div>
  631. </div>
  632. </div>
  633. </div>
  634. <div class="daily-challenge-progress--badge">
  635. <div class="daily-challenge-progress--success"></div>
  636. <div class="daily-challenge-progress--xp">
  637. <span class="daily-challenge-progress--value"></span><span class="daily-challenge-progress--divider">/</span><span class="daily-challenge-progress--target"></span>
  638. </div>
  639. <div class="daily-challenge-progress--label"></div>
  640. </div>
  641. </div>`
  642.  
  643. const updateDailyChallengeNode = (node, challenge) => {
  644. let progressPercentage = challenge.goal > 0 ? (challenge.progress / challenge.goal) * 100 : 0
  645. if (challenge.progress === challenge.goal) {
  646. progressPercentage = 100
  647. node.querySelector(".daily-challenge-progress").classList.add("is-complete")
  648. } else {
  649. node.querySelector(".daily-challenge-progress").classList.remove("is-complete")
  650. }
  651. node.querySelector(".daily-challenge-progress--name div div").textContent = challenge.title
  652. node.querySelector(".daily-challenge-progress--label").textContent = `${challenge.field}s`
  653. node.querySelector(".daily-challenge-progress--value").textContent = challenge.progress
  654. node.querySelector(".daily-challenge-progress--target").textContent = challenge.goal
  655. node.querySelector(".daily-challenge-progress--status").textContent = `Earn ${Math.floor(challenge.reward / 100) / 10}k XP`
  656. node.querySelectorAll(".daily-challenge-progress--progress-bar, .daily-challenge-progress--progress-bar--earned").forEach((bar) => {
  657. bar.style.width = `${progressPercentage}%`
  658. })
  659. }
  660.  
  661. let dailyChallengeNodes = null
  662.  
  663. getStats().then(({
  664. dailyChallenges
  665. }) => {
  666. const dailyChallengeFragment = document.createDocumentFragment()
  667.  
  668. dailyChallengeNodes = dailyChallenges.map((c) => {
  669. const node = dailyChallengeItem.cloneNode(true)
  670. updateDailyChallengeNode(node, c)
  671.  
  672. dailyChallengeFragment.append(node)
  673.  
  674. return node
  675. })
  676. dailyChallengesContainer.append(dailyChallengeFragment)
  677. })
  678.  
  679. const updateStats = (data) => {
  680. if (!data || !dailyChallengeNodes || data.length === 0) {
  681. return
  682. }
  683. if (data[0] && data[0].expiration) {
  684. const t = 1000 * data[0].expiration
  685. if (!isNaN(t)) {
  686. dailyChallengesExpiry.textContent = moment(t).fromNow()
  687. }
  688. }
  689. data.forEach((c, i) => {
  690. if (dailyChallengeNodes[i]) {
  691. updateDailyChallengeNode(dailyChallengeNodes[i], c)
  692. }
  693. })
  694. }
  695.  
  696. return {
  697. root,
  698. updateStats,
  699. }
  700. })()
  701.  
  702. /** Display Season Progress and next Reward. */
  703. const SeasonProgressWidget = ((raceObj) => {
  704. const currentSeason = NTGLOBALS.ACTIVE_SEASONS.find((s) => {
  705. const now = Date.now()
  706. return now >= s.startStamp * 1e3 && now <= s.endStamp * 1e3
  707. })
  708.  
  709. const seasonRewards = raceObj.props?.seasonRewards,
  710. user = raceObj.props?.user
  711.  
  712. const root = document.createElement("div")
  713. root.classList.add("nt-stats-season-progress", "theme--pDefault")
  714. root.innerHTML = `
  715. <div class="season-progress-widget">
  716. <div class="season-progress-widget--info">
  717. <div class="season-progress-widget--title">Season Progress${currentSeason ? "" : " (starting soon)"}</div>
  718. <div class="season-progress-widget--current-xp"></div>
  719. <div class="season-progress-widget--current-level">
  720. <div class="season-progress-widget--current-level--prefix">Level</div>
  721. <div class="season-progress-widget--current-level--number"></div>
  722. </div>
  723. <div class="season-progress-widget--level-progress">
  724. <div class="season-progress-widget--level-progress-bar" style="width: 0%;"></div>
  725. </div>
  726. </div>
  727. <div class="season-progress-widget--next-reward">
  728. <div class="season-progress-widget--next-reward--display">
  729. <div class="season-reward-mini-preview">
  730. <div class="season-reward-mini-preview--locked">
  731. <div class="tooltip--season tooltip--xs tooltip--c" data-ttcopy="Upgrade to Nitro Gold to Unlock!">
  732. <svg class="icon icon-lock"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-lock"></use></svg>
  733. </div>
  734. </div>
  735. <a class="season-reward-mini-preview" href="/season">
  736. <div class="season-reward-mini-preview--frame">
  737. <div class="rarity-frame rarity-frame--small">
  738. <div class="rarity-frame--extra"></div>
  739. <div class="rarity-frame--content">
  740. <div class="season-reward-mini-preview--preview"></div>
  741. <div class="season-reward-mini-preview--label"></div>
  742. </div>
  743. </div>
  744. </div>
  745. </a>
  746. </div>
  747. </div>
  748. </div>
  749. </div>`
  750.  
  751. const xpTextNode = root.querySelector(".season-progress-widget--current-xp"),
  752. xpProgressBarNode = root.querySelector(".season-progress-widget--level-progress-bar"),
  753. levelNode = root.querySelector(".season-progress-widget--current-level--number"),
  754. nextRewardRootNode = root.querySelector(".season-reward-mini-preview"),
  755. nextRewardTypeLabelNode = root.querySelector(".season-reward-mini-preview--label"),
  756. nextRewardTypeLockedNode = root.querySelector(".season-reward-mini-preview--locked"),
  757. nextRewardTypePreviewNode = root.querySelector(".season-reward-mini-preview--preview"),
  758. nextRewardTypePreviewImgNode = document.createElement("img"),
  759. nextRewardRarityFrameNode = root.querySelector(".rarity-frame.rarity-frame--small")
  760.  
  761. nextRewardTypePreviewImgNode.classList.add("season-reward-mini-previewImg")
  762.  
  763. if (!currentSeason) {
  764. nextRewardRootNode.remove()
  765. }
  766.  
  767. /** Work out how much experience required to reach specific level. */
  768. const getExperienceRequired = (lvl) => {
  769. if (lvl < 1) {
  770. lvl = 1
  771. }
  772. const {
  773. startingLevels,
  774. experiencePerStartingLevel,
  775. experiencePerAchievementLevel,
  776. experiencePerExtraLevels
  777. } = NTGLOBALS.SEASON_LEVELS
  778.  
  779. let totalExpRequired = 0,
  780. amountExpRequired = experiencePerStartingLevel
  781. for (let i = 1; i < lvl; i++) {
  782. if (i <= startingLevels) {
  783. totalExpRequired += experiencePerStartingLevel
  784. } else if (currentSeason && i > currentSeason.totalRewards) {
  785. totalExpRequired += experiencePerExtraLevels
  786. amountExpRequired = experiencePerExtraLevels
  787. } else {
  788. totalExpRequired += experiencePerAchievementLevel
  789. amountExpRequired = experiencePerAchievementLevel
  790. }
  791. }
  792. return [amountExpRequired, totalExpRequired]
  793. }
  794.  
  795. /** Get next reward. */
  796. const getNextRewardID = (currentXP) => {
  797. currentXP = currentXP || user.experience
  798. if (!seasonRewards || seasonRewards.length === 0) {
  799. return null
  800. }
  801. if (user.experience === 0) {
  802. return seasonRewards[0] ? seasonRewards[0].achievementID : null
  803. }
  804. let claimed = false
  805. let nextReward = seasonRewards.find((r, i) => {
  806. if (!r.bonus && (claimed || r.experience === currentXP)) {
  807. claimed = true
  808. return false
  809. }
  810. return r.experience > currentXP || i + 1 === seasonRewards.length
  811. })
  812. if (!nextReward) {
  813. nextReward = seasonRewards[seasonRewards.length - 1]
  814. }
  815. return nextReward ? nextReward.achievementID : null
  816. }
  817.  
  818. return {
  819. root,
  820. updateStats: (data) => {
  821. // XP Progress
  822. if (typeof data.experience === "number") {
  823. const [amountExpRequired, totalExpRequired] = getExperienceRequired(data.level + 1),
  824. progress = Math.max(5, ((amountExpRequired - (totalExpRequired - data.experience)) / amountExpRequired) * 100.0) || 5
  825. xpTextNode.textContent = `${(amountExpRequired - (totalExpRequired - data.experience)).toLocaleString()} / ${amountExpRequired / 1e3}k XP`
  826. xpProgressBarNode.style.width = `${progress}%`
  827. }
  828. levelNode.textContent = currentSeason && data.level > currentSeason.totalRewards + 1 ? `∞${data.level - currentSeason.totalRewards - 1}` : data.level || 1
  829.  
  830. // Next Reward
  831. if (typeof data.experience !== "number") {
  832. return
  833. }
  834. const nextRewardID = getNextRewardID(data.experience),
  835. achievement = nextRewardID ? NTGLOBALS.ACHIEVEMENTS.LIST.find((a) => a.achievementID === nextRewardID) : null
  836. if (!achievement) {
  837. return
  838. }
  839. const {
  840. type,
  841. value
  842. } = achievement.reward
  843. if (["loot", "car"].includes(type)) {
  844. const item = type === "loot" ? NTGLOBALS.LOOT.find((l) => l.lootID === value) : NTGLOBALS.CARS.find((l) => l.carID === value)
  845. if (!item) {
  846. logging.warn("Update")(`Unable to find next reward ${type}`, achievement.reward)
  847. return
  848. }
  849.  
  850. nextRewardRootNode.className = `season-reward-mini-preview season-reward-mini-preview--${type === "loot" ? item?.type : "car"}`
  851. nextRewardTypeLabelNode.textContent = type === "loot" ? item.type || "???" : "car"
  852. nextRewardRarityFrameNode.className = `rarity-frame rarity-frame--small${item.options?.rarity ? ` rarity-frame--${item.options.rarity}` : ""}`
  853.  
  854. if (item?.type === "title") {
  855. nextRewardTypePreviewImgNode.remove()
  856. nextRewardTypePreviewNode.textContent = `"${item.name}"`
  857. } else {
  858. nextRewardTypePreviewImgNode.src = type === "loot" ? item.options?.src : `/cars/${item.options?.smallSrc}`
  859. nextRewardTypePreviewNode.innerHTML = ""
  860. nextRewardTypePreviewNode.append(nextRewardTypePreviewImgNode)
  861. }
  862. } else if (type === "money") {
  863. nextRewardTypeLabelNode.innerHTML = `<div class="as-nitro-cash--prefix">$${value.toLocaleString()}</div>`
  864. nextRewardTypePreviewImgNode.src = "/dist/site/images/pages/race/race-results-prize-cash.2.png"
  865. nextRewardRootNode.className = "season-reward-mini-preview season-reward-mini-preview--money"
  866. nextRewardRarityFrameNode.className = "rarity-frame rarity-frame--small rarity-frame--legendary"
  867. nextRewardTypePreviewNode.innerHTML = ""
  868. nextRewardTypePreviewNode.append(nextRewardTypePreviewImgNode)
  869. } else {
  870. logging.warn("Update")(`Unhandled next reward type ${type}`, achievement.reward)
  871. return
  872. }
  873.  
  874. if (!achievement.free && user.membership === "basic") {
  875. nextRewardRootNode.firstElementChild.before(nextRewardTypeLockedNode)
  876. } else {
  877. nextRewardTypeLockedNode.remove()
  878. }
  879. },
  880. }
  881. })(raceObj)
  882.  
  883. /** Displays list of player stats. */
  884. const StatWidget = (() => {
  885. const root = document.createElement("div")
  886. root.classList.add("nt-stats-info")
  887. root.innerHTML = `
  888. <div class="nt-stats-metric-row">
  889. <span class="nt-stats-metric nt-stats-metric-session-races">
  890. <span class="nt-stats-metric-heading">Session:</span>
  891. <span class="nt-stats-metric-value">0</span>
  892. </span>
  893. <span class="nt-stats-metric-separator">|</span>
  894. <span class="nt-stats-metric nt-stats-metric-rta">
  895. <span class="nt-stats-metric-heading">Real time:</span>
  896. <span class="nt-stats-metric-value">0</span>
  897. </span>
  898. </div>
  899. <div class="nt-stats-metric-row">
  900. <span class="nt-stats-metric nt-stats-metric-total-races">
  901. <span class="nt-stats-metric-heading">Races:</span>
  902. <span class="nt-stats-metric-value">0</span>
  903. </span>
  904. <span class="nt-stats-metric-separator">(</span>
  905. <span class="nt-stats-metric nt-stats-metric-season-races">
  906. <span class="nt-stats-metric-heading">Season:</span>
  907. <span class="nt-stats-metric-value">N/A</span>
  908. <span class="nt-stats-metric-separator">|</span>
  909. </span>
  910. ${
  911. currentUser.tag
  912. ? `<span class="nt-stats-metric nt-stats-metric-team-races">
  913. <span class="nt-stats-metric-heading">Team:</span>
  914. <span class="nt-stats-metric-value">N/A</span>
  915. <span class="nt-stats-metric-separator">)</span>
  916. </span>`
  917. : ``
  918. }
  919. </div>
  920. <div class="nt-stats-metric-row">
  921. <span class="nt-stats-metric nt-stats-metric-playtime">
  922. <span class="nt-stats-metric-heading">Playtime:</span>
  923. <span class="nt-stats-metric-value">0</span>
  924. </span>
  925.  
  926. </div>
  927. <div class="nt-stats-metric-row">
  928. <span class="nt-stats-metric nt-stats-metric-avg-speed">
  929. <span class="nt-stats-metric-heading">Avg:</span>
  930. <span class="nt-stats-metric-value">0</span>
  931. <span class="nt-stats-metric-suffix">WPM | </span>
  932. </span>
  933. <span class="nt-stats-metric nt-stats-metric-avg-accuracy">
  934. <span class="nt-stats-metric-value">0</span>
  935. <span class="nt-stats-metric-suffix nt-stats-metric-suffix-no-space">% | </span>
  936. </span>
  937. <span class="nt-stats-metric nt-stats-metric-avg-time">
  938. <span class="nt-stats-metric-value">0</span>
  939. <span class="nt-stats-metric-suffix nt-stats-metric-suffix-no-space">s</span>
  940. </span>
  941. </div>
  942. <div class="nt-stats-metric-row">
  943. <span class="nt-stats-metric nt-stats-metric-last-race">
  944. <span class="nt-stats-metric-heading">Last:</span>
  945. <span class="nt-stats-metric-value">N/A</span>
  946. </span>
  947. </div>
  948. </div>`
  949.  
  950. if (greedyStatsReload) {
  951. var currentTime = JSON.parse(JSON.parse(localStorage.getItem("persist:nt")).user).lastConsecRace;
  952. //document.querySelector('.race-hiddenInput').click()
  953. function checkendgreedy(lasttime) {
  954. if(document.querySelector('.modal--raceError')){
  955. clearInterval(intervalId);
  956. location.reload();
  957. return;
  958. }
  959. // console.log("Running another interval");
  960. const newtime = JSON.parse(JSON.parse(localStorage.getItem("persist:nt")).user).lastConsecRace;
  961. if (newtime > lasttime) {
  962. // console.log("new time is different!");
  963. clearInterval(intervalId);
  964. getStats().then(({
  965. user,
  966. dailyChallenges
  967. }) => {
  968. StatWidget.updateStats(user)
  969. if (reloadOnStats) {
  970. if (my_race_started) {
  971. location.reload()
  972. } else {
  973. document.querySelector('.race-hiddenInput').click()
  974. currentTime = newtime;
  975. intervalId = setInterval(checkendgreedy, greedyStatsReloadInt, currentTime);
  976. }
  977. }
  978. })
  979. }
  980. }
  981. var intervalId = setInterval(checkendgreedy, greedyStatsReloadInt, currentTime);
  982. }
  983.  
  984.  
  985. const totalRaces = root.querySelector(".nt-stats-metric-total-races .nt-stats-metric-value"),
  986. sessionRaces = root.querySelector(".nt-stats-metric-session-races .nt-stats-metric-value"),
  987. teamRaces = currentUser.tag ? root.querySelector(".nt-stats-metric-team-races .nt-stats-metric-value") : null,
  988. seasonRaces = root.querySelector(".nt-stats-metric-season-races .nt-stats-metric-value"),
  989. avgSpeed = root.querySelector(".nt-stats-metric-avg-speed .nt-stats-metric-value"),
  990. avgAccuracy = root.querySelector(".nt-stats-metric-avg-accuracy .nt-stats-metric-value"),
  991. lastRace = root.querySelector(".nt-stats-metric-last-race .nt-stats-metric-value"),
  992. playtime = root.querySelector(".nt-stats-metric-playtime .nt-stats-metric-value"),
  993. rta = root.querySelector(".nt-stats-metric-rta .nt-stats-metric-value"),
  994. avgtime = root.querySelector(".nt-stats-metric-avg-time .nt-stats-metric-value")
  995.  
  996.  
  997. // Function to save the current timestamp using GM_setValue
  998. function saveTimestamp() {
  999. const currentTimestamp = Date.now(); // Get current time in milliseconds since Unix epoch
  1000. GM_setValue("savedTimestamp", currentTimestamp.toString()); // Convert to string and save the timestamp
  1001. }
  1002.  
  1003. // Function to load the timestamp and calculate the time difference
  1004. function loadTimeDif() {
  1005. const savedTimestampStr = GM_getValue("savedTimestamp", null); // Load the saved timestamp as a string
  1006.  
  1007. if (savedTimestampStr === null) {
  1008. console.log("No timestamp saved.");
  1009. return null;
  1010. }
  1011.  
  1012. // Convert the retrieved string back to a number
  1013. const savedTimestamp = parseInt(savedTimestampStr, 10);
  1014.  
  1015. // Validate the loaded timestamp
  1016. if (isNaN(savedTimestamp)) {
  1017. console.log("Invalid timestamp.");
  1018. return null;
  1019. }
  1020.  
  1021. const currentTimestamp = Date.now(); // Get the current timestamp
  1022. const timeDiff = currentTimestamp - savedTimestamp; // Calculate the difference in milliseconds
  1023.  
  1024. // Convert the time difference to minutes and seconds
  1025. const minutes = Math.floor(timeDiff / 60000); // Convert to minutes
  1026. const seconds = Math.floor((timeDiff % 60000) / 1000); // Convert remaining milliseconds to seconds
  1027.  
  1028. // Format the time difference as "00:00 MM:SS"
  1029. const formattedTimeDiff = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
  1030.  
  1031. return formattedTimeDiff;
  1032. }
  1033.  
  1034. function formatPlayTime(seconds) {
  1035. let hours = Math.floor(seconds / 3600);
  1036. let minutes = Math.floor((seconds % 3600) / 60);
  1037. let remainingSeconds = seconds % 60;
  1038.  
  1039. return `${hours}h ${minutes}m ${remainingSeconds}s`;
  1040. }
  1041.  
  1042. function lastRaceStat(data) {
  1043. let lastRaceT = data.lastRaces.split('|').pop();
  1044. console.log(lastRaceT);
  1045. let [chars, duration, errors] = lastRaceT.split(',').map(Number);
  1046.  
  1047. let speed = (chars / duration) * 12;
  1048. let accuracy = ((chars - errors) * 100) / chars;
  1049. accuracy = accuracy.toFixed(2);
  1050.  
  1051. return `${speed.toFixed(2)} WPM | ${accuracy} % | ${duration.toFixed(2)} s`;
  1052. }
  1053.  
  1054. function getAverageTime(data) {
  1055. let races = data.lastRaces.split('|');
  1056. let totalDuration = 0;
  1057.  
  1058. races.forEach(race => {
  1059. let [, duration] = race.split(',').map(Number);
  1060. totalDuration += duration;
  1061. });
  1062.  
  1063. let averageDuration = totalDuration / races.length;
  1064. return averageDuration.toFixed(2); // Return average duration rounded to 2 decimal places
  1065. }
  1066. function getAverageWPM(data) {
  1067. let races = data.lastRaces.split('|');
  1068. let totalSpeed = 0;
  1069.  
  1070. races.forEach(race => {
  1071. let [chars, duration, errors] = race.split(',').map(Number);
  1072. let speed = (chars / duration) * 12;
  1073. totalSpeed += speed;
  1074. });
  1075.  
  1076. let averageSpeed = totalSpeed / races.length;
  1077. return averageSpeed.toFixed(2); // Return average duration rounded to 2 decimal places
  1078. }
  1079. function timeSinceLastLogin(data) {
  1080. let lastLogin = data.lastLogin; // Timestamp of last login (in seconds)
  1081. let currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
  1082. currentTime = data.lastConsecRace;
  1083. let elapsedTime = currentTime - lastLogin; // Time since last login in seconds
  1084. let minutes = Math.floor(elapsedTime / 60);
  1085. let seconds = elapsedTime % 60;
  1086.  
  1087. // Format the output as "MM:SS"
  1088. return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  1089. }
  1090.  
  1091. function handleSessionRaces(data) {
  1092. const sessionRaces = data.sessionRaces; // Get sessionRaces from data
  1093.  
  1094. if (sessionRaces === 0) {
  1095. const lastSavedTimestampStr = GM_getValue("savedTimestamp", null);
  1096.  
  1097. if (lastSavedTimestampStr !== null) {
  1098. const lastSavedTimestamp = parseInt(lastSavedTimestampStr, 10);
  1099.  
  1100. // Check if the last saved timestamp was less than 30 minutes ago
  1101. // otherwise, it is not possible, because game resets session after at least 30 minutes
  1102. // necessary, because it might call save function multiple times for same session at the end of the race
  1103. // it would not fix value if page was loaded at first race and it was not succesful
  1104. // so value would overshoot in that case by whenever frist race attempt of the session started
  1105. const fifteenMinutesInMs = 30 * 60 * 1000;
  1106. const currentTimestamp = Date.now();
  1107.  
  1108. if (currentTimestamp - lastSavedTimestamp < fifteenMinutesInMs) {
  1109. return; // Exit the function to avoid saving again
  1110. }
  1111. }
  1112.  
  1113. // If no recent timestamp or no timestamp at all, save the current time
  1114. saveTimestamp();
  1115. } else {
  1116. // If sessionRaces is not 0, load the time difference
  1117. const timeDifference = loadTimeDif();
  1118.  
  1119. if (timeDifference !== null) {
  1120. rta.textContent = timeDifference;
  1121. } else {
  1122. rta.textContent = "N/A";
  1123. }
  1124. }
  1125. }
  1126. return {
  1127. root,
  1128. updateStats: (data) => {
  1129. if (typeof data?.playTime === "number") {
  1130. playtime.textContent = formatPlayTime(data.playTime);
  1131. }
  1132. if (typeof data?.lastRaces === "string") {
  1133. lastRace.textContent = lastRaceStat(data);
  1134. avgtime.textContent = getAverageTime(data);
  1135. avgSpeed.textContent = getAverageWPM(data);
  1136. }
  1137. if (typeof data?.racesPlayed === "number") {
  1138. //console.log(data);
  1139. totalRaces.textContent = data.racesPlayed.toLocaleString();
  1140. if (teamRaces) {
  1141. const trueTeamRaces = (data.racesPlayed - TEAM_RACES_DIF).toLocaleString();
  1142. teamRaces.textContent = `${trueTeamRaces}`;
  1143. }
  1144. const trueSeasonRaces = (data.racesPlayed - CURRENT_SEASON_DIF).toLocaleString();
  1145. seasonRaces.textContent = `${trueSeasonRaces}`;
  1146. }
  1147. if (typeof data?.sessionRaces === "number") {
  1148. sessionRaces.textContent = data.sessionRaces.toLocaleString();
  1149. handleSessionRaces(data);
  1150. }
  1151. if (typeof data?.avgAcc === "string" || typeof data?.avgAcc === "number") {
  1152. avgAccuracy.textContent = data.avgAcc
  1153. }
  1154. if (typeof data?.avgSpeed === "number") {
  1155. //avgSpeed.textContent = data.avgSpeed
  1156. } else if (typeof data?.avgScore === "number") {
  1157. //avgSpeed.textContent = data.avgScore
  1158. }
  1159. },
  1160. }
  1161. })()
  1162.  
  1163. ////////////
  1164. // Main //
  1165. ////////////
  1166.  
  1167. /* Add stats into race page with current values */
  1168. getStats().then(({
  1169. user,
  1170. dailyChallenges
  1171. }) => {
  1172. StatWidget.updateStats(user)
  1173. SeasonProgressWidget.updateStats(user)
  1174. DailyChallengeWidget.updateStats(dailyChallenges)
  1175. ToolbarWidget.updateStats(user)
  1176. logging.info("Update")("Start of race")
  1177.  
  1178. const root = document.createElement("div"),
  1179. body = document.createElement("div")
  1180. root.classList.add("nt-stats-root")
  1181. body.classList.add("nt-stats-body")
  1182.  
  1183. const leftSection = document.createElement("div")
  1184. leftSection.classList.add("nt-stats-left-section")
  1185. leftSection.append(DailyChallengeWidget.root)
  1186.  
  1187. const rightSection = document.createElement("div")
  1188. rightSection.classList.add("nt-stats-right-section")
  1189.  
  1190. rightSection.append(StatWidget.root, SeasonProgressWidget.root)
  1191. if(enableStats){
  1192. body.append(leftSection, rightSection)
  1193. root.append(body, ToolbarWidget.root)
  1194.  
  1195. raceContainer.parentElement.append(root)
  1196. }
  1197. })
  1198.  
  1199. getTeamStats().then(
  1200. (data) => {
  1201. const {
  1202. member,
  1203. season
  1204. } = data
  1205. StatWidget.updateStats({
  1206. teamRaces: member.played,
  1207. seasonPoints: season.points,
  1208. })
  1209. },
  1210. (err) => {
  1211. if (err.message !== "User is not in a team") {
  1212. return Promise.reject(err)
  1213. }
  1214. }
  1215. )
  1216.  
  1217. getSummaryStats().then(({
  1218. seasonBoard
  1219. }) => {
  1220. if (!seasonBoard) {
  1221. return
  1222. }
  1223. StatWidget.updateStats({
  1224. seasonRaces: seasonBoard.played,
  1225. })
  1226. })
  1227.  
  1228. /** Broadcast Channel to let other windows know that stats updated. */
  1229. const MESSGAE_LAST_RACE_UPDATED = "last_race_updated",
  1230. MESSAGE_DAILY_CHALLANGE_UPDATED = "stats_daily_challenge_updated",
  1231. MESSAGE_USER_STATS_UPDATED = "stats_user_updated"
  1232.  
  1233. const statChannel = new BroadcastChannel("NTRacingStats")
  1234. statChannel.onmessage = (e) => {
  1235. const [type, payload] = e.data
  1236. switch (type) {
  1237. case MESSGAE_LAST_RACE_UPDATED:
  1238. getStats().then(({
  1239. user,
  1240. dailyChallenges
  1241. }) => {
  1242. StatWidget.updateStats(user)
  1243. SeasonProgressWidget.updateStats(user)
  1244. DailyChallengeWidget.updateStats(dailyChallenges)
  1245. ToolbarWidget.updateStats(user)
  1246. })
  1247. break
  1248. case MESSAGE_DAILY_CHALLANGE_UPDATED:
  1249. DailyChallengeWidget.updateStats(payload)
  1250. break
  1251. case MESSAGE_USER_STATS_UPDATED:
  1252. StatWidget.updateStats(payload)
  1253. SeasonProgressWidget.updateStats(payload)
  1254. break
  1255. }
  1256. }
  1257.  
  1258. /** Sync Daily Challenge data. */
  1259. server.on("setup", (e) => {
  1260. const dailyChallenges = mergeDailyChallengeData(e.challenges)
  1261. DailyChallengeWidget.updateStats(dailyChallenges)
  1262. statChannel.postMessage([MESSAGE_DAILY_CHALLANGE_UPDATED, dailyChallenges])
  1263. })
  1264.  
  1265. /** Sync some of the User Stat data. */
  1266. server.on("joined", (e) => {
  1267. if (e.userID !== currentUser.userID) {
  1268. return
  1269. }
  1270. const payload = {
  1271. level: e.profile?.level,
  1272. racesPlayed: e.profile?.racesPlayed,
  1273. sessionRaces: e.profile?.sessionRaces,
  1274. avgSpeed: e.profile?.avgSpeed,
  1275. }
  1276. StatWidget.updateStats(payload)
  1277. SeasonProgressWidget.updateStats(payload)
  1278. statChannel.postMessage([MESSAGE_USER_STATS_UPDATED, payload])
  1279. })
  1280.  
  1281. /** Track Race Finish exact time. */
  1282. let hasCollectedResultStats = false
  1283.  
  1284. server.on("update", (e) => {
  1285. const me = e?.racers?.find((r) => r.userID === currentUser.userID)
  1286. if (me.progress.completeStamp > 0 && me.rewards?.current && !hasCollectedResultStats) {
  1287. hasCollectedResultStats = true
  1288. db.backupStatData.put({
  1289. ...me.rewards.current,
  1290. challenges: me.challenges,
  1291. userID: currentUser.userID
  1292. }).then(() => {
  1293. statChannel.postMessage([MESSGAE_LAST_RACE_UPDATED])
  1294. })
  1295. }
  1296. })
  1297.  
  1298. /** Mutation observer to check if Racing Result has shown up. */
  1299. const resultObserver = new MutationObserver(([mutation], observer) => {
  1300. for (const node of mutation.addedNodes) {
  1301. if (node.classList?.contains("race-results")) {
  1302. observer.disconnect()
  1303. logging.info("Update")("Race Results received")
  1304.  
  1305. //AUTO RELOAD
  1306. //logstats();
  1307. //setTimeout(() => location.reload(), autoReloadMS);
  1308. //AUTO RELOAD
  1309.  
  1310. getStats().then(({
  1311. user,
  1312. dailyChallenges
  1313. }) => {
  1314. StatWidget.updateStats(user)
  1315. SeasonProgressWidget.updateStats(user)
  1316. DailyChallengeWidget.updateStats(dailyChallenges)
  1317. ToolbarWidget.updateStats(user)
  1318. if (reloadOnStats) {
  1319. location.reload()
  1320. }
  1321. })
  1322. break
  1323. }
  1324. }
  1325. })
  1326. resultObserver.observe(raceContainer, {
  1327. childList: true,
  1328. subtree: true
  1329. })
  1330.  
  1331.  
  1332. ///MINI MAP
  1333.  
  1334.  
  1335.  
  1336.  
  1337. PIXI.utils.skipHello()
  1338.  
  1339. style.appendChild(
  1340. document.createTextNode(`
  1341. .nt-racing-mini-map-root canvas {
  1342. display: block;
  1343. }`))
  1344. document.head.appendChild(style)
  1345.  
  1346. const racingMiniMap = new PIXI.Application({
  1347. width: 1024,
  1348. height: 100,
  1349. backgroundColor: config.colors.background,
  1350. backgroundAlpha: 0.66
  1351. }),
  1352. container = document.createElement("div");
  1353.  
  1354. container.className = "nt-racing-mini-map-root"
  1355.  
  1356. ///////////////////////
  1357. // Prepare Objects //
  1358. ///////////////////////
  1359. if(scrollPage){window.scrollTo(0, document.body.scrollHeight);}
  1360.  
  1361. const RACER_WIDTH = 28,
  1362. CROSSING_LINE_WIDTH = 32,
  1363. PADDING = 2,
  1364. racers = Array(5).fill(null),
  1365. currentUserID = raceObj.props.user.userID
  1366.  
  1367. // Draw mini racetrack
  1368. const raceTrackBG = new PIXI.TilingSprite(PIXI.Texture.EMPTY, racingMiniMap.renderer.width, racingMiniMap.renderer.height),
  1369. startLine = PIXI.Sprite.from(PIXI.Texture.WHITE),
  1370. finishLine = PIXI.Sprite.from(PIXI.Texture.WHITE)
  1371.  
  1372. startLine.x = CROSSING_LINE_WIDTH
  1373. startLine.y = 0
  1374. startLine.width = 1
  1375. startLine.height = racingMiniMap.renderer.height
  1376. startLine.tint = config.colors.startLine
  1377.  
  1378. finishLine.x = racingMiniMap.renderer.width - CROSSING_LINE_WIDTH - 1
  1379. finishLine.y = 0
  1380. finishLine.width = 1
  1381. finishLine.height = racingMiniMap.renderer.height
  1382. finishLine.tint = config.colors.finishLine
  1383.  
  1384. raceTrackBG.addChild(startLine, finishLine)
  1385.  
  1386. for (let i = 1; i < 5; i++) {
  1387. const lane = PIXI.Sprite.from(PIXI.Texture.WHITE)
  1388. lane.x = 0
  1389. lane.y = i * (racingMiniMap.renderer.height / 5)
  1390. lane.width = racingMiniMap.renderer.width
  1391. lane.height = 1
  1392. lane.tint = config.colors.raceLane
  1393. raceTrackBG.addChild(lane)
  1394. }
  1395.  
  1396. racingMiniMap.stage.addChild(raceTrackBG)
  1397.  
  1398. /* Mini Map movement animation update. */
  1399. function animateRacerTicker() {
  1400. const r = this
  1401. const lapse = Date.now() - r.lastUpdated
  1402. if (r.sprite.x < r.toX) {
  1403. const distance = r.toX - r.fromX
  1404. r.sprite.x = r.fromX + Math.min(distance, distance * (lapse / r.moveMS))
  1405. if (r.ghostSprite && r.sprite.x === r.ghostSprite.x) {
  1406. r.ghostSprite.renderable = false
  1407. }
  1408. }
  1409. if (r.skipped > 0) {
  1410. const nitroTargetWidth = r.nitroToX - r.nitroFromX
  1411. if (r.nitroSprite.width < nitroTargetWidth) {
  1412. r.nitroSprite.width = Math.min(nitroTargetWidth, r.sprite.x - r.nitroFromX)
  1413. } else if (r.nitroSprite.width === nitroTargetWidth && r.nitroSprite.alpha > 0 && !r.nitroDisableFade) {
  1414. if (r.nitroSprite.alpha === 1) {
  1415. r.nitroStartFadeStamp = Date.now() - 1
  1416. }
  1417. r.nitroSprite.alpha = Math.max(0, 1 - ((Date.now() - r.nitroStartFadeStamp) / 1e3))
  1418. }
  1419. }
  1420. if (r.completeStamp !== null && r.sprite.x === r.toX && r.nitroSprite.alpha === 0) {
  1421. racingMiniMap.ticker.remove(animateRacerTicker, this)
  1422. }
  1423. }
  1424.  
  1425. /* Handle adding in players on the mini map. */
  1426. server.on("joined", (e) => {
  1427. //console.log(my_race_started);
  1428. my_race_started = true;
  1429. if(scrollPage){window.scrollTo(0, document.body.scrollHeight);}
  1430. const {
  1431. lane,
  1432. userID
  1433. } = e
  1434.  
  1435. let color = config.colors.opponentBot
  1436. if (userID === currentUserID) {
  1437. color = config.colors.me
  1438. } else if (!e.robot) {
  1439. color = config.colors.opponentPlayer
  1440. } else if (e.profile.specialRobot === "wampus") {
  1441. color = config.colors.opponentWampus
  1442. }
  1443.  
  1444. if (racers[lane]) {
  1445. racers[lane].ghostSprite.tint = color
  1446. racers[lane].sprite.tint = color
  1447. racers[lane].sprite.x = 0 - RACER_WIDTH + PADDING
  1448. racers[lane].lastUpdated = Date.now()
  1449. racers[lane].fromX = racers[lane].sprite.x
  1450. racers[lane].toX = PADDING
  1451. racers[lane].sprite.renderable = true
  1452. return
  1453. }
  1454.  
  1455. const r = PIXI.Sprite.from(PIXI.Texture.WHITE)
  1456. r.x = 0 - RACER_WIDTH + PADDING
  1457. r.y = PADDING + (lane > 0 ? 1 : 0) + (lane * (racingMiniMap.renderer.height / 5))
  1458. r.tint = color
  1459. r.width = RACER_WIDTH
  1460. r.height = 16 - (lane > 0 ? 1 : 0)
  1461.  
  1462. const n = PIXI.Sprite.from(PIXI.Texture.WHITE)
  1463. n.y = r.y + ((16 - (lane > 0 ? 1 : 0)) / 2) - 1
  1464. n.renderable = false
  1465. n.tint = config.colors.nitro
  1466. n.width = 1
  1467. n.height = 2
  1468.  
  1469. racers[lane] = {
  1470. lane,
  1471. sprite: r,
  1472. userID: userID,
  1473. ghostSprite: null,
  1474. nitroSprite: n,
  1475. lastUpdated: Date.now(),
  1476. fromX: r.x,
  1477. toX: PADDING,
  1478. skipped: 0,
  1479. nitroStartFadeStamp: null,
  1480. nitroFromX: null,
  1481. nitroToX: null,
  1482. nitroDisableFade: false,
  1483. moveMS: 250,
  1484. completeStamp: null,
  1485. }
  1486.  
  1487. if (config.moveDestination.enabled) {
  1488. const g = PIXI.Sprite.from(PIXI.Texture.WHITE)
  1489. g.x = PADDING
  1490. g.y = PADDING + (lane > 0 ? 1 : 0) + (lane * (racingMiniMap.renderer.height / 5))
  1491. g.tint = color
  1492. g.alpha = config.moveDestination.alpha
  1493. g.width = RACER_WIDTH
  1494. g.height = 16 - (lane > 0 ? 1 : 0)
  1495. g.renderable = false
  1496.  
  1497. racers[lane].ghostSprite = g
  1498. racingMiniMap.stage.addChild(g)
  1499. }
  1500.  
  1501. racingMiniMap.stage.addChild(n)
  1502. racingMiniMap.stage.addChild(r)
  1503.  
  1504. racingMiniMap.ticker.add(animateRacerTicker, racers[lane])
  1505. })
  1506.  
  1507. /* Handle any players leaving the race track. */
  1508. server.on("left", (e) => {
  1509. const lane = racers.findIndex((r) => r?.userID === e)
  1510. if (racers[lane]) {
  1511. racers[lane].sprite.renderable = false
  1512. racers[lane].ghostSprite.renderable = false
  1513. racers[lane].nitroSprite.renderable = false
  1514. }
  1515. })
  1516.  
  1517. /* Handle race map progress position updates. */
  1518. server.on("update", (e) => {
  1519. if(scrollPage){window.scrollTo(0, document.body.scrollHeight);}
  1520. let moveFinishMS = 100
  1521.  
  1522. const payloadUpdateRacers = e.racers.slice().sort((a, b) => {
  1523. if (a.progress.completeStamp === b.progress.completeStamp) {
  1524. return 0
  1525. }
  1526. if (a.progress.completeStamp === null) {
  1527. return 1
  1528. }
  1529. return a.progress.completeStamp > 0 && b.progress.completeStamp > 0 && a.progress.completeStamp > b.progress.completeStamp ? 1 : -1
  1530. })
  1531.  
  1532. for (let i = 0; i < payloadUpdateRacers.length; i++) {
  1533. const r = payloadUpdateRacers[i],
  1534. {
  1535. completeStamp,
  1536. skipped
  1537. } = r.progress,
  1538. racerObj = racers[r.lane]
  1539. if (!racerObj || racerObj.completeStamp > 0 || (r.userID === currentUserID && completeStamp <= 0 && config.trackLocally)) {
  1540. continue
  1541. }
  1542.  
  1543. if (r.disqualified) {
  1544. racingMiniMap.ticker.remove(animateRacerTicker, racerObj)
  1545. racingMiniMap.stage.removeChild(racerObj.sprite, racerObj.nitroSprite)
  1546. if (racerObj.ghostSprite) {
  1547. racingMiniMap.stage.removeChild(racerObj.ghostSprite)
  1548. }
  1549. racerObj.sprite.destroy()
  1550. racerObj.ghostSprite.destroy()
  1551. racerObj.nitroSprite.destroy()
  1552.  
  1553. racers[r.lane] = null
  1554. continue
  1555. }
  1556.  
  1557. racerObj.lastUpdated = Date.now()
  1558. racerObj.fromX = racerObj.sprite.x
  1559.  
  1560. if (racerObj.completeStamp === null && completeStamp > 0) {
  1561. racerObj.completeStamp = completeStamp
  1562. racerObj.toX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
  1563. racerObj.moveMS = moveFinishMS
  1564.  
  1565. if (racerObj.nitroDisableFade) {
  1566. racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
  1567. racerObj.nitroDisableFade = false
  1568. }
  1569. } else {
  1570. racerObj.moveMS = 1e3
  1571. racerObj.toX = r.progress.percentageFinished * (racingMiniMap.renderer.width - RACER_WIDTH - CROSSING_LINE_WIDTH - PADDING - 1)
  1572. racerObj.sprite.x = racerObj.fromX
  1573. }
  1574.  
  1575. if (racerObj.ghostSprite) {
  1576. racerObj.ghostSprite.x = racerObj.toX
  1577. racerObj.ghostSprite.renderable = true
  1578. }
  1579.  
  1580. if (skipped !== racerObj.skipped) {
  1581. if (racerObj.skipped === 0) {
  1582. racerObj.nitroFromX = racerObj.fromX
  1583. racerObj.nitroSprite.x = racerObj.fromX
  1584. racerObj.nitroSprite.renderable = true
  1585. }
  1586. racerObj.skipped = skipped // because infinite nitros exist? :/
  1587. racerObj.nitroToX = racerObj.toX
  1588. racerObj.nitroSprite.alpha = 1
  1589. if (racerObj.completeStamp !== null) {
  1590. racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
  1591. }
  1592. }
  1593.  
  1594. if (completeStamp > 0 && i + 1 < payloadUpdateRacers.length) {
  1595. const nextRacer = payloadUpdateRacers[i + 1],
  1596. nextRacerObj = racers[nextRacer?.lane]
  1597. if (nextRacerObj && nextRacerObj.completeStamp === null && nextRacer.progress.completeStamp > 0 && nextRacer.progress.completeStamp > completeStamp) {
  1598. moveFinishMS += 100
  1599. }
  1600. }
  1601. }
  1602. })
  1603.  
  1604. if (config.trackLocally) {
  1605. let lessonLength = 0
  1606. server.on("status", (e) => {
  1607. if (e.status === "countdown") {
  1608. lessonLength = e.lessonLength
  1609. }
  1610. })
  1611.  
  1612. const originalSendPlayerUpdate = server.sendPlayerUpdate
  1613. server.sendPlayerUpdate = (data) => {
  1614. originalSendPlayerUpdate(data)
  1615. const racerObj = racers.find((r) => r?.userID === currentUserID)
  1616. if (!racerObj) {
  1617. return
  1618. }
  1619.  
  1620. const percentageFinished = (data.t / (lessonLength || 1))
  1621. racerObj.lastUpdated = Date.now()
  1622. racerObj.fromX = racerObj.sprite.x
  1623. racerObj.moveMS = 100
  1624. racerObj.toX = percentageFinished * (racingMiniMap.renderer.width - RACER_WIDTH - CROSSING_LINE_WIDTH - PADDING - 1)
  1625. racerObj.sprite.x = racerObj.fromX
  1626.  
  1627. if (racerObj.ghostSprite) {
  1628. racerObj.ghostSprite.x = racerObj.toX
  1629. racerObj.ghostSprite.renderable = true
  1630. }
  1631.  
  1632. if (data.s) {
  1633. if (racerObj.skipped === 0) {
  1634. racerObj.nitroFromX = racerObj.fromX
  1635. racerObj.nitroSprite.x = racerObj.fromX
  1636. racerObj.nitroSprite.renderable = true
  1637. }
  1638. racerObj.skipped = data.s // because infinite nitros exist? but I'm not going to test that! :/
  1639. racerObj.nitroToX = racerObj.toX
  1640. racerObj.nitroSprite.alpha = 1
  1641. racerObj.nitroDisableFade = percentageFinished === 1
  1642.  
  1643. if (racerObj.completeStamp !== null) {
  1644. racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
  1645. }
  1646. }
  1647. }
  1648. }
  1649.  
  1650. /////////////
  1651. // Final //
  1652. /////////////
  1653.  
  1654. if (ENABLE_MINI_MAP) {
  1655. container.append(racingMiniMap.view)
  1656. raceContainer.after(container)
  1657. }
  1658.  
  1659. //alt wpm thingy
  1660.  
  1661. /** Get Nitro Word Length. */
  1662. const nitroWordLength = (words, i) => {
  1663. let wordLength = words[i].length + 1
  1664. if (i > 0 && i + 1 < words.length) {
  1665. wordLength++
  1666. }
  1667. return wordLength
  1668. }
  1669.  
  1670. /** Get Player Avg using lastRaces data. */
  1671. const getPlayerAvg = (prefix, raceObj, lastRaces) => {
  1672. const raceLogs = (lastRaces || raceObj.props.user.lastRaces)
  1673. .split("|")
  1674. .map((r) => {
  1675. const data = r.split(","),
  1676. typed = parseInt(data[0], 10),
  1677. time = parseFloat(data[1]),
  1678. errs = parseInt(data[2])
  1679. if (isNaN(typed) || isNaN(time) || isNaN(errs)) {
  1680. return false
  1681. }
  1682. return {
  1683. time,
  1684. acc: 1 - errs / typed,
  1685. wpm: typed / 5 / (time / 60),
  1686. }
  1687. })
  1688. .filter((r) => r !== false)
  1689.  
  1690. const avgSpeed = raceLogs.reduce((prev, current) => prev + current.wpm, 0.0) / Math.max(raceLogs.length, 1)
  1691.  
  1692. logging.info(prefix)("Avg Speed", avgSpeed)
  1693. console.table(raceLogs, ["time", "acc", "wpm"])
  1694.  
  1695. return avgSpeed
  1696. }
  1697.  
  1698. ///////////////
  1699. // Backend //
  1700. ///////////////
  1701.  
  1702. if (config.targetWPM <= 0) {
  1703. logging.error("Init")("Invalid target WPM value")
  1704. return
  1705. }
  1706.  
  1707. let raceTimeLatency = null
  1708.  
  1709. /** Styles for the following components. */
  1710. const styleNew = document.createElement("style")
  1711. styleNew.appendChild(
  1712. document.createTextNode(`
  1713. /* Some Overrides */
  1714. .race-results {
  1715. z-index: 6;
  1716. }
  1717.  
  1718. /* Sandbagging Tool */
  1719. .nt-evil-sandbagging-root {
  1720. position: absolute;
  1721. top: 0px;
  1722. left: 0px;
  1723. z-index: 5;
  1724. color: #eee;
  1725. touch-action: none;
  1726. }
  1727. .nt-evil-sandbagging-metric-value {
  1728. font-weight: 600;
  1729. font-family: "Roboto Mono", "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
  1730. }
  1731. .nt-evil-sandbagging-metric-suffix {
  1732. color: #aaa;
  1733. }
  1734. .nt-evil-sandbagging-live {
  1735. padding: 5px;
  1736. border-radius: 8px;
  1737. color: #FF69B4;
  1738. background-color: rgb(0, 0, 0, 0.5);
  1739. text-align: center;
  1740. }
  1741. .nt-evil-sandbagging-live span.live-wpm-inactive {
  1742. opacity: 1;
  1743. }
  1744. .nt-evil-sandbagging-live > span:not(.live-wpm-inactive) .nt-evil-sandbagging-metric-value {
  1745. color: #ffe275;
  1746. }
  1747. .nt-evil-sandbagging-best-live-wpm {
  1748. font-size: 10px;
  1749. }
  1750. .nt-evil-sandbagging-section {
  1751. padding: 5px;
  1752. border-top: 1px solid rgba(255, 255, 255, 0.15);
  1753. font-size: 10px;
  1754. text-align: center;
  1755. }
  1756. .nt-evil-sandbagging-stats {
  1757. background-color: rgba(20, 20, 20, 0.95);
  1758. }
  1759. .nt-evil-sandbagging-results {
  1760. border-bottom-left-radius: 8px;
  1761. border-bottom-right-radius: 8px;
  1762. background-color: rgba(55, 55, 55, 0.95);
  1763. }`)
  1764. )
  1765. document.head.appendChild(styleNew);
  1766.  
  1767. /** Manages and displays the race timer. */
  1768. const RaceTimer = ((config) => {
  1769. // Restore widget settings
  1770. let widgetSettings = null
  1771. try {
  1772. const data = localStorage.getItem("nt_sandbagging_tool")
  1773. if (typeof data === "string") {
  1774. widgetSettings = JSON.parse(data)
  1775. }
  1776. } catch {
  1777. widgetSettings = null
  1778. }
  1779. if (widgetSettings === null) {
  1780. widgetSettings = { x: 384, y: 285 }
  1781. }
  1782.  
  1783. // Setup Widget
  1784. const root = document.createElement("div")
  1785. root.classList.add("nt-evil-sandbagging-root", "has-live-wpm")
  1786. root.dataset.x = widgetSettings.x
  1787. root.dataset.y = widgetSettings.y
  1788. root.style.transform = `translate(${parseFloat(root.dataset.x) || 0}px, ${parseFloat(root.dataset.y) || 0}px)`
  1789. root.innerHTML = `
  1790. <div class="nt-evil-sandbagging-live">
  1791. <span class="nt-evil-sandbagging-current-live-wpm live-wpm-inactive">
  1792. <small class="nt-evil-sandbagging-metric-suffix">Prepare for your race!</small><span class="nt-evil-sandbagging-live-wpm nt-evil-sandbagging-metric-value"></span>
  1793. </span>
  1794. <span class="nt-evil-sandbagging-best-live-wpm live-wpm-inactive">
  1795. (<span class="nt-evil-sandbagging-metric-value">0.00</span> <small class="nt-evil-sandbagging-metric-suffix">WPM</small>)
  1796. </span>
  1797. </div>
  1798. <div class="nt-evil-sandbagging-section nt-evil-sandbagging-stats">
  1799. Timer: <span class="nt-evil-sandbagging-live-time nt-evil-sandbagging-metric-value">0.00</span> / <span class="nt-evil-sandbagging-target-time nt-evil-sandbagging-metric-value">0.00</span> <small class="nt-evil-sandbagging-metric-suffix">sec</small> |
  1800. Target: <span class="nt-evil-sandbagging-metric-value">${config.targetWPM}</span> <small class="nt-evil-sandbagging-metric-suffix">WPM</small> |
  1801. Avg: <span class="nt-evil-sandbagging-current-avg-wpm nt-evil-sandbagging-metric-value">?</span> <small class="nt-evil-sandbagging-metric-suffix">WPM</small>
  1802. </div>
  1803. <div class="nt-evil-sandbagging-section nt-evil-sandbagging-results">
  1804. Time: <span class="nt-evil-sandbagging-result-time nt-evil-sandbagging-metric-value">?</span> <small class="nt-evil-sandbagging-metric-suffix">secs</small> |
  1805. Speed: <span class="nt-evil-sandbagging-result-wpm nt-evil-sandbagging-metric-value">?</span> <small class="nt-evil-sandbagging-metric-suffix">WPM</small> |
  1806. Avg: <span class="nt-evil-sandbagging-new-avg-wpm nt-evil-sandbagging-metric-value">?</span> <small class="nt-evil-sandbagging-metric-suffix">WPM</small> |
  1807. Latency: <span class="nt-evil-sandbagging-latency nt-evil-sandbagging-metric-value">?</span> <small class="nt-evil-sandbagging-metric-suffix">ms</small>
  1808. </div>`
  1809.  
  1810. const liveContainerNode = root.querySelector(".nt-evil-sandbagging-live"),
  1811. liveCurrentWPMContainerNode = liveContainerNode.querySelector(".nt-evil-sandbagging-current-live-wpm"),
  1812. liveWPMValueNode = liveCurrentWPMContainerNode.querySelector(".nt-evil-sandbagging-live-wpm"),
  1813. liveBestWPMContainerNode = liveContainerNode.querySelector(".nt-evil-sandbagging-best-live-wpm"),
  1814. liveBestWPMValueNode = liveBestWPMContainerNode.querySelector(".nt-evil-sandbagging-metric-value"),
  1815. statContainerNode = root.querySelector(".nt-evil-sandbagging-stats"),
  1816. liveTimeNode = statContainerNode.querySelector(".nt-evil-sandbagging-live-time"),
  1817. targetTimeNode = statContainerNode.querySelector(".nt-evil-sandbagging-target-time"),
  1818. currentAvgWPMNode = statContainerNode.querySelector(".nt-evil-sandbagging-current-avg-wpm"),
  1819. resultContainerNode = root.querySelector(".nt-evil-sandbagging-results"),
  1820. resultTimeNode = resultContainerNode.querySelector(".nt-evil-sandbagging-result-time"),
  1821. resultWPMNode = resultContainerNode.querySelector(".nt-evil-sandbagging-result-wpm"),
  1822. resultNewAvgWPMNode = resultContainerNode.querySelector(".nt-evil-sandbagging-new-avg-wpm"),
  1823. resultLatencyNode = resultContainerNode.querySelector(".nt-evil-sandbagging-latency")
  1824.  
  1825. resultContainerNode.remove()
  1826.  
  1827. statContainerNode.style.display = 'none';
  1828. liveBestWPMContainerNode.style.display = 'none';
  1829. resultContainerNode.style.display = 'none';
  1830.  
  1831. let timer = null,
  1832. targetWPM = config.targetWPM || 79.49,
  1833. startTime = null,
  1834. finishTime = null,
  1835. skipLength = null,
  1836. bestSkipLength = null,
  1837. lessonLength = null,
  1838. onTargetTimeUpdate = null,
  1839. onTimeUpdate = null
  1840.  
  1841. /** Updates the race timer metrics. */
  1842. const refreshCurrentTime = () => {
  1843. if (startTime === null) {
  1844. logging.warn("Update")("Invalid last time, unable to update current timer")
  1845. return
  1846. }
  1847. if (finishTime !== null) {
  1848. return
  1849. }
  1850.  
  1851. let diff = Date.now() - startTime
  1852. if (onTimeUpdate) {
  1853. onTimeUpdate(diff)
  1854. }
  1855. liveTimeNode.textContent = (diff / 1e3).toFixed(2);
  1856.  
  1857. diff /= 6e4;
  1858. const suffixwpm = document.querySelector(".nt-evil-sandbagging-metric-suffix");
  1859. const currentWPM = (lessonLength - skipLength) / 5 / diff,
  1860. bestWPM = (lessonLength - bestSkipLength) / 5 / diff
  1861. if (currentWPM < (config.targetWPM+20)){
  1862. liveWPMValueNode.textContent = (currentWPM-config.dif).toFixed(1);
  1863. suffixwpm.style.display = 'block';
  1864. }
  1865. else {
  1866.  
  1867. suffixwpm.style.display = 'none';
  1868. liveWPMValueNode.textContent = "Just type...!"
  1869. }
  1870. liveBestWPMValueNode.textContent = bestWPM.toFixed(2)
  1871.  
  1872. if (currentWPM - targetWPM <= config.indicateWPMWithin) {
  1873. liveCurrentWPMContainerNode.classList.remove("live-wpm-inactive")
  1874. }
  1875. if (bestWPM - targetWPM <= config.indicateWPMWithin) {
  1876. liveBestWPMContainerNode.classList.remove("live-wpm-inactive")
  1877. }
  1878. timer = setTimeout(refreshCurrentTime, config.timerRefreshIntervalMS)
  1879. }
  1880.  
  1881. /** Toggle whether to show best wpm counter or not (the small text). */
  1882. const toggleBestLiveWPM = (show) => {
  1883. if (show) {
  1884. liveContainerNode.append(liveBestWPMContainerNode)
  1885. } else {
  1886. liveBestWPMContainerNode.remove()
  1887. }
  1888. }
  1889.  
  1890. /** Save widget settings. */
  1891. const saveSettings = () => {
  1892. localStorage.setItem("nt_sandbagging_tool", JSON.stringify(widgetSettings))
  1893. }
  1894. saveSettings()
  1895.  
  1896. /** Setup draggable widget. */
  1897. interact(root).draggable({
  1898. modifiers: [
  1899. interact.modifiers.restrictRect({
  1900. //restriction: "parent",
  1901. endOnly: true,
  1902. }),
  1903. ],
  1904. listeners: {
  1905. move: (event) => {
  1906. const target = event.target,
  1907. x = (parseFloat(target.dataset.x) || 0) + event.dx,
  1908. y = (parseFloat(target.dataset.y) || 0) + event.dy
  1909.  
  1910. target.style.transform = "translate(" + x + "px, " + y + "px)"
  1911.  
  1912. target.dataset.x = x
  1913. target.dataset.y = y
  1914.  
  1915. widgetSettings.x = x
  1916. widgetSettings.y = y
  1917.  
  1918. saveSettings()
  1919. },
  1920. },
  1921. })
  1922.  
  1923. return {
  1924. root,
  1925. setTargetWPM: (wpm) => {
  1926. targetWPM = wpm
  1927. },
  1928. setLessonLength: (l) => {
  1929. lessonLength = l
  1930. },
  1931. getLessonLength: () => lessonLength,
  1932. setSkipLength: (l) => {
  1933. skipLength = l
  1934. toggleBestLiveWPM(false)
  1935. if (skipLength !== bestSkipLength) {
  1936. const newTime = ((lessonLength - skipLength) / 5 / targetWPM) * 60
  1937. if (onTargetTimeUpdate) {
  1938. onTargetTimeUpdate(newTime * 1e3)
  1939. }
  1940. targetTimeNode.textContent = newTime.toFixed(2)
  1941. }
  1942. },
  1943. setBestSkipLength: (l) => {
  1944. bestSkipLength = l
  1945. const newTime = ((lessonLength - bestSkipLength) / 5 / targetWPM) * 60
  1946. if (onTargetTimeUpdate) {
  1947. onTargetTimeUpdate(newTime * 1e3)
  1948. }
  1949. targetTimeNode.textContent = newTime.toFixed(2)
  1950. },
  1951. start: (t) => {
  1952. if (timer) {
  1953. clearTimeout(timer)
  1954. }
  1955. //startTime = t
  1956. startTime = Date.now();
  1957. refreshCurrentTime()
  1958. },
  1959. stop: () => {
  1960. if (timer) {
  1961. finishTime = Date.now()
  1962. clearTimeout(timer)
  1963. }
  1964. },
  1965. setCurrentAvgSpeed: (wpm) => {
  1966. currentAvgWPMNode.textContent = wpm.toFixed(2)
  1967. },
  1968. reportFinishResults: (speed, avgSpeed, actualStartTime, actualFinishTime) => {
  1969. const latency = actualFinishTime - finishTime,
  1970. output = (latency / 1e3).toFixed(2)
  1971.  
  1972. resultTimeNode.textContent = ((actualFinishTime - actualStartTime) / 1e3).toFixed(2)
  1973. resultWPMNode.textContent = speed.toFixed(2)
  1974. liveWPMValueNode.textContent = speed.toFixed(2)
  1975. resultNewAvgWPMNode.textContent = avgSpeed.toFixed(2)
  1976. resultLatencyNode.textContent = latency
  1977. toggleBestLiveWPM(false)
  1978.  
  1979. root.append(resultContainerNode)
  1980.  
  1981. logging.info("Finish")(`Race Finish acknowledgement latency: ${output} secs (${latency}ms)`)
  1982. return output
  1983. },
  1984. setOnTargetTimeUpdate: (c) => {
  1985. onTargetTimeUpdate = c
  1986. },
  1987. setOnTimeUpdate: (c) => {
  1988. onTimeUpdate = c
  1989. },
  1990. }
  1991. })(config)
  1992.  
  1993. window.NTRaceTimer = RaceTimer
  1994.  
  1995. /** Track Racing League for analysis. */
  1996. server.on("setup", (e) => {
  1997. if (e.scores && e.scores.length === 2) {
  1998. const [from, to] = e.scores
  1999. logging.info("Init")("Racing League", JSON.stringify({ from, to, trackLeader: e.trackLeader }))
  2000. RaceTimer.setCurrentAvgSpeed(getPlayerAvg("Init", raceObj))
  2001. }
  2002. })
  2003. var countdownTimer = -1;
  2004. /** Track whether to start the timer and manage target goals. */
  2005. server.on("status", (e) => {
  2006. if (e.status === "countdown") {
  2007. const wpmtextnode = document.querySelector(".nt-evil-sandbagging-live-wpm");
  2008. const wpmsuffix = document.querySelector(".nt-evil-sandbagging-metric-suffix");
  2009. if (countdownTimer !== -1) {
  2010. return
  2011. }
  2012. var lastCountdown = 400;
  2013. wpmsuffix.textContent = "Race starts in... ";
  2014. countdownTimer = setInterval(() => {
  2015. wpmtextnode.textContent = (lastCountdown/100).toFixed(2);
  2016. lastCountdown--;
  2017. }, 10)
  2018.  
  2019. RaceTimer.setLessonLength(e.lessonLength)
  2020.  
  2021. const words = e.lesson.split(" ")
  2022.  
  2023. let mostLetters = null,
  2024. nitroWordCount = 0
  2025. words.forEach((_, i) => {
  2026. let wordLength = nitroWordLength(words, i)
  2027. if (mostLetters === null || mostLetters < wordLength) {
  2028. mostLetters = wordLength
  2029. }
  2030. })
  2031. RaceTimer.setBestSkipLength(mostLetters)
  2032. } else if (e.status === "racing") {
  2033. const wpmsuffix = document.querySelector(".nt-evil-sandbagging-metric-suffix");
  2034. wpmsuffix.textContent = "Possible WPM: ";
  2035. clearInterval(countdownTimer);
  2036. RaceTimer.start(e.startStamp - config.raceLatencyMS)
  2037.  
  2038. const originalSendPlayerUpdate = server.sendPlayerUpdate
  2039. server.sendPlayerUpdate = (data) => {
  2040. originalSendPlayerUpdate(data)
  2041. if (data.t >= RaceTimer.getLessonLength()) {
  2042. RaceTimer.stop()
  2043. }
  2044. if (typeof data.s === "number") {
  2045. RaceTimer.setSkipLength(data.s)
  2046. }
  2047. }
  2048. }
  2049. })
  2050.  
  2051. /** Track Race Finish exact time. */
  2052. server.on("update", (e) => {
  2053. const me = e?.racers?.find((r) => r.userID === currentUserID)
  2054. if (raceTimeLatency === null && me.progress.completeStamp > 0 && me.rewards) {
  2055. const { typed, skipped, startStamp, completeStamp } = me.progress
  2056.  
  2057. raceTimeLatency = RaceTimer.reportFinishResults(
  2058. (typed - skipped) / 5 / ((completeStamp - startStamp) / 6e4),
  2059. getPlayerAvg("Finish", raceObj, me.rewards.current.lastRaces),
  2060. startStamp,
  2061. completeStamp
  2062. )
  2063. }
  2064. })
  2065.  
  2066. /////////////
  2067. // Final //
  2068. /////////////
  2069. if (ENABLE_ALT_WPM_COUNTER){
  2070. raceContainer.append(RaceTimer.root);
  2071. }
  2072.  
  2073.  
  2074.  
  2075.