您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and estimates hourly XP rate in Nitro Type races
// ==UserScript== // @name Nitro Type XP Tracker // @version 3.8 // @description Tracks and estimates hourly XP rate in Nitro Type races // @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 // @require https://update.gf.qytechs.cn/scripts/501960/1418069/findReact.js // @require https://update.gf.qytechs.cn/scripts/501961/1418070/CreateLogger.js // @require https://update.gf.qytechs.cn/scripts/501962/1418071/drawXpPieChart.js // @license MIT // @namespace https://gf.qytechs.cn/users/1331131-tensorflow-dvorak // ==/UserScript== /* globals Dexie, findReact, createLogger, drawXpPieChart */ const logging = createLogger("Nitro Type XP Tracker"); // Config storage const db = new Dexie("XPTracker"); db.version(31).stores({ races: "++id, timestamp, xp, placement, accuracy, wampus, friends, goldBonus, speed, other", totalXp: "key, value", session: "key, value" }); db.open().catch(function (e) { logging.error("Init")("Failed to open up the config database", e); }); // Initialize variables for XP tracking let xpAtStartOfRace = 0; let totalXpEarned = 0; let raceStartTime = 0; let firstRaceStartTime = null; let drawerOpen = true; const xpCategories = { "placement": 0, "accuracy": 0, "wampus": 0, "friends": 0, "goldBonus": 0, "speed": 0, "other": 0 }; const cumulativeXpCategories = { "placement": 0, "accuracy": 0, "wampus": 0, "friends": 0, "goldBonus": 0, "speed": 0, "other": 0 }; function createXpInfoUI() { let xpInfoContainer = document.getElementById("xp-info-container"); if (!xpInfoContainer) { xpInfoContainer = document.createElement("div"); xpInfoContainer.id = "xp-info-container"; xpInfoContainer.style.zIndex = "1000"; xpInfoContainer.style.backgroundColor = "rgba(34, 34, 34, 0.9)"; xpInfoContainer.style.color = "#fff"; xpInfoContainer.style.padding = "20px"; xpInfoContainer.style.fontFamily = "'Roboto', sans-serif"; xpInfoContainer.style.fontSize = "16px"; xpInfoContainer.style.boxShadow = "0 6px 12px rgba(0, 0, 0, 0.15)"; xpInfoContainer.style.position = "fixed"; xpInfoContainer.style.top = "20px"; xpInfoContainer.style.right = "0px"; xpInfoContainer.style.width = "300px"; xpInfoContainer.style.transition = "right 0.3s ease-in-out"; xpInfoContainer.innerHTML = ` <div id="xp-drawer-tab" style="position: absolute; top: 0; left: -40px; width: 40px; height: 100%; background-color: rgba(34, 34, 34, 0.9); border-radius: 10px 0 0 10px; cursor: pointer;"></div> <h3 style="margin-top: 0; font-size: 18px; text-align: center;">XP Meter</h3> <table style="width: 100%; margin-top: 10px;"> <tr> <td>Total XP Earned:</td> <td id='total-xp-earned'>0</td> </tr> <tr> <td>Estimated Hourly XP:</td> <td id='hourly-xp-rate'>0</td> </tr> <tr> <td>Average XP per Race:</td> <td id='avg-xp-per-race'>0</td> </tr> <tr> <td>Races in Last Hour:</td> <td id='races-last-hour'>0</td> </tr> <tr> <td>Estimated Hourly Races:</td> <td id='hourly-races-rate'>0</td> </tr> </table> <button id="reset-xp-tracker" style="margin-top: 10px; padding: 8px 15px; width: 100%; background-color: #ff4d4d; border: none; color: #fff; border-radius: 5px; cursor: pointer;">Reset</button> <canvas id="xpPieChart" style="margin-top: 20px;" width="300" height="300"></canvas> <!-- Updated width and height --> `; document.body.appendChild(xpInfoContainer); // Add event listener for drawer tab document.getElementById("xp-drawer-tab").addEventListener("click", toggleDrawer); // Add event listener for reset button document.getElementById("reset-xp-tracker").addEventListener("click", resetXpTracker); // Draw initial pie chart drawXpPieChart(); } } function toggleDrawer() { const xpInfoContainer = document.getElementById("xp-info-container"); if (xpInfoContainer.style.right === "0px") { xpInfoContainer.style.right = "-320px"; drawerOpen = false; } else { xpInfoContainer.style.right = "0px"; drawerOpen = true; } } async function loadSessionData() { logging.info("LoadSessionData")("Loading session data..."); const totalXpResult = await db.totalXp.get("totalXpEarned"); if (totalXpResult) { totalXpEarned = totalXpResult.value; document.getElementById('total-xp-earned').textContent = formatNumber(totalXpEarned); } else { totalXpEarned = 0; } logging.info("Total XP Earned")(totalXpEarned); const firstRaceStartTimeResult = await db.session.get("firstRaceStartTime"); if (firstRaceStartTimeResult) { firstRaceStartTime = firstRaceStartTimeResult.value; } else { firstRaceStartTime = Date.now(); await db.session.put({ key: "firstRaceStartTime", value: firstRaceStartTime }); } logging.info("First Race Start Time")(new Date(firstRaceStartTime).toLocaleString()); updateHourlyXpRate(); } async function updateXpInfo() { const raceEndTime = Date.now(); const currentXp = getCurrentXp(); const xpEarned = currentXp - xpAtStartOfRace; totalXpEarned += xpEarned; logging.info("XP Earned This Race")(xpEarned); logging.info("Total XP Earned")(totalXpEarned); // Save race data const reactFiberNode = getReactFiberNode(); const categorizedXp = categorizeXp(reactFiberNode.state.rewards); await db.races.add({ timestamp: raceEndTime, xp: xpEarned, placement: categorizedXp.placement, accuracy: categorizedXp.accuracy, wampus: categorizedXp.wampus, friends: categorizedXp.friends, goldBonus: categorizedXp.goldBonus, speed: categorizedXp.speed, other: categorizedXp.other }); await db.totalXp.put({ key: "totalXpEarned", value: totalXpEarned }); // Update the XP categories Object.keys(categorizedXp).forEach(category => { xpCategories[category] += categorizedXp[category]; }); // Update cumulative XP categories for the last hour const currentTime = Date.now(); const oneHourAgo = currentTime - (60 * 60 * 1000); const recentRaces = await db.races.where("timestamp").above(oneHourAgo).toArray(); cumulativeXpCategories.placement = recentRaces.reduce((acc, race) => acc + race.placement, 0); cumulativeXpCategories.accuracy = recentRaces.reduce((acc, race) => acc + race.accuracy, 0); cumulativeXpCategories.wampus = recentRaces.reduce((acc, race) => acc + race.wampus, 0); cumulativeXpCategories.friends = recentRaces.reduce((acc, race) => acc + race.friends, 0); cumulativeXpCategories.goldBonus = recentRaces.reduce((acc, race) => acc + race.goldBonus, 0); cumulativeXpCategories.speed = recentRaces.reduce((acc, race) => acc + race.speed, 0); cumulativeXpCategories.other = recentRaces.reduce((acc, race) => acc + race.other, 0); // Draw updated pie chart drawXpPieChart(); // Update the hourly XP rate updateHourlyXpRate(); // Prepare for the next race raceStartTime = Date.now(); xpAtStartOfRace = getCurrentXp(); } function getReactFiberNode() { const xpElements = document.getElementsByClassName("raceResults-reward-xp tar tss"); if (xpElements.length > 0) { const lastXpElement = xpElements[xpElements.length - 1]; const reactFiberNode = findReact(lastXpElement); if (reactFiberNode) { return reactFiberNode; } } return null; } function getCurrentXp() { const xpElements = document.getElementsByClassName("raceResults-reward-xp tar tss"); if (xpElements.length > 0) { const lastXpElement = xpElements[xpElements.length - 1]; const reactFiberNode = findReact(lastXpElement); if (reactFiberNode) { logging.debug("React Fiber XP Rewards")(reactFiberNode.state.rewards); const totalXp = reactFiberNode.state.rewards.reduce((acc, reward) => acc + reward.experience, 0); return totalXp; } } return 0; // Default value if XP element is not found } function categorizeXp(rewards) { const categories = { "placement": 0, "accuracy": 0, "wampus": 0, "friends": 0, "goldBonus": 0, "speed": 0, "other": 0 }; rewards.forEach(reward => { const xp = reward.experience; if (reward.label.includes("Place")) { categories.placement += xp; } else if (reward.label.includes("Accuracy")) { categories.accuracy += xp; } else if (reward.label.includes("Wampus")) { categories.wampus += xp; } else if (reward.label.includes("Friends")) { categories.friends += xp; } else if (reward.label.includes("Gold Bonus")) { categories.goldBonus += xp; } else if (reward.label.includes("Speed")) { categories.speed += xp; } else { categories.other += xp; } }); return categories; } async function updateHourlyXpRate() { const currentTime = Date.now(); const oneHourAgo = currentTime - (60 * 60 * 1000); const recentRaces = await db.races.where("timestamp").above(oneHourAgo).toArray(); const racesCount = recentRaces.length; if (racesCount > 0) { const totalPlacementXp = recentRaces.reduce((acc, race) => acc + race.placement, 0); const totalAccuracyXp = recentRaces.reduce((acc, race) => acc + race.accuracy, 0); const totalWampusXp = recentRaces.reduce((acc, race) => acc + race.wampus, 0); const totalFriendsXp = recentRaces.reduce((acc, race) => acc + race.friends, 0); const totalGoldBonusXp = recentRaces.reduce((acc, race) => acc + race.goldBonus, 0); const totalSpeedXp = recentRaces.reduce((acc, race) => acc + race.speed, 0); const totalOtherXp = recentRaces.reduce((acc, race) => acc + race.other, 0); const totalXp = totalPlacementXp + totalAccuracyXp + totalWampusXp + totalFriendsXp + totalGoldBonusXp + totalSpeedXp + totalOtherXp; const firstRecentRaceTime = recentRaces[0].timestamp; const totalDurationInMinutes = (currentTime - firstRecentRaceTime) / 1000 / 60; // in minutes const xpPerMinute = totalXp / totalDurationInMinutes; const projectedHourlyXpRate = xpPerMinute * 60; // Projected XP for 60 minutes const totalRaceTime = recentRaces.reduce((acc, race, index, array) => { if (index === 0) return acc; return acc + (array[index].timestamp - array[index - 1].timestamp); }, 0); const avgRaceTime = totalRaceTime / (racesCount - 1) / 1000; // in seconds const avgXpPerRace = totalXp / racesCount; // Average XP per race const estimatedHourlyRaces = 60 / (avgRaceTime / 60); // Estimated races per hour document.getElementById('hourly-xp-rate').textContent = formatNumber(Math.round(projectedHourlyXpRate)); document.getElementById('avg-xp-per-race').textContent = formatNumber(avgXpPerRace.toFixed(2)); document.getElementById('hourly-races-rate').textContent = formatNumber(Math.round(estimatedHourlyRaces)); // Update the XP categories xpCategories.placement = totalPlacementXp / racesCount; xpCategories.accuracy = totalAccuracyXp / racesCount; xpCategories.wampus = totalWampusXp / racesCount; xpCategories.friends = totalFriendsXp / racesCount; xpCategories.goldBonus = totalGoldBonusXp / racesCount; xpCategories.speed = totalSpeedXp / racesCount; xpCategories.other = totalOtherXp / racesCount; // Update cumulative XP categories for the last hour cumulativeXpCategories.placement = totalPlacementXp; cumulativeXpCategories.accuracy = totalAccuracyXp; cumulativeXpCategories.wampus = totalWampusXp; cumulativeXpCategories.friends = totalFriendsXp; cumulativeXpCategories.goldBonus = totalGoldBonusXp; cumulativeXpCategories.speed = totalSpeedXp; cumulativeXpCategories.other = totalOtherXp; // Draw updated pie chart drawXpPieChart(); } else { document.getElementById('hourly-xp-rate').textContent = "0"; document.getElementById('avg-xp-per-race').textContent = "0"; document.getElementById('hourly-races-rate').textContent = "0"; } document.getElementById('races-last-hour').textContent = formatNumber(racesCount); } async function resetXpTracker() { await db.races.clear(); await db.totalXp.clear(); await db.session.clear(); totalXpEarned = 0; xpCategories.placement = 0; xpCategories.accuracy = 0; xpCategories.wampus = 0; xpCategories.friends = 0; xpCategories.goldBonus = 0; xpCategories.speed = 0; xpCategories.other = 0; cumulativeXpCategories.placement = 0; cumulativeXpCategories.accuracy = 0; cumulativeXpCategories.wampus = 0; cumulativeXpCategories.friends = 0; cumulativeXpCategories.goldBonus = 0; cumulativeXpCategories.speed = 0; cumulativeXpCategories.other = 0; firstRaceStartTime = Date.now(); document.getElementById('total-xp-earned').textContent = "0"; document.getElementById('hourly-xp-rate').textContent = "0"; document.getElementById('races-last-hour').textContent = "0"; document.getElementById('avg-xp-per-race').textContent = "0"; document.getElementById('hourly-races-rate').textContent = "0"; // Draw updated pie chart drawXpPieChart(); await db.session.put({ key: "firstRaceStartTime", value: firstRaceStartTime }); logging.info("Reset")("XP Tracker has been reset"); } function initializeXpTracker() { createXpInfoUI(); const raceContainer = document.getElementById("raceContainer"); if (raceContainer) { const resultObserver = new MutationObserver(([mutation], observer) => { for (const node of mutation.addedNodes) { if (node.classList?.contains("race-results")) { logging.info("Update")("Race Results received"); // Update XP info at the end of the race updateXpInfo(); observer.observe(raceContainer, { childList: true, subtree: true }); break; } } }); resultObserver.observe(raceContainer, { childList: true, subtree: true }); } else { logging.error("Init")("Race container not found, retrying..."); setTimeout(initializeXpTracker, 1000); } } window.addEventListener("load", async () => { createXpInfoUI(); await loadSessionData(); initializeXpTracker(); // Start a new race session raceStartTime = Date.now(); xpAtStartOfRace = getCurrentXp(); }); // Utility function to format timestamp function formatTimestamp(timestamp) { const date = new Date(timestamp); return date.toString(); } // Utility function to format numbers with commas function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址