Nitro Type - Race Result Enhancements

Shows NT Season Points earned, Accuracy and Nitros Used on Race Result screen.

目前为 2022-03-13 提交的版本。查看 最新版本

// ==UserScript==
// @name         Nitro Type - Race Result Enhancements
// @version      0.2.1
// @description  Shows NT Season Points earned, Accuracy and Nitros Used on Race Result screen.
// @author       Toonidy
// @match        *://*.nitrotype.com/race
// @match        *://*.nitrotype.com/race/*
// @icon         data:image/png;base64,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
// @grant        none
// @license      MIT
// @namespace    https://gf.qytechs.cn/users/858426
// ==/UserScript==

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

/** Finds the React Component from given dom. */
const findReact = (dom, traverseUp = 0) => {
	const key = Object.keys(dom).find((key) => key.startsWith("__reactFiber$"))
	const domFiber = dom[key]
	if (domFiber == null) return null
	const getCompFiber = (fiber) => {
		let parentFiber = fiber?.return
		while (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
}

/** Console logging with some prefixing. */
const logging = (() => {
	const logPrefix = (prefix = "") => {
		const formatMessage = `%c[Nitro Type Race Result Enhancements]${prefix ? `%c[${prefix}]` : ""}`
		let args = [console, `${formatMessage}%c`, "background-color: #D62F3A; color: #fff; font-weight: bold"]
		if (prefix) {
			args = args.concat("background-color: #4f505e; color: #fff; font-weight: bold")
		}
		return args.concat("color: unset")
	}
	return {
		info: (prefix) => Function.prototype.bind.apply(console.info, logPrefix(prefix)),
		warn: (prefix) => Function.prototype.bind.apply(console.warn, logPrefix(prefix)),
		error: (prefix) => Function.prototype.bind.apply(console.error, logPrefix(prefix)),
		log: (prefix) => Function.prototype.bind.apply(console.log, logPrefix(prefix)),
		debug: (prefix) => Function.prototype.bind.apply(console.debug, logPrefix(prefix)),
	}
})()

/** 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))

	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,
	currentUserID = raceObj.props.user.userID

let racerStatNodes = null

server.on("update", (e) => {
	if (!racerStatNodes) {
		return
	}

	raceObj.state.racers
		.slice()
		.sort(sortRacersHandler)
		.forEach((r, i) => {
			if (!r || r.robot || r.userID === currentUserID || !racerStatNodes[r.userID]) {
				return
			}

			const statNodes = racerStatNodes[r.userID]
			statNodes.points.remove()
			statNodes.nitros.remove()
			statNodes.accuracy.remove()

			if (r.disqualified) {
				return
			}

			const { accuracy, points, nitros } = getUserRaceResult(r),
				wpmNode = document.querySelector(
					`#raceContainer .gridTable-row:nth-of-type(${i + 1}) .list .list-item:nth-of-type(1)`
				),
				suffixClass = wpmNode?.querySelector("span")?.className || "tc-ts"
			if (!wpmNode) {
				logging.warn("Finish")("Unable to find race results", r)
				return
			}

			statNodes.points.innerHTML = `${points} <span class="${suffixClass}">Points</span>`
			statNodes.nitros.innerHTML = `${nitros} <span class="${suffixClass}">Nitro${nitros !== 1 ? "s" : ""}</span>`
			statNodes.accuracy.innerHTML = `${accuracy}<span class="${suffixClass}">% Acc</span>`

			wpmNode.after(statNodes.nitros, statNodes.points)
			wpmNode.before(statNodes.accuracy)
		})
})

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

			let newStatNodes = {}

			raceObj.state.racers
				.slice()
				.sort(sortRacersHandler)
				.forEach((r, i) => {
					if (!r || r.robot || r.disqualified) {
						return
					}
					if (!r.progress) {
						logging.warn("Finish")("Unable to find race results", r)
						return
					}

					const listRow = document.querySelector(
							`#raceContainer .gridTable-row:nth-of-type(${i + 1}) .split.split--flag`
						),
						wpmNode = listRow?.querySelector(
							`.list .list-item:nth-of-type(${r.userID === currentUserID ? 2 : 1})`
						),
						suffixClass = wpmNode?.querySelector("span")?.className || "tc-ts"
					if (!listRow || !wpmNode) {
						logging.warn("Finish")("Unable to update user's race result")
						return
					}

					// Populate current race results
					const { accuracy, points, nitros } = getUserRaceResult(r)

					let statNodes = {
						points: document.createElement("div"),
						nitros: document.createElement("div"),
						accuracy: null,
					}

					statNodes.nitros.classList.add("list-item")
					statNodes.nitros.innerHTML = `${nitros} <span class="${suffixClass}">Nitro${
						nitros !== 1 ? "s" : ""
					}</span>`

					statNodes.points.classList.add("list-item")
					statNodes.points.innerHTML = `${points} <span class="${suffixClass}">Points</span>`

					wpmNode.after(statNodes.nitros, statNodes.points)

					if (r.userID !== currentUserID) {
						statNodes.accuracy = document.createElement("div")
						statNodes.accuracy.classList.add("list-item")
						statNodes.accuracy.innerHTML = `${accuracy}<span class="${suffixClass}">% Acc</span>`
						wpmNode.before(statNodes.accuracy)

						newStatNodes[r.userID] = statNodes
					}

					// Move list to new row to fit the Racer's Title
					const statContainer = document.createElement("div")
					statContainer.classList.add("split", "split--flag", "split--reverse", "dwf")
					statContainer.innerHTML = `<div class="split-cell"></div><div class="split-cell"></div>`
					statContainer.querySelector(".split-cell:nth-of-type(2)").append(wpmNode.parentNode)

					listRow.after(statContainer)
				})

			racerStatNodes = newStatNodes

			break
		}
	}
})

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址