Automatically invites users to your team or adds as friend after a race
// ==UserScript==// @name Nitro Type Auto Invite// @namespace https://www.nitrotype.com/// @version 2.8.3// @description Automatically invites users to your team or adds as friend after a race// @author Isaac Weber// @match https://www.nitrotype.com/race/*// @match https://www.nitrotype.com/race// @grant none// @license MIT// ==/UserScript==(function() {'use strict';// Simple configurationconst config = {minDelay: 100,maxDelay: 200,debug: true,checkInterval: 200,startupDelay: 250};// Processed players trackingconst processed = new Set();// Track if processing has started to avoid duplicate detectionlet processingStarted = false;// Helper functionsfunction log(message) {if (config.debug) console.log(`[Team Inviter] ${message}`);}function randomDelay() {return Math.floor(Math.random() * (config.maxDelay - config.minDelay + 1)) + config.minDelay;}// Find player rows using various selectorsfunction findPlayerRows() {const selectors = ['.player-row','[class*="player-container"]','[class*="racer"]','[id*="racer"]','[id*="player"]','[class*="player"]','.race-r###lts-player'];for (const selector of selectors) {const elements = document.querySelectorAll(selector);if (elements.length > 0) {log(`Found ${elements.length} players using selector: ${selector}`);return Array.from(elements);}}log("No players found");return [];}// Reliable hover simulationfunction simulateHover(element) {try {const rect = element.getBoundingClientRect();const centerX = rect.left + (rect.width / 2);const centerY = rect.top + (rect.height / 2);// Clear existing hoversdocument.dispatchEvent(new MouseEvent('mouseout', {bubbles: true,cancelable: true}));// Hover eventselement.dispatchEvent(new MouseEvent('mouseenter', {bubbles: true,cancelable: true,clientX: centerX,clientY: centerY}));element.dispatchEvent(new MouseEvent('mouseover', {bubbles: true,cancelable: true,clientX: centerX,clientY: centerY}));return true;} catch (e) {log(`Hover error: ${e.message}`);return false;}}// Find and click relevant buttonsfunction findAndClickButtons() {try {// Team invite button by textconst inviteButtons = Array.from(document.querySelectorAll('a, button, .btn, [role="button"], div[class*="button"]')).filter(el => {const text = (el.textContent || '').toLowerCase();const isVisible = el.offsetParent !== null;return isVisible && text.includes('invite') && text.includes('team');});if (inviteButtons.length > 0) {log("Clicking team invite button");inviteButtons[0].click();return true;}// Team invite button by classconst specificButton = document.querySelector('a[class*="invite-team"], a[class*="team-invite"], [class*="invite-to-team"]');if (specificButton && specificButton.offsetParent !== null) {log("Clicking invite button by class");specificButton.click();return true;}// Add friend buttonconst friendButtons = Array.from(document.querySelectorAll('a, button, .btn, [role="button"], div[class*="button"]')).filter(el => {const text = (el.textContent || '').toLowerCase();const isVisible = el.offsetParent !== null;return isVisible && text.includes('add') && text.includes('friend');});if (friendButtons.length > 0) {log("Clicking Add Friend button");friendButtons[0].click();return true;}log("No buttons found");return false;} catch (e) {log(`Button error: ${e.message}`);return false;}}// Process players sequentiallyfunction processPlayers(players) {let currentIndex = 0;function processNext() {// Check if we're doneif (currentIndex >= players.length) {log("Finished processing all players");setTimeout(() => window.location.reload(), randomDelay());return;}const player = players[currentIndex];// Skip if already processedif (processed.has(player)) {currentIndex++;processNext();return;}log(`Processing player ${currentIndex + 1} of ${players.length}`);processed.add(player);// Hover over playerif (simulateHover(player)) {// Check for buttons after hover with a short delay using the existing checkIntervalsetTimeout(() => {const buttonFound = findAndClickButtons();// Move to next playercurrentIndex++;// If button was found, apply the full delay// If no button was found, move to the next player immediatelyif (buttonFound) {setTimeout(processNext, randomDelay());} else {setTimeout(processNext, 20); // tiny delay when no button's found}}, randomDelay); // Use randomDelay} else {// If hover failed, move to next without extra delaycurrentIndex++;setTimeout(processNext, randomDelay());}}// Start processingprocessNext();}// Enhanced race completion detectionfunction detectRaceCompletion() {return (document.querySelector(".raceR###lts") ||document.querySelector("[class*='race-r###lts']") ||document.querySelector(".race-r###lts-container") ||document.querySelector("[class*='finished']") ||document.querySelector("[class*='complete']") ||document.querySelector("[class*='raceOver']") ||(document.querySelector("[class*='race-stats']") && document.querySelectorAll("[class*='player']").length > 1));}// Early race detection with simplified approachfunction monitorRace() {// Variables to track race statelet raceInProgress = false;let raceCheckInterval = null;// Function to detect race activityfunction checkRaceActivity() {// Indicators that a race is in progressconst raceActive =document.querySelector("[class*='race-stats']") ||document.querySelector("[class*='racer-progress']") ||document.querySelector("[class*='typing-input']") ||document.querySelector("input[type='text'][class*='race']");// If race wasn't in progress before but is now, mark it as startedif (!raceInProgress && raceActive) {log("Race started");raceInProgress = true;}// If race was in progress but is no longer active, it just endedelse if (raceInProgress && !raceActive) {log("Race just ended - checking for r###lts");raceInProgress = false;// Race just ended - immediately check for r###ltsif (!processingStarted) {processingStarted = true;clearInterval(raceCheckInterval);// Allow a brief moment for UI to updatesetTimeout(() => {startTeamInviter();}, 300);}}}// Start monitoring for race activityraceCheckInterval = setInterval(checkRaceActivity, 250);// Also start the normal r###lts detection as a backupcheckForRaceR###lts();}// Start the team inviter processfunction startTeamInviter() {log("Starting team inviter process");// Wait a moment for UI to stabilizesetTimeout(() => {// First check if race is completeif (detectRaceCompletion()) {log("Race completion confirmed");// Find players to processconst players = findPlayerRows();if (players.length > 0) {log(`Found ${players.length} players to process`);processPlayers(players);} else {log("No players found, trying again in 500ms");// Try again after a short delaysetTimeout(() => {const playersRetry = findPlayerRows();if (playersRetry.length > 0) {processPlayers(playersRetry);} else {log("Still no players found, reloading page");window.location.reload();}}, 500);}} else {log("Race not complete yet, waiting for race r###lts");// If no race completion found, fall back to normal detectionprocessingStarted = false;}}, config.startupDelay);}// Original check for race r###lts - kept as fallbackfunction checkForRaceR###lts() {let hasChecked = false;const interval = setInterval(() => {if (hasChecked || processingStarted) {clearInterval(interval);return;}const raceComplete = detectRaceCompletion();if (raceComplete) {log("Race r###lts detected through fallback method");hasChecked = true;processingStarted = true;clearInterval(interval);// Wait for UI to stabilizesetTimeout(() => {const players = findPlayerRows();if (players.length > 0) {processPlayers(players);} else {log("No players found, reloading");window.location.reload();}}, 500);}}, config.checkInterval);// Safety timeoutsetTimeout(() => {if (!hasChecked && !processingStarted) {log("Safety reload triggered");window.location.reload();}}, 600000);}// Initializefunction init() {log("Team Inviter initialized");processingStarted = false;// Start monitoring for race completionmonitorRace();}// Start when page is readyif (document.readyState !== "loading") {setTimeout(init, 300);} else {document.addEventListener("DOMContentLoaded", () => setTimeout(init, 300));}})();