Torn Target List Enhanced

Enhances Torn's Target list with hospital timers, travel status details, and sorting options

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Target List Enhanced
// @namespace    xentac
// @version      1.1.0
// @description  Enhances Torn's Target list with hospital timers, travel status details, and sorting options
// @author       xentac (optimized version)
// @license      MIT
// @match        https://www.torn.com/page.php?sid=list&type=targets*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==
(function() {
  'use strict';

  // Configuration
  const CONFIG = {
    storageKey: 'xentac-torn_playerlist_enhanced-apikey',
    defaultKey: '###PDA-APIKEY###',
    refreshInterval: 15000, // Refresh player list every 15 seconds
    statusCheckInterval: 20000, // Check player statuses every 20 seconds
    hospitalWarningThreshold: 60, // 1 minute
    enableSorting: true,
    maxConcurrentRequests: 5, // Limit concurrent API requests
    requestDelay: 500, // Delay between API requests in ms
    cacheTime: 30000, // Cache player data for 30 seconds
    batchSize: 10, // Number of players to check in each batch
    playerCheckCycle: 120000, // Full cycle time to check all players (2 minutes)
  };

  // Country abbreviations lookup
  const COUNTRY_ABBREVIATIONS = {
    'South Africa': 'SA',
    'Cayman Islands': 'CI',
    'United Kingdom': 'UK',
    'Argentina': 'Arg',
    'Switzerland': 'Switz',
    'Mexico': 'Mex',
    'Canada': 'Can',
    'Hawaii': 'HI',
    'Japan': 'JP',
    'China': 'CN',
    'UAE': 'UAE',
    'United Arab Emirates': 'UAE'
  };

  // Status sort priorities (lower = higher priority)
  const STATUS_PRIORITIES = {
    'Hospital': 1,
    'Jail': 2,
    'Returning': 3,
    'Abroad': 4,
    'Traveling': 5,
    'Offline': 6,
    'Online': 7,
    'Default': 10
  };

  // Styles
  GM_addStyle(`
    .playerlist_highlight {
      background-color: rgba(255, 165, 0, 0.3) !important;
    }
    .playerlist_traveling .status___o6u8R span {
      color: #F287FF !important;
    }
    .playerlist_hospital_timer {
      font-weight: bold;
    }
    .playerlist_sort_controls {
      display: flex;
      justify-content: flex-end;
      margin: 5px 0;
      gap: 5px;
      padding: 5px;
    }
    .playerlist_status_changed {
      animation: flash-status 1s;
    }
    @keyframes flash-status {
      0%, 100% { background-color: transparent; }
      50% { background-color: rgba(255, 255, 0, 0.3); }
    }
  `);

  // State
  let apiKey = localStorage.getItem(CONFIG.storageKey) ?? CONFIG.defaultKey;
  const hospitalTimers = new Map();
  const playerCache = new Map();
  const requestQueue = [];
  let processingQueue = false;
  let refreshInterval = null;
  let statusCheckInterval = null;
  let playerStatusMap = new Map(); // Track player statuses for change detection
  let pendingUpdates = new Set(); // Track player IDs with pending updates

  // Track player check status
  const playerCheckTracker = {
    lastFullCycleTime: 0,
    playerLastChecked: new Map(),
    currentIndex: 0,

    // Mark a player as checked
    markChecked(playerId) {
      this.playerLastChecked.set(playerId, Date.now());
    },

    // Get time since last check
    getTimeSinceLastCheck(playerId) {
      const lastChecked = this.playerLastChecked.get(playerId) || 0;
      return Date.now() - lastChecked;
    },

    // Reset the cycle if needed
    resetCycleIfNeeded() {
      const now = Date.now();
      if (now - this.lastFullCycleTime > CONFIG.playerCheckCycle) {
        this.lastFullCycleTime = now;
        this.currentIndex = 0;
      }
    }
  };

  // Register menu command for API key
  try {
    GM_registerMenuCommand("Set API Key", () => promptForApiKey());
  } catch (error) {
    console.log("Menu command registration failed, likely running in Torn PDA");
  }

  // API key management
  function promptForApiKey() {
    const userInput = prompt(
      "Please enter a PUBLIC API Key for basic player information:",
      apiKey === CONFIG.defaultKey ? "" : apiKey
    );

    if (userInput && userInput.length === 16) {
      apiKey = userInput;
      localStorage.setItem(CONFIG.storageKey, userInput);
      return true;
    } else if (userInput !== null) {
      alert("Invalid API key. Please enter a 16-character key.");
    }
    return false;
  }

  // Check if API key is valid
  function validateApiKey() {
    if (apiKey === CONFIG.defaultKey || apiKey.length !== 16) {
      return promptForApiKey();
    }
    return true;
  }

  // Format time remaining
  function formatTimeRemaining(seconds) {
    if (seconds <= 0) return "Okay";

    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    const s = Math.floor(seconds % 60);

    return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  }

  // Process travel status text
  function processTravelStatus(description) {
    if (!description) return "Unknown";

    // Replace country names with abbreviations
    let result = description;
    for (const [full, abbr] of Object.entries(COUNTRY_ABBREVIATIONS)) {
      result = result.replace(full, abbr);
    }

    // Format based on travel direction
    if (result.includes("Traveling to ")) {
      return "► " + result.split("Traveling to ")[1];
    } else if (result.includes("In ")) {
      return result.split("In ")[1];
    } else if (result.includes("Returning")) {
      return "◄ " + result.split("Returning to Torn from ")[1];
    } else if (result.includes("Traveling")) {
      return "Traveling";
    }

    return result;
  }

  // Process API request queue
  async function processRequestQueue() {
    if (processingQueue || requestQueue.length === 0) return;

    processingQueue = true;

    try {
      // Process up to maxConcurrentRequests at a time
      const batch = requestQueue.splice(0, CONFIG.maxConcurrentRequests);

      // Execute requests in parallel with Promise.all
      await Promise.all(batch.map(async (request, index) => {
        // Add delay between requests
        if (index > 0) {
          await new Promise(resolve => setTimeout(resolve, CONFIG.requestDelay));
        }

        try {
          const data = await fetchPlayerData(request.playerId);
          request.resolve(data);
        } catch (error) {
          request.reject(error);
        }
      }));
    } finally {
      processingQueue = false;

      // Continue processing if there are more requests
      if (requestQueue.length > 0) {
        setTimeout(processRequestQueue, CONFIG.requestDelay);
      }
    }
  }

  // Queue player data request
  function queuePlayerDataRequest(playerId, forceFresh = false) {
    return new Promise((resolve, reject) => {
      // Check cache first (unless forceFresh is true)
      if (!forceFresh) {
        const cachedData = playerCache.get(playerId);
        if (cachedData && (Date.now() - cachedData.timestamp < CONFIG.cacheTime)) {
          resolve(cachedData.data);
          return;
        }
      }

      // Add to queue
      requestQueue.push({ playerId, resolve, reject });

      // Start processing queue if not already processing
      if (!processingQueue) {
        processRequestQueue();
      }
    });
  }

  // Fetch player data from API
  async function fetchPlayerData(playerId) {
    if (!validateApiKey()) return null;

    try {
      const response = await fetch(`https://api.torn.com/user/${playerId}?selections=profile,basic&key=${apiKey}`);
      const data = await response.json();

      if (data.error) {
        console.error(`API Error: ${data.error.error}`);
        return null;
      }

      // Cache the result
      playerCache.set(playerId, {
        data: data,
        timestamp: Date.now()
      });

      return data;
    } catch (error) {
      console.error("Error fetching player data:", error);
      return null;
    }
  }

  // Check if player status has changed
  function hasStatusChanged(playerId, newStatus) {
    if (!playerStatusMap.has(playerId)) return true;

    const oldStatus = playerStatusMap.get(playerId);

    // Check if state has changed
    if (oldStatus.state !== newStatus.state) return true;

    // If in hospital, check if time has changed significantly
    if (newStatus.state === 'Hospital' && oldStatus.state === 'Hospital') {
      // If hospital time changed by more than 5 seconds, consider it changed
      return Math.abs(oldStatus.until - newStatus.until) > 5;
    }

    return false;
  }

  // Update player status in our tracking map
  function updatePlayerStatus(playerId, status) {
    playerStatusMap.set(playerId, {
      state: status.state,
      until: status.until,
      description: status.description
    });
  }

  // Update player row with status information
  async function updatePlayerRow(playerRow, forceFresh = false) {
    try {
      // Extract player ID from the row
      const playerLink = playerRow.querySelector('a[href^="/profiles.php"]');
      if (!playerLink) return;

      const playerId = playerLink.href.split('XID=')[1];
      if (!playerId) return;

      // Skip if already being updated
      if (pendingUpdates.has(playerId)) return;
      pendingUpdates.add(playerId);

      try {
        // Skip if already processed recently and not forcing fresh data
        if (!forceFresh) {
          const lastUpdate = parseInt(playerRow.getAttribute('data-last-update') || '0');
          const timeSinceUpdate = Date.now() - lastUpdate;

          // If updated in the last 15 seconds and not in hospital, skip
          if (timeSinceUpdate < 15000 &&
              playerRow.getAttribute('data-status') !== 'Hospital') {
            return;
          }

          // If updated in the last 5 seconds even if in hospital, skip
          if (timeSinceUpdate < 5000) {
            return;
          }
        }

        // Get player data
        const playerData = await queuePlayerDataRequest(playerId, forceFresh);
        if (!playerData) return;

        // Check if status has changed
        const statusChanged = hasStatusChanged(playerId, playerData.status);

        // Update our status tracking
        updatePlayerStatus(playerId, playerData.status);

        // Mark as processed
        playerRow.classList.add('playerlist_processed');
        playerRow.setAttribute('data-last-update', Date.now().toString());

        // Update status cell
        const statusCell = playerRow.querySelector('.status___o6u8R');
        if (!statusCell) return;

        const statusSpan = statusCell.querySelector('span');
        if (!statusSpan) return;

        // Set data attributes for sorting
        playerRow.setAttribute('data-player-id', playerId);
        playerRow.setAttribute('data-last-action', playerData.last_action?.timestamp || '0');

        // Clear any existing timer
        if (hospitalTimers.has(playerId)) {
          clearInterval(hospitalTimers.get(playerId));
          hospitalTimers.delete(playerId);
        }

        // Process based on status
        const status = playerData.status || { state: 'Unknown' };
        playerRow.setAttribute('data-status', status.state);
        playerRow.setAttribute('data-until', status.until || '0');

        // If status changed, add flash effect
        if (statusChanged && playerRow.classList.contains('playerlist_processed')) {
          playerRow.classList.remove('playerlist_status_changed');
          // Force reflow
          void playerRow.offsetWidth;
          playerRow.classList.add('playerlist_status_changed');
        }

        switch (status.state) {
          case 'Hospital':
            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Hospital);
            playerRow.classList.remove('playerlist_traveling');

            // Update the status span to show "Hospital" if it doesn't already
            if (!statusSpan.classList.contains('user-red-status')) {
              statusSpan.className = '';
              statusSpan.classList.add('user-red-status');
            }

            // Create hospital timer
            const timerFunction = () => {
              const timeRemaining = Math.round(status.until - Date.now() / 1000) - 2;

              if (timeRemaining <= 0) {
                // Don't automatically change to Okay - we'll let the status check handle this
                statusSpan.textContent = "Checking...";
                playerRow.classList.remove('playerlist_highlight');
                clearInterval(hospitalTimers.get(playerId));
                hospitalTimers.delete(playerId);

                // Force a fresh check after hospital timer expires
                setTimeout(() => {
                  const row = document.querySelector(`li[data-player-id="${playerId}"]`);
                  if (row) updatePlayerRow(row, true);
                }, 2000);

                return;
              }

              statusSpan.textContent = formatTimeRemaining(timeRemaining);
              statusSpan.classList.add('playerlist_hospital_timer');

              // Highlight if close to release
              if (timeRemaining < CONFIG.hospitalWarningThreshold) {
                playerRow.classList.add('playerlist_highlight');
              } else {
                playerRow.classList.remove('playerlist_highlight');
              }
            };

            // Run immediately and then set interval
            timerFunction();
            hospitalTimers.set(playerId, setInterval(timerFunction, 1000));
            break;

          case 'Jail':
            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Jail);
            playerRow.classList.remove('playerlist_traveling', 'playerlist_highlight');

            // Update the status span
            if (!statusSpan.classList.contains('user-red-status')) {
              statusSpan.className = '';
              statusSpan.classList.add('user-red-status');
              statusSpan.textContent = "Jail";
            }
            break;

          case 'Traveling':
            statusSpan.textContent = processTravelStatus(status.description);
            playerRow.classList.add('playerlist_traveling');
            playerRow.classList.remove('playerlist_highlight');
// Update the status span
            if (!statusSpan.classList.contains('user-blue-status')) {
              statusSpan.className = '';
              statusSpan.classList.add('user-blue-status');
            }
            if (status.description && status.description.includes('Returning')) {
              playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Returning);
            } else {
              playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Traveling);
            }
            break;

          case 'Abroad':
            statusSpan.textContent = processTravelStatus(status.description);
            playerRow.classList.add('playerlist_traveling');
            playerRow.classList.remove('playerlist_highlight');

            // Update the status span
            if (!statusSpan.classList.contains('user-blue-status')) {
              statusSpan.className = '';
              statusSpan.classList.add('user-blue-status');
            }

            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Abroad);
            break;

          case 'Offline':
            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Offline);
            playerRow.classList.remove('playerlist_traveling', 'playerlist_highlight');

            // Update the status span
            statusSpan.className = '';
            statusSpan.textContent = "Offline";
            break;

          case 'Online':
            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Online);
            playerRow.classList.remove('playerlist_traveling', 'playerlist_highlight');

            // Update the status span
            statusSpan.className = '';
            statusSpan.textContent = "Online";
            break;

          default:
            playerRow.setAttribute('data-sort-priority', STATUS_PRIORITIES.Default);
            playerRow.classList.remove('playerlist_traveling', 'playerlist_highlight');

            // Update the status span
            statusSpan.className = '';
            statusSpan.textContent = status.state || "Unknown";
            break;
        }

        // If status changed and we're in a list, resort the list
        if (statusChanged) {
          const playerList = playerRow.closest('ul');
          if (playerList) {
            sortPlayerList(playerList, 'status');
          }
        }
      } finally {
        // Always remove from pending updates
        pendingUpdates.delete(playerId);
      }
    } catch (error) {
      console.error("Error updating player row:", error);
      pendingUpdates.delete(playerId);
    }
  }

  // Sort player list (optimized with memoization)
  function sortPlayerList(playerList, sortBy = 'status') {
    if (!CONFIG.enableSorting || !playerList) return;

    try {
      const rows = Array.from(playerList.querySelectorAll('li.tableRow___UgA6S'));
      if (rows.length === 0) return;

      // Create a map for memoizing sort values to avoid repeated DOM access
      const sortValueMap = new Map();

      const getSortValue = (row, type) => {
        const key = `${row.getAttribute('data-player-id')}-${type}`;
        if (sortValueMap.has(key)) return sortValueMap.get(key);

        let value;
        switch (type) {
          case 'priority':
            value = parseInt(row.getAttribute('data-sort-priority') || '10');
            break;
          case 'until':
            value = parseInt(row.getAttribute('data-until') || '0');
            break;
          case 'lastAction':
            value = parseInt(row.getAttribute('data-last-action') || '0');
            break;
          case 'level':
            value = parseInt(row.querySelector('.level___z78dn')?.textContent || '0');
            break;
        }

        sortValueMap.set(key, value);
        return value;
      };

      // Sort with optimized comparator
      rows.sort((a, b) => {
        switch (sortBy) {
          case 'status':
            // Sort by status priority first
            const priorityDiff = getSortValue(a, 'priority') - getSortValue(b, 'priority');
            if (priorityDiff !== 0) return priorityDiff;

            // Then by time remaining (for hospital/jail)
            return getSortValue(a, 'until') - getSortValue(b, 'until');

          case 'lastAction':
            return getSortValue(b, 'lastAction') - getSortValue(a, 'lastAction');

          case 'level':
            return getSortValue(b, 'level') - getSortValue(a, 'level');

          default:
            return 0;
        }
      });

      // Use DocumentFragment for efficient DOM manipulation
      const fragment = document.createDocumentFragment();
      rows.forEach(row => fragment.appendChild(row));
      playerList.appendChild(fragment);
    } catch (error) {
      console.error("Error sorting player list:", error);
    }
  }

  // Find new player rows in a list
  function findNewPlayerRows(playerList) {
    if (!playerList) return [];

    try {
      // Use a more efficient selector to get only unprocessed rows
      return Array.from(playerList.querySelectorAll('li.tableRow___UgA6S:not(.playerlist_processed)'));
    } catch (error) {
      console.error("Error finding new player rows:", error);
      return [];
    }
  }

  // Process player list with throttling
  async function processPlayerList(tableWrapper) {
    if (!tableWrapper) return;

    try {
      // Get player list
      const playerList = tableWrapper.querySelector('ul');
      if (!playerList) return;

      // Find new player rows
      const newPlayerRows = findNewPlayerRows(playerList);
      if (newPlayerRows.length === 0) return;

      // Process rows in batches to avoid freezing the page
      const batchSize = 10;
      for (let i = 0; i < newPlayerRows.length; i += batchSize) {
        const batch = newPlayerRows.slice(i, i + batchSize);

        // Process batch in parallel
        await Promise.all(batch.map(row => updatePlayerRow(row)));

        // Small delay between batches
        if (i + batchSize < newPlayerRows.length) {
          await new Promise(resolve => setTimeout(resolve, 100));
        }
      }

      // Sort the list
      sortPlayerList(playerList, 'status');

    } catch (error) {
      console.error("Error processing player list:", error);
    }
  }

  // Check for status changes in visible players
  async function checkPlayerStatusChanges() {
    try {
      // Reset cycle if needed
      playerCheckTracker.resetCycleIfNeeded();

      // Get all visible player rows
      const allPlayerRows = document.querySelectorAll('li.tableRow___UgA6S.playerlist_processed');
      if (allPlayerRows.length === 0) return;

      const allRows = Array.from(allPlayerRows);

      // Determine which players to check in this batch
      let rowsToCheck = [];

      // 1. First priority: Hospital patients
      const hospitalRows = allRows.filter(row =>
        row.getAttribute('data-status') === 'Hospital'
      );

      // Add hospital patients to check list
      rowsToCheck = [...hospitalRows];

      // 2. Second priority: Players in the current cycle segment
      if (rowsToCheck.length < CONFIG.batchSize) {
        const remainingSlots = CONFIG.batchSize - rowsToCheck.length;

        // Calculate how many players to check per interval to cover all in one cycle
        const playersPerInterval = Math.max(1, Math.ceil(allRows.length /
          (CONFIG.playerCheckCycle / CONFIG.statusCheckInterval)));

        // Get non-hospital rows that haven't been checked yet
        const otherRows = allRows.filter(row =>
          !hospitalRows.includes(row)
        );

        if (otherRows.length > 0) {
          // Get the next batch of players in sequence
          for (let i = 0; i < remainingSlots && i < playersPerInterval; i++) {
            const index = (playerCheckTracker.currentIndex + i) % otherRows.length;
            if (!rowsToCheck.includes(otherRows[index])) {
              rowsToCheck.push(otherRows[index]);
            }
          }

          // Update the current index for next time
          playerCheckTracker.currentIndex =
            (playerCheckTracker.currentIndex + playersPerInterval) % otherRows.length;
        }
      }

      // Check each player in the batch with parallel processing
      await Promise.all(rowsToCheck.map(async (row, index) => {
        // Add small staggered delay to prevent API rate limiting
        if (index > 0) {
          await new Promise(resolve => setTimeout(resolve, 200 * index));
        }

        const playerId = row.getAttribute('data-player-id');
        if (!playerId) return;

        await updatePlayerRow(row, true); // Force fresh data
        playerCheckTracker.markChecked(playerId);
      }));

    } catch (error) {
      console.error("Error checking player status changes:", error);
    }
  }

  // Clean up resources
  function cleanUp() {
    // Clear all hospital timers
    hospitalTimers.forEach((timer) => {
      clearInterval(timer);
    });
    hospitalTimers.clear();

    // Clear refresh interval
    if (refreshInterval) {
      clearInterval(refreshInterval);
      refreshInterval = null;
    }

    // Clear status check interval
    if (statusCheckInterval) {
      clearInterval(statusCheckInterval);
      statusCheckInterval = null;
    }

    // Clear observer
    if (observer) {
      observer.disconnect();
    }
  }

  // Set up mutation observer with debouncing
  let debounceTimer = null;
  const observer = new MutationObserver(mutations => {
    // Debounce to prevent excessive processing
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      // Check for player list changes
      let tableWrapperFound = false;
      let playerRowsAdded = false;

      for (const mutation of mutations) {
        // Check for added nodes
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          // Check for player rows added to existing lists
          const playerList = mutation.target.closest('ul');
          if (playerList) {
            for (const node of mutation.addedNodes) {
              if (node.nodeType === Node.ELEMENT_NODE &&
                  node.classList?.contains('tableRow___UgA6S')) {
                playerRowsAdded = true;
                break;
              }
            }
          }

          // Check for new table wrappers
          for (const node of mutation.addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE) {
              const tableWrapper = node.classList?.contains('tableWrapper___Imc7p')
                ? node
                : node.querySelector('.tableWrapper___Imc7p');

              if (tableWrapper) {
                tableWrapperFound = true;
                processPlayerList(tableWrapper);
              }
            }
          }
        }
      }

      // Process player lists if rows were added
      if (playerRowsAdded) {
        const tableWrappers = document.querySelectorAll('.tableWrapper___Imc7p');
        tableWrappers.forEach(wrapper => {
          processPlayerList(wrapper);
        });
      }
    }, 200); // 200ms debounce
  });

  // Start observing with error handling
  try {
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  } catch (error) {
    console.error("Error starting observer:", error);
  }

  // Set up periodic refresh with error handling
  refreshInterval = setInterval(() => {
    try {
      const tableWrappers = document.querySelectorAll('.tableWrapper___Imc7p');
      if (tableWrappers.length === 0) return;

      // Process all wrappers to check for new players
      tableWrappers.forEach(wrapper => {
        processPlayerList(wrapper);
      });
    } catch (error) {
      console.error("Error in refresh interval:", error);
    }
  }, CONFIG.refreshInterval);

  // Set up status check interval
  statusCheckInterval = setInterval(() => {
    try {
      // Only run if there are player lists visible
      const tableWrappers = document.querySelectorAll('.tableWrapper___Imc7p');
      if (tableWrappers.length > 0) {
        checkPlayerStatusChanges();
      }
    } catch (error) {
      console.error("Error in status check interval:", error);
    }
  }, CONFIG.statusCheckInterval);

  // Initial run with delay
  setTimeout(() => {
    try {
      const tableWrappers = document.querySelectorAll('.tableWrapper___Imc7p');
      tableWrappers.forEach(wrapper => {
        processPlayerList(wrapper);
      });
    } catch (error) {
      console.error("Error in initial run:", error);
    }
  }, 1000); // 1 second delay to ensure the page has loaded

  // Clean up on page unload
  window.addEventListener('beforeunload', cleanUp);
})();