Greasy Fork is available in English.

Nitro Type - Admin Panel

Always displays the selected car, hue and trail on the race track.

  1. // ==UserScript==
  2. // @name Nitro Type - Admin Panel
  3. // @version 0.2.1
  4. // @description Always displays the selected car, hue and trail on the race track.
  5. // @author Toonidy
  6. // @match *://*.nitrotype.com/race
  7. // @match *://*.nitrotype.com/race/*
  8. // @match *://*.nitrotype.com/garage
  9. // @match *://*.nitrotype.com/garage/customizer
  10. // @match *://*.nitrotype.com/garage/customizer/*
  11. // @icon https://i.ibb.co/YRs06pc/toonidy-userscript.png
  12. // @grant GM_getResourceURL
  13. // @resource icon_tab https://i.ibb.co/28Ts3Xd/key-icon.png#sha512=3b8723fb0a6f220c9fa03ea38d9a600df2efe9dc38217b0be4a71132d12457edd6344c7c12370e5a451ea6199fb39a06dabc94c7c8c9782c1c4584b6e5d04a53
  14. // @require https://greatest.deepsurf.us/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.2/dexie.min.js#sha512-/Aa8vGWIh0EnOTIVN/ZWTS3UqyJJDhWYtIPS/IqtaaSG0VA6hC6CSvtWdh2+T72q74+2l1RFgu+ig91LGLX57A==
  16. // @license MIT
  17. // @namespace https://greatest.deepsurf.us/users/858426
  18. // ==/UserScript==
  19.  
  20. /* global NTGLOBALS findReact createLogger Dexie */
  21.  
  22. const logging = createLogger("Nitro Type Admin Panel")
  23.  
  24. // Config storage
  25. const db = new Dexie("NTAdminPanel")
  26. db.version(1).stores({
  27. savedCar: "userID",
  28. })
  29. db.open().catch(function (e) {
  30. logging.error("Init")("Failed to open up the config database", e)
  31. })
  32.  
  33. let currentUser = null
  34. try {
  35. currentUser = JSON.parse(JSON.parse(localStorage.getItem("persist:nt")).user)
  36. if (!currentUser.loggedIn) {
  37. logging.error("Init")("Custom Car is only available for logged in users.")
  38. return
  39. }
  40. } catch (err) {
  41. logging.error("Init")("Failed to identify current logged in user")
  42. return
  43. }
  44.  
  45. db.savedCar.get(currentUser.userID).then(main)
  46.  
  47. ////////////
  48. // Main //
  49. ////////////
  50.  
  51. function main(config) {
  52. ///////////////////////
  53. // Customizer Page //
  54. ///////////////////////
  55.  
  56. if (window.location.pathname === "/garage/customizer" || window.location.pathname.startsWith("/garage/customizer/")) {
  57. const container = document.querySelector("#root main.structure-content div.customizer"),
  58. reactObj = container ? findReact(container) : null
  59. if (!container || !reactObj) {
  60. logging.error("Init")("Unable to find customizer container")
  61. return
  62. }
  63.  
  64. const RARITY_VALUES = {
  65. common: 1,
  66. uncommon: 2,
  67. rare: 3,
  68. epic: 4,
  69. legendary: 5,
  70. }
  71.  
  72. const tabContainer = container.querySelector(".customizer--tabs.nav-list"),
  73. titleHeading = container.querySelector(".customizer--about--title"),
  74. previewer = container.querySelector(".customizer--previewer"),
  75. previewerCanvas = previewer.querySelector("canvas")
  76. if (!tabContainer || !titleHeading) {
  77. logging.error("Init")("Unable to modify tab navigation")
  78. return
  79. }
  80.  
  81. /* Styles */
  82. const style = document.createElement("style")
  83. style.appendChild(
  84. document.createTextNode(`
  85. .section-nt-admin-panel .customizer--previewer {
  86. right: 714px;
  87. bottom: 230px;
  88. }
  89. .section-nt-admin-panel.nt-admin-panel-unset .customizer--previewer {
  90. right: 580px;
  91. bottom: 230px;
  92. }
  93. .nt-admin-panel-label.customizer--preview {
  94. left: 10px;
  95. right: 715px;
  96. top: 285px;
  97. bottom: 230px;
  98. }
  99. .nt-admin-panel-label.customizer--preview .customizer--vehicle-selection--name {
  100. font-size: 16px;
  101. }
  102. .nt-admin-panel-label.customizer--preview .customizer--vehicle-selection--rarity {
  103. padding-bottom: 0;
  104. }
  105. .nt-admin-panel-no-cars {
  106. position: absolute;
  107. top: 90px;
  108. left: 10px;
  109. right: 580px;
  110. bottom: 230px;
  111. display: none;
  112. align-items: center;
  113. justify-content: center;
  114. border-radius: 3px;
  115. background-color: #202020;
  116. font-size: 18px;
  117. font-weight: 600;
  118. text-shadow: 0 2px 3px rgb(0 0 0 / 50%);
  119. color: #fff;
  120. z-index: 2;
  121. }
  122. .nt-admin-panel-unset .nt-admin-panel-no-cars {
  123. display: flex;
  124. }
  125. .nt-admin-panel-scrollable {
  126. overflow-y: scroll;
  127. scrollbar-face-color: #1C99F4;
  128. scrollbar-track-color: #232633;
  129. }
  130. .nt-admin-panel-scrollable::-webkit-scrollbar {
  131. width: 10px;
  132. height: 10px;
  133. }
  134. .nt-admin-panel-scrollable::-webkit-scrollbar-thumb {
  135. background-color: #1C99F4;
  136. }
  137. .nt-admin-panel-scrollable::-webkit-scrollbar-track {
  138. background-color: #232633;
  139. }
  140. .section-nt-admin-panel .customizer--item-selector-controls {
  141. grid-template-columns: 1fr 210px;
  142. }
  143. .customizer--item-selector.nt-admin-panel-car-selector {
  144. width: 560px;
  145. }
  146. .customizer--item-selector.nt-admin-panel-car-selector .customizer--item-selector-items {
  147. grid-template-columns: repeat(4, 1fr);
  148. }
  149. .customizer--item-selector.nt-admin-panel-trail-selector {
  150. top: 380px;
  151. left: 10px;
  152. }
  153. .customizer--item-selector.nt-admin-panel-paint-selector {
  154. top: 90px;
  155. bottom: 230px;
  156. left: 320px;
  157. width: 125px;
  158. }
  159. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-paint-selector-heading {
  160. display: flex;
  161. align-items: center;
  162. column-gap: 10px;
  163. height: 35px;
  164. padding: 0 10px;
  165. margin-bottom: 5px;
  166. border-top-left-radius: 4px;
  167. border-top-right-radius: 4px;
  168. background-color: #282b3a;
  169. color: #eee;
  170. font-weight: bold;
  171. font-size: 13px;
  172. }
  173. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-paint-selector-heading .nt-admin-panel-paint-selector-heading-icon,
  174. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-paint-selector-heading .nt-admin-panel-paint-selector-heading-icon svg{
  175. width: 20px;
  176. height: 20px;
  177. }
  178. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-paint-selector-heading .nt-admin-panel-paint-selector-heading-icon svg {
  179. fill: #ccc;
  180. }
  181. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-paint-selector-heading .nt-admin-panel-paint-selector-heading-label {
  182. flex-grow: 1;
  183. color: #ccc;
  184. }
  185. .customizer--item-selector.nt-admin-panel-paint-selector .nt-admin-panel-scrollable {
  186. position: absolute;
  187. left: 0;
  188. top: 40px;
  189. right: 0;
  190. bottom: 0;
  191. }
  192. .customizer--item-selector.nt-admin-panel-paint-selector .customizer--item-selector-items {
  193. grid-template-columns: 1fr;
  194. grid-gap: 5px;
  195. margin: 0 5px 5px;
  196. }
  197. .nt-admin-panel-unset .customizer--item-selector.nt-admin-panel-paint-selector {
  198. display: none;
  199. }
  200. .nt-admin-panel-paint-selector .paint-select-preview {
  201. width: 100%;
  202. height: 100%;
  203. background-repeat: no-repeat;
  204. background-position: 50% 50%;
  205. background-size: auto 40px;
  206. }`),
  207. )
  208. document.head.appendChild(style)
  209.  
  210. const saveConfig = (carID, hue, trailID) => {
  211. config = {
  212. userID: currentUser.userID,
  213. car: carID || null,
  214. hue: hue || 0,
  215. trail: trailID || null,
  216. }
  217. db.savedCar.put(config)
  218. }
  219.  
  220. const setCar = (carID, hue, trailID) => {
  221. const carData = reactObj.props.getCarMetaData(carID),
  222. isAnimated = carData.isAnimated,
  223. trailData = trailID ? NTGLOBALS.LOOT.find((l) => l.lootID === trailID && l.type === "trail")?.assetKey : undefined
  224. reactObj.previewer.setCar({
  225. type: isAnimated ? carData.assetKey : carID,
  226. hue,
  227. isAnimated,
  228. trail: trailData,
  229. tweaks: carData.tweaks,
  230. })
  231.  
  232. // TODO: Sound N/A, will have to figure out how to trigger manually
  233. }
  234.  
  235. /* Car Paint Worker */
  236. const CarPainter = ((reactObj) => {
  237. // Source: https://www.nitrotype.com/dist/site/js/cu.js
  238. /* eslint-disable */
  239. function carHueShiftWorkerScript() {
  240. this.onmessage = function (e) {
  241. for (var t = e.data, r = t.pixels, n = t.hue, a = t.id, o = r.length, i = 0; i < o; i += 4) {
  242. var l,
  243. s,
  244. c = r[i] / 255,
  245. u = r[i + 1] / 255,
  246. f = r[i + 2] / 255,
  247. d = Math.min(c, u, f),
  248. p = Math.max(c, u, f),
  249. m = p - d,
  250. h = 0
  251. ;(h = 0 === m ? 0 : p === c ? ((u - f) / m) % 6 : p === u ? (f - c) / m + 2 : (c - u) / m + 4),
  252. (h = Math.round(60 * h)),
  253. (h += n) < 0 && (h += 360),
  254. (h %= 360),
  255. (s = (p + d) / 2),
  256. (l = 0 === m ? 0 : m / (1 - Math.abs(2 * s - 1)))
  257. var v = (1 - Math.abs(2 * s - 1)) * l,
  258. b = v * (1 - Math.abs(((h / 60) % 2) - 1)),
  259. g = s - v / 2
  260. 0 <= h && h < 60
  261. ? ((c = v), (u = b), (f = 0))
  262. : 60 <= h && h < 120
  263. ? ((c = b), (u = v), (f = 0))
  264. : 120 <= h && h < 180
  265. ? ((c = 0), (u = v), (f = b))
  266. : 180 <= h && h < 240
  267. ? ((c = 0), (u = b), (f = v))
  268. : 240 <= h && h < 300
  269. ? ((c = b), (u = 0), (f = v))
  270. : 300 <= h && h < 360 && ((c = v), (u = 0), (f = b)),
  271. (r[i] = Math.round(255 * (c + g))),
  272. (r[i + 1] = Math.round(255 * (u + g))),
  273. (r[i + 2] = Math.round(255 * (f + g)))
  274. }
  275. this.postMessage(
  276. {
  277. id: a,
  278. updated: r,
  279. },
  280. [r.buffer],
  281. )
  282. }
  283. }
  284. /* eslint-enable */
  285.  
  286. let carPaintRequestIndex = 0,
  287. completed = {},
  288. pending = {},
  289. onCarPaintCreated = null
  290.  
  291. const carHueShiftWorker = (() => {
  292. try {
  293. const data = carHueShiftWorkerScript
  294. .toString()
  295. .replace(/^[^{]*{\s*/, "")
  296. .replace(/\s*}[^}]*$/, ""),
  297. scriptBlobData = new Blob([data], { type: "text/javascript" }),
  298. scriptBlobURL = URL.createObjectURL(scriptBlobData)
  299. return new Worker(scriptBlobURL)
  300. } catch (e) {
  301. logging.error("Init")("Failed to setup worker")
  302. }
  303. })(carHueShiftWorkerScript)
  304.  
  305. carHueShiftWorker.onmessage = (e) => {
  306. const { id, updated } = e.data,
  307. { canvas, ctx, width, height } = pending[id],
  308. newImgData = ctx.createImageData(width, height)
  309.  
  310. delete pending[id]
  311. newImgData.data.set(updated)
  312. ctx.putImageData(newImgData, 0, 0)
  313.  
  314. completed[id] = canvas.toDataURL()
  315.  
  316. if (onCarPaintCreated) {
  317. onCarPaintCreated(id, completed[id])
  318. }
  319. }
  320.  
  321. const performHueShift = (carImg, hue) => {
  322. const renderCanvas = document.createElement("canvas"),
  323. ctx = renderCanvas.getContext("2d"),
  324. { width, height } = carImg
  325.  
  326. renderCanvas.width = width
  327. renderCanvas.height = height
  328. ctx.drawImage(carImg, 0, 0)
  329.  
  330. const imgData = ctx.getImageData(0, 0, width, height)
  331.  
  332. pending[hue] = {
  333. id: hue,
  334. width,
  335. height,
  336. canvas: renderCanvas,
  337. ctx,
  338. }
  339.  
  340. carHueShiftWorker.postMessage(
  341. {
  342. id: hue,
  343. hue,
  344. pixels: imgData.data,
  345. },
  346. [imgData.data.buffer],
  347. )
  348. }
  349.  
  350. return {
  351. generateSampleCarPaints: (carID) => {
  352. const carImgSrc = reactObj.props.getCarUrl(carID, false, 0)
  353. if (!carImgSrc) {
  354. return
  355. }
  356.  
  357. const carImgNode = document.createElement("img")
  358. carImgNode.addEventListener("load", () => {
  359. pending = {}
  360. completed = {}
  361. for (let hue = 0; hue <= 340; hue += 10) {
  362. performHueShift(carImgNode, hue)
  363. }
  364. })
  365. carImgNode.src = carImgSrc
  366. },
  367. setCarPaintCreatedHandler: (fn) => {
  368. onCarPaintCreated = fn
  369. },
  370. }
  371. })(reactObj)
  372.  
  373. /* Sort Handlers */
  374. const sortAlphaHandler = (reverse) => {
  375. return (a, b) => (reverse ? b.data.name.localeCompare(a.data.name) : a.data.name.localeCompare(b.data.name))
  376. }
  377. const sortRarityHandler = (reverse) => {
  378. return (a, b) => {
  379. const rarityA = RARITY_VALUES[a.data.options?.rarity] || 0,
  380. rarityB = RARITY_VALUES[b.data.options?.rarity] || 0
  381. if (rarityA === rarityB) {
  382. return 0
  383. }
  384. if (reverse) {
  385. return rarityA < rarityB ? -1 : 1
  386. }
  387. return rarityA > rarityB ? -1 : 1
  388. }
  389. }
  390. const sortIDHandler = (idKey, reverse) => {
  391. return (a, b) => {
  392. if (a.data[idKey] === b.data[idKey]) {
  393. return 0
  394. }
  395. if (reverse) {
  396. return a.data[idKey] < b.data[idKey] ? 1 : -1
  397. }
  398. return a.data[idKey] > b.data[idKey] ? 1 : -1
  399. }
  400. }
  401.  
  402. /* Selected Car Label */
  403. const selectedCarLabel = document.createElement("div")
  404. selectedCarLabel.className = "nt-admin-panel-label customizer--preview vehicle-preview"
  405. selectedCarLabel.innerHTML = `
  406. <div class="customizer--vehicle-selection">
  407. <div class="customizer--vehicle-selection--name"></div>
  408. <div class="customizer--vehicle-selection--rarity">
  409. <div class="rarity-badge rarity-badge--small">
  410. <div class="rarity-badge--extra"></div>
  411. <div class="rarity-badge--content"></div>
  412. </div>
  413. </div>
  414. <div class="customizer--vehicle-selection--equipped">Currently Equipped</div>
  415. </div>`
  416.  
  417. const carLabel = selectedCarLabel.querySelector(".customizer--vehicle-selection--name"),
  418. rarityBadge = selectedCarLabel.querySelector(".customizer--vehicle-selection--rarity .rarity-badge"),
  419. rarityLabel = selectedCarLabel.querySelector(".customizer--vehicle-selection--rarity .rarity-badge--content")
  420.  
  421. const updateCarLabel = (c) => {
  422. carLabel.textContent = c.name
  423. if (c.options?.rarity) {
  424. rarityBadge.className = `rarity-badge rarity-badge--small rarity-badge--${c.options.rarity}`
  425. rarityLabel.textContent = `${c.options.rarity[0].toUpperCase() + c.options.rarity.substr(1)} Car`
  426. }
  427. }
  428.  
  429. /* Selector UI */
  430. const selectItemTemplate = document.createElement("div")
  431. selectItemTemplate.className = "customizer--item-selector-item"
  432. selectItemTemplate.innerHTML = `
  433. <div class="customizer--item-selector-item--labels">
  434. <div class="customizer--item-selector-item--equipped">Equipped</div>
  435. </div>
  436. <div class="customizer--item-selector-item--controls">
  437. <div class="customizer--item-selector-item--favorite">Favorite</div>
  438. <div class="customizer--item-selector-item--hide">Hide</div>
  439. </div>
  440. <div class="customizer--item-selector-item--content">
  441. <div class="rarity-frame rarity-frame--small">
  442. <div class="rarity-frame--extra"></div>
  443. <div class="rarity-frame--content">
  444. <div class="customizer--item-selector-item--container">
  445. <div>
  446. <div class="customizer--item-selector-item--vehicle"></div>
  447. <div class="customizer--tooltip"></div>
  448. </div>
  449. </div>
  450. </div>
  451. </div>
  452. </div>`
  453.  
  454. /* Custom Car Selector UI */
  455. const customCarUI = document.createElement("div")
  456. customCarUI.className = "nt-admin-panel-car-selector customizer--item-selector vehicle-selector show-search scrollable"
  457. customCarUI.innerHTML = `
  458. <div class="customizer--item-selector-controls">
  459. <div class="customizer--item-selector-controls--filter">
  460. <input type="filter" class="input-field customizer--item-selector-controls--filter-input" placeholder="Search" value="">
  461. <button class="customizer--item-selector-controls--filter-clear">×</button>
  462. </div>
  463. <div class="customizer--item-selector-controls--sort">
  464. <div class="customizer--item-selector-controls--sort-label">Sort By</div>
  465. <select class="input-select customizer--item-selector-controls--sort-options">
  466. <option value="rarity_commons">Rarity: Least Rarest</option>
  467. <option value="rarity_rarests">Rarity: Most Rarest</option>
  468. <option value="name_a-z" selected>Name: A - Z</option>
  469. <option value="name_z-a">Name: Z - A</option>
  470. <option value="id_asc">ID: Ascending</option>
  471. <option value="id_desc">ID: Descending</option>
  472. </select>
  473. </div>
  474. </div>
  475. <div class="customizer--item-selector-container nt-admin-panel-scrollable">
  476. <div class="customizer--item-selector-items"></div>
  477. </div>`
  478.  
  479. const customCarSelectorContainer = customCarUI.querySelector(".customizer--item-selector-items")
  480.  
  481. const customCarNoPreview = document.createElement("div")
  482. customCarNoPreview.className = "nt-admin-panel-no-cars"
  483. customCarNoPreview.textContent = "Choose your Custom Car"
  484. if (!config?.car) {
  485. container.classList.add("nt-admin-panel-unset")
  486. }
  487.  
  488. const noCarItem = document.createElement("div")
  489. noCarItem.className = " customizer--item-selector-item"
  490. noCarItem.innerHTML = `
  491. <div class="customizer--item-selector-item--labels">
  492. <div class="customizer--item-selector-item--equipped">Equipped</div>
  493. </div>
  494. <div class="customizer--item-selector-item--content">
  495. <div class="customizer--item-selector-item--container">
  496. <div>
  497. <div class="customizer--item-selector-item--remove">No Car</div>
  498. <div class="customizer--tooltip">Remove Car</div>
  499. </div>
  500. </div>
  501. </div>`
  502. noCarItem.addEventListener("pointerup", () => {
  503. if (!config?.car) {
  504. return
  505. }
  506. container.classList.add("nt-admin-panel-unset")
  507. saveConfig(null, config?.hue, config?.trail)
  508. customCarSelectorContainer.querySelectorAll(".is-equipped").forEach((node) => node.classList.remove("is-equipped"))
  509. noCarItem.classList.add("is-equipped")
  510. selectedCarLabel.remove()
  511. })
  512. if (!config?.car) {
  513. noCarItem.classList.add("is-equipped")
  514. } else {
  515. noCarItem.classList.remove("is-equipped")
  516. }
  517.  
  518. const carSelectItems = NTGLOBALS.CARS.map((c) => {
  519. const item = selectItemTemplate.cloneNode(true)
  520. item.querySelector(".rarity-frame").classList.add(`rarity-frame--${c.options?.rarity}`)
  521. item.querySelector(".customizer--tooltip").textContent = c.name
  522. item.querySelector(".customizer--item-selector-item--vehicle").style.backgroundImage = `url(/cars/${c.options?.smallSrc})`
  523. item.addEventListener("pointerup", () => {
  524. if (c.id === config?.car) {
  525. return
  526. }
  527. container.classList.remove("nt-admin-panel-unset")
  528. setCar(c.id, config?.hue, config?.trail)
  529. saveConfig(c.id, config?.hue, config?.trail)
  530. CarPainter.generateSampleCarPaints(config.car)
  531. customCarSelectorContainer.querySelectorAll(".is-equipped").forEach((node) => node.classList.remove("is-equipped"))
  532. item.classList.add("is-equipped")
  533. container.querySelector(".customizer--preview").after(selectedCarLabel)
  534. updateCarLabel(c)
  535. })
  536. if (c.id === config?.car) {
  537. item.classList.add("is-equipped")
  538. }
  539. return { data: c, node: item }
  540. })
  541.  
  542. const populateList = (search, sortBy) => {
  543. if (sortBy === "name_a-z") {
  544. carSelectItems.sort(sortAlphaHandler(false))
  545. } else if (sortBy === "name_z-a") {
  546. carSelectItems.sort(sortAlphaHandler(true))
  547. } else if (sortBy === "rarity_rarests") {
  548. carSelectItems.sort(sortRarityHandler(false))
  549. } else if (sortBy === "rarity_commons") {
  550. carSelectItems.sort(sortRarityHandler(true))
  551. } else if (sortBy === "id_asc") {
  552. carSelectItems.sort(sortIDHandler("id", false))
  553. } else if (sortBy === "id_desc") {
  554. carSelectItems.sort(sortIDHandler("id", true))
  555. }
  556. const carSelectorFragment = document.createDocumentFragment()
  557. carSelectorFragment.append(noCarItem)
  558. carSelectItems.forEach((c) => {
  559. if (search && c.data.name.toLowerCase().indexOf(search.toLowerCase()) === -1) {
  560. return
  561. }
  562. carSelectorFragment.append(c.node)
  563. })
  564. while (customCarSelectorContainer.firstChild !== null) {
  565. customCarSelectorContainer.removeChild(customCarSelectorContainer.firstChild)
  566. }
  567. customCarSelectorContainer.append(carSelectorFragment)
  568. }
  569.  
  570. const customCarSortBy = customCarUI.querySelector(".input-select.customizer--item-selector-controls--sort-options"),
  571. customCarSearch = customCarUI.querySelector(".input-field.customizer--item-selector-controls--filter-input"),
  572. customCarSearchClear = customCarUI.querySelector(".customizer--item-selector-controls--filter-clear")
  573.  
  574. customCarSearch.placeholder = "Search Car"
  575.  
  576. const customCarApplyFilters = (e) => {
  577. populateList(customCarSearch.value, customCarSortBy.value)
  578. }
  579.  
  580. customCarSortBy.addEventListener("change", customCarApplyFilters)
  581. customCarSearch.addEventListener("keyup", customCarApplyFilters)
  582. customCarSearchClear.addEventListener("click", () => {
  583. customCarSearch.value = ""
  584. populateList(customCarSearch.value, customCarSortBy.value)
  585. })
  586.  
  587. /* Custom Car Trail Selector UI */
  588. const customTrailUI = customCarUI.cloneNode(true)
  589. customTrailUI.className = "nt-admin-panel-trail-selector customizer--item-selector vehicle-selector show-search scrollable"
  590.  
  591. const customTrailSelectorContainer = customTrailUI.querySelector(".customizer--item-selector-items")
  592.  
  593. const noTrailItem = noCarItem.cloneNode(true)
  594. noTrailItem.querySelector(".customizer--item-selector-item--remove").textContent = "No Trail"
  595. noTrailItem.querySelector(".customizer--tooltip").textContent = "Remove Trail"
  596. noTrailItem.addEventListener("pointerup", () => {
  597. if (!config?.trail) {
  598. return
  599. }
  600. if (config?.car) {
  601. setCar(config.car, config?.hue, null)
  602. }
  603. saveConfig(config?.car, config?.hue, null)
  604. customTrailSelectorContainer.querySelectorAll(".is-equipped").forEach((node) => node.classList.remove("is-equipped"))
  605. noTrailItem.classList.add("is-equipped")
  606. })
  607. if (!config?.trail) {
  608. noTrailItem.classList.add("is-equipped")
  609. } else {
  610. noTrailItem.classList.remove("is-equipped")
  611. }
  612.  
  613. const trailSelectItems = NTGLOBALS.LOOT.filter((l) => l.type === "trail").map((t) => {
  614. const item = selectItemTemplate.cloneNode(true)
  615. item.querySelector(".rarity-frame").classList.add(`rarity-frame--${t.options?.rarity}`)
  616. item.querySelector(".customizer--tooltip").textContent = t.name
  617. item.querySelector(".customizer--item-selector-item--vehicle").style.backgroundImage = `url(${t.options?.src})`
  618. item.addEventListener("pointerup", () => {
  619. if (t.lootID === config?.trail) {
  620. return
  621. }
  622. if (config?.car) {
  623. setCar(config.car, config?.hue, t.lootID)
  624. }
  625. saveConfig(config?.car, config?.hue, t.lootID)
  626. customTrailSelectorContainer.querySelectorAll(".is-equipped").forEach((node) => node.classList.remove("is-equipped"))
  627. item.classList.add("is-equipped")
  628. })
  629. if (t.lootID === config?.trail) {
  630. item.classList.add("is-equipped")
  631. }
  632. return { data: t, node: item }
  633. })
  634.  
  635. const populateTrailList = (search, sortBy) => {
  636. if (sortBy === "name_a-z") {
  637. trailSelectItems.sort(sortAlphaHandler(false))
  638. } else if (sortBy === "name_z-a") {
  639. trailSelectItems.sort(sortAlphaHandler(true))
  640. } else if (sortBy === "rarity_rarests") {
  641. trailSelectItems.sort(sortRarityHandler(false))
  642. } else if (sortBy === "rarity_commons") {
  643. trailSelectItems.sort(sortRarityHandler(true))
  644. } else if (sortBy === "id_asc") {
  645. trailSelectItems.sort(sortIDHandler("lootID", false))
  646. } else if (sortBy === "id_desc") {
  647. trailSelectItems.sort(sortIDHandler("lootID", true))
  648. }
  649. const trailSelectorFragment = document.createDocumentFragment()
  650. trailSelectorFragment.append(noTrailItem)
  651. trailSelectItems.forEach((t) => {
  652. if (search && t.data.name.toLowerCase().indexOf(search.toLowerCase()) === -1) {
  653. return
  654. }
  655. trailSelectorFragment.append(t.node)
  656. })
  657. while (customTrailSelectorContainer.firstChild !== null) {
  658. customTrailSelectorContainer.removeChild(customTrailSelectorContainer.firstChild)
  659. }
  660. customTrailSelectorContainer.append(trailSelectorFragment)
  661. }
  662.  
  663. const customTrailSortBy = customTrailUI.querySelector(".input-select.customizer--item-selector-controls--sort-options"),
  664. customTrailSearch = customTrailUI.querySelector(".input-field.customizer--item-selector-controls--filter-input"),
  665. customTrailSearchClear = customTrailUI.querySelector(".customizer--item-selector-controls--filter-clear")
  666.  
  667. customTrailSearch.placeholder = "Search Trail"
  668.  
  669. const customTrailApplyFilters = (e) => {
  670. populateTrailList(customTrailSearch.value, customTrailSortBy.value)
  671. }
  672.  
  673. customTrailSortBy.addEventListener("change", customTrailApplyFilters)
  674. customTrailSearch.addEventListener("keyup", customTrailApplyFilters)
  675. customTrailSearchClear.addEventListener("click", () => {
  676. customTrailSearch.value = ""
  677. populateTrailList(customTrailSearch.value, customTrailSortBy.value)
  678. })
  679.  
  680. /* Paint Selector UI */
  681. const customCarPaintUI = document.createElement("div")
  682. customCarPaintUI.className = "nt-admin-panel-paint-selector customizer--item-selector paint-selector"
  683. customCarPaintUI.innerHTML = `
  684. <div class="customizer--item-selector-container">
  685. <div class="nt-admin-panel-paint-selector-heading">
  686. <div class="nt-admin-panel-paint-selector-heading-icon">
  687. <svg width="296" height="298" viewBox="0 0 296 298" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M122.94 67.8906C125.687 80.7934 131.287 90.9781 139.312 99.0844C149.684 109.561 163.054 115.395 174.89 120.029C177.209 120.938 179.494 121.814 181.74 122.677L181.742 122.677L181.749 122.68C203.696 131.103 222.001 138.128 233.456 159.298L234.766 168.903L185.238 218.432L78.8184 112.012L122.94 67.8906ZM71.7473 119.083L42.0488 148.782C38.1436 152.687 38.1436 159.018 42.0488 162.924L66.7976 187.672L8.99159 245.478C-2.82176 257.292 -2.82177 276.445 8.99159 288.258C20.805 300.072 39.9582 300.072 51.7716 288.258L109.578 230.452L134.326 255.201C138.232 259.106 144.563 259.106 148.468 255.201L178.167 225.503L71.7473 119.083ZM19.775 277.829C26.6091 284.663 37.6895 284.663 44.5237 277.829C51.3579 270.994 51.3579 259.914 44.5237 253.08C37.6895 246.246 26.6091 246.246 19.775 253.08C12.9408 259.914 12.9408 270.994 19.775 277.829Z"></path><path d="M176.206 5.20904L137.797 45.0956C136.002 46.959 134.976 49.5184 135.082 52.1031C137.946 122.171 214.991 94.7163 246.062 153.181C246.687 154.356 247.092 155.673 247.272 156.992L251.312 186.622C252.279 193.713 258.336 199 265.492 199C272.213 199 278.028 194.324 279.47 187.761L294.699 118.468C296.136 111.929 294.207 105.105 289.558 100.287L197.805 5.19812C191.903 -0.918788 182.102 -0.913825 176.206 5.20904Z"></path><circle cx="265" cy="226" r="14"></circle></svg>
  688. </div>
  689. <div class="nt-admin-panel-paint-selector-heading-label">Paint</div>
  690. </div>
  691. <div class="nt-admin-panel-scrollable">
  692. <div class="customizer--item-selector-items"></div>
  693. </div>
  694. </div>`
  695.  
  696. const customCarPaintContainer = customCarPaintUI.querySelector(".customizer--item-selector-items")
  697.  
  698. ;(() => {
  699. const customCarPaintItemTemplate = document.createElement("div")
  700. customCarPaintItemTemplate.className = "customizer--item-selector-item"
  701. customCarPaintItemTemplate.innerHTML = `
  702. <div class="customizer--item-selector-item--labels">
  703. <div class="customizer--item-selector-item--equipped">Equipped</div>
  704. </div>
  705. <div class="paint-select-preview"></div>`
  706.  
  707. const carSelectPointerHandler = (e) => {
  708. const item = e.target.closest(".customizer--item-selector-item"),
  709. hue = parseInt(item.dataset.selectedhue)
  710. if (hue === (config?.hue || 0)) {
  711. return
  712. }
  713. if (config?.car) {
  714. setCar(config.car, hue, config.trail)
  715. }
  716. saveConfig(config?.car, hue, config.trail)
  717. customCarPaintContainer.querySelectorAll(".is-equipped").forEach((node) => node.classList.remove("is-equipped"))
  718. item.classList.add("is-equipped")
  719. }
  720.  
  721. const fragment = document.createDocumentFragment()
  722. for (let hue = 0; hue <= 340; hue += 10) {
  723. const item = customCarPaintItemTemplate.cloneNode(true)
  724. item.dataset.selectedhue = hue
  725. if (hue === config?.hue) {
  726. item.classList.add("is-equipped")
  727. }
  728. item.addEventListener("pointerup", carSelectPointerHandler)
  729. fragment.append(item)
  730. }
  731. customCarPaintContainer.append(fragment)
  732. })()
  733.  
  734. CarPainter.setCarPaintCreatedHandler((hue, imgDataURL) => {
  735. const target = customCarPaintContainer.querySelector(`.customizer--item-selector-item[data-selectedhue="${hue}"] .paint-select-preview`)
  736. if (!target) {
  737. logging.warn("Update Car Paints")("Unable to place custom car paint image on selector")
  738. return
  739. }
  740. target.style.backgroundImage = `url(${imgDataURL})`
  741. })
  742. if (config?.car) {
  743. CarPainter.generateSampleCarPaints(config.car)
  744. }
  745.  
  746. /* Custom Car Tab */
  747. const customCarTab = tabContainer.firstElementChild.cloneNode(true)
  748. customCarTab.setAttribute("customizertabindex", null)
  749. customCarTab.classList.remove("customizer--tab--selected", "is-current")
  750. customCarTab.querySelector(".customizer--tab--label").textContent = "Admin Panel"
  751. customCarTab.querySelector(".customizer--tab--icon").innerHTML = `<img src="${GM_getResourceURL(
  752. "icon_tab",
  753. )}" alt="admin_panel_icon" width="25" height="24" />`
  754.  
  755. customCarTab.addEventListener("pointerup", (e) => {
  756. e.preventDefault()
  757. showCustomCarPage(true)
  758. })
  759.  
  760. /* Setup Custom Car Section */
  761. const changeTitle = (title) => {
  762. const titleHeading = container.querySelector(".customizer--about--title")
  763. if (!titleHeading) {
  764. logging.warn("Init")("Unable to update title heading")
  765. return
  766. }
  767. titleHeading.textContent = title
  768. }
  769.  
  770. const showCustomCarPage = (show, tabName) => {
  771. const customizerItemSelector = container.querySelector(".customizer--item-selector"),
  772. customizerPreview = container.querySelector(".customizer--preview")
  773. customizerItemSelector.hidden = show
  774.  
  775. if (show) {
  776. customizerPreview.style.opacity = 0
  777. customizerPreview.style.zIndex = -1000
  778. container.classList.remove("section-paint", "section-trails", "section-stickers", "section-titles", "no-preview")
  779. container.classList.add("section-cars", "section-nt-admin-panel")
  780. previewerCanvas.style.width = "300px"
  781. previewerCanvas.style.height = "280px"
  782. otherTabs.forEach((tab) => {
  783. tab.classList.remove("customizer--tab--selected", "is-current")
  784. })
  785. customCarTab.classList.add("customizer--tab--selected", "is-current")
  786. changeTitle("Admin Panel")
  787.  
  788. reactObj.previewer.setFocus("car")
  789. if (config?.car) {
  790. setCar(config.car, config.hue || 0, config.trail)
  791. }
  792. customizerItemSelector.after(customCarUI, customTrailUI, customCarPaintUI)
  793. previewer.after(customCarNoPreview)
  794.  
  795. if (config?.car) {
  796. updateCarLabel(NTGLOBALS.CARS.find((c) => c.id === config.car))
  797. customizerPreview.after(selectedCarLabel)
  798. }
  799.  
  800. const selectedCar = customCarSelectorContainer.querySelector(".is-equipped"),
  801. selectedTrail = customTrailSelectorContainer.querySelector(".is-equipped"),
  802. selectedPaint = customCarPaintContainer.querySelector(".is-equipped")
  803. if (selectedCar) {
  804. customCarSelectorContainer.parentNode.scrollTop = selectedCar.offsetTop
  805. }
  806. if (selectedTrail) {
  807. customTrailSelectorContainer.parentNode.scrollTop = selectedTrail.offsetTop
  808. }
  809. if (selectedPaint) {
  810. customCarPaintContainer.parentNode.scrollTop = selectedPaint.offsetTop
  811. }
  812. } else {
  813. customizerPreview.style.opacity = 1
  814. customizerPreview.style.zIndex = ""
  815. container.classList.add(`section-${tabName.toLowerCase()}`)
  816. container.classList.remove("section-nt-admin-panel")
  817. previewerCanvas.style.width = "559px"
  818. previewerCanvas.style.height = "500px"
  819. if (tabName !== "Cars") {
  820. container.classList.remove("section-cars")
  821. }
  822. if (tabName === "Trails") {
  823. reactObj.previewer.setFocus("trails")
  824. }
  825. if (["Stickers", "Titles"].includes(tabName)) {
  826. container.classList.add("no-preview")
  827. }
  828. customCarTab.classList.remove("customizer--tab--selected", "is-current")
  829. changeTitle(tabName)
  830.  
  831. const realConfig = reactObj.props.config
  832. if (!realConfig) {
  833. logging.warn("Init")("Unable to find user's customizer settings")
  834. return
  835. }
  836.  
  837. const { id: realCarID, hueAngle: realHue } = realConfig.find((c) => c.type === "car") || { id: null, hueAngle: null },
  838. realTrailID = realConfig.find((c) => c.type === "trail")?.id
  839. setCar(realCarID, realHue, realTrailID)
  840.  
  841. customCarUI.remove()
  842. customTrailUI.remove()
  843. customCarPaintUI.remove()
  844. customCarNoPreview.remove()
  845. selectedCarLabel.remove()
  846. }
  847. }
  848.  
  849. const otherTabs = tabContainer.querySelectorAll(".nav-list-item.customizer--tab")
  850. otherTabs.forEach((tab) => {
  851. tab.addEventListener("pointerup", (e) => {
  852. const title = tab.querySelector(".customizer--tab--label").textContent
  853. showCustomCarPage(false, title)
  854. tab.classList.add("customizer--tab--selected", "is-current")
  855. })
  856. })
  857.  
  858. populateList("", "name_a-z")
  859. populateTrailList("", "name_a-z")
  860.  
  861. tabContainer.firstElementChild.before(customCarTab)
  862. }
  863.  
  864. ///////////////////
  865. // Garage Page //
  866. ///////////////////
  867. else if (config?.car && window.location.pathname === "/garage") {
  868. const profileContainer = document.querySelector("section.profile"),
  869. profileObj = profileContainer ? findReact(profileContainer) : null
  870. if (!profileObj) {
  871. logging.error("Init")("Could not find profile container")
  872. return
  873. }
  874.  
  875. /* Styles */
  876. const style = document.createElement("style")
  877. style.appendChild(
  878. document.createTextNode(`
  879. .nt-admin-panel-label-garage {
  880. position: absolute;
  881. bottom: 150px;
  882. left: calc((100% - 202px) / 2);
  883. z-index: -1;
  884. margin: 0 auto;
  885. padding: 2px 8px;
  886. border-radius: 4px;
  887. font-size: 12px;
  888. text-align: center;
  889. text-shadow: 0 2px 2px rgba(2, 2, 2, 0.25);
  890. color: #fff;
  891. background-color: #136cac;
  892. }`),
  893. )
  894. document.head.appendChild(style)
  895.  
  896. /* Replace car with custom car */
  897. const car = NTGLOBALS.CARS.find((c) => c.id === config.car)
  898. if (!car) {
  899. logging.error("Init")("Custom Car setting is invalid")
  900. return
  901. }
  902. const oldGetCarParamsFn = profileObj.getCarParams
  903. profileObj.getCarParams = function () {
  904. this.props.carID = car.id
  905. this.props.carHueAngle = config.hue || 0
  906. this.props.selectedTrail = config.trail ? NTGLOBALS.LOOT.find((l) => l.lootID === config.trail && l.type === "trail") : undefined
  907. return oldGetCarParamsFn()
  908. }
  909. profileObj.forceUpdate()
  910.  
  911. /* Attach label about custom cars */
  912. const customCarText = document.createElement("div")
  913. customCarText.className = "nt-admin-panel-label-garage"
  914. customCarText.textContent = "Custom Car from Admin Panel"
  915.  
  916. const targetElement = document.querySelector("div.profile--grid--center div")
  917. if (targetElement) {
  918. targetElement.after(customCarText)
  919. }
  920. }
  921.  
  922. ///////////////////
  923. // Racing Page //
  924. ///////////////////
  925. else if (config?.car && (window.location.pathname === "/race" || window.location.pathname.startsWith("/race/"))) {
  926. const raceContainer = document.getElementById("raceContainer"),
  927. raceObj = raceContainer ? findReact(raceContainer) : null
  928. if (!raceObj) {
  929. logging.error("Init")("Could not find the race track")
  930. return
  931. }
  932.  
  933. const car = NTGLOBALS.CARS.find((c) => c.id === config.car)
  934. if (!car) {
  935. logging.error("Init")("Custom Car setting is invalid")
  936. return
  937. }
  938.  
  939. /* Styles */
  940. const style = document.createElement("style")
  941. style.appendChild(
  942. document.createTextNode(`
  943. .nt-admin-panel-label {
  944. position: absolute;
  945. right: -20px;
  946. top: 595px;
  947. z-index: 3;
  948. color: #383c4f;
  949. font-size: 10px;
  950. }`),
  951. )
  952. document.head.appendChild(style)
  953.  
  954. /* Show label on dashboard indicating using custom car */
  955. const customCarText = document.createElement("div")
  956. customCarText.className = "nt-admin-panel-label"
  957. customCarText.textContent = "CC"
  958.  
  959. raceContainer.after(customCarText)
  960.  
  961. /** Display Custom Car on Race Track. **/
  962. const oldGameAddPlayer = raceObj.game.track.addPlayer
  963. raceObj.game.track.addPlayer = (e, r) => {
  964. if (e.isPlayer) {
  965. e.type = !!car.options?.isAnimated ? car.key || car.assetKey || car.carID : car.carID
  966. e.isAnimated = !!car.options?.isAnimated
  967. e.hue = config.hue || 0
  968. e.mods.trail = config.trail ? NTGLOBALS.LOOT.find((l) => l.lootID === config.trail && l.type === "trail")?.assetKey : undefined
  969. }
  970. oldGameAddPlayer(e, r)
  971. }
  972.  
  973. /** Display Custom Car on Race Results. */
  974. raceObj.server.on("status", (e) => {
  975. if (e.status === "racing") {
  976. raceObj.state.racers = raceObj.state.racers.map((r) => {
  977. if (r.userID === raceObj.props.user.userID) {
  978. r.profile.carHueAngle = config.hue || 0
  979. r.profile.carID = config.car
  980. }
  981. return r
  982. })
  983. }
  984. })
  985.  
  986. /** Display Custom Car on Popover (race results). **/
  987. const racerPopupObserver = new MutationObserver((mutations) => {
  988. for (const m of mutations) {
  989. for (const node of m.addedNodes) {
  990. if (node.classList?.contains("pane--overlay")) {
  991. const reactObj = findReact(node, 1)
  992. if (!reactObj) {
  993. logging.warn("Race Results")("Unable to review popup racer modal")
  994. }
  995. if (reactObj.props.userID !== raceObj.props.user.userID) {
  996. return
  997. }
  998. reactObj.props.profile.carID = config.car
  999. reactObj.props.profile.carHueAngle = config.hue
  1000. reactObj.forceUpdate()
  1001. return
  1002. }
  1003. }
  1004. }
  1005. })
  1006. racerPopupObserver.observe(document.body, { childList: true })
  1007. }
  1008. }