NitroType - More Detailed Stats

Gets more data about your races automatically for N pages (includes races length / errors!)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NitroType - More Detailed Stats
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Gets more data about your races automatically for N pages (includes races length / errors!)
// @author       dphdmn
// @match        https://www.nitrotype.com/racelog/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nitrotype.com
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let maxRaces = -1;
    let maxPages = -1;
    const button = document.querySelector('.btn.btn--light.btn--outline.btn--thin.btn--xs');
    if (button) {
        button.style.display = 'none';
    }
    // Function to extract and validate the race ID
    function matchPagesN(url) {
        // Use a regular expression to extract the number after the last "/"
        const match = url.match(/\/(\d+)$/);

        // Check if a match was found and if the number is valid
        if (match && match[1]) {
            const raceId = parseInt(match[1], 10);

            // Check if the extracted number is a valid integer greater than 0
            if (Number.isInteger(raceId) && raceId > 0) {
                console.log(`Race ID: ${raceId}`);
                return raceId;
            } else {
                console.error('Invalid Race ID: Number must be an integer greater than 0');
                return null;
            }
        } else {
            console.error('Race ID not found in the URL');
            return null;
        }
    }

    // Call the function with the current URL
    const raceId = matchPagesN(window.location.href);
    const NT_TOKEN = `Bearer ${localStorage.getItem("player_token")}`;

    // Converts a single race log entry to the new structure
    function parseRaceLog(log) {
        return {
            WPM: ((log.typed / 5) / (log.secs / 60)).toFixed(2),
            Accuracy: ((1 - log.errs / log.typed) * 100).toFixed(2),
            Duration: log.secs,
            Length: log.typed,
            Errors: log.errs,
            Placed: log.placed,
            Cash: log.reward?.money ?? 0,
            Exp: log.reward?.exp ?? 0,
            Nitro_Used: log.nitros > 0,
            Timestamp: log.stamp,
            Date: new Date(log.stamp * 1000).toLocaleString()
        };
    }

    // Fetches a single page of race log data, with retry logic
    async function fetchRaceLogPage(pageNumber, retries = 3) {
        const url = `https://www.nitrotype.com/api/v2/stats/data/racelog?page=${pageNumber}&limit=30`;

        for (let attempt = 1; attempt <= retries; attempt++) {
            try {
                const response = await fetch(url, {
                    headers: {
                        "accept": "application/json, text/plain, */*",
                        "authorization": NT_TOKEN,
                        "sec-fetch-dest": "empty",
                        "sec-fetch-mode": "cors",
                        "sec-fetch-site": "same-origin",
                    },
                    referrer: "https://www.nitrotype.com/racelog/racelog",
                    referrerPolicy: "same-origin",
                    method: "GET",
                    mode: "cors",
                    credentials: "include"
                });

                const data = await response.json();

                if (data.status === "OK" && data.results && Array.isArray(data.results.logs)) {
                    maxRaces = data.results.totalRecords;
                    maxPages = Math.ceil(maxRaces / 30);
                    return data.results.logs;
                } else {
                    console.warn(`Attempt ${attempt} failed for page ${pageNumber}: Status not OK or logs not found.`);
                }
            } catch (error) {
                console.error(`Attempt ${attempt} failed for page ${pageNumber}:`, error);
            }

            // Small delay before retrying (optional)
            await new Promise(resolve => setTimeout(resolve, 1000));
        }

        console.warn(`Failed to fetch page ${pageNumber} after ${retries} attempts.`);
        return null;
    }

    // Generates and inserts a new row in the table with parsed log data
    function addRaceLogRow(parsedLog) {
        const table = document.querySelector(".well--l_p table");

        const row = document.createElement("tr");
        row.className = "table-row";

        row.innerHTML = `
    <td class="table-cell">${parsedLog.WPM}</td>
    <td class="table-cell">${parsedLog.Accuracy}</td>
    <td class="table-cell">${parsedLog.Duration}</td>
    <td class="table-cell">${parsedLog.Length}</td>
    <td class="table-cell">${parsedLog.Errors}</td>
    <td class="table-cell">${parsedLog.Placed}</td>
    <td class="table-cell">${parsedLog.Cash}</td>
    <td class="table-cell">${parsedLog.Exp}</td>
    <td class="table-cell">${parsedLog.Nitro_Used ? "Yes" : "No"}</td>
    <td class="table-cell">${parsedLog.Timestamp}</td>
    <td class="table-cell">${parsedLog.Date}</td>
  `;

        table.appendChild(row);
    }

    // Displays the current page loading progress
    function updateProgress(currentPage, totalPages, races) {
        if (maxPages !== -1) {
            totalPages = Math.min(maxPages, totalPages);
        }
        const progressElement = document.querySelector(".well--l_p .loading-progress");

        if (!progressElement) {
            // If no progress element exists, create and insert it above the table
            const newProgressElement = document.createElement("div");
            newProgressElement.className = "loading-progress";
            newProgressElement.textContent = `Loaded ${currentPage} out of ${totalPages} pages... (${races} latest races)`;
            const tableContainer = document.querySelector(".well--l_p");
            tableContainer.insertBefore(newProgressElement, tableContainer.firstChild);
        } else {
            // Update existing progress element
            if (currentPage === totalPages) {
                // If all pages are loaded, make the text bold and bright green
                progressElement.textContent = `All ${totalPages} pages are loaded (${races} latest races)`;
                progressElement.style.color = "rgb(0, 255, 0)"; // Bright green
                progressElement.style.fontWeight = "bold";
            } else {
                progressElement.textContent = `Loaded ${currentPage} out of ${totalPages} pages... (${races} latest races)`;
                progressElement.style.color = ""; // Reset to default color
                progressElement.style.fontWeight = ""; // Reset to normal font weight
            }
        }
    }

    let total = 0;
    // Fetches race logs for the first N pages, parses, and displays them progressively
    async function fetchRaceLogsForNPages(n) {
        const tableContainer = document.querySelector(".well--l_p");

        // Create the table and insert it into the container
        const table = document.createElement("table");
        table.className = "table table--striped table--selectable";
        table.innerHTML = `
    <thead class="table-head">
      <tr class="table-row">
        <th class="table-cell">WPM</th>
        <th class="table-cell">Accuracy</th>
        <th class="table-cell">Duration</th>
        <th class="table-cell">Length</th>
        <th class="table-cell">Errors</th>
        <th class="table-cell">Placed</th>
        <th class="table-cell">Cash</th>
        <th class="table-cell">Exp</th>
        <th class="table-cell">Nitro</th>
        <th class="table-cell">Timestamp</th>
        <th class="table-cell">Date</th>
      </tr>
    </thead>
    <tbody></tbody>
  `;
        tableContainer.appendChild(table);

        for (let i = 0; i < n; i++) {
            if (maxPages === -1 || i < maxPages) {
                const pageLogs = await fetchRaceLogPage(i);

                if (pageLogs) {
                    // Parse and add each log entry as a row
                    pageLogs.map(parseRaceLog).forEach(addRaceLogRow);
                } else {
                    alert(`Error loading at page ${i} due to repeated failures.`);
                    i = 100000;
                }
                total += pageLogs.length;
                // Update loading progress
                updateProgress(i + 1, n, total);
            } else {
                updateProgress(maxPages, maxPages, total);
            }
        }
    }
    if (raceId) {
        fetchRaceLogsForNPages(raceId);
    }


    function addButtonAndHandleInput() {
        // Find the element
        const splitCell = document.querySelector('div.split-cell h1.h2.tbs');

        // Create the button element
        const button = document.createElement('button');
        button.classList.add('btn', 'btn--primary');
        button.textContent = 'Load Multiple Pages Data';

        // Make the button glow cyan
        button.style.boxShadow = '0 0 10px purple';

        // Insert the button below the header
        splitCell.insertAdjacentElement('afterend', button);

        // Handle button click
        button.addEventListener('click', () => {
            // Ask user for input
            const pages = prompt("Please input pages to load (each page = 30 races)");

            // Check if the input is a valid positive integer
            if (pages && !isNaN(pages) && Number(pages) > 0) {

                // Navigate to the corresponding page
                window.location.href = `https://www.nitrotype.com/racelog/${pages}`;
            } else {
                // Alert if input is invalid
                alert("Please enter a valid number greater than 0.");
            }
        });
    }

    // Call the function to add the button and handle the logic
    addButtonAndHandleInput();


})();