Nitro Type - Race Result Enhancements

Shows NT Season Points earned and Skipped Characters (Nitros) Used on Race Result screen.

// ==UserScript==
// @name         Nitro Type - Race Result Enhancements
// @version      0.4.4
// @description  Shows NT Season Points earned and Skipped Characters (Nitros) Used on Race Result screen.
// @author       Toonidy
// @match        *://*.nitrotype.com/race
// @match        *://*.nitrotype.com/race/*
// @icon         https://i.ibb.co/YRs06pc/toonidy-userscript.png
// @require      https://greasyfork.org/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
// @grant        none
// @license      MIT
// @namespace    https://greasyfork.org/users/858426
// ==/UserScript==

/* global createLogger findReact */

const logging = createLogger("Nitro Type Race Result Enhancements")

/////////////
//  Utils  //
/////////////

/** Calculate User's Race score. */
const getUserRaceResult = (user) => {
    const { typed, nitros, skipped, startStamp, completeStamp, errors } = user.progress

    let endStamp = completeStamp || Date.now()

    const wpm = Math.round((typed - skipped) / 5 / ((endStamp - startStamp) / 6e4)),
        accuracy = ((1 - errors / (typed - skipped)) * 100).toFixed(2),
        points = Math.round((100 + wpm / 2) * (1 - errors / (typed - skipped)))

    return { accuracy, points, wpm, nitros, skipped }
}

/** Sort Handler to sort by rank position. */
const sortRacersHandler = (e, t) => {
    // Source: https://www.nitrotype.com/dist/site/js/ra.js
    return e.disqualified && !t.disqualified
        ? 1
        : (t.disqualified && !e.disqualified) || (e.progress.completeStamp && !t.progress.completeStamp)
        ? -1
        : t.progress.completeStamp && !e.progress.completeStamp
        ? 1
        : e.progress.completeStamp && t.progress.completeStamp
        ? e.progress.completeStamp < t.progress.completeStamp
            ? -1
            : 1
        : e.progress.percentageFinished === t.progress.percentageFinished
        ? 0
        : e.progress.percentageFinished > t.progress.percentageFinished
        ? -1
        : 1
}

///////////////////
//  Racing Page  //
///////////////////

const raceContainer = document.getElementById("raceContainer"),
    raceObj = raceContainer ? findReact(raceContainer) : null
if (!raceContainer || !raceObj) {
    logging.error("Init")("Could not find the race track")
    return
}

const server = raceObj.server

/** Mutation obverser to track whether results screen showed up. */
const resultObserver = new MutationObserver((mutations, observer) => {
    for (const mutation of mutations) {
        for (const newNode of mutation.addedNodes) {
            if (newNode.classList?.contains("race-results")) {
                observer.disconnect()

                // Setup New Racer Stats Container
                const racers = raceObj.state.racers.slice().sort(sortRacersHandler)

                const dummyCell = document.createElement("div")
                dummyCell.classList.add("split-cell")

                let racerRankNewNodes = []

                const racerRankNodes = newNode.querySelectorAll(".gridTable-row")
                racerRankNodes.forEach((node, i) => {
                    const r = racers[i]

                    const listRow = node.querySelector(".gridTable-cell:nth-of-type(2) .split"),
                        statRow = listRow.querySelector(".split-cell:nth-of-type(2)")

                    // Add in the new stat fields
                    const { points, skipped } = getUserRaceResult(r),
                        accuracyNode = statRow.querySelector(".list .list-item:nth-of-type(2)"),
                        suffixClass = accuracyNode?.querySelector("span")?.className || "tc-ts"

                    const newStatRow = document.createElement("div")
                    newStatRow.className = `${listRow.className} new-stat-row`
                    newStatRow.append(dummyCell.cloneNode(), statRow)
                    listRow.append(dummyCell.cloneNode())

                    listRow.after(newStatRow)

                    const skippedNode = document.createElement("div")
                    skippedNode.classList.add("list-item", "skipped")
                    skippedNode.innerHTML = `${r.robot ? "N/A" : skipped} <span class="${suffixClass}">Skipped</span>`

                    const pointsNode = document.createElement("div")
                    pointsNode.classList.add("list-item", "points")
                    pointsNode.innerHTML = `${r.robot ? "N/A" : points} <span class="${suffixClass}">Points</span>`

                    racerRankNewNodes[i] = [skippedNode, pointsNode]

                    if (!accuracyNode) {
                        if (!node.classList.contains("is-wampus") && !r.disqualified) {
                            logging.warn(`Race Result")("Unable to setup new stats on row ${i}`)
                        }
                        return
                    }
                    accuracyNode.after(skippedNode, pointsNode)
                })

                /* Track new progress updates */
                server.on("update", (e) => {
                    const racers = raceObj.state.racers.slice().sort(sortRacersHandler)
                    racerRankNodes.forEach((node, i) => {
                        const r = racers[i],
                            { points, skipped } = getUserRaceResult(r),
                            [skippedNode, pointsNode] = racerRankNewNodes[i],
                            accuracyNode = node.querySelector(".new-stat-row .list .list-item:nth-of-type(2)")

                        if (r.disqualified || node.classList.contains("is-wampus")) {
                            skippedNode.remove()
                            pointsNode.remove()
                            return
                        }

                        skippedNode.childNodes[0].textContent = `${r.robot ? "N/A" : skipped} `
                        pointsNode.childNodes[0].textContent = `${r.robot ? "N/A" : points} `

                        if (!accuracyNode) {
                            logging.warn(`Race Result")("Unable to insert new stats back into row ${i}`)
                            return
                        }
                        accuracyNode.after(skippedNode, pointsNode)
                    })
                })
                return
            }
        }
    }
})

resultObserver.observe(raceContainer, { childList: true, subtree: true })

logging.info("Init")("Race Result listener has been setup")