Nitro Type Current Race Tracker w/ Leagues and NT Comps

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
}