Auto Typer for Nitro Type (NEW 2025)

An auto-typing script with customizable speed and accuracy for Nitro Type.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Auto Typer for Nitro Type (NEW 2025)
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  An auto-typing script with customizable speed and accuracy for Nitro Type.
// @author       Anon2004
// @match        https://www.nitrotype.com/race
// @match        https://www.nitrotype.com/race/*
// @icon         https://cdn2.steamgriddb.com/icon/2b16a44bb65751bb0ebe5d8b42644bc4/32/512x512.png
// @license      MIT
// @grant        none
// ==/UserScript==

/*
===================================================================================
🛠️ SETUP INSTRUCTIONS – PLEASE READ CAREFULLY
===================================================================================

To customize the WPM (Words Per Minute) and Accuracy ranges for typing sessions:

---------------------------------------------------
STEP 1: Update the configuration values
---------------------------------------------------

⬇️ DO NOT edit anything in this comment block. ⬇️

Scroll down and find the following lines in the code:

  AppConfig.get wpmValues
  AppConfig.get accuracies

You’ll see something like:

  const MIN_WPM = 100;
  const MAX_WPM = 125;

  const MIN_ACC = 95;
  const MAX_ACC = 99;

🎯 Change these `MIN_` and `MAX_` values to set your preferred WPM and accuracy ranges.

💾 After making changes, save the file:

  File > Save   (or press Ctrl+S / Cmd+S)

⚠️ Stick to updating the min/max values only — leave `SESSIONS` untouched.

---------------------------------------------------
STEP 2: Clear the session cache
---------------------------------------------------

WPM and accuracy values are cached in `localStorage` when sessions start.
To apply your new settings, you **must** reset the cache.

⚠️ Run the following code from the browser console
**on the Nitro Type race page**, not elsewhere.

How to do it:

1. Go to nitrotype.com/race.
2. Open Developer Tools:
   - Press F12 or Ctrl+Shift+I (Windows/Linux)
   - Or Cmd+Option+I (Mac)
3. Go to the “Console” tab.
4. Paste and run this code:

(() => {
  localStorage.setItem('typingWpmSessionCount', '1');
  localStorage.setItem('typingAccuracySessionCount', '1');
  localStorage.setItem('typingAccuracyBufferCount', '1');

  localStorage.removeItem('dynamicWpmValues');
  localStorage.removeItem('dynamicAccuracies');

  window.location.reload();
})();

✅ This clears cached values and resets session counters.
Your updated min/max settings will take effect after the page reloads.

---------------------------------------------------
💬 Additional Notes
---------------------------------------------------

✔️ All changes are local to your browser. Nitro Type won’t detect them —
unless you set unusually high or suspicious values (e.g. WPM 300+, or 100% accuracy every time).

👀 Keep the **Console tab open** while using this setup — important debug info (like current WPM, accuracy, and mode-switch instructions) is printed there.

🎮 Mode Descriptions:

Auto Mode: Automatically simulates and injects keystrokes at the configured typing speed without user intervention.
Manual Mode: Waits for a user trigger before injecting each keystroke, giving you step-by-step control over the typing.
Normal Mode: Disables all automation hacks and behaves like a standard, unmodified typing experience.

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

// ===== CORE CONSTANTS AND CONFIGURATION =====

/**
 * Application modes with state preservation
 */
const AppModes = {
  AUTO: 'auto',
  MANUAL: 'manual',
  NORMAL: 'normal',
  STORAGE_KEY: 'typingAppMode'
};

/**
 * Application-wide constants for typing parameters, events, storage keys, and behavior factors
 */
const Constants = {
  TYPING: { CHARS_PER_WORD: 5, MS_PER_MINUTE: 60 * 1000 },
  EVENTS: {
    MODE_CHANGE: 'modeChange',
    METRICS_UPDATED: 'metricsUpdated',
    SESSION_COMPLETED: 'sessionCompleted',
    COUNTERS_INCREMENTED: 'countersIncremented',
    MANUAL_SESSION_COMPLETED: 'manualSessionCompleted',
    SESSION_COMPLETED_REFRESH: 'sessionCompletedRefresh'
  },
  STORAGE: {
    WPM_SESSION_COUNT: 'typingWpmSessionCount',
    ACCURACY_SESSION_COUNT: 'typingAccuracySessionCount',
    ACCURACY_BUFFER_COUNT: 'typingAccuracyBufferCount',
    DYNAMIC_WPM_VALUES: 'dynamicWpmValues',
    DYNAMIC_ACCURACIES: 'dynamicAccuracies'
  },
  BEHAVIOR: {
    MIN_DELAY_FACTOR: 0.4,
    MAX_DELAY_FACTOR: 1.8,
    WORD_BOUNDARY_MIN: 1.05,
    WORD_BOUNDARY_MAX: 0.1,
    COMMON_SEQ_MIN: 0.85,
    COMMON_SEQ_MAX: 0.05
  }
};

/**
 * Centralized application configuration with target WPM, accuracy values, and typing parameters
 */
const AppConfig = {
  // Helper to generate and cache arrays
  _generateDynamicArray(storageKey, count, min, max) {
    let arr = JSON.parse(localStorage.getItem(storageKey) || 'null');
    if (!Array.isArray(arr) || arr.length !== count) {
      arr = Array.from(
        { length: count },
        () => Math.floor(Math.random() * (max - min + 1)) + min
      );
      localStorage.setItem(storageKey, JSON.stringify(arr));
    }
    return arr;
  },
  // Target WPM values for each session, generated once and saved to localStorage
  get wpmValues() {
    const SESSIONS = 10;
    const MIN_WPM  = 100;
    const MAX_WPM  = 125;
    return this._generateDynamicArray(
      Constants.STORAGE.DYNAMIC_WPM_VALUES,
      SESSIONS,
      MIN_WPM,
      MAX_WPM
    );
  },
  // Target accuracy values for each session, generated once and saved to localStorage
  get accuracies() {
    const SESSIONS = 10;
    const MIN_ACC  = 95;
    const MAX_ACC  = 99;
    return this._generateDynamicArray(
      Constants.STORAGE.DYNAMIC_ACCURACIES,
      SESSIONS,
      MIN_ACC,
      MAX_ACC
    );
  },

  // Accuracy buffer values that cycle
  accuracyBuffers: [0.7, 0.7, 0.7, 0.7, 0.7, 0, 0, 0, 0, 0],

  // Common sequences typed faster by humans
  commonSequences: ['th', 'he', 'in', 'er', 'an', 'en', 'ing', 'the', 'and'],

  // Word boundary characters
  wordBoundaries: [' ', '.', ',', ';', ':'],

  // Parameters controlling typing behavior
  typingParams: {
    allowedDeviation: 2,
    correctionStrength: 0.8,
    humanVariationStrength: 0.25,
    microPauseChance: 0.06,
    burstChance: 0.12,
    hesitationChance: 0.05,
    maxMicroPause: 200,
    feedbackInterval: 800,
    wpmSmoothingFactor: 0.6,
    overcompensationFactor: 1.5,
    speedBoost: 0.85,
    adaptationRate: 0.2,
    earlyBoostFactor: 0.7
  }
};

// ===== CORE UTILITY CLASSES =====

/**
 * Simple event system to enable communication between components
 */
class EventBus {
  constructor() {
    this.events = {};
  }

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

  // Emit an event to all listeners
  emit(event, ...args) {
    if (!this.events[event]) return false;
    this.events[event].forEach(listener => listener(...args));
    return true;
  }

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

/**
 * Centralized logging with consistent message formatting
 */
class Logger {
  // Shared logging method to standardize format
  static _log(prefix, message, context = '') {
    const contextPrefix = context ? `[${context}] ` : '';
    console.log(`${contextPrefix}${prefix}${message}`);
  }

  // Standard logging methods with different prefixes
  static log(message, context = '') {
    this._log('', message, context);
  }

  static info(message, context = '') {
    this._log('', message, context);
  }

  static warn(message, context = '') {
    this._log('⚠️ ', message, context);
  }

  static error(message, context = '') {
    console.error(`${context ? `[${context}] ` : ''}🔴 ${message}`);
  }

  static success(message, context = '') {
    this._log('✅ ', message, context);
  }

  // Helper for logging metrics in consistent format
  static logMetric(name, value, target = null, context = '') {
    this.info(`${name}: ${Utils.formatNumber(value)}${target !== null ? ` (Target: ${target})` : ''}`, context);
  }

  // Helper for logging session info in consistent format
  static logSessionInfo(config, context = '') {
    this.info(`Session info: WPM #${config.wpmSessionCount}/${AppConfig.wpmValues.length} (${config.targetWPM} WPM) | Accuracy #${config.accuracySessionCount}/${AppConfig.accuracies.length} (${config.targetAccuracy}%)`, context);
  }
}

/**
 * Handles localStorage operations to persist session data
 */
class StorageService {
  // Get value from localStorage with default fallback
  static get(key, defaultValue) {
    return parseInt(localStorage.getItem(key) || defaultValue.toString());
  }

  // Set value in localStorage
  static set(key, value) {
    localStorage.setItem(key, value);
  }

  /**
   * Increments a counter in localStorage, cycling back to 1 when reaching max
   */
  static increment(key, maxValue, defaultValue = 0) {
    let count = this.get(key, defaultValue) + 1;
    if (count > maxValue) {
      Logger.info(`Completed full ${key} cycle. Resetting counter (was at ${count})`);
      count = 1;
    }
    this.set(key, count);
    return count;
  }
}

/**
 * General utilities for common operations
 */
class Utils {
  // Create a promise that resolves after specific time
  static delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Analyze content to find the longest word and its position
  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;
  }

  // Format number with specified decimal places
  static formatNumber(num, decimals = 2) {
    if (num === undefined || num === null) return "0.00";
    return num.toFixed(decimals);
  }

  // Apply random variation within a range
  static applyRandomVariation(base, minFactor, maxVariation) {
    return base * (minFactor + Math.random() * maxVariation);
  }

  // Check if string starts with any pattern from array
  static startsWithAny(str, patterns) {
    return patterns.some(pattern => str.startsWith(pattern));
  }

  // Check if character is in array
  static isInArray(char, array) {
    return array.includes(char);
  }

  // Keep value within bounds
  static boundValue(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  // Retry operation until success
  static async retryUntilSuccess(operation, interval = 500) {
    if (operation()) return true;

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

  // Apply a pattern if condition is met, otherwise return null
  static applyPatternIf(condition, factorFn, baseFactor) {
    if (condition) return factorFn(baseFactor);
    return null;
  }
}

// ===== CONFIGURATION =====

/**
 * Manages configuration and session state
 */
class ConfigService {
  constructor(eventBus) {
    this.eventBus = eventBus;
    this.loadConfig();
  }

  // Load configuration values from storage
  loadConfig() {
    const savedMode = localStorage.getItem(AppModes.STORAGE_KEY) || AppModes.AUTO;
    this._typingMode = savedMode;
    this._accuracySessionCount = StorageService.get(Constants.STORAGE.ACCURACY_SESSION_COUNT, 1);
    this._wpmSessionCount      = StorageService.get(Constants.STORAGE.WPM_SESSION_COUNT, 1);
    this._accuracyBufferCount  = StorageService.get(Constants.STORAGE.ACCURACY_BUFFER_COUNT, 1);
    this.typingParams          = AppConfig.typingParams;
  }

  // Getters and setters for the typing mode
  get typingMode() {
    return this._typingMode;
  }

  set typingMode(value) {
    this._typingMode = value;
    localStorage.setItem(AppModes.STORAGE_KEY, value);
    this.eventBus.emit(Constants.EVENTS.MODE_CHANGE, value);
  }

  // Helper methods to check specific modes
  get isAutoTypingMode() {
    return this._typingMode === AppModes.AUTO;
  }

  get isManualTypingMode() {
    return this._typingMode === AppModes.MANUAL;
  }

  get isNormalTypingMode() {
    return this._typingMode === AppModes.NORMAL;
  }

  // Return the target WPM for current session
  get targetWPM() {
    return AppConfig.wpmValues[this._wpmSessionCount - 1];
  }

  // Return the target accuracy for current session
  get targetAccuracy() {
    return AppConfig.accuracies[this._accuracySessionCount - 1];
  }

  // Return the target accuracy buffer for current session
  get targetAccuracyBuffer() {
    return AppConfig.accuracyBuffers[this._accuracyBufferCount - 1];
  }

  get wpmSessionCount() {
    return this._wpmSessionCount;
  }

  get accuracySessionCount() {
    return this._accuracySessionCount;
  }

  get accuracyBufferCount() {
    return this._accuracyBufferCount;
  }

  // Increment session counters and emit event
  incrementCounters() {
   // Skip incrementing counters in normal mode
   if (this.isNormalTypingMode) {
     return {
       wpmSession: this._wpmSessionCount,
       accuracySession: this._accuracySessionCount,
       accuracyBufferSession: this._accuracyBufferCount
     };
   }

  // Increment all counters with a helper method
     this._wpmSessionCount = this._incrementCounter(
       Constants.STORAGE.WPM_SESSION_COUNT,
       AppConfig.wpmValues.length
     );
     this._accuracySessionCount = this._incrementCounter(
       Constants.STORAGE.ACCURACY_SESSION_COUNT,
       AppConfig.accuracies.length
     );
     this._accuracyBufferCount = this._incrementCounter(
       Constants.STORAGE.ACCURACY_BUFFER_COUNT,
       AppConfig.accuracyBuffers.length
     );

    // If we've wrapped back to the first WPM session, clear cached arrays so they're regenerated
    if (this._wpmSessionCount === 1) {
      localStorage.removeItem(Constants.STORAGE.DYNAMIC_WPM_VALUES);
      localStorage.removeItem(Constants.STORAGE.DYNAMIC_ACCURACIES);
    }

    const counters = {
      wpmSession: this._wpmSessionCount,
      accuracySession: this._accuracySessionCount,
      accuracyBufferSession: this._accuracyBufferCount
    };

    this.eventBus.emit(Constants.EVENTS.COUNTERS_INCREMENTED, counters);
    return counters;
  }

   // Helper method to increment a counter
   _incrementCounter(key, maxValue) {
     return StorageService.increment(key, maxValue);
   }
}

// ===== DOM INTERFACE =====

/**
 * Handles DOM interactions and provides access to typing app node
 */
class DOMInterface {
  // Extract typing app node from the DOM
  getTypingAppNode() {
    try {
      return Object.values(document.querySelector("div.dash-copyContainer"))[1].children._owner.stateNode;
    } catch (error) {
      Logger.error(`Could not access typing app node: ${error}`, 'DOMInterface');
      return null;
    }
  }
}

// ===== METRICS TRACKING =====

/**
 * Tracks and calculates typing metrics and performance
 */
class MetricsService {
  constructor(configService, eventBus) {
    this.config = configService;
    this.eventBus = eventBus;
    this.reset();
  }

  // Reset all metrics to initial values
  reset() {
    this.totalKeystrokes = 0;
    this.correctKeystrokes = 0;
    this.typingStartTime = null;
    this.charactersTyped = 0;
    this.lastFeedbackTime = 0;
    this.currentWPM = 0;
    this.smoothedWPM = 0;

    // Calculate target CPM and base delay
    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;

    // Tracking variables
    this.typingPattern = [];
    this.lastKeystrokeTime = 0;
    this.cumulativeDeviation = 0;
    this.wpmHistory = [];
    this.adjustmentHistory = [];
    this.delayOffset = 0;

    // Startup phase tracking
    this.startupPhaseDone = false;
    this.startupCounter = 0;
    this.initialBoostApplied = false;

    this.sessionCompleted = false;
  }

  // Calculate current typing accuracy
  get currentAccuracy() {
    return this.totalKeystrokes > 0 ? (this.correctKeystrokes / this.totalKeystrokes) * 100 : 100;
  }

  // Update keystroke stats (correct or incorrect)
  updateKeystrokeStats(isCorrect) {
    this.totalKeystrokes++;
    if (isCorrect) this.correctKeystrokes++;
    this.emitMetricsUpdate();
  }

  // Track a correct keystroke
  trackCorrectKeystroke() {
    this.updateKeystrokeStats(true);
  }

  // Track an incorrect keystroke
  trackIncorrectKeystroke() {
    this.updateKeystrokeStats(false);
  }

  // Update WPM calculation based on typing progress
  updateWPM(newChars) {
    // Initialize start time on first update
    if (this.typingStartTime === null) {
      this.typingStartTime = Date.now();
      this.lastKeystrokeTime = Date.now();
      return 0;
    }

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

    // Calculate WPM if enough time has passed
    if (elapsedMinutes > 0) {
      // Calculate instant WPM and apply smoothing
      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;
      this._updateWpmHistory(currentTime);
      return this.currentWPM;
    }

    return 0;
  }

  // Update WPM history and provide feedback
  _updateWpmHistory(currentTime) {
    // Maintain a rolling window of recent WPM values
    if (this.wpmHistory.length > 10) this.wpmHistory.shift();
    this.wpmHistory.push(this.currentWPM);

    // Provide periodic feedback on typing speed
    if (currentTime - this.lastFeedbackTime > this.config.typingParams.feedbackInterval) {
      Logger.logMetric('Current typing speed', this.currentWPM, this.config.targetWPM, 'WPM');
      this.lastFeedbackTime = currentTime;
      this._handleSpeedDeviation();
    }

    this._handleStartupPhase();
  }

  // Handle deviations from target WPM
  _handleSpeedDeviation() {
    const deviation = this.currentWPM - this.config.targetWPM;
    const deviationPercent = Utils.formatNumber(Math.abs(deviation) / this.config.targetWPM * 100, 1);

    if (Math.abs(deviation) > this.config.typingParams.allowedDeviation) {
      if (deviation < 0) {
        Logger.warn(`TOO SLOW: ${Utils.formatNumber(Math.abs(deviation), 1)} WPM below target (${deviationPercent}%) - Increasing typing speed...`, 'WPM');
      } else {
        Logger.warn(`TOO FAST: ${Utils.formatNumber(deviation, 1)} WPM above target (${deviationPercent}%) - Reducing typing speed...`, 'WPM');
      }

      this._adjustForConsistentDeviation(deviation);
    }
  }

  // Make adjustments for consistent deviations
  _adjustForConsistentDeviation(deviation) {
    if (this.wpmHistory.length >= 5) {
      const recentAvg = this.wpmHistory.slice(-5).reduce((sum, wpm) => sum + wpm, 0) / 5;
      const avgDeviation = recentAvg - this.config.targetWPM;

      if (Math.abs(avgDeviation) > this.config.typingParams.allowedDeviation) {
        const adjustmentFactor = -avgDeviation * 0.015;
        this.delayOffset += adjustmentFactor;
        Logger.info(`🔄 Making permanent base delay adjustment: ${Utils.formatNumber(adjustmentFactor, 3)}ms (cumulative: ${Utils.formatNumber(this.delayOffset, 3)}ms)`, 'WPM');
      }
    }
  }

  // Handle special adjustments during startup phase
  _handleStartupPhase() {
    if (!this.startupPhaseDone) {
      this.startupCounter++;

      // Apply initial boost if typing too slow
      if (this.startupCounter === 10 && !this.initialBoostApplied) {
        if (this.currentWPM < this.config.targetWPM * 0.9) {
          const boostFactor = 0.7; // 30% faster typing
          this.baseDelayMs *= boostFactor;
          this.currentDelayMs *= boostFactor;
          this.initialBoostApplied = true;
          Logger.info(`🚀 Initial speed boost applied! Base delay reduced to ${Utils.formatNumber(this.baseDelayMs)}ms`, 'WPM');
        }
      }

      // End startup phase after sufficient keystrokes
      if (this.startupCounter >= 30) {
        this.startupPhaseDone = true;
        Logger.info("Startup phase complete, switching to normal control mode", 'WPM');
      }
    }
  }

  // Emit metrics updates and log feedback
  emitMetricsUpdate() {
    // Log accuracy periodically
    if (this.totalKeystrokes % 20 === 0) {
       this._logAccuracyFeedback();
    }

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

    // Log accuracy feedback in consistent format
   _logAccuracyFeedback() {
     Logger.logMetric('Current accuracy', this.currentAccuracy, this.config.targetAccuracy, 'Accuracy');
     Logger.info(`(${this.correctKeystrokes}/${this.totalKeystrokes})`, 'Accuracy');
   }

  // Mark session as complete and emit event
  markSessionComplete() {
    if (this.sessionCompleted) return;
    this.sessionCompleted = true;

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

  // Log history of WPM adjustments
  logAdjustmentHistory() {
    Logger.info("WPM and delay adjustment history:", 'Metrics');
    this.adjustmentHistory.forEach((item, i) => {
      Logger.info(`${i}: WPM=${Utils.formatNumber(item.wpm, 1)}, Target=${item.targetWpm}, Delay=${Utils.formatNumber(item.adjustedDelay, 1)}ms`, 'Metrics');
    });
  }
}

// ===== TIMING CONTROLLER =====

/**
 * Controls timing between keystrokes to achieve target WPM
 */
class TimingController {
  constructor(metricsService, configService) {
    this.metrics = metricsService;
    this.config = configService;
  }

  /**
   * Calculate delay for next keystroke based on current metrics
   */
  calculateNextDelay(content, typedIndex) {
    const now = Date.now();
    let delay = this.metrics.baseDelayMs;

    // Apply early boost during startup
    if (!this.metrics.startupPhaseDone) {
      delay *= this.config.typingParams.earlyBoostFactor;
    }

    // Adjust based on current WPM
    if (this.metrics.charactersTyped > 5 && this.metrics.currentWPM > 0) {
      delay = this._calculateSpeedAdjustedDelay(delay);
    }

    // Apply human-like variations
    const humanFactor = this._applyHumanTypingPatterns(content, typedIndex);
    delay *= humanFactor;

    // Keep delay within reasonable bounds
    delay = Utils.boundValue(
      delay,
      this.metrics.baseDelayMs * Constants.BEHAVIOR.MIN_DELAY_FACTOR,
      this.metrics.baseDelayMs * Constants.BEHAVIOR.MAX_DELAY_FACTOR
    );

    this._updateDelayHistory(delay);

    this.metrics.currentDelayMs = delay;
    this.metrics.lastKeystrokeTime = now;

    return delay;
  }

  /**
   * Calculate delay adjustments based on WPM difference
   */
  _calculateSpeedAdjustedDelay(baseDelay) {
    const wpmDifference = this.metrics.currentWPM - this.config.targetWPM;
    let correctionFactor = wpmDifference * this.config.typingParams.correctionStrength;

    // Apply stronger correction when below target
    if (wpmDifference < 0) {
      correctionFactor *= this.config.typingParams.overcompensationFactor;
    }

    // Apply correction and offset
    let delay = baseDelay * (1 + correctionFactor / 10);
    delay += this.metrics.delayOffset;

    // Track cumulative deviation
    this.metrics.cumulativeDeviation += wpmDifference * this.config.typingParams.adaptationRate;

    return this._applyTrendCorrections(delay, wpmDifference);
  }

  /**
   * Apply corrections based on observed typing trends
   */
  _applyTrendCorrections(delay, wpmDifference) {
    // Apply cumulative corrections periodically
    if (this.metrics.charactersTyped % 3 === 0 && Math.abs(this.metrics.cumulativeDeviation) > 0.5) {
      delay *= (1 + this.metrics.cumulativeDeviation * 0.02);

      // Prevent overcompensation
      if (Math.abs(this.metrics.cumulativeDeviation) > 3) {
        this.metrics.cumulativeDeviation *= 0.7;
      }
    }

    // Analyze recent WPM trends
    if (this.metrics.wpmHistory.length >= 3) {
      const trend = this.metrics.wpmHistory.slice(-3);
      const avgTrend = trend.reduce((sum, wpm) => sum + wpm, 0) / trend.length;
      const trendDeviation = avgTrend - this.config.targetWPM;

      // If trend consistently deviates, apply stronger correction
      if (Math.abs(trendDeviation) > this.config.typingParams.allowedDeviation &&
          Math.sign(trendDeviation) === Math.sign(wpmDifference)) {
        delay *= (1 - (trendDeviation * 0.01));
      }
    }

    return delay;
  }

  /**
   * Apply human-like variations to typing rhythm
   */
  _applyHumanTypingPatterns(content, typedIndex) {
    // Base human factor with random variation
    let humanFactor = 0.9 + (Math.random() * 0.2);
    const pattern = Math.random();
    const params = this.config.typingParams;

    // Apply pattern-based variations (micro-pause, burst, hesitation)
    const microPause = Utils.applyPatternIf(
      pattern < params.microPauseChance,
      (factor) => factor * (1 + Math.random() * 0.3),
       humanFactor
    );
    if (microPause !== null) return microPause;

    const burst = Utils.applyPatternIf(
      pattern < params.microPauseChance + params.burstChance,
      (factor) => factor * (0.75 + Math.random() * 0.1),
       humanFactor
    );
    if (burst !== null) return burst;

    const hesitation = Utils.applyPatternIf(
      pattern < params.microPauseChance + params.burstChance + params.hesitationChance,
      (factor) => factor * (1.1 + Math.random() * 0.4),
       humanFactor
    );
    if (hesitation !== null) return hesitation;

    // Apply content-based variations (word boundaries, common sequences)
    const currentChar = content[typedIndex];
    const wordBoundary = Utils.applyPatternIf(
      Utils.isInArray(currentChar, AppConfig.wordBoundaries),
      (factor) => Utils.applyRandomVariation(
        factor,
        Constants.BEHAVIOR.WORD_BOUNDARY_MIN,
        Constants.BEHAVIOR.WORD_BOUNDARY_MAX
      ),
       humanFactor
    );
    if (wordBoundary !== null) return wordBoundary;

    let nextFewChars = content.slice(typedIndex, typedIndex + 3);
    const commonSeq = Utils.applyPatternIf(
      Utils.startsWithAny(nextFewChars, AppConfig.commonSequences),
      (factor) => Utils.applyRandomVariation(
        factor,
        Constants.BEHAVIOR.COMMON_SEQ_MIN,
        Constants.BEHAVIOR.COMMON_SEQ_MAX
      ),
       humanFactor
    );
    if (commonSeq !== null) return commonSeq;

    return humanFactor;
  }

  /**
   * Update delay history for analysis
   */
  _updateDelayHistory(delay) {
    // Maintain limited history of patterns
    if (this.metrics.typingPattern.length > 15) this.metrics.typingPattern.shift();
    this.metrics.typingPattern.push(delay);

    // Keep adjustment history for analysis
    if (this.metrics.adjustmentHistory.length > 10) this.metrics.adjustmentHistory.shift();
    this.metrics.adjustmentHistory.push({
      wpm: this.metrics.currentWPM,
      targetWpm: this.config.targetWPM,
      baseDelay: this.metrics.baseDelayMs,
      adjustedDelay: delay
    });
  }

  /**
   * Add random micro-pauses for human-like typing
   */
  async addMicroPause() {
    if (Math.random() < 0.07) {
      const pauseTime = Math.random() * this.config.typingParams.maxMicroPause;
      return Utils.delay(pauseTime);
    }
    return Promise.resolve();
  }
}

// ===== ACCURACY CONTROLLER =====

/**
 * Controls typing accuracy to match target percentage
 */
class AccuracyController {
  constructor(metricsService, configService) {
    this.metrics = metricsService;
    this.config = configService;

    // Use the cyclic buffer value
    this.accuracyBuffer = this.config.targetAccuracyBuffer;
  }

  /**
   * Get the current accuracy buffer for logging purposes
   */
  getBufferInfo() {
    return `+${Utils.formatNumber(this.accuracyBuffer, 2)}%`;
  }

  /**
   * Determine if an error should be made to maintain target accuracy
   */
  shouldMakeError() {
    // Don't make errors at the beginning
    if (this.metrics.totalKeystrokes === 0) return false;

    // Target accuracy with buffer
    const targetWithBuffer = this.config.targetAccuracy + this.accuracyBuffer;

    // Dynamic error probability based on current vs. target accuracy
    if (this.metrics.currentAccuracy > targetWithBuffer) {
      return true; // More likely to make errors when above target
    } else if (this.metrics.currentAccuracy < targetWithBuffer) {
      return false; // Less likely when below target
    }

    // When at target accuracy, error probability matches adjusted target
    return Math.random() > (targetWithBuffer / 100);
  }
}

// ===== KEYBOARD HANDLER =====

/**
 * Handles keyboard input and simulates keystrokes
 */
class KeyboardHandler {
  constructor(metricsService, configService, eventBus) {
    this.metrics = metricsService;
    this.config = configService;
    this.eventBus = eventBus;
  }

  /**
   * Process keystroke and update metrics
   */
  processKeystroke(appNode, makeCorrect) {
    // Update metrics based on keystroke correctness
    if (makeCorrect) {
      this.metrics.trackCorrectKeystroke();
      if (this.config.isAutoTypingMode) {
        this.metrics.updateWPM(1);
      }
    } else {
      this.metrics.trackIncorrectKeystroke();
    }

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

    this._checkSessionComplete(appNode);
  }

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

  /**
   * Check if session is complete in manual mode
   */
  _checkSessionComplete(appNode) {
    if (!this.config.isAutoTypingMode && this._isSessionComplete(appNode)) {
      this.handleSessionCompletion();
    }
  }

  /**
   * Check if typing session is complete
   */
  _isSessionComplete(appNode) {
    return appNode.typedIndex >= appNode.props.lessonContent.length;
  }

  /**
   * Handle completion of a manual typing session
   */
  handleSessionCompletion() {
    this.metrics.markSessionComplete();
    this.eventBus.emit(Constants.EVENTS.MANUAL_SESSION_COMPLETED);
  }

  /**
   * Create key handlers for different typing modes
   */
  createKeyHandlers(appNode, originalKeyHandler, accuracyController, longestWord) {
    return {
      // Normal typing mode handler
      createNormalHandler: () => {
         return this._createNormalKeyHandler(appNode, originalKeyHandler, accuracyController, longestWord);
      },

      // Countdown phase handler
      createCountdownHandler: () => {
         return this._createCountdownKeyHandler(originalKeyHandler);
      }
    };
  }

  /**
   * Create normal typing mode key handler
   */
  _createNormalKeyHandler(appNode, originalKeyHandler, accuracyController, longestWord) {
    return (e, n) => {
      if ("character" === e) {
        if (appNode.typedIndex === longestWord.index) {
          this.simulateEnterKey(appNode);
          return;
        }
        this.processKeystroke(appNode, !accuracyController.shouldMakeError());
      } else if (n.key === "\n") {
        this.simulateEnterKey(appNode);
      } else {
        return originalKeyHandler(e, n);
      }
   };
 }

  /**
   * Create countdown phase key handler
   */
  _createCountdownKeyHandler(originalKeyHandler) {
    return (e, n) => {
     // Toggle modes with different keys: 'a' for auto, 'm' for manual, 'n' for normal
     if ("character" === e) {
       if (n.key === "a") {
         this.config.typingMode = AppModes.AUTO;
         Logger.info(`Mode switched to: AUTO typing`, 'KeyboardHandler');
         return;
       } else if (n.key === "m") {
         this.config.typingMode = AppModes.MANUAL;
         Logger.info(`Mode switched to: MANUAL typing`, 'KeyboardHandler');
         return;
       } else if (n.key === "n") {
         this.config.typingMode = AppModes.NORMAL;
         Logger.info(`Mode switched to: NORMAL typing (script disabled)`, 'KeyboardHandler');
         return;
       }
     }

      // Pass through specific keys
      if (n.key >= '1' && n.key <= '8' || n.key === 'Shift' || n.key === 'Control') {
        return originalKeyHandler(e, n);
      }
    };
  }
}

// ===== SESSION MANAGER =====

/**
 * Manages typing session lifecycle and transitions
 */
class SessionManager {
  constructor(configService, metricsService, eventBus) {
    this.config = configService;
    this.metrics = metricsService;
    this.eventBus = eventBus;
    this.isHandlingCompletion = false;

    // Subscribe to session completion event
    this.eventBus.on(Constants.EVENTS.SESSION_COMPLETED, data => {
      if (!this.isHandlingCompletion) {
        this.handleSessionCompleted(data);
      }
    });
  }

  /**
   * Handle session completion and report results
   */
  handleSessionCompleted(sessionData) {
    this.isHandlingCompletion = true;

    // Save target values before incrementing
    const currentTargetWPM = this.config.targetWPM;
    const currentTargetAccuracy = this.config.targetAccuracy;

    // Get final metrics
    const finalWPM = sessionData?.finalWPM || 0;
    const finalAccuracy = sessionData?.finalAccuracy || 0;

    // Log completion results
    this._logSessionResults(finalWPM, finalAccuracy, currentTargetWPM, currentTargetAccuracy);

    // Increment counters for next session
    const newCounters = this.config.incrementCounters();
    Logger.info(`New WPM session: ${newCounters.wpmSession}/${AppConfig.wpmValues.length} | New Accuracy session: ${newCounters.accuracySession}/${AppConfig.accuracies.length} | New Buffer session: ${newCounters.accuracyBufferSession}/${AppConfig.accuracyBuffers.length}`, 'SessionManager');

    // Log additional metrics in auto mode
    if (this.config.isAutoTypingMode) {
      this.metrics.logAdjustmentHistory();
    }

    Logger.info("Waiting 4 seconds before refreshing page...", 'SessionManager');

    // Emit refresh notification event
    this.eventBus.emit(Constants.EVENTS.SESSION_COMPLETED_REFRESH, {
      isAutoMode: this.config.isAutoTypingMode,
      isManualMode: this.config.isManualTypingMode,
      isNormalMode: this.config.isNormalTypingMode,
      nextWpmSession: newCounters.wpmSession,
      nextAccuracySession: newCounters.accuracySession,
      nextBufferSession: newCounters.accuracyBufferSession
    });

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

  /**
   * Log session completion results
   */
  _logSessionResults(finalWPM, finalAccuracy, targetWPM, targetAccuracy) {
    if (this.config.isAutoTypingMode) {
      Logger.success("Auto-typing completed!", 'SessionManager');
      Logger.logMetric('Final WPM', finalWPM, targetWPM, 'SessionManager');
    } else {
      Logger.success("Manual typing session completed!", 'SessionManager');
    }

    Logger.logMetric('Final accuracy', finalAccuracy, targetAccuracy, 'SessionManager');
  }
}

// ===== AUTO-TYPER =====

/**
 * Main controller for auto-typing functionality
 */
class AutoTyper {
  constructor(services) {
    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;
  }

  /**
   * Initialize auto-typer and wait for page to load
   */
  async init() {
    Logger.info("Waiting for page to fully load...", 'AutoTyper');

    // Retry initialization until typing app is ready
    await Utils.retryUntilSuccess(() => {
      const result = this.initTypingScript();
      if (result) {
        Logger.info("Script is now running!", 'AutoTyper');
      }
      return result;
    }, 500);
  }

  /**
   * Set up typing script once page is loaded
   */
  initTypingScript() {
    try {
      this.appNode = this.dom.getTypingAppNode();
      if (!this.appNode) return false;

      // Get content and analyze
      const content = this.appNode.props.lessonContent;
      this.longestWord = Utils.getLongestWord(content);
      this.originalKeyHandler = this.appNode.input.keyHandler;

      // Log setup information
      Logger.info("Script successfully loaded", 'AutoTyper');
      Logger.logSessionInfo(this.config, 'AutoTyper');
      Logger.info(`Accuracy target buffer: ${this.accuracyController.getBufferInfo()} (Session ${this.config.accuracyBufferCount}/${AppConfig.accuracyBuffers.length})`, 'AutoTyper');
      Logger.info(`Longest word detected: '${this.longestWord.word}' at position ${this.longestWord.index}`, 'AutoTyper');
      let modeDisplay = "UNKNOWN";
      if (this.config.isAutoTypingMode) modeDisplay = "AUTO";
      else if (this.config.isManualTypingMode) modeDisplay = "MANUAL";
      else if (this.config.isNormalTypingMode) modeDisplay = "NORMAL (script disabled)";
      Logger.info(`Mode: ${modeDisplay} typing`, 'AutoTyper');

      this.startTypingSession();
      return true;
    } catch (error) {
      Logger.info("Content not ready yet, retrying...", 'AutoTyper');
      return false;
    }
  }

  /**
   * Start typing session with countdown and mode selection
   */
  async startTypingSession() {
    // Set up key handlers
    const keyHandlers = this.keyboardHandler.createKeyHandlers(
      this.appNode,
      this.originalKeyHandler,
      this.accuracyController,
      this.longestWord
    );

    // Start with countdown handler for initial setup
    this.appNode.input.keyHandler = keyHandlers.createCountdownHandler();

    Logger.info("Press: 'a' for AUTO typing, 'm' for MANUAL typing, 'n' for NORMAL typing", 'AutoTyper');
    Logger.info("Countdown started. Auto-typing will begin in 4 seconds...", 'AutoTyper');

    await Utils.delay(4000);

    // If in normal mode, use the original key handler and return
    if (this.config.isNormalTypingMode) {
      Logger.info("Starting in NORMAL mode. Script disabled.", 'AutoTyper');
      this.appNode.input.keyHandler = this.originalKeyHandler;
      return;
    }

    // Switch to normal typing handler after countdown
    this.appNode.input.keyHandler = keyHandlers.createNormalHandler();

    // Start appropriate typing mode
    this._startTypingMode();
  }

  /**
   * Start appropriate typing mode with proper logging
   */
  _startTypingMode() {
    if (this.config.isAutoTypingMode) {
      Logger.info(`Starting auto-typing - Target WPM: ${this.config.targetWPM} (Session ${this.config.wpmSessionCount}/${AppConfig.wpmValues.length})`, 'AutoTyper');
      Logger.info(`Base delay between keystrokes: ${Utils.formatNumber(this.metrics.baseDelayMs)}ms (with ${this.config.typingParams.speedBoost.toFixed(2)}x speed boost)`, 'AutoTyper');
      this._runAutoTyper();
    } else if (this.config.isManualTypingMode) {
      Logger.info(`Starting manual typing - Target accuracy: ${this.config.targetAccuracy}% (Session ${this.config.accuracySessionCount}/${AppConfig.accuracies.length})`, 'AutoTyper');
    }
  }

  /**
   * Run auto-typing process character by character
   */
  async _runAutoTyper() {
    let currentIndex = 0;
    let isTyping = true;

    // Initialize WPM tracking
    this.metrics.typingStartTime = Date.now();
    this.metrics.lastKeystrokeTime = Date.now();

    /**
     * Type next character with appropriate timing
     */
    const typeNextCharacter = async () => {
      // Stop if typing is cancelled or complete
      if (!isTyping || !this.config.isAutoTypingMode || this.config.isNormalTypingMode || this._isSessionComplete(currentIndex)) {
        if (this._isSessionComplete(currentIndex)) {
          this.metrics.markSessionComplete();
        }
        return;
      }

      // Calculate delay for natural typing rhythm
      const delay = this.timingController.calculateNextDelay(
        this.appNode.props.lessonContent,
        this.appNode.typedIndex
      );

      // Add random micro-pauses for realism
      await this.timingController.addMicroPause();

      setTimeout(() => {
        if (!this.config.isAutoTypingMode || this.config.isNormalTypingMode) return;

        // Handle special case for longest word (press Enter)
        if (currentIndex === this.longestWord.index) {
          this.keyboardHandler.simulateEnterKey(this.appNode);
          currentIndex++;
          typeNextCharacter();
          return;
        }

        // Determine whether to make an error
        const makeError = this.accuracyController.shouldMakeError();
        this.keyboardHandler.processKeystroke(this.appNode, !makeError);

        // Only advance index on correct keystrokes
        if (!makeError) {
          currentIndex++;
        }

        // Continue typing next character
        typeNextCharacter();
      }, delay);
    };

    // Start the typing process
    typeNextCharacter();
  }

  /**
   * Check if typing session is complete
   */
  _isSessionComplete(currentIndex) {
    return currentIndex >= this.appNode.props.lessonContent.length;
  }
}

// ===== SERVICE CONTAINER =====

/**
 * Manages service instances and dependencies
 */
class ServiceContainer {
  constructor() {
    this.services = {};
  }

  /**
   * Register a service in the container
   */
  register(name, service) {
    this.services[name] = service;
    return this;
  }

  /**
   * Retrieve a service from the container
   */
  get(name) {
    return this.services[name];
  }

  /**
   * Create and register all services with dependencies
   */
  createServices() {
    // Create core services
    const eventBus = new EventBus();
    this.register('eventBus', eventBus);

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

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

    // Create dependent services
    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);

    // Create main application controller
    const autoTyper = new AutoTyper(this.services);
    this.register('autoTyper', autoTyper);

    return this;
  }
}

// ===== MAIN APP =====

/**
 * Main application entry point
 */
class TypingApp {
  constructor() {
    // Create and initialize services
    this.container = new ServiceContainer().createServices();
    this.autoTyper = this.container.get('autoTyper');
  }

  /**
   * Start the typing application
   */
  async start() {
    await this.autoTyper.init();
  }
}

// ===== INITIALIZE THE APP =====

/**
 * Self-executing function to start the application
 */
(function() {
  const app = new TypingApp();
  app.start();
})();