Torn Player Tracker

Track player status, hospital time, and attack links in Torn with persistent data, enhanced UI. Adds players by user ID.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Player Tracker
// @namespace    https://www.torn.com/
// @version      2.0
// @description  Track player status, hospital time, and attack links in Torn with persistent data, enhanced UI. Adds players by user ID.
// @author       Xenocide [2216313]
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.torn.com
// ==/UserScript==

(function () {
  "use strict";

  // === Configuration ===
  const UPDATE_INTERVAL = 30000; // Update interval in milliseconds (30 seconds)

  // Retrieve API key from localStorage or prompt user to enter it
  let API_KEY = localStorage.getItem("tornAPIKey");

  // If API key is not found, prompt the user to input it
  if (!API_KEY) {
    const apiKeyInputSection = document.createElement("div");
    apiKeyInputSection.innerHTML = `
      <div id="apiKeySection" style="position: fixed; top: 10px; right: 10px; background-color: #333; color: white; padding: 10px; border-radius: 8px; z-index: 10001; font-family: Arial, sans-serif;">
        <h3>Enter Your Torn API Key</h3>
        <input type="text" id="apiKeyInput" placeholder="API Key" style="width: 100%; padding: 5px; margin-bottom: 5px;" />
        <button id="saveAPIKeyButton" style="width: 100%; padding: 5px; background-color: green; color: white;">Save API Key</button>
        <small style="color: yellow;">You need an API key to fetch player data.</small>
      </div>
    `;
    document.body.appendChild(apiKeyInputSection);

    // Save API key when the user submits it
    document.getElementById("saveAPIKeyButton").addEventListener("click", () => {
      API_KEY = document.getElementById("apiKeyInput").value.trim();
      if (API_KEY) {
        localStorage.setItem("tornAPIKey", API_KEY); // Save to localStorage
        document.getElementById("apiKeySection").style.display = "none"; // Hide the input form
        alert("API Key saved successfully!"); // Provide feedback to the user
      } else {
        alert("Please enter a valid API Key.");
      }
    });
  }

  // If API key is already stored, proceed with the script as usual
  if (API_KEY) {
    // Retrieve tracked players from localStorage or initialize default IDs
    let trackedPlayers = JSON.parse(localStorage.getItem("trackedPlayers")) || [123456, 654321, 789012];

    // === Create the Floating Box ===
    const box = document.createElement("div");
    box.id = "statusTracker";
    box.innerHTML = `
      <h3>Player Tracker</h3>
      <ul id="statusList">Loading...</ul>
      <div id="userControls">
        <hr>
        <input type="text" id="addPlayerInput" placeholder="Add Player ID" />
        <button id="addPlayerButton">Add</button>
        <ul id="trackedPlayersList"></ul>
      </div>
      <button id="toggleUI">Hide Add Section</button>
    `;
    document.body.appendChild(box);

    // Add some styles for the box
    GM_addStyle(`
      #statusTracker {
        position: fixed;
        top: 10px;
        right: 10px;
        width: 350px;
        background-color: #333;
        color: white;
        border: 2px solid #555;
        border-radius: 8px;
        padding: 10px;
        z-index: 10000;
        font-family: Arial, sans-serif;
      }
      #statusTracker h3 {
        margin: 0 0 10px 0;
        font-size: 16px;
        text-align: center;
      }
      #statusTracker ul {
        list-style-type: none;
        padding: 0;
        margin: 0;
      }
      #statusTracker li {
        margin: 5px 0;
      }
      #statusTracker li span {
        font-weight: bold;
      }
      #statusTracker input {
        width: calc(100% - 60px);
        margin-right: 5px;
        padding: 5px;
      }
      #statusTracker button {
        padding: 5px;
        cursor: pointer;
      }
      #statusTracker hr {
        margin: 10px 0;
        border: 0.5px solid #555;
      }
    `);

    // === Function to Format Time in Minutes and Seconds ===
    function formatTime(seconds) {
      const minutes = Math.floor(seconds / 60);
      const remainingSeconds = seconds % 60;
      return `${minutes}m ${remainingSeconds}s`;
    }

    // === Function to Fetch Player Info by ID ===
    function fetchPlayerStatus(playerId) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: `https://api.torn.com/user/${playerId}?selections=basic,profile&key=${API_KEY}`,
          onload: (response) => {
            if (response.status === 200) {
              const data = JSON.parse(response.responseText);

              // Check for API errors
              if (data.error) {
                console.error(`Error fetching player ${playerId}:`, data.error);
                return resolve({
                  playerId,
                  name: `Error: ${data.error.error}`,
                  status: "Unknown",
                  hospitalTimeLeft: 0,
                  attackLink: "",
                  lastActivity: "N/A",
                });
              }

              // Extract data from API response
              const name = data.name || `ID ${playerId}`;
              const status = data.status?.state || "Unknown";
              const hospitalTimeLeft = data.status?.until ? Math.max(0, Math.ceil((data.status.until * 1000 - Date.now()) / 1000)) : 0;
              const attackLink = `https://www.torn.com/loader.php?sid=attack&user2ID=${playerId}`;
              const lastActivity = data.last_action?.relative || "N/A";

              resolve({ playerId, name, status, hospitalTimeLeft, attackLink, lastActivity });
            } else {
              console.error(`Error fetching status for player ${playerId}:`, response.status);
              reject(`Error fetching status for player ${playerId}`);
            }
          },
          onerror: (error) => {
            console.error(`Error fetching status for player ${playerId}:`, error);
            reject(`Error fetching status for player ${playerId}`);
          },
        });
      });
    }

    // === Function to Update the Status List ===
    async function updateStatusList() {
      const statusList = document.getElementById("statusList");
      statusList.innerHTML = "Updating...";

      try {
        const statusPromises = trackedPlayers.map((id) => fetchPlayerStatus(id));
        const statuses = await Promise.all(statusPromises);

        statusList.innerHTML = "";
        statuses.forEach(({ playerId, name, status, hospitalTimeLeft, attackLink, lastActivity }) => {
          const color =
            status === "Okay"
              ? "green"
              : status === "Hospital"
              ? "red"
              : status === "Traveling"
              ? "blue"
              : "gray";

          const hospitalText = status === "Hospital" ? ` (Time left: ${formatTime(hospitalTimeLeft)})` : "";
          const listItem = document.createElement("li");
          listItem.innerHTML = `
            <span style="color: ${color};">${status}</span> - 
            <a href="${attackLink}" target="_blank" style="color: yellow;">${name}</a> 
            (ID: ${playerId})${hospitalText} <br>
            <small>Last Activity: ${lastActivity}</small>`;
          statusList.appendChild(listItem);
        });
      } catch (error) {
        console.error("Error updating statuses:", error);
        statusList.innerHTML = "Error updating statuses.";
      }
    }

    // === Function to Update Tracked Players List ===
    function updateTrackedPlayersList() {
      const trackedPlayersList = document.getElementById("trackedPlayersList");
      trackedPlayersList.innerHTML = "";
      trackedPlayers.forEach((playerId) => {
        const listItem = document.createElement("li");
        listItem.innerHTML = `Player ID: ${playerId} <button data-id="${playerId}" class="removePlayerButton">Remove</button>`;
        trackedPlayersList.appendChild(listItem);
      });

      // Add event listeners to remove buttons
      document.querySelectorAll(".removePlayerButton").forEach((button) => {
        button.addEventListener("click", (event) => {
          const playerId = parseInt(event.target.getAttribute("data-id"));
          trackedPlayers = trackedPlayers.filter((id) => id !== playerId);
          localStorage.setItem("trackedPlayers", JSON.stringify(trackedPlayers)); // Save to localStorage
          updateTrackedPlayersList();
          updateStatusList();
        });
      });
    }

    // === Add Player by ID from Input ===
    document.getElementById("addPlayerButton").addEventListener("click", () => {
      const input = document.getElementById("addPlayerInput");
      const playerId = parseInt(input.value);

      if (!isNaN(playerId) && !trackedPlayers.includes(playerId)) {
        trackedPlayers.push(playerId);
        localStorage.setItem("trackedPlayers", JSON.stringify(trackedPlayers)); // Save to localStorage
        input.value = "";
        updateTrackedPlayersList();
        updateStatusList();
      } else if (isNaN(playerId)) {
        alert("Please enter a valid player ID.");
      } else {
        alert("This player is already tracked.");
      }
    });

    // === Hide/Show Add Section ===
    const toggleUI = document.getElementById("toggleUI");
    const userControls = document.getElementById("userControls");

    // Retrieve the saved state of the visibility (default is "shown")
    const isHidden = localStorage.getItem("isUserControlsHidden") === "true";

    // If hidden, hide the controls
    if (isHidden) {
      userControls.style.display = "none";
      toggleUI.innerText = "Show Add Section";
    } else {
      toggleUI.innerText = "Hide Add Section";
    }

    toggleUI.addEventListener("click", () => {
      const isCurrentlyHidden = userControls.style.display === "none";
      if (isCurrentlyHidden) {
        userControls.style.display = "block";
        localStorage.setItem("isUserControlsHidden", "false"); // Store the state
        toggleUI.innerText = "Hide Add Section";
      } else {
        userControls.style.display = "none";
        localStorage.setItem("isUserControlsHidden", "true"); // Store the state
        toggleUI.innerText = "Show Add Section";
      }
    });

    // === Periodic Updates ===
    setInterval(updateStatusList, UPDATE_INTERVAL);
    updateStatusList(); // Initial update
    updateTrackedPlayersList(); // Initial list update
  }
})();