您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays various user stats below the race track.
- // ==UserScript==
- // @name Nitro Type - Racing Stats
- // @version 0.1.6
- // @description Displays various user stats below the race track.
- // @author Toonidy
- // @match *://*.nitrotype.com/race
- // @match *://*.nitrotype.com/race/*
- // @icon https://i.ibb.co/YRs06pc/toonidy-userscript.png
- // @grant none
- // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js#sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==
- // @require https://cdnjs.cloudflare.com/ajax/libs/dexie/3.2.1/dexie.min.js#sha512-ybuxSW2YL5rQG/JjACOUKLiosgV80VUfJWs4dOpmSWZEGwdfdsy2ldvDSQ806dDXGmg9j/csNycIbqsrcqW6tQ==
- // @require https://greasyfork.org/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
- // @license MIT
- // @namespace https://greasyfork.org/users/858426
- // ==/UserScript==
- /* global Dexie moment NTGLOBALS createLogger findReact */
- const logging = createLogger("Nitro Type Racing Stats")
- /* Config storage */
- const db = new Dexie("NTRacingStats")
- db.version(1).stores({
- backupStatData: "userID",
- })
- db.open().catch(function (e) {
- logging.error("Init")("Failed to open up the racing stat cache database", e)
- })
- ////////////
- // Init //
- ////////////
- const raceContainer = document.getElementById("raceContainer"),
- raceObj = raceContainer ? findReact(raceContainer) : null,
- server = raceObj?.server,
- currentUser = raceObj?.props.user
- if (!raceContainer || !raceObj) {
- logging.error("Init")("Could not find the race track")
- return
- }
- if (!currentUser?.loggedIn) {
- logging.error("Init")("Not available for Guest Racing")
- return
- }
- //////////////////
- // Components //
- //////////////////
- /** Styles for the following components. */
- const style = document.createElement("style")
- style.appendChild(
- document.createTextNode(`
- #raceContainer {
- margin-bottom: 0;
- }
- .nt-stats-root {
- background-color: #222;
- }
- .nt-stats-body {
- display: flex;
- justify-content: space-between;
- padding: 8px;
- }
- .nt-stats-left-section, .nt-stats-right-section {
- display: flex;
- flex-direction: column;
- row-gap: 8px;
- }
- .nt-stats-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-left: 8px;
- color: rgba(255, 255, 255, 0.8);
- background-color: #03111a;
- font-size: 12px;
- }
- .nt-stats-toolbar-status {
- display: flex;
- }
- .nt-stats-toolbar-status .nt-stats-toolbar-status-item {
- padding: 0 8px;
- background-color: #0a2c42;
- }
- .nt-stats-toolbar-status .nt-stats-toolbar-status-item-alt {
- padding: 0 8px;
- background-color: #22465c;
- }
- .nt-stats-daily-challenges {
- width: 350px;
- }
- .nt-stats-daily-challenges .daily-challenge-progress--badge {
- z-index: 0;
- }
- .nt-stats-season-progress {
- padding: 8px;
- margin: 0 auto;
- border-radius: 8px;
- background-color: #1b83d0;
- box-shadow: 0 28px 28px 0 rgb(2 2 2 / 5%), 0 17px 17px 0 rgb(2 2 2 / 20%), 0 8px 8px 0 rgb(2 2 2 / 15%);
- }
- .nt-stats-season-progress .season-progress-widget {
- width: 350px;
- }
- .nt-stats-season-progress .season-progress-widget--level-progress-bar {
- transition: width 0.3s ease;
- }
- .nt-stats-info {
- text-align: center;
- color: #eee;
- font-size: 14px;
- }
- .nt-stats-metric-row {
- margin-bottom: 4px;
- }
- .nt-stats-metric-value, .nt-stats-metric-suffix {
- font-weight: 600;
- }
- .nt-stats-metric-suffix {
- color: #aaa;
- }
- .nt-stats-right-section {
- flex-grow: 1;
- margin-left: 15px;
- }`)
- )
- document.head.appendChild(style)
- /** Populates daily challenge data merges in the given progress. */
- const mergeDailyChallengeData = (progress) => {
- const { CHALLENGES, CHALLENGE_TYPES } = NTGLOBALS,
- now = Math.floor(Date.now() / 1000)
- return CHALLENGES.filter((c) => c.expiration > now)
- .slice(0, 3)
- .map((c, i) => {
- const userProgress = progress.find((p) => p.challengeID === c.challengeID),
- challengeType = CHALLENGE_TYPES[c.type],
- field = challengeType[1],
- title = challengeType[0].replace(/\$\{goal\}/, c.goal).replace(/\$\{field\}/, `${challengeType[1]}${c.goal !== 1 ? "s" : ""}`)
- return {
- ...c,
- title,
- field,
- goal: c.goal,
- progress: userProgress?.progress || 0,
- }
- })
- }
- /** Grab NT Racing Stats from various sources. */
- const getStats = async () => {
- let backupUserStats = null
- try {
- backupUserStats = await db.backupStatData.get(currentUser.userID)
- } catch (ex) {
- logging.warn("Update")("Unable to get backup stats", ex)
- }
- try {
- const persistStorageStats = JSON.parse(JSON.parse(localStorage.getItem("persist:nt")).user),
- user =
- !backupUserStats || typeof backupUserStats.lastConsecRace !== "number" || persistStorageStats.lastConsecRace >= backupUserStats.lastConsecRace
- ? persistStorageStats
- : backupUserStats,
- dailyChallenges = mergeDailyChallengeData(user.challenges)
- return { user, dailyChallenges }
- } catch (ex) {
- logging.error("Update")("Unable to get stats", ex)
- }
- return Promise.reject(new Error("Unable to get stats"))
- }
- /** Grab Summary Stats. */
- const getSummaryStats = () => {
- const authToken = localStorage.getItem("player_token")
- return fetch("/api/v2/stats/summary", {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- })
- .then((r) => r.json())
- .then((r) => {
- return {
- seasonBoard: r?.results?.racingStats?.find((b) => b.board === "season"),
- dailyBoard: r?.results?.racingStats?.find((b) => b.board === "daily"),
- }
- })
- .catch((err) => Promise.reject(err))
- }
- /** Grab Stats from Team Data. */
- const getTeamStats = () => {
- if (!currentUser?.tag) {
- return Promise.reject(new Error("User is not in a team"))
- }
- const authToken = localStorage.getItem("player_token")
- return fetch(`/api/v2/teams/${currentUser.tag}`, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- })
- .then((r) => r.json())
- .then((r) => {
- return {
- leaderboard: r?.results?.leaderboard,
- motd: r?.results?.motd,
- info: r?.results?.info,
- stats: r?.results?.stats,
- member: r?.results?.members?.find((u) => u.userID === currentUser.userID),
- season: r?.results?.season?.find((u) => u.userID === currentUser.userID),
- }
- })
- .catch((err) => Promise.reject(err))
- }
- /** Stat Manager widget (basically a footer with settings button). */
- const ToolbarWidget = ((user) => {
- const root = document.createElement("div")
- root.classList.add("nt-stats-toolbar")
- root.innerHTML = `
- <div>
- NOTE: Team Stats and Season Stats are cached.
- </div>
- <div class="nt-stats-toolbar-status">
- <div class="nt-stats-toolbar-status-item">
- <span class=" nt-cash-status as-nitro-cash--prefix">N/A</span>
- </div>
- <div class="nt-stats-toolbar-status-item-alt">
- 📦 Mystery Box: <span class="mystery-box-status">N/A</span>
- </div>
- </div>`
- /** Mystery Box **/
- const rewardCountdown = user.rewardCountdown,
- mysteryBoxStatus = root.querySelector(".mystery-box-status")
- let isDisabled = Date.now() < user.rewardCountdown * 1e3,
- timer = null
- const syncCountdown = () => {
- isDisabled = Date.now() < user.rewardCountdown * 1e3
- if (!isDisabled) {
- if (timer) {
- clearInterval(timer)
- }
- mysteryBoxStatus.textContent = "Claim Now!"
- return
- }
- mysteryBoxStatus.textContent = moment(user.rewardCountdown * 1e3).fromNow(false)
- }
- syncCountdown()
- if (isDisabled) {
- timer = setInterval(syncCountdown, 6e3)
- }
- /** NT Cash. */
- const amountNode = root.querySelector(".nt-cash-status")
- return {
- root,
- updateStats: (user) => {
- if (typeof user?.money === "number") {
- amountNode.textContent = `$${user.money.toLocaleString()}`
- }
- },
- }
- })(raceObj.props.user)
- /** Daily Challenge widget. */
- const DailyChallengeWidget = (() => {
- const root = document.createElement("div")
- root.classList.add("nt-stats-daily-challenges", "profile-dailyChallenges", "card", "card--open", "card--d", "card--grit", "card--shadow-l")
- root.innerHTML = `
- <div class="daily-challenge-list--heading">
- <h4>Daily Challenges</h4>
- <div class="daily-challenge-list--arriving">
- <div class="daily-challenge-list--arriving-label">
- <svg class="icon icon-recent-time"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.1494.svg#icon-recent-time"></use></svg>
- New <span></span>
- </div>
- </div>
- </div>
- <div class="daily-challenge-list--challenges"></div>`
- const dailyChallengesContainer = root.querySelector(".daily-challenge-list--challenges"),
- dailyChallengesExpiry = root.querySelector(".daily-challenge-list--arriving-label span")
- const dailyChallengeItem = document.createElement("div")
- dailyChallengeItem.classList.add("raceResults--dailyChallenge")
- dailyChallengeItem.innerHTML = `
- <div class="daily-challenge-progress">
- <div class="daily-challenge-progress--info">
- <div class="daily-challenge-progress--requirements">
- <div class="daily-challenge-progress--name">
- <div style="height: 19px;">
- <div align="left" style="white-space: nowrap; pavgSpeedosition: absolute; transform: translate(0%, 0px) scale(1, 1); left: 0px;">
- </div>
- </div>
- </div>
- <div class="daily-challenge-progress--status"></div>
- </div>
- <div class="daily-challenge-progress--progress">
- <div class="daily-challenge-progress--progress-bar-container">
- <div class="daily-challenge-progress--progress-bar" style="width: 40%"></div>
- <div class="daily-challenge-progress--progress-bar--earned" style="width: 40%"></div>
- </div>
- </div>
- </div>
- <div class="daily-challenge-progress--badge">
- <div class="daily-challenge-progress--success"></div>
- <div class="daily-challenge-progress--xp">
- <span class="daily-challenge-progress--value"></span><span class="daily-challenge-progress--divider">/</span><span class="daily-challenge-progress--target"></span>
- </div>
- <div class="daily-challenge-progress--label"></div>
- </div>
- </div>`
- const updateDailyChallengeNode = (node, challenge) => {
- let progressPercentage = challenge.goal > 0 ? (challenge.progress / challenge.goal) * 100 : 0
- if (challenge.progress === challenge.goal) {
- progressPercentage = 100
- node.querySelector(".daily-challenge-progress").classList.add("is-complete")
- } else {
- node.querySelector(".daily-challenge-progress").classList.remove("is-complete")
- }
- node.querySelector(".daily-challenge-progress--name div div").textContent = challenge.title
- node.querySelector(".daily-challenge-progress--label").textContent = `${challenge.field}s`
- node.querySelector(".daily-challenge-progress--value").textContent = challenge.progress
- node.querySelector(".daily-challenge-progress--target").textContent = challenge.goal
- node.querySelector(".daily-challenge-progress--status").textContent = `Earn ${Math.floor(challenge.reward / 100) / 10}k XP`
- node.querySelectorAll(".daily-challenge-progress--progress-bar, .daily-challenge-progress--progress-bar--earned").forEach((bar) => {
- bar.style.width = `${progressPercentage}%`
- })
- }
- let dailyChallengeNodes = null
- getStats().then(({ dailyChallenges }) => {
- const dailyChallengeFragment = document.createDocumentFragment()
- dailyChallengeNodes = dailyChallenges.map((c) => {
- const node = dailyChallengeItem.cloneNode(true)
- updateDailyChallengeNode(node, c)
- dailyChallengeFragment.append(node)
- return node
- })
- dailyChallengesContainer.append(dailyChallengeFragment)
- })
- const updateStats = (data) => {
- if (!data || !dailyChallengeNodes || data.length === 0) {
- return
- }
- if (data[0] && data[0].expiration) {
- const t = 1000 * data[0].expiration
- if (!isNaN(t)) {
- dailyChallengesExpiry.textContent = moment(t).fromNow()
- }
- }
- data.forEach((c, i) => {
- if (dailyChallengeNodes[i]) {
- updateDailyChallengeNode(dailyChallengeNodes[i], c)
- }
- })
- }
- return {
- root,
- updateStats,
- }
- })()
- /** Display Season Progress and next Reward. */
- const SeasonProgressWidget = ((raceObj) => {
- const currentSeason = NTGLOBALS.ACTIVE_SEASONS.find((s) => {
- const now = Date.now()
- return now >= s.startStamp * 1e3 && now <= s.endStamp * 1e3
- })
- const seasonRewards = raceObj.props?.seasonRewards,
- user = raceObj.props?.user
- const root = document.createElement("div")
- root.classList.add("nt-stats-season-progress", "theme--pDefault")
- root.innerHTML = `
- <div class="season-progress-widget">
- <div class="season-progress-widget--info">
- <div class="season-progress-widget--title">Season Progress${currentSeason ? "" : " (starting soon)"}</div>
- <div class="season-progress-widget--current-xp"></div>
- <div class="season-progress-widget--current-level">
- <div class="season-progress-widget--current-level--prefix">Level</div>
- <div class="season-progress-widget--current-level--number"></div>
- </div>
- <div class="season-progress-widget--level-progress">
- <div class="season-progress-widget--level-progress-bar" style="width: 0%;"></div>
- </div>
- </div>
- <div class="season-progress-widget--next-reward">
- <div class="season-progress-widget--next-reward--display">
- <div class="season-reward-mini-preview">
- <div class="season-reward-mini-preview--locked">
- <div class="tooltip--season tooltip--xs tooltip--c" data-ttcopy="Upgrade to Nitro Gold to Unlock!">
- <svg class="icon icon-lock"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-lock"></use></svg>
- </div>
- </div>
- <a class="season-reward-mini-preview" href="/season">
- <div class="season-reward-mini-preview--frame">
- <div class="rarity-frame rarity-frame--small">
- <div class="rarity-frame--extra"></div>
- <div class="rarity-frame--content">
- <div class="season-reward-mini-preview--preview"></div>
- <div class="season-reward-mini-preview--label"></div>
- </div>
- </div>
- </div>
- </a>
- </div>
- </div>
- </div>
- </div>`
- const xpTextNode = root.querySelector(".season-progress-widget--current-xp"),
- xpProgressBarNode = root.querySelector(".season-progress-widget--level-progress-bar"),
- levelNode = root.querySelector(".season-progress-widget--current-level--number"),
- nextRewardRootNode = root.querySelector(".season-reward-mini-preview"),
- nextRewardTypeLabelNode = root.querySelector(".season-reward-mini-preview--label"),
- nextRewardTypeLockedNode = root.querySelector(".season-reward-mini-preview--locked"),
- nextRewardTypePreviewNode = root.querySelector(".season-reward-mini-preview--preview"),
- nextRewardTypePreviewImgNode = document.createElement("img"),
- nextRewardRarityFrameNode = root.querySelector(".rarity-frame.rarity-frame--small")
- nextRewardTypePreviewImgNode.classList.add("season-reward-mini-previewImg")
- if (!currentSeason) {
- nextRewardRootNode.remove()
- }
- /** Work out how much experience required to reach specific level. */
- const getExperienceRequired = (lvl) => {
- if (lvl < 1) {
- lvl = 1
- }
- const { startingLevels, experiencePerStartingLevel, experiencePerAchievementLevel, experiencePerExtraLevels } = NTGLOBALS.SEASON_LEVELS
- let totalExpRequired = 0,
- amountExpRequired = experiencePerStartingLevel
- for (let i = 1; i < lvl; i++) {
- if (i <= startingLevels) {
- totalExpRequired += experiencePerStartingLevel
- } else if (currentSeason && i > currentSeason.totalRewards) {
- totalExpRequired += experiencePerExtraLevels
- amountExpRequired = experiencePerExtraLevels
- } else {
- totalExpRequired += experiencePerAchievementLevel
- amountExpRequired = experiencePerAchievementLevel
- }
- }
- return [amountExpRequired, totalExpRequired]
- }
- /** Get next reward. */
- const getNextRewardID = (currentXP) => {
- currentXP = currentXP || user.experience
- if (!seasonRewards || seasonRewards.length === 0) {
- return null
- }
- if (user.experience === 0) {
- return seasonRewards[0] ? seasonRewards[0].achievementID : null
- }
- let claimed = false
- let nextReward = seasonRewards.find((r, i) => {
- if (!r.bonus && (claimed || r.experience === currentXP)) {
- claimed = true
- return false
- }
- return r.experience > currentXP || i + 1 === seasonRewards.length
- })
- if (!nextReward) {
- nextReward = seasonRewards[seasonRewards.length - 1]
- }
- return nextReward ? nextReward.achievementID : null
- }
- return {
- root,
- updateStats: (data) => {
- // XP Progress
- if (typeof data.experience === "number") {
- const [amountExpRequired, totalExpRequired] = getExperienceRequired(data.level + 1),
- progress = Math.max(5, ((amountExpRequired - (totalExpRequired - data.experience)) / amountExpRequired) * 100.0) || 5
- xpTextNode.textContent = `${(amountExpRequired - (totalExpRequired - data.experience)).toLocaleString()} / ${amountExpRequired / 1e3}k XP`
- xpProgressBarNode.style.width = `${progress}%`
- }
- levelNode.textContent = currentSeason && data.level > currentSeason.totalRewards + 1 ? `∞${data.level - currentSeason.totalRewards - 1}` : data.level || 1
- // Next Reward
- if (typeof data.experience !== "number") {
- return
- }
- const nextRewardID = getNextRewardID(data.experience),
- achievement = nextRewardID ? NTGLOBALS.ACHIEVEMENTS.LIST.find((a) => a.achievementID === nextRewardID) : null
- if (!achievement) {
- return
- }
- const { type, value } = achievement.reward
- if (["loot", "car"].includes(type)) {
- const item = type === "loot" ? NTGLOBALS.LOOT.find((l) => l.lootID === value) : NTGLOBALS.CARS.find((l) => l.carID === value)
- if (!item) {
- logging.warn("Update")(`Unable to find next reward ${type}`, achievement.reward)
- return
- }
- nextRewardRootNode.className = `season-reward-mini-preview season-reward-mini-preview--${type === "loot" ? item?.type : "car"}`
- nextRewardTypeLabelNode.textContent = type === "loot" ? item.type || "???" : "car"
- nextRewardRarityFrameNode.className = `rarity-frame rarity-frame--small${item.options?.rarity ? ` rarity-frame--${item.options.rarity}` : ""}`
- if (item?.type === "title") {
- nextRewardTypePreviewImgNode.remove()
- nextRewardTypePreviewNode.textContent = `"${item.name}"`
- } else {
- nextRewardTypePreviewImgNode.src = type === "loot" ? item.options?.src : `/cars/${item.options?.smallSrc}`
- nextRewardTypePreviewNode.innerHTML = ""
- nextRewardTypePreviewNode.append(nextRewardTypePreviewImgNode)
- }
- } else if (type === "money") {
- nextRewardTypeLabelNode.innerHTML = `<div class="as-nitro-cash--prefix">$${value.toLocaleString()}</div>`
- nextRewardTypePreviewImgNode.src = "/dist/site/images/pages/race/race-results-prize-cash.2.png"
- nextRewardRootNode.className = "season-reward-mini-preview season-reward-mini-preview--money"
- nextRewardRarityFrameNode.className = "rarity-frame rarity-frame--small rarity-frame--legendary"
- nextRewardTypePreviewNode.innerHTML = ""
- nextRewardTypePreviewNode.append(nextRewardTypePreviewImgNode)
- } else {
- logging.warn("Update")(`Unhandled next reward type ${type}`, achievement.reward)
- return
- }
- if (!achievement.free && user.membership === "basic") {
- nextRewardRootNode.firstElementChild.before(nextRewardTypeLockedNode)
- } else {
- nextRewardTypeLockedNode.remove()
- }
- },
- }
- })(raceObj)
- /** Displays list of player stats. */
- const StatWidget = (() => {
- const root = document.createElement("div")
- root.classList.add("nt-stats-info")
- root.innerHTML = `
- <div class="nt-stats-metric-row">
- <span class="nt-stats-metric nt-stats-metric-total-races">
- <span class="nt-stats-metric-heading">Total Races:</span>
- <span class="nt-stats-metric-value">0</span>
- </span>
- <span class="nt-stats-metric-separator">|</span>
- <span class="nt-stats-metric nt-stats-metric-season-races">
- <span class="nt-stats-metric-heading">Season Races:</span>
- <span class="nt-stats-metric-value">N/A</span>
- <span class="nt-stats-metric-separator">|</span>
- </span>
- <span class="nt-stats-metric nt-stats-metric-session-races">
- <span class="nt-stats-metric-heading">Current Session:</span>
- <span class="nt-stats-metric-value">0</div>
- </span>
- </div>
- <div class="nt-stats-metric-row">
- ${
- currentUser.tag
- ? `<span class="nt-stats-metric nt-stats-metric-team-races">
- <span class="nt-stats-metric-heading">Team Races:</span>
- <span class="nt-stats-metric-value">N/A</span>
- </span>
- <span class="nt-stats-metric-separator">|</span>
- <span class="nt-stats-metric nt-stats-metric-season-points">
- <span class="nt-stats-metric-heading">Season Points:</span>
- <span class="nt-stats-metric-value">N/A</span>
- </span>
- <span class="nt-stats-metric-separator">|</span>`
- : ``
- }
- <span class="nt-stats-metric nt-stats-metric-avg-speed">
- <span class="nt-stats-metric-heading">Avg Speed:</span>
- <span class="nt-stats-metric-value">0</span>
- <span class="nt-stats-metric-suffix">WPM</span>
- </span>
- <span class="nt-stats-metric-separator">|</span>
- <span class="nt-stats-metric nt-stats-metric-avg-accuracy">
- <span class="nt-stats-metric-heading">Avg Acc:</span>
- <span class="nt-stats-metric-value">0</span><span class="nt-stats-metric-suffix nt-stats-metric-suffix-no-space">%</span>
- </span>
- </div>`
- const totalRaces = root.querySelector(".nt-stats-metric-total-races .nt-stats-metric-value"),
- sessionRaces = root.querySelector(".nt-stats-metric-session-races .nt-stats-metric-value"),
- teamRaces = currentUser.tag ? root.querySelector(".nt-stats-metric-team-races .nt-stats-metric-value") : null,
- seasonRaces = root.querySelector(".nt-stats-metric-season-races .nt-stats-metric-value"),
- seasonPoints = root.querySelector(".nt-stats-metric-season-points .nt-stats-metric-value"),
- avgSpeed = root.querySelector(".nt-stats-metric-avg-speed .nt-stats-metric-value"),
- avgAccuracy = root.querySelector(".nt-stats-metric-avg-accuracy .nt-stats-metric-value")
- return {
- root,
- updateStats: (data) => {
- if (typeof data?.racesPlayed === "number") {
- totalRaces.textContent = data.racesPlayed.toLocaleString()
- }
- if (typeof data?.sessionRaces === "number") {
- sessionRaces.textContent = data.sessionRaces.toLocaleString()
- }
- if (typeof data?.seasonRaces === "string") {
- const value = parseInt(data.seasonRaces, 10)
- seasonRaces.textContent = isNaN(value) ? data.seasonRaces : value.toLocaleString()
- }
- if (typeof data?.seasonPoints === "number") {
- seasonPoints.textContent = data.seasonPoints.toLocaleString()
- }
- if (typeof data?.teamRaces === "number" && teamRaces) {
- teamRaces.textContent = data.teamRaces.toLocaleString()
- }
- if (typeof data?.avgAcc === "string" || typeof data?.avgAcc === "number") {
- avgAccuracy.textContent = data.avgAcc
- }
- if (typeof data?.avgSpeed === "number") {
- avgSpeed.textContent = data.avgSpeed
- } else if (typeof data?.avgScore === "number") {
- avgSpeed.textContent = data.avgScore
- }
- },
- }
- })()
- ////////////
- // Main //
- ////////////
- /* Add stats into race page with current values */
- getStats().then(({ user, dailyChallenges }) => {
- StatWidget.updateStats(user)
- SeasonProgressWidget.updateStats(user)
- DailyChallengeWidget.updateStats(dailyChallenges)
- ToolbarWidget.updateStats(user)
- logging.info("Update")("Start of race")
- const root = document.createElement("div"),
- body = document.createElement("div")
- root.classList.add("nt-stats-root")
- body.classList.add("nt-stats-body")
- const leftSection = document.createElement("div")
- leftSection.classList.add("nt-stats-left-section")
- leftSection.append(DailyChallengeWidget.root)
- const rightSection = document.createElement("div")
- rightSection.classList.add("nt-stats-right-section")
- rightSection.append(StatWidget.root, SeasonProgressWidget.root)
- body.append(leftSection, rightSection)
- root.append(body, ToolbarWidget.root)
- raceContainer.parentElement.append(root)
- })
- getTeamStats().then(
- (data) => {
- const { member, season } = data
- StatWidget.updateStats({
- teamRaces: member.played,
- seasonPoints: season.points,
- })
- },
- (err) => {
- if (err.message !== "User is not in a team") {
- return Promise.reject(err)
- }
- }
- )
- getSummaryStats().then(({ seasonBoard }) => {
- if (!seasonBoard) {
- return
- }
- StatWidget.updateStats({
- seasonRaces: seasonBoard.played,
- })
- })
- /** Broadcast Channel to let other windows know that stats updated. */
- const MESSGAE_LAST_RACE_UPDATED = "last_race_updated",
- MESSAGE_DAILY_CHALLANGE_UPDATED = "stats_daily_challenge_updated",
- MESSAGE_USER_STATS_UPDATED = "stats_user_updated"
- const statChannel = new BroadcastChannel("NTRacingStats")
- statChannel.onmessage = (e) => {
- const [type, payload] = e.data
- switch (type) {
- case MESSGAE_LAST_RACE_UPDATED:
- getStats().then(({ user, dailyChallenges }) => {
- StatWidget.updateStats(user)
- SeasonProgressWidget.updateStats(user)
- DailyChallengeWidget.updateStats(dailyChallenges)
- ToolbarWidget.updateStats(user)
- })
- break
- case MESSAGE_DAILY_CHALLANGE_UPDATED:
- DailyChallengeWidget.updateStats(payload)
- break
- case MESSAGE_USER_STATS_UPDATED:
- StatWidget.updateStats(payload)
- SeasonProgressWidget.updateStats(payload)
- break
- }
- }
- /** Sync Daily Challenge data. */
- server.on("setup", (e) => {
- const dailyChallenges = mergeDailyChallengeData(e.challenges)
- DailyChallengeWidget.updateStats(dailyChallenges)
- statChannel.postMessage([MESSAGE_DAILY_CHALLANGE_UPDATED, dailyChallenges])
- })
- /** Sync some of the User Stat data. */
- server.on("joined", (e) => {
- if (e.userID !== currentUser.userID) {
- return
- }
- const payload = {
- level: e.profile?.level,
- racesPlayed: e.profile?.racesPlayed,
- sessionRaces: e.profile?.sessionRaces,
- avgSpeed: e.profile?.avgSpeed,
- }
- StatWidget.updateStats(payload)
- SeasonProgressWidget.updateStats(payload)
- statChannel.postMessage([MESSAGE_USER_STATS_UPDATED, payload])
- })
- /** Track Race Finish exact time. */
- let hasCollectedResultStats = false
- server.on("update", (e) => {
- const me = e?.racers?.find((r) => r.userID === currentUser.userID)
- if (me.progress.completeStamp > 0 && me.rewards?.current && !hasCollectedResultStats) {
- hasCollectedResultStats = true
- db.backupStatData.put({ ...me.rewards.current, challenges: me.challenges, userID: currentUser.userID }).then(() => {
- statChannel.postMessage([MESSGAE_LAST_RACE_UPDATED])
- })
- }
- })
- /** Mutation observer to check if Racing Result has shown up. */
- 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")
- getStats().then(({ user, dailyChallenges }) => {
- StatWidget.updateStats(user)
- SeasonProgressWidget.updateStats(user)
- DailyChallengeWidget.updateStats(dailyChallenges)
- ToolbarWidget.updateStats(user)
- })
- getSummaryStats().then(({ seasonBoard }) => {
- if (!seasonBoard) {
- return
- }
- StatWidget.updateStats({
- seasonRaces: seasonBoard.played,
- })
- })
- getTeamStats().then((data) => {
- const { member, season } = data
- StatWidget.updateStats({
- teamRaces: member.played,
- seasonPoints: season.points,
- })
- })
- break
- }
- }
- })
- resultObserver.observe(raceContainer, { childList: true, subtree: true })