xeno auto typer

Ultra-simple Lite autotyper: Only WPM and Accuracy configurable. Purple theme option added.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        xeno auto typer 
// @namespace    http://tampermonkey.net/
// @version      1.0.7
// @description  Ultra-simple Lite autotyper: Only WPM and Accuracy configurable. Purple theme option added.
// @author       convicted
// @match        https://www.nitrotype.com/race
// @match        https://www.nitrotype.com/race/*
// @match        *://www.google.com/recaptcha/api2/anchor*
// @icon         https://cdn2.steamgriddb.com/icon/2b16a44bb65751bb0ebe5d8b42644bc2/32/512x512.png
// @license      MIT
// @grant        none
// @run-at       document-start
// ==/UserScript==
/*
===================================================================================
🛠️ CONFIGURATION – ULTRA-SIMPLE FOR LITE VERSION
===================================================================================
*/

// ===== USER CONFIGURABLE OPTIONS (Only WPM, Accuracy, and Theme) =====

/**
 * Configure your desired typing speed and accuracy here.
 * Defaults: WPM 81, Accuracy 95%, Theme 'blue'.
 */
const AppConfig = {
  defaults: {
    // Default WPM value.
    WPM_VALUE: 81,

    // Default Accuracy value (minimum target).
    ACCURACY_VALUE: 95,

    // Default theme
    theme: 'blue',

// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
    // Default WPM band range (e.g., +/- 5 WPM)
    WPM_BAND_RANGE: 5,

    // Default number of races to run before stopping
    RACES_TO_RUN: 10,
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---

    // Fixed basic parameters (no advanced stuff)
    TYPING_PARAMS: {
      allowedDeviation: 3, // Simple deviation
      correctionStrength: 0.5, // Basic correction
      speedBoost: 0.95, // Basic initial boost
      wpmSmoothingFactor: 0.8 // Simple smoothing
    }
  },

  // Internal configuration values
  config: {},

  /**
   * Initializes the configuration.
   */
  init(savedSettings = null) {
    this.config = JSON.parse(JSON.stringify(this.defaults)); // Deep copy defaults

    if (savedSettings) {
      this.loadFromSettings(savedSettings);
    }
  },

  /**
   * Overrides default configuration with saved settings (only basic keys).
   */
  loadFromSettings(settings) {
    if (settings.WPM_VALUE !== undefined) {
        this.config.WPM_VALUE = settings.WPM_VALUE;
    }
    if (settings.ACCURACY_VALUE !== undefined) {
        this.config.ACCURACY_VALUE = settings.ACCURACY_VALUE;
    }
    if (settings.theme !== undefined) {
        this.config.theme = settings.theme;
    }
// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
    if (settings.WPM_BAND_RANGE !== undefined) {
        this.config.WPM_BAND_RANGE = settings.WPM_BAND_RANGE;
    }
    if (settings.RACES_TO_RUN !== undefined) {
        this.config.RACES_TO_RUN = settings.RACES_TO_RUN;
    }
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---
    console.log('[AppConfig] Loaded settings:', this.config);
  },

  /**
   * Provides access to WPM value.
   */
  get wpmValue() {
    return this.config.WPM_VALUE;
  },

  /**
   * Provides access to Accuracy value.
   */
  get accuracyValue() {
    return this.config.ACCURACY_VALUE;
  },

  /**
   * Provides access to theme.
   */
  get theme() {
    return this.config.theme;
  },

// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
  /**
   * Provides access to WPM Band Range.
   */
  get wpmBandRange() {
    return this.config.WPM_BAND_RANGE;
  },

  /**
   * Provides access to Races to Run value.
   */
  get racesToRun() {
    return this.config.RACES_TO_RUN;
  },
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---

  /**
   * Provides access to basic Typing Params (fixed).
   */
  get typingParams() {
    return this.config.TYPING_PARAMS;
  }
};

/**
 * Configure elements to hide for performance.
 */
const ElementHiderConfig = {
    elementsToHide: [
        '.structure-leaderboard',
        '.ad--side',
        '.ad--side-extra',
        '.raceChat',
        '.sound-controls',
        '.header-bar--return-to-garage',
        '.dropdown--account',
        '.header-nav',
        '.structure-footer',
        '.pre-header-bar',
        '.site-takeover--header',
        '.site-takeover--footer'
    ]
};

/**
 * Hides specified elements using CSS.
 */
function hideElements() {
    const style = document.createElement('style');
    style.id = 'nitro-type-hider-style';
    let css = '';
    ElementHiderConfig.elementsToHide.forEach(selector => {
        css += `${selector} { display: none !important; }`;
    });
    style.appendChild(document.createTextNode(css));
    document.head.appendChild(style);
}

hideElements();

/*
===================================================================================
*/

// ===== CORE CONSTANTS =====

/**
 * Application-wide constants
 */
const Constants = {
  TYPING: {
    CHARS_PER_WORD: 5,
    MS_PER_MINUTE: 60 * 1000
  },
  EVENTS: {
    METRICS_UPDATED: 'metricsUpdated',
    SESSION_COMPLETED: 'sessionCompleted',
  },
  STORAGE: {
    CONFIG_KEY: 'typingBotConfig',
// --- START: ADDED CODE FOR RACE COUNTER ---
    RACE_COUNT_KEY: 'typingBotRaceCount',
// --- END: ADDED CODE FOR RACE COUNTER ---
  },
  BEHAVIOR: {
    MIN_DELAY_FACTOR: 0.9,
    MAX_DELAY_FACTOR: 1.1
  }
};

class EventBus {
// ... (existing EventBus code)
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(listener);
    return this;
  }

  emit(event, ...args) {
    if (!this.events[event]) return false;
    this.events[event].forEach(listener => listener(...args));
    return true;
  }

  off(event, listener) {
    if (!this.events[event]) return this;
    this.events[event] = this.events[event].filter(l => l !== listener);
    return this;
  }
}

/**
 * Centralized logging for errors only
 */
class Logger {
  static error(message, context = '') {
    console.error(`${context ? `[${context}] ` : ''}🔴 ${message}`);
  }
}

/**
 * Handles localStorage operations
 */
class StorageService {
  static getJson(key) {
    try {
        const item = localStorage.getItem(key);
        return item ? JSON.parse(item) : null;
    } catch (e) {
        Logger.error('Error parsing JSON from storage', 'StorageService');
        return null;
    }
  }

  static setJson(key, value) {
    try {
        localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
        Logger.error('Error stringifying JSON for storage', 'StorageService');
    }
  }

  static getMoney() {
    try {
      const persisted = JSON.parse(localStorage.getItem("persist:nt"));
      if (persisted && persisted.user) {
        const parsed = JSON.parse(persisted.user);
        return parsed.money || 0;
      }
    } catch (e) {
      console.error("[StorageService] Error getting money from storage:", e);
    }
    return 0;
  }
}

/**
 * General utilities
 */
class Utils {
// ... (existing Utils code)
  static delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  static getLongestWord(content) {
    const words = content.split(/\s+/);
    let longest = { word: "", index: 0 };

    for (let i = 0; i < words.length; i++) {
      if (words[i].length > longest.word.length) {
        longest.word = words[i];
        longest.index = content.indexOf(longest.word);
      }
    }

    return longest;
  }

  static boundValue(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  static async retryUntilSuccess(operation, interval = 500) {
    if (operation()) return true;

    return new Promise(resolve => {
      const checkInterval = setInterval(() => {
        if (operation()) {
          clearInterval(checkInterval);
          resolve(true);
        }
      }, interval);
    });
  }

  static getWordAtIndex(content, index) {
    if (index < 0 || index >= content.length) return "";
    const words = content.split(/(\s+)/);
    let currentLength = 0;
    for (const wordOrSpace of words) {
        if (index >= currentLength && index < currentLength + wordOrSpace.length) {
            return wordOrSpace.trim();
        }
        currentLength += wordOrSpace.length;
    }
    return "";
  }
}

// ===== SETTINGS MANAGER (ULTRA-SIMPLE GUI WITH THEME TOGGLE) =====

class SettingsManager {
    constructor(appConfig) {
        this.config = appConfig;
        this.settings = {};
        this.modalId = 'nt-bot-settings-modal';
        this.toggleId = 'nt-bot-settings-toggle';
        this.dragElement = null;
        this.dragOffsetX = 0;
        this.dragOffsetY = 0;
    }

    /**
     * Public entry point to load settings and create the GUI.
     */
    init() {
        this.loadSettings();
        this.config.init(this.settings);

        // FIX: Wait for document.body to be available since we run at document-start
        Utils.retryUntilSuccess(() => {
            if (document.body) {
                this.createGUI();
                return true;
            }
            return false;
        }, 100);
    }

    /**
     * Loads settings from localStorage or uses defaults (only WPM/Acc/Theme).
     */
    loadSettings() {
        const saved = StorageService.getJson(Constants.STORAGE.CONFIG_KEY);

        const defaults = this.config.defaults;
        this.settings = {
            WPM_VALUE: saved?.WPM_VALUE ?? defaults.WPM_VALUE,
            ACCURACY_VALUE: saved?.ACCURACY_VALUE ?? defaults.ACCURACY_VALUE,
            theme: saved?.theme ?? defaults.theme,
// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
            WPM_BAND_RANGE: saved?.WPM_BAND_RANGE ?? defaults.WPM_BAND_RANGE,
            RACES_TO_RUN: saved?.RACES_TO_RUN ?? defaults.RACES_TO_RUN
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---
        };
    }

    /**
     * Saves the current settings state to localStorage (only basic).
     */
    saveSettings() {
        const settingsToSave = {
            WPM_VALUE: this.config.config.WPM_VALUE,
            ACCURACY_VALUE: this.config.config.ACCURACY_VALUE,
            theme: this.config.config.theme,
// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
            WPM_BAND_RANGE: this.config.config.WPM_BAND_RANGE,
            RACES_TO_RUN: this.config.config.RACES_TO_RUN
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---
        };
        StorageService.setJson(Constants.STORAGE.CONFIG_KEY, settingsToSave);
        console.log('[SettingsManager] Configuration saved.');
    }

    /**
     * Creates and injects the ultra-simple GUI.
     */
    createGUI() {
        // Inject Styles
        const style = document.createElement('style');
        style.textContent = this._getStyles();
        document.head.appendChild(style);

        // Inject Modal HTML
        const modal = document.createElement('div');
        modal.id = this.modalId;
        modal.classList.add('nt-bot-modal', 'hidden', `nt-bot-theme--${this.config.config.theme}`);
        modal.innerHTML = this._getModalHTML();
        document.body.appendChild(modal); // This line is now safe because init() waits for document.body

        // Inject Toggle Button
        const toggle = document.createElement('button');
        toggle.id = this.toggleId;
        toggle.classList.add('nt-bot-toggle-btn', `nt-bot-theme--${this.config.config.theme}`);
        toggle.textContent = '⚙️ Bot Config';
        document.body.appendChild(toggle);

        // Populate and Attach Listeners
        this._populateInputs();
        this._attachListeners(modal, toggle);
    }

    /**
     * Populates input fields with current config values (only 2 + theme).
     */
    _populateInputs() {
        const config = this.config.config;

        this._updateInput('WPM_VALUE', config.WPM_VALUE);
        this._updateInput('ACCURACY_VALUE', config.ACCURACY_VALUE);
// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
        this._updateInput('WPM_BAND_RANGE', config.WPM_BAND_RANGE);
        this._updateInput('RACES_TO_RUN', config.RACES_TO_RUN);
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---

        // Theme checkbox
        const themeCheckbox = document.getElementById('config-theme-purple');
        if (themeCheckbox) themeCheckbox.checked = (config.theme === 'purple');
    }

    /**
     * Helper to update an input element and its label.
     */
    _updateInput(key, value) {
        const input = document.getElementById('config-' + key);
        const label = document.getElementById('label-' + key);
        if (input) {
            input.value = value;
        }
        if (label) {
            let formattedValue = Math.round(value);
            label.textContent = formattedValue;
        }
    }

    /**
     * Attaches listeners (basic only).
     */
    _attachListeners(modal, toggle) {
        // Toggle button
        toggle.addEventListener('click', () => modal.classList.toggle('hidden'));

        // Close button
        modal.querySelector('.nt-bot-modal-close').addEventListener('click', () => modal.classList.add('hidden'));

        // Input change listener
        modal.addEventListener('input', (e) => this._handleInputChange(e));
        modal.addEventListener('change', (e) => this._handleInputChange(e));

        // Dragging listeners
        const header = modal.querySelector('.nt-bot-modal-header');
        header.addEventListener('mousedown', (e) => this._dragStart(e, modal));
        document.addEventListener('mousemove', (e) => this._dragMove(e, modal));
        document.addEventListener('mouseup', () => this._dragEnd());
    }

    /**
     * Handles updates from GUI inputs.
     */
    _handleInputChange(e) {
        const input = e.target;
        if (!input.id || !input.id.startsWith('config-')) return;

        const key = input.id.replace('config-', '');
        let value = input.type === 'checkbox' ? input.checked : parseInt(input.value);

        // Update config
        if (key === 'WPM_VALUE') {
            this.config.config.WPM_VALUE = value;
        } else if (key === 'ACCURACY_VALUE') {
            this.config.config.ACCURACY_VALUE = value;
// --- START: ADDED CODE FOR WPM BAND AND RACE COUNT ---
        } else if (key === 'WPM_BAND_RANGE') {
            this.config.config.WPM_BAND_RANGE = value;
        } else if (key === 'RACES_TO_RUN') {
            this.config.config.RACES_TO_RUN = value;
// --- END: ADDED CODE FOR WPM BAND AND RACE COUNT ---
        } else if (key === 'theme-purple') {
            const newTheme = value ? 'purple' : 'blue';
            this.config.config.theme = newTheme;
            // Apply theme change immediately
            const currentModal = document.getElementById(this.modalId);
            const currentToggle = document.getElementById(this.toggleId);
            currentModal.classList.remove('nt-bot-theme--blue', 'nt-bot-theme--purple');
            currentModal.classList.add(`nt-bot-theme--${newTheme}`);
            currentToggle.classList.remove('nt-bot-theme--blue', 'nt-bot-theme--purple');
            currentToggle.classList.add(`nt-bot-theme--${newTheme}`);
        }

        // Update display
        if (key !== 'theme-purple') {
            this._updateInput(key, value);
        }

        // Save
        this.saveSettings();
    }

    // Dragging Logic
    _dragStart(e, modal) {
        this.dragElement = modal;
        this.dragElement.style.transition = 'none';
        this.dragOffsetX = e.clientX - this.dragElement.offsetLeft;
        this.dragOffsetY = e.clientY - this.dragElement.offsetTop;
        e.preventDefault();
    }

    _dragMove(e) {
        if (this.dragElement) {
            this.dragElement.style.left = `${e.clientX - this.dragOffsetX}px`;
            this.dragElement.style.top = `${e.clientY - this.dragOffsetY}px`;
        }
    }

    _dragEnd() {
        if (this.dragElement) {
            this.dragElement.style.transition = 'all 0.2s ease-out';
            this.dragElement = null;
        }
    }

    // Ultra-Simple GUI HTML with Theme Checkbox
    _getModalHTML() {
        return `
            <div class="nt-bot-modal-header">
                <h2>Private Bot</h2>
                <button class="nt-bot-modal-close">&times;</button>
            </div>
            <div class="nt-bot-modal-content">
                <div class="nt-bot-section-title">Base Targets</div>
                ${this._createInputGroup('WPM_VALUE', 'Target WPM', 50, 200, 1)}
                ${this._createInputGroup('ACCURACY_VALUE', 'Target Accuracy (%)', 90, 100, 1)}

                <div class="nt-bot-input-group nt-bot-checkbox-group">
                    <label for="config-theme-purple" class="nt-bot-label">Purple Theme</label>
                    <input type="checkbox" id="config-theme-purple" name="config-theme-purple" />
                </div>

                <div class="nt-bot-section-title">WPM Band & Session</div>
                ${this._createInputGroup('WPM_BAND_RANGE', 'WPM Band Range (±)', 0, 20, 1)}
                ${this._createInputGroup('RACES_TO_RUN', 'Races to Run', 1, 500, 1)}

                <p class="nt-bot-info-footer">Changes saved automatically. Takes effect next race.</p>
                <p class="nt-bot-info-footer">Developed by @fd7u Owned by @yoraritystar on discord.</p>
                
            </div>
        `;
    }

    _createInputGroup(key, label, min, max, step = 1, unit = '') {
        const currentValue = this.config.config[key] ?? this.config.defaults[key];

        return `
            <div class="nt-bot-input-group">
                <label for="config-${key}" class="nt-bot-label">${label}</label>
                <div class="nt-bot-input-row">
                    <input type="range" id="config-${key}" name="config-${key}"
                           min="${min}" max="${max}" step="${step}" value="${currentValue}"
                           class="nt-bot-slider">
                    <span id="label-${key}" class="nt-bot-value-label">${Math.round(currentValue)}${unit}</span>
                </div>
            </div>
        `;
    }

    _getStyles() {
        return `
            /* Core Modal Styles */
            .nt-bot-modal {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 300px;
                max-width: 90vw;
                background: #2c3e50; /* Default blue */
                color: #ecf0f1;
                border-radius: 12px;
                box-shadow: 0 10px 25px rgba(0, 0, 0, 0.7);
                z-index: 100000;
                font-family: 'Inter', sans-serif;
                border: 2px solid #3498db;
                transition: all 0.2s ease-out;
            }

            .nt-bot-modal.nt-bot-theme--blue {
                background: #2c3e50;
                color: #ecf0f1;
                border: 2px solid #3498db;
            }

            .nt-bot-modal.nt-bot-theme--purple {
                background: #2c1b3a;
                color: #f0e6ff;
                border: 2px solid #8e44ad;
            }

            .nt-bot-modal.hidden {
                display: none !important;
            }

            /* Header */
            .nt-bot-modal-header {
                background: #34495e; /* Default blue */
                padding: 12px 15px;
                border-top-left-radius: 10px;
                border-top-right-radius: 10px;
                cursor: move;
                display: flex;
                justify-content: space-between;
                align-items: center;
                border-bottom: 1px solid #3498db;
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-modal-header {
                background: #34495e;
                border-bottom: 1px solid #3498db;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-modal-header {
                background: #3b2265;
                border-bottom: 1px solid #8e44ad;
            }

            .nt-bot-modal-header h2 {
                margin: 0;
                font-size: 18px;
                font-weight: 600;
            }

            .nt-bot-modal-close {
                background: none;
                border: none;
                color: #e74c3c;
                font-size: 24px;
                cursor: pointer;
                transition: color 0.2s;
            }
            .nt-bot-modal-close:hover {
                color: #c0392b;
            }

            /* Content - Increased padding for better internal spacing */
            .nt-bot-modal-content {
                padding: 15px 20px;
                max-height: 70vh;
                overflow-y: auto;
            }

            /* Section Titles - Ensure consistent separation and style */
            .nt-bot-section-title {
                font-size: 14px;
                font-weight: 700;
                color: #3498db; /* Default blue */
                margin-top: 15px;
                margin-bottom: 8px;
                border-bottom: 1px dashed #3498db50;
                padding-bottom: 4px;
            }

            /* Add margin separation for sections after the first one */
            .nt-bot-section-title:not(:first-child) {
                margin-top: 20px;
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-section-title {
                color: #3498db;
                border-bottom: 1px dashed #3498db50;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-section-title {
                color: #8e44ad;
                border-bottom: 1px dashed #8e44ad50;
            }

            /* Input Groups - Ensure consistent vertical spacing */
            .nt-bot-input-group {
                margin-bottom: 12px; /* Increased margin for better separation */
            }

            /* Make checkbox group use flex to align label and box */
            .nt-bot-checkbox-group {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding-right: 0px;
            }

            .nt-bot-label {
                display: block;
                font-size: 13px;
                margin-bottom: 4px;
                color: #bdc3c7; /* Default blue */
            }

            /* Remove margin-bottom override for checkbox group label */
            .nt-bot-checkbox-group .nt-bot-label {
                margin-bottom: 0;
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-label {
                color: #bdc3c7;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-label {
                color: #d7bde2;
            }

            .nt-bot-input-group input[type="checkbox"] {
                width: auto;
                margin-left: 10px;
                transform: scale(1.2);
            }

            /* Input Row - Crucial for slider alignment */
            .nt-bot-input-row {
                display: flex;
                align-items: center;
                gap: 10px;
            }

            /* Improved Slider Look */
            .nt-bot-slider {
                flex-grow: 1;
                -webkit-appearance: none;
                width: 100%;
                height: 10px; /* Increased height */
                background: #34495e; /* Default blue */
                outline: none;
                opacity: 0.9;
                transition: opacity 0.2s;
                border-radius: 5px; /* Rounded track */
                box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); /* Added inner shadow */
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-slider {
                background: #34495e;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-slider {
                background: #3b2265;
            }

            .nt-bot-slider::-webkit-slider-thumb {
                -webkit-appearance: none;
                appearance: none;
                width: 18px; /* Increased size */
                height: 18px;
                border-radius: 50%;
                background: #3498db; /* Default blue */
                cursor: pointer;
                border: 2px solid #fff;
                box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); /* Added thumb shadow */
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-slider::-webkit-slider-thumb {
                background: #3498db;
                border: 2px solid #fff;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-slider::-webkit-slider-thumb {
                background: #8e44ad;
                border: 2px solid #f0e6ff;
                box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); /* Added thumb shadow */
            }

            .nt-bot-value-label {
                min-width: 40px;
                font-size: 13px;
                font-weight: bold;
                color: #95a5a6; /* Default blue */
                text-align: right;
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-value-label {
                color: #95a5a6;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-value-label {
                color: #bb8fce;
            }

            .nt-bot-info-footer {
                margin-top: 20px;
                padding: 10px;
                border-top: 1px solid #3498db; /* Default blue */
                font-size: 12px;
                text-align: center;
                color: #95a5a6;
            }

            .nt-bot-modal.nt-bot-theme--blue .nt-bot-info-footer {
                border-top: 1px solid #3498db;
                color: #95a5a6;
            }

            .nt-bot-modal.nt-bot-theme--purple .nt-bot-info-footer {
                border-top: 1px solid #8e44ad;
                color: #bb8fce;
            }

            /* Toggle Button */
            .nt-bot-toggle-btn {
                position: fixed;
                top: 10px;
                left: 10px;
                background-color: #3498db; /* Default blue */
                color: white;
                border: none;
                padding: 8px 15px;
                border-radius: 8px;
                cursor: pointer;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
                z-index: 99999;
                font-weight: bold;
                transition: background-color 0.2s, transform 0.1s;
            }

            .nt-bot-toggle-btn.nt-bot-theme--blue {
                background-color: #3498db;
                color: #fff;
            }

            .nt-bot-toggle-btn.nt-bot-theme--purple {
                background-color: #8e44ad;
                color: #f0e6ff;
            }

            .nt-bot-toggle-btn:hover {
                background-color: #2980b9; /* Default blue hover */
                transform: scale(1.02);
            }

            .nt-bot-toggle-btn.nt-bot-theme--blue:hover {
                background-color: #2980b9;
            }

            .nt-bot-toggle-btn.nt-bot-theme--purple:hover {
                background-color: #7d3c98;
            }
        `;
    }
}

// ===== AUTO-CAPTCHA HANDLER =====
(function() {
// ... (existing Auto-Captcha Handler code)
    'use strict';

    const RECAPTCHA_CHECKBOX_SELECTOR = '#recaptcha-anchor';
    const CHECK_INTERVAL_MS = 400;

    const isRunningInRecaptchaIframe = window.location.href.includes('/recaptcha/api2/anchor');

    if (isRunningInRecaptchaIframe || window.top === window) {

        let clickSuccessful = false;

        function simulateClick(element) {
            element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
            element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
            element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
        }

        function attemptClick() {
            if (clickSuccessful) {
                return;
            }

            const checkbox = document.querySelector(RECAPTCHA_CHECKBOX_SELECTOR);

            if (checkbox) {
                console.log(`[AutoClicker] Found checkbox. Simulating click.`);
                simulateClick(checkbox);
                clickSuccessful = true;
                clearInterval(intervalCheck);
            }
        }

        const intervalCheck = setInterval(attemptClick, CHECK_INTERVAL_MS);
    }
})();

function autoClickCaptchaContinue() {
  const check = setInterval(() => {
    const button = document.querySelector('.modal-body .btn--primary');
    if (button) {
      button.click();
      clearInterval(check);
    }
  }, 1000);
}

autoClickCaptchaContinue();

// ===== CORE SERVICES (SIMPLIFIED) =====

/**
 * Manages configuration and session state (simple).
 */
class ConfigService {
  constructor(eventBus, settingsManager) {
    this.eventBus = eventBus;
    this.settingsManager = settingsManager;
    this.typingParams = AppConfig.typingParams;
    this.actualTargetWPM = AppConfig.wpmValue; // Set initial target
    this.calculateActualTargetWPM(); // Calculate randomized target
  }

  // --- START: ADDED CODE FOR WPM BAND ---
  /**
   * Calculates a randomized WPM target within the defined band on startup.
   */
  calculateActualTargetWPM() {
    const baseWPM = AppConfig.wpmValue;
    const range = AppConfig.wpmBandRange;
    // Calculate a random offset between -range and +range
    const offset = Math.random() * (range * 2) - range;

    this.actualTargetWPM = Math.round(baseWPM + offset);

    // Ensure it's not too low (e.g., min of 50 WPM)
    this.actualTargetWPM = Math.max(50, this.actualTargetWPM);

    console.log(`[ConfigService] Base WPM: ${baseWPM}, Range: ±${range}. Actual Target WPM: ${this.actualTargetWPM}`);
  }
  // --- END: ADDED CODE FOR WPM BAND ---

  get targetWPM() {
    // --- START: MODIFIED TO USE ACTUAL TARGET WPM ---
    return this.actualTargetWPM;
    // --- END: MODIFIED TO USE ACTUAL TARGET WPM ---
  }

  get targetAccuracy() {
    return AppConfig.accuracyValue;
  }
}

/**
 * Handles DOM interactions
 */
class DOMInterface {
  getTypingAppNode() {
    try {
      const dashContainer = document.querySelector("div.dash-copyContainer");
      if (dashContainer) {
          return Object.values(dashContainer)[1].children._owner.stateNode;
      }
      return null;
    } catch (error) {
      return null;
    }
  }
}

/**
 * Tracks typing metrics (simplified: no stamina, no history, basic WPM/Acc).
 */
class MetricsService {
// ... (existing MetricsService code)
  constructor(configService, eventBus) {
    this.config = configService;
    this.eventBus = eventBus;
    this.reset();
  }

  reset() {
    this.totalKeystrokes = 0;
    this.correctKeystrokes = 0;
    this.typingStartTime = null;
    this.charactersTyped = 0;
    this.currentWPM = 0;
    this.smoothedWPM = 0;

    this.targetCPM = this.config.targetWPM * Constants.TYPING.CHARS_PER_WORD;
    this.baseDelayMs = Constants.TYPING.MS_PER_MINUTE / this.targetCPM * this.config.typingParams.speedBoost;
    this.currentDelayMs = this.baseDelayMs;

    this.sessionCompleted = false;
  }

  get currentAccuracy() {
// ... (existing MetricsService methods)
    if (this.totalKeystrokes === 0) return 100;
    return (this.correctKeystrokes / this.totalKeystrokes) * 100;
  }

  updateKeystrokeStats(isCorrect) {
    this.totalKeystrokes++;
    if (isCorrect) this.correctKeystrokes++;
    this.emitMetricsUpdate();
  }

  trackCorrectKeystroke() {
    this.updateKeystrokeStats(true);
  }

  trackIncorrectKeystroke() {
    this.updateKeystrokeStats(false);
  }

  updateWPM(newChars) {
    if (this.typingStartTime === null) {
      this.typingStartTime = Date.now();
      return 0;
    }

    this.charactersTyped += newChars;
    const currentTime = Date.now();
    const elapsedMinutes = (currentTime - this.typingStartTime) / Constants.TYPING.MS_PER_MINUTE;

    if (elapsedMinutes > 0) {
      const instantWPM = (this.charactersTyped / Constants.TYPING.CHARS_PER_WORD) / elapsedMinutes;
      this.smoothedWPM = this.smoothedWPM === 0 ?
        instantWPM :
        this.smoothedWPM * this.config.typingParams.wpmSmoothingFactor +
        instantWPM * (1 - this.config.typingParams.wpmSmoothingFactor);

      this.currentWPM = this.smoothedWPM;
      return this.currentWPM;
    }

    return 0;
  }

  emitMetricsUpdate() {
    this.eventBus.emit(Constants.EVENTS.METRICS_UPDATED, {
      accuracy: this.currentAccuracy,
      wpm: this.currentWPM,
      keystrokes: this.totalKeystrokes,
      correctKeystrokes: this.correctKeystrokes,
    });
  }

  markSessionComplete() {
    if (this.sessionCompleted) return;
    this.sessionCompleted = true;

    this.eventBus.emit(Constants.EVENTS.SESSION_COMPLETED, {
      finalWPM: this.currentWPM || 0,
      finalAccuracy: this.currentAccuracy || 0,
    });
  }
}

/**
 * Controls timing (simplified: basic delay with minor correction, no patterns/pauses).
 */
class TimingController {
  constructor(metricsService, configService) {
    this.metrics = metricsService;
    this.config = configService;
  }

  calculateNextDelay(content, typedIndex) {
    let delay = this.metrics.baseDelayMs;

    // Basic speed adjustment
    if (this.metrics.charactersTyped > 5 && this.metrics.currentWPM > 0) {
      const wpmDiff = this.metrics.currentWPM - this.config.targetWPM;
      const correction = wpmDiff * this.config.typingParams.correctionStrength;
      delay = delay * (1 + correction / 10);
    }

    // Minor random variation for basic human feel
    delay *= (0.95 + Math.random() * 0.1);

    // Bound delay
    delay = Utils.boundValue(
      delay,
      this.metrics.baseDelayMs * Constants.BEHAVIOR.MIN_DELAY_FACTOR,
      this.metrics.baseDelayMs * Constants.BEHAVIOR.MAX_DELAY_FACTOR
    );

    this.metrics.currentDelayMs = delay;

    return delay;
  }
}

/**
 * Controls typing accuracy (simple probability-based).
 */
class AccuracyController {
// ... (existing AccuracyController code)
  constructor(metricsService, configService) {
    this.metrics = metricsService;
    this.config = configService;

    this.targetMinimum = this.config.targetAccuracy;
    this.errorScalingFactor = 0.02; // Simple scaling
  }

  shouldMakeError() {
    if (this.metrics.totalKeystrokes < 10) return false;

    const currentAcc = this.metrics.currentAccuracy;

    if (currentAcc <= this.targetMinimum) {
      return false;
    }

    const accDiff = currentAcc - this.targetMinimum;
    let errorProb = accDiff * this.errorScalingFactor;
    errorProb = Utils.boundValue(errorProb, 0.0, 0.15);

    return Math.random() < errorProb;
  }
}

/**
 * Handles keyboard input simulation (simple).
 */
class KeyboardHandler {
// ... (existing KeyboardHandler code)
  constructor(metricsService, configService, eventBus) {
    this.metrics = metricsService;
    this.config = configService;
    this.eventBus = eventBus;
  }

  processKeystroke(appNode, makeCorrect) {
    if (makeCorrect) {
      this.metrics.trackCorrectKeystroke();
      this.metrics.updateWPM(1);
    } else {
      this.metrics.trackIncorrectKeystroke();
    }

    appNode.handleKeyPress("character", new KeyboardEvent("keypress", {
      key: makeCorrect ? appNode.props.lessonContent[appNode.typedIndex] : "$"
    }));

    this._checkSessionComplete(appNode);
  }

  simulateEnterKey(appNode) {
    appNode.handleKeyPress("character", new KeyboardEvent("keypress", { key: "\n" }));
    this._checkSessionComplete(appNode);
  }

  _checkSessionComplete(appNode) {
    if (this._isSessionComplete(appNode)) {
      this.handleSessionCompletion();
    }
  }

  _isSessionComplete(appNode) {
    return appNode.typedIndex >= appNode.props.lessonContent.length;
  }

  handleSessionCompletion() {
    this.metrics.markSessionComplete();
  }

  createAutoKeyHandler(appNode, originalKeyHandler, accuracyController, longestWord) {
    return (e, n) => {
      if (n.key >= '1' && n.key <= '8' || n.key === 'Shift' || n.key === 'Control') {
        return originalKeyHandler(e, n);
      }
      return false;
    };
  }
}

// --- START: ADDED CODE FOR RACE COUNTER ---
/**
 * Handles the race count limit
 */
class RaceCounter {
  constructor(configService) {
    this.config = configService;
    this.limit = this.config.settingsManager.settings.RACES_TO_RUN;
    this.currentCount = this.loadCount();

    if (this.currentCount >= this.limit) {
      this.reset();
    }
  }

  loadCount() {
    return parseInt(localStorage.getItem(Constants.STORAGE.RACE_COUNT_KEY) || '0', 10);
  }

  saveCount(count) {
    localStorage.setItem(Constants.STORAGE.RACE_COUNT_KEY, count.toString());
  }

  increment() {
    this.currentCount++;
    this.saveCount(this.currentCount);
    console.log(`[RaceCounter] Current race: ${this.currentCount} of ${this.limit}`);
  }

  reset() {
    this.saveCount(0);
    this.currentCount = 0;
    console.log('[RaceCounter] Counter reset.');
  }

  hasReachedLimit() {
    return this.currentCount >= this.limit;
  }
}
// --- END: ADDED CODE FOR RACE COUNTER ---


/**
 * Manages typing session lifecycle (simple reload).
 */
class SessionManager {
  constructor(configService, metricsService, eventBus) {
    this.config = configService;
    this.metrics = metricsService;
    this.eventBus = eventBus;
    this.isHandlingCompletion = false;
    // --- START: ADDED CODE FOR RACE COUNTER ---
    this.raceCounter = new RaceCounter(configService);
    // --- END: ADDED CODE FOR RACE COUNTER ---

    this.eventBus.on(Constants.EVENTS.SESSION_COMPLETED, data => {
      if (!this.isHandlingCompletion) {
        this.handleSessionCompleted(data);
      }
    });
  }

  handleSessionCompleted(sessionData) {
    this.isHandlingCompletion = true;

    // --- START: ADDED CODE FOR RACE COUNTER ---
    this.raceCounter.increment();
    if (this.raceCounter.hasReachedLimit()) {
      console.log(`[SessionManager] Reached race limit (${this.raceCounter.currentCount}/${this.raceCounter.limit}). Stopping autotyper.`);
      return; // Do not reload
    }
    // --- END: ADDED CODE FOR RACE COUNTER ---

    // Simple reload after delay
    setTimeout(() => window.location.reload(), 4000);
  }
}

/**
 * Main auto-typer controller (simplified).
 */
class AutoTyper {
  constructor(services) {
// ... (existing AutoTyper constructor)
    this.dom = services.dom;
    this.config = services.config;
    this.metrics = services.metrics;
    this.timingController = services.timingController;
    this.accuracyController = services.accuracyController;
    this.keyboardHandler = services.keyboardHandler;
    this.eventBus = services.eventBus;
    this.sessionManager = services.sessionManager;
  }

  async init() {
    await Utils.retryUntilSuccess(() => {
// --- START: ADDED CODE FOR RACE COUNTER CHECK ---
      if (this.sessionManager.raceCounter.hasReachedLimit()) {
        console.log('[AutoTyper] Race limit reached. Not starting new session.');
        return true;
      }
// --- END: ADDED CODE FOR RACE COUNTER CHECK ---
      const result = this.initTypingScript();
      return result;
    }, 200);
  }

  initTypingScript() {
    try {
      this.appNode = this.dom.getTypingAppNode();
      if (!this.appNode) return false;

      const content = this.appNode.props.lessonContent;
      this.longestWord = Utils.getLongestWord(content);
      this.originalKeyHandler = this.appNode.input.keyHandler;
      this._nitroUsed = false;

      this.startTypingSession();
      return true;
    } catch (error) {
      return false;
    }
  }

  async startTypingSession() {
    this.appNode.input.keyHandler = this.keyboardHandler.createAutoKeyHandler(
      this.appNode,
      this.originalKeyHandler,
      this.accuracyController,
      this.longestWord
    );

    await Utils.delay(Math.random() * 2000 + 1000);

    this._runAutoTyper();
  }

  async _runAutoTyper() {
    let isTyping = true;

    this.metrics.typingStartTime = Date.now();

    this._nitroUsed = false;

    const typeNextCharacter = async () => {
      const currentTypedIndex = this.appNode.typedIndex;

      if (!isTyping || this._isSessionComplete(currentTypedIndex)) {
        if (this._isSessionComplete(currentTypedIndex)) {
          this.metrics.markSessionComplete();
        }
        return;
      }

      const delay = this.timingController.calculateNextDelay(
        this.appNode.props.lessonContent,
        currentTypedIndex
      );

      setTimeout(() => {
        // Nitro at longest word
        if (!this._nitroUsed && currentTypedIndex === this.longestWord.index) {
          try {
            this.keyboardHandler.simulateEnterKey(this.appNode);
            this._nitroUsed = true;
            console.log("Nitro used on:", this.longestWord.word);
          } catch (e) {
            console.error("Nitro error:", e);
          }
        }

        const makeError = this.accuracyController.shouldMakeError();
        this.keyboardHandler.processKeystroke(this.appNode, !makeError);

        typeNextCharacter();
      }, delay);
    };

    typeNextCharacter();
  }

  _isSessionComplete(currentTypedIndex) {
    return currentTypedIndex >= this.appNode.props.lessonContent.length;
  }
}

/**
 * Service container
 */
class ServiceContainer {
// ... (existing ServiceContainer code)
  constructor() {
    this.services = {};
  }

  register(name, service) {
    this.services[name] = service;
    return this;
  }

  get(name) {
    return this.services[name];
  }

  createServices() {
    const eventBus = new EventBus();
    this.register('eventBus', eventBus);

    const settingsManager = new SettingsManager(AppConfig);
    settingsManager.init(); // This now waits for the DOM before creating GUI
    this.register('settingsManager', settingsManager);

    const configService = new ConfigService(eventBus, settingsManager);
    this.register('config', configService);

    const domService = new DOMInterface();
    this.register('dom', domService);

    const metricsService = new MetricsService(configService, eventBus);
    this.register('metrics', metricsService);

    const timingController = new TimingController(metricsService, configService);
    this.register('timingController', timingController);

    const accuracyController = new AccuracyController(metricsService, configService);
    this.register('accuracyController', accuracyController);

    const keyboardHandler = new KeyboardHandler(metricsService, configService, eventBus);
    this.register('keyboardHandler', keyboardHandler);

    const sessionManager = new SessionManager(configService, metricsService, eventBus);
    this.register('sessionManager', sessionManager);

    const autoTyper = new AutoTyper(this.services);
    this.register('autoTyper', autoTyper);

    return this;
  }
}

/**
 * Main application
 */
class TypingApp {
  constructor() {
    this.container = new ServiceContainer().createServices();
    this.autoTyper = this.container.get('autoTyper');
  }

  async start() {
    await this.autoTyper.init();
  }
}

// ===== INITIALIZE =====
(function() {
  const app = new TypingApp();
  app.start();
})();