Nitro Type Current Race Tracker w/ Leagues and NT Comps

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Nitro Type Current Race Tracker w/ Leagues and NT Comps
// @version      1.8
// @description  Nitro Type Current Race Tracker w/ Leagues and NT Comps WIP
// @author       TensorFlow - Dvorak
// @match        *://*.nitrotype.com/race
// @match        *://*.nitrotype.com/race/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.1/dexie.min.js#sha512-ybuxSW2YL5rQG/JjACOUKLiosgV80VUfJWs4dOpmSWZEGwdfdsy2ldvDSQ806dDXGmg9j/csNycIbqsrcqW6tQ==
// @license      MIT
// @namespace    https://greasyfork.org/users/1331131-tensorflow-dvorak

// ==/UserScript==

/* globals Dexie */

const findReact = (dom, traverseUp = 0) => {
  const key = Object.keys(dom).find((key) => key.startsWith("__reactFiber$"));
  const domFiber = dom[key];
  if (!domFiber) return null;

  const getCompFiber = (fiber) => {
    let parentFiber = fiber?.return;
    while (parentFiber && typeof parentFiber.type === "string") {
      parentFiber = parentFiber.return;
    }
    return parentFiber;
  };

  let compFiber = getCompFiber(domFiber);
  for (let i = 0; i < traverseUp && compFiber; i++) {
    compFiber = getCompFiber(compFiber);
  }
  return compFiber?.stateNode || null;
};

const createLogger = (namespace) => {
  const logPrefix = (prefix = "") => {
    const formatMessage = `%c[${namespace}]${prefix ? `%c[${prefix}]` : ""}`;
    let args = [
      console,
      `${formatMessage}%c`,
      "background-color: #4285f4; color: #fff; font-weight: bold",
    ];
    if (prefix) {
      args = args.concat(
        "background-color: #4f505e; color: #fff; font-weight: bold"
      );
    }
    return args.concat("color: unset");
  };

  const bindLog = (logFn, prefix) =>
    Function.prototype.bind.apply(logFn, logPrefix(prefix));

  return {
    info: (prefix) => bindLog(console.info, prefix),
    warn: (prefix) => bindLog(console.warn, prefix),
    error: (prefix) => bindLog(console.error, prefix),
    log: (prefix) => bindLog(console.log, prefix),
    debug: (prefix) => bindLog(console.debug, prefix),
  };
};

const logging = createLogger("Nitro Type Current Race Tracker");

// Config storage
const db = new Dexie("CurrentRaceTracker");
db.version(1).stores({
  users: "id, &username, team, displayName, status, league",
});
db.open().catch(function (e) {
  logging.error("Init")("Failed to open up the config database", e);
});

//Race
if (
  window.location.pathname === "/race" ||
  window.location.pathname.startsWith("/race/")
) {
  const raceContainer = document.getElementById("raceContainer");
  const raceObj = raceContainer ? findReact(raceContainer) : null;
  if (!raceContainer || !raceObj) {
    logging.error("Init")("Could not find the race container or race object");
    return;
  }
  if (!raceObj.props.user.loggedIn) {
    logging.error("Init")("Extractor is not available for Guest Racing");
    return;
  }

  const displayedUserIDs = new Set();

  const leagueTierText = {
    0: "None",
    1: "Learner",
    2: "Novice",
    3: "Rookie",
    4: "Pro",
    5: "Ace",
    6: "Expert",
    7: "Champion",
    8: "Master",
    9: "Epic",
    10: "Legend",
    11: "Tournament",
    12: "Semi-Finals",
    13: "Finals",
  };

  function createLeagueInfoUI() {
    const leagueInfoContainer = document.createElement("div");
    leagueInfoContainer.id = "league-info-container";
    leagueInfoContainer.style.zIndex = "1000";
    leagueInfoContainer.style.backgroundColor = "rgba(0, 0, 0, 0.85)";
    leagueInfoContainer.style.color = "#fff";
    leagueInfoContainer.style.padding = "15px";
    leagueInfoContainer.style.borderRadius = "8px";
    leagueInfoContainer.style.fontFamily = "'Nunito', sans-serif";
    leagueInfoContainer.style.fontSize = "14px";
    leagueInfoContainer.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1)";
    leagueInfoContainer.style.width = "100%";
    leagueInfoContainer.style.overflowY = "auto";
    leagueInfoContainer.style.maxHeight = "70vh";
    leagueInfoContainer.innerHTML = `
            <h2 style="margin: 0 0 10px; font-size: 18px; border-bottom: 1px solid #ccc; padding-bottom: 5px;">Racers Info</h2>
            <table id='league-info-table' style="width: 100%; border-collapse: collapse; text-align: left;">
                <thead>
                    <tr style="border-bottom: 1px solid #444;">
                        <th style="padding: 5px; color: #f4b400;">Name</th>
                        <th style="padding: 5px; color: #e74c3c;">Level</th>
                        <th style="padding: 5px; color: #0f9d58;">League</th>
                        <th style="padding: 5px; color: #4285f4;">Session Races</th>
                        <th style="padding: 5px; color: #ff5722;">Races This Week</th>
                    </tr>
                </thead>
                <tbody id='league-info-list' style="color: #ddd;"></tbody>
            </table>
        `;
    const targetElement = document.querySelector("#raceContainer");
    if (targetElement) {
      targetElement.appendChild(leagueInfoContainer);
    } else {
      document.body.appendChild(leagueInfoContainer);
    }
  }

  function fetchAdditionalData(username, listItem) {
    const proxyUrl = "https://api.allorigins.win/get?url=";
    const targetUrl = `https://www.ntcomps.com/racers/${username}`;
    fetch(`${proxyUrl}${encodeURIComponent(targetUrl)}`)
      .then((response) => response.json())
      .then((data) => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(data.contents, "text/html");
        const foundIndicator = "<td>This week</td>";

        if (!data.contents.includes(foundIndicator)) {
          listItem.querySelector(".races-this-week").textContent = "N/A";
          return;
        }

        let racesCompleted = doc
          .querySelector("tr:nth-child(4) td:nth-child(2)")
          .textContent.trim();
        racesCompleted = racesCompleted.replace(/,/g, "");
        let racesCompletedInt = parseInt(racesCompleted, 10);
        if (isNaN(racesCompletedInt) || racesCompletedInt > 100000) {
          racesCompleted = "N/A";
        } else {
          racesCompleted = racesCompletedInt.toLocaleString();
        }

        listItem.querySelector(".races-this-week").textContent = racesCompleted;
      })
      .catch((error) => {
        console.error("Error fetching additional data:", error);
        listItem.querySelector(".races-this-week").textContent = "N/A";
      });
  }
  function updateLeagueInfoUI(user) {
    const leagueInfoList = document.getElementById("league-info-list");
    if (!leagueInfoList || displayedUserIDs.has(user.userID)) return;
    displayedUserIDs.add(user.userID);
    const listItem = document.createElement("tr");
    const leagueText = leagueTierText[user.profile.leagueTier] || "Unknown";
    listItem.style.borderBottom = "1px solid #444";
    listItem.style.fontWeight = "bold";
    listItem.innerHTML = `
            <td style="padding: 5px; color: #f4b400;">${user.profile.displayName}</td>
            <td style="padding: 5px; color: #e74c3c;">${user.profile.level}</td>
            <td style="padding: 5px; color: #0f9d58;">${leagueText}</td>
            <td style="padding: 5px; color: #4285f4;">${user.profile.sessionRaces}</td>
            <td style="padding: 5px; color: #ff5722;" class="races-this-week">Loading...</td>
        `;
    leagueInfoList.appendChild(listItem);
    fetchAdditionalData(user.profile.userID, listItem);
  }

  function logPlayerIDsAndLeagues() {
    const server = raceObj.server;

    server.on("joined", (user) => {
      if (!user.robot) {
        updateLeagueInfoUI(user);
      }
    });

    server.on("update", (e) => {
      if (e && e.racers) {
        e.racers.forEach((racer) => {
          if (!racer.robot && !displayedUserIDs.has(racer.userID)) {
            updateLeagueInfoUI(racer);
          }
        });
      }
    });
  }

  function initializeRaceObj() {
    const raceContainer = document.getElementById("raceContainer");

    if (raceContainer) {
      createLeagueInfoUI();
      logPlayerIDsAndLeagues();

      const resultObserver = new MutationObserver(([mutation], observer) => {
        for (const node of mutation.addedNodes) {
          if (node.classList?.contains("race-results")) {
            observer.disconnect();
            logging.info("Update")("Race Results received");

            const leagueInfoContainer = document.getElementById(
              "league-info-container"
            );
            const targetElement =
              document.querySelector("#raceContainer").parentElement;
            if (leagueInfoContainer && targetElement) {
              targetElement.appendChild(leagueInfoContainer);
            }
            break;
          }
        }
      });
      resultObserver.observe(raceContainer, { childList: true, subtree: true });
    } else {
      logging.error("Init")("Race container not found, retrying...");
      setTimeout(initializeRaceObj, 1000);
    }
  }

  window.addEventListener("load", initializeRaceObj);
}