Attack screen improvements

Improvements to the Attacking screen.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name            Attack screen improvements
// @namespace       http://tampermonkey.net/
// @version         1.3.1
// @description     Improvements to the Attacking screen.
// @author          Cypher-[2641265]
// @license         MIT
// @match           https://www.torn.com/page.php?sid=attack&user2ID=*
// @icon            https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant           none
// ==/UserScript==

//-----Changelog-----
// V1.3.1 - 2026-04-25
// - Changed API entry window to be toast style and click through, will be less intrusive if any issues.
// v1.3 - 2026-04-22
// - Added defender health percent anchoring

(function () {
  "use strict";

  // API Key management
  function getAPIKey() {
    return localStorage.getItem("torn_minimal_key");
  }

  function setAPIKey(key) {
    localStorage.setItem("torn_minimal_key", key);
  }

  // Health percent label placement: 'below' (under bar) or 'above' (under defender name)
  const HEALTH_PERCENT_ANCHOR = "above";
  const HEALTH_PERCENT_OFFSET_X = 15;
  const HEALTH_PERCENT_OFFSET_Y = 12;
  let healthPercentObserverInitialized = false;
  let healthPercentObserverRetryTimer = null;

  function syncHealthPercentOverlay() {
    const rosePanel = document.querySelector('div[class*="rose"]');
    if (!rosePanel) return;

    const wrap = rosePanel.querySelector("div.wrap___Gl_Ua.pbWrap___K0uUO");
    if (!wrap) return;

    const progressBar = wrap.querySelector('div[aria-label^="Progress:"]');
    if (!progressBar) return;

    const aria = progressBar.getAttribute("aria-label");
    const match = aria && aria.match(/Progress: ([\d.]+)%/);
    if (!match) return;

    const percent = match[1];
    let percentDiv = rosePanel.querySelector(".torn-health-percent");
    if (!percentDiv) {
      percentDiv = document.createElement("div");
      percentDiv.className = "torn-health-percent";
      percentDiv.style.position = "absolute";
      percentDiv.style.transform = "none";
      percentDiv.style.fontWeight = "bold";
      percentDiv.style.fontSize = "11px";
      percentDiv.style.color = "#fff";
      percentDiv.style.textShadow = "1px 1px 2px #222, 0 0 6px #000";
      percentDiv.style.pointerEvents = "none";
      percentDiv.style.zIndex = "100001";
      percentDiv.style.userSelect = "none";
      percentDiv.style.cursor = "default";
      rosePanel.appendChild(percentDiv);
    }

    if (getComputedStyle(rosePanel).position === "static") {
      rosePanel.style.position = "relative";
    }
    rosePanel.style.overflow = "visible";

    percentDiv.textContent = `${percent}%`;
    percentDiv.title = `Defender health: ${percent}%`;
    percentDiv.style.left = `${wrap.offsetLeft + 4 + HEALTH_PERCENT_OFFSET_X}px`;
    percentDiv.style.top = `${wrap.offsetTop + (HEALTH_PERCENT_ANCHOR === "above" ? -14 : 14) + HEALTH_PERCENT_OFFSET_Y}px`;
  }

  function ensureHealthPercentObserver() {
    if (healthPercentObserverInitialized) return;

    const rosePanel = document.querySelector('div[class*="rose"]');
    const wrap =
      rosePanel && rosePanel.querySelector("div.wrap___Gl_Ua.pbWrap___K0uUO");
    const progressBar =
      wrap && wrap.querySelector('div[aria-label^="Progress:"]');

    if (!progressBar) {
      if (healthPercentObserverRetryTimer === null) {
        healthPercentObserverRetryTimer = window.setTimeout(() => {
          healthPercentObserverRetryTimer = null;
          ensureHealthPercentObserver();
        }, 300);
      }
      return;
    }

    const observer = new MutationObserver(() => {
      syncHealthPercentOverlay();
    });
    observer.observe(progressBar, {
      attributes: true,
      attributeFilter: ["aria-label", "style"],
    });
    healthPercentObserverInitialized = true;
    syncHealthPercentOverlay();
  }

  const SVGs = {
    Online: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="-1.5 -1.2 14 14"><circle cx="6" cy="6" r="6" fill="#43d854" stroke="#fff" stroke-width="0"/></svg>`,
    Idle: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="-1.5 -1.2 14 14"><circle cx="6" cy="6" r="6" fill="#f7c325" stroke="#fff" stroke-width="0"/><rect x="5" y="3" width="4" height="4" fill="#f2f2f2"/></svg>`,
    Offline: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="-1.5 -1.2 14 14"><circle cx="6" cy="6" r="6" fill="#b3b3b3" stroke="#fff" stroke-width="0"/><rect x="3" y="5" width="6" height="2" fill="#f2f2f2"/></svg>`,
  };

  const urlParams = new URLSearchParams(window.location.search);
  const userID = urlParams.get("user2ID");
  if (!userID) return;

  // Check if API key is available
  let API_KEY = getAPIKey();
  if (!API_KEY) {
    showAPIKeySetup();
    return;
  }

  // API Key setup interface
  function showAPIKeySetup() {
    // Prevent multiple toasts from appearing
    if (document.getElementById('torn-api-toast')) return;
    
    // Create non-blocking toast container
    const toastContainer = document.createElement("div");
    toastContainer.id = 'torn-api-toast';
    toastContainer.style.position = "fixed";
    toastContainer.style.top = "0";
    toastContainer.style.left = "0";
    toastContainer.style.width = "100%";
    toastContainer.style.height = "100%";
    toastContainer.style.pointerEvents = "none"; // Click-through
    toastContainer.style.zIndex = "10000";
    
    // Create toast popup in upper right corner
    const toast = document.createElement("div");
    toast.style.position = "absolute";
    toast.style.top = "100px";
    toast.style.right = "20px";
    toast.style.width = "220px";
    toast.style.backgroundColor = "#2a2a2a";
    toast.style.border = "2px solid #00b7ff";
    toast.style.borderRadius = "8px";
    toast.style.padding = "15px";
    toast.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
    toast.style.fontFamily = "Arial, sans-serif";
    toast.style.fontSize = "14px";
    toast.style.color = "#fff";
    toast.style.pointerEvents = "auto"; // Toast itself is interactive
    toast.style.animation = "slideInFromRight 0.3s ease-out";

    // Add CSS animation
    if (!document.getElementById('toast-animations')) {
      const style = document.createElement('style');
      style.id = 'toast-animations';
      style.textContent = `
        @keyframes slideInFromRight {
          0% { transform: translateX(100%); opacity: 0; }
          100% { transform: translateX(0); opacity: 1; }
        }
        @keyframes slideOutToRight {
          0% { transform: translateX(0); opacity: 1; }
          100% { transform: translateX(100%); opacity: 0; }
        }
      `;
      document.head.appendChild(style);
    }

    const title = document.createElement("div");
    title.textContent = "API Key Required";
    title.style.fontWeight = "bold";
    title.style.marginBottom = "10px";
    title.style.fontSize = "16px";
    title.style.color = "#007bff";

    const message = document.createElement("div");
    message.textContent = "Requires a Minimal API key.";
    message.style.marginBottom = "15px";
    message.style.lineHeight = "1.4";

    const input = document.createElement("input");
    input.type = "text";
    input.placeholder = "Enter your Torn API key";
    input.style.width = "100%";
    input.style.padding = "8px";
    input.style.marginBottom = "15px";
    input.style.backgroundColor = "#1a1a1a";
    input.style.color = "white";
    input.style.border = "1px solid #444";
    input.style.borderRadius = "4px";
    input.style.boxSizing = "border-box";

    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.gap = "8px";
    buttonContainer.style.justifyContent = "space-between";

    const getApiButton = document.createElement("button");
    getApiButton.textContent = "Get API";
    getApiButton.style.padding = "6px 12px";
    getApiButton.style.backgroundColor = "#007bff";
    getApiButton.style.color = "white";
    getApiButton.style.border = "none";
    getApiButton.style.borderRadius = "4px";
    getApiButton.style.cursor = "pointer";
    getApiButton.style.fontSize = "12px";

    const okButton = document.createElement("button");
    okButton.textContent = "Save";
    okButton.style.padding = "6px 12px";
    okButton.style.backgroundColor = "#0ea01fff";
    okButton.style.color = "white";
    okButton.style.border = "none";
    okButton.style.borderRadius = "4px";
    okButton.style.cursor = "pointer";
    okButton.style.fontSize = "12px";

    const dismissButton = document.createElement("button");
    dismissButton.textContent = "Later";
    dismissButton.style.padding = "6px 12px";
    dismissButton.style.backgroundColor = "#6c757d";
    dismissButton.style.color = "white";
    dismissButton.style.border = "none";
    dismissButton.style.borderRadius = "4px";
    dismissButton.style.cursor = "pointer";
    dismissButton.style.fontSize = "12px";

    function removeToast() {
      toast.style.animation = "slideOutToRight 0.3s ease-in";
      setTimeout(() => {
        if (toastContainer.parentNode) {
          toastContainer.remove();
        }
      }, 300);
    }

    getApiButton.addEventListener("click", () => {
      window.open("https://www.torn.com/preferences.php#tab=api", "_blank");
    });

    okButton.addEventListener("click", () => {
      const apiKey = input.value.trim();
      if (apiKey) {
        setAPIKey(apiKey);
        API_KEY = apiKey;
        removeToast();
        initializeScript();
      }
    });

    dismissButton.addEventListener("click", () => {
      removeToast();
    });

    // Enter key submits
    input.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        okButton.click();
      }
    });

    // Auto-dismiss after 30 seconds to prevent permanent interference
    setTimeout(() => {
      if (document.getElementById('torn-api-toast')) {
        removeToast();
      }
    }, 30000);

    buttonContainer.appendChild(getApiButton);
    buttonContainer.appendChild(okButton);
    buttonContainer.appendChild(dismissButton);
    
    toast.appendChild(title);
    toast.appendChild(message);
    toast.appendChild(input);
    toast.appendChild(buttonContainer);
    toastContainer.appendChild(toast);
    document.body.appendChild(toastContainer);

    // Focus the input after a short delay to ensure it's visible
    setTimeout(() => {
      input.focus();
    }, 350);
  } // Initialize script features
  function initializeScript() {
    // Initial fetches
    fetchDefenderStatus();
    fetchAttackerEnergy();
    syncHealthPercentOverlay();
    ensureHealthPercentObserver();
  }

  // Unified refresh function for all elements
  function refreshAllData() {
    fetchDefenderStatus();
    fetchAttackerEnergy();
    syncHealthPercentOverlay();
  }

  // Fetch defender status
  function fetchDefenderStatus() {
    fetch(
      `https://api.torn.com/user/${userID}?selections=profile&key=${API_KEY}&comment=attackpageimprovements`,
    )
      .then((res) => res.json())
      .then((data) => {
        if (!data) return;

        // Check for API errors or missing expected data
        if (data.error || (!data.last_action && !data.status)) {
          console.log(
            "API issue detected in defender status - triggering API setup",
          );
          showAPIKeySetup();
          return;
        }

        // Handle online/offline status icon
        if (data.last_action && data.last_action.status) {
          const state = data.last_action.status;
          const svg = SVGs[state] || SVGs.Offline;

          function insertIcon() {
            const usernameElement = document.querySelector(
              'div[class*="rose"] .user-name',
            );
            if (usernameElement) {
              // Remove existing icon if present
              const existingIcon =
                usernameElement.parentNode.querySelector(".torn-status-icon");
              if (existingIcon) {
                existingIcon.remove();
              }

              const iconSpan = document.createElement("span");
              iconSpan.className = "torn-status-icon";
              iconSpan.innerHTML = svg;
              iconSpan.style.verticalAlign = "middle";
              iconSpan.style.marginRight = "4px";
              iconSpan.style.cursor = "pointer";
              iconSpan.title = state + " - Click to refresh";

              // Add click handler to refresh defender status
              iconSpan.addEventListener("click", () => {
                refreshAllData();
              });

              usernameElement.parentNode.insertBefore(
                iconSpan,
                usernameElement,
              );
            } else {
              setTimeout(insertIcon, 200);
            }
          }
          insertIcon();
        }

        // Handle health status display
        if (data.status && data.status.state) {
          const statusState = data.status.state;
          const statusColor = data.status.color || "gray";
          const statusUntil = data.status.until;

          function insertHealthStatus() {
            const usernameElement = document.querySelector(
              'div[class*="rose"] .user-name',
            );
            if (usernameElement) {
              // Remove existing health status if present
              const existingHealthStatus =
                usernameElement.parentNode.querySelector(".torn-health-status");
              if (existingHealthStatus) {
                existingHealthStatus.remove();
              }

              const healthContainer = document.createElement("span");
              healthContainer.className = "torn-health-status";
              healthContainer.style.marginLeft = "8px";
              healthContainer.style.fontSize = "0.85em";
              healthContainer.style.fontWeight = "bold";
              healthContainer.style.cursor = "pointer";

              // Color mapping for different states
              const colorMap = {
                red: "#dc3545",
                orange: "#fd7e14",
                yellow: "#ffc107",
                green: "#28a745",
                blue: "#007bff",
                gray: "#6c757d",
              };

              healthContainer.style.color = colorMap[statusColor] || "#6c757d";

              // Add click handler for refresh
              healthContainer.addEventListener("click", () => {
                // Check if the display shows "Click to refresh"
                if (healthContainer.textContent.includes("Click to refresh")) {
                  // Timer expired, refresh whole page
                  location.reload();
                } else {
                  // Timer still active, refresh all data
                  refreshAllData();
                }
              });

              syncHealthPercentOverlay();
              ensureHealthPercentObserver();

              function updateCountdown() {
                let displayText = statusState;
                // Add countdown timer if available
                if (statusUntil) {
                  const currentTime = Math.floor(Date.now() / 1000);
                  const timeRemaining = statusUntil - currentTime;
                  if (timeRemaining > 0) {
                    const hours = Math.floor(timeRemaining / 3600);
                    const minutes = Math.floor((timeRemaining % 3600) / 60);
                    const seconds = timeRemaining % 60;
                    if (hours > 0) {
                      displayText += ` (${hours}h ${minutes}m)`;
                    } else if (minutes > 0) {
                      displayText += ` (${minutes}m ${seconds}s)`;
                    } else {
                      displayText += ` (${seconds}s)`;
                    }
                  } else {
                    // Timer expired, show click to refresh message
                    displayText = statusState + " - Click to refresh";
                    healthContainer.style.textDecoration = "underline";
                  }
                }
                healthContainer.textContent = displayText;
              }
              // Initial update
              updateCountdown();
              // Update countdown every second if there's a timer
              if (statusUntil) {
                setInterval(updateCountdown, 1000);
              }
              usernameElement.parentNode.insertBefore(
                healthContainer,
                usernameElement.nextSibling,
              );
            } else {
              setTimeout(insertHealthStatus, 200);
            }
          }
          insertHealthStatus();
        }
      })
      .catch((error) => {
        console.log(
          "Network error in defender status fetch - triggering API setup",
        );
        showAPIKeySetup();
      });
  }

  // Fetch attacker energy
  function fetchAttackerEnergy() {
    fetch(
      `https://api.torn.com/user/?selections=bars&key=${API_KEY}&comment=attackerEnergy&comment=attackpageimprovements`,
    )
      .then((res) => res.json())
      .then((data) => {
        // Check for API errors or missing expected data (energy property)
        if (!data || data.error || !data.energy) {
          console.log(
            "API issue detected in energy fetch - triggering API setup",
          );
          showAPIKeySetup();
          return;
        }

        const currentEnergy = data.energy.current;
        const maxEnergy = data.energy.maximum;

        function insertEnergyDisplay() {
          const attackerUsernameElement = document.querySelector(
            'div[class*="green"] .user-name',
          );
          if (attackerUsernameElement) {
            // Remove existing energy display if present
            const existingEnergyDisplay =
              attackerUsernameElement.parentNode.querySelector(
                ".torn-energy-display",
              );
            if (existingEnergyDisplay) {
              existingEnergyDisplay.remove();
            }

            const energyContainer = document.createElement("div");
            energyContainer.className = "torn-energy-display";
            energyContainer.style.display = "inline-block";
            energyContainer.style.marginLeft = "8px";
            energyContainer.style.verticalAlign = "middle";
            energyContainer.style.cursor = "pointer";
            energyContainer.title = `Energy: ${currentEnergy}/${maxEnergy} - Click to refresh, Long press to change API key`;

            // Long press functionality for API key change
            let longPressTimer;
            let isLongPress = false;

            energyContainer.addEventListener("mousedown", () => {
              isLongPress = false;
              longPressTimer = setTimeout(() => {
                isLongPress = true;
                showAPIKeySetup();
              }, 1000); // 1 second long press
            });

            energyContainer.addEventListener("mouseup", () => {
              clearTimeout(longPressTimer);
            });

            energyContainer.addEventListener("mouseleave", () => {
              clearTimeout(longPressTimer);
            });

            // Touch events for mobile
            energyContainer.addEventListener("touchstart", (e) => {
              e.preventDefault();
              isLongPress = false;
              longPressTimer = setTimeout(() => {
                isLongPress = true;
                showAPIKeySetup();
              }, 1000);
            });

            energyContainer.addEventListener("touchend", (e) => {
              e.preventDefault();
              clearTimeout(longPressTimer);
              // If it wasn't a long press, treat as regular click
              if (!isLongPress) {
                refreshAllData();
              }
            });

            energyContainer.addEventListener("touchcancel", () => {
              clearTimeout(longPressTimer);
            });

            // Regular click handler (for mouse)
            energyContainer.addEventListener("click", (e) => {
              // Only refresh if it wasn't a long press
              if (!isLongPress) {
                refreshAllData();
              }
            }); // Create progress bar container
            const progressContainer = document.createElement("div");
            progressContainer.style.position = "relative";
            progressContainer.style.width = "80px";
            progressContainer.style.height = "12px";
            progressContainer.style.backgroundColor = "#2a2a2a";
            progressContainer.style.borderRadius = "8px";
            progressContainer.style.overflow = "hidden";
            progressContainer.style.border = "1px solid #444";

            // Create progress bar fill
            const progressBar = document.createElement("div");
            const percentage = (currentEnergy / maxEnergy) * 100;
            progressBar.style.width = `${percentage}%`;
            progressBar.style.height = "100%";
            progressBar.style.backgroundColor = "#0ea01fff";
            progressBar.style.borderRadius = "8px";
            progressBar.style.transition = "width 0.3s ease";

            // Create text display (overlaid on bar)
            const textDisplay = document.createElement("span");
            textDisplay.textContent = `${currentEnergy}/${maxEnergy}`;
            textDisplay.style.position = "absolute";
            textDisplay.style.top = "50%";
            textDisplay.style.left = "50%";
            textDisplay.style.transform = "translate(-50%, -50%)";
            textDisplay.style.fontSize = "10px";
            textDisplay.style.color = "#fff";
            textDisplay.style.fontWeight = "bold";
            textDisplay.style.textShadow = "1px 1px 2px rgba(0,0,0,0.8)";
            textDisplay.style.zIndex = "10";

            progressContainer.appendChild(progressBar);
            progressContainer.appendChild(textDisplay);
            energyContainer.appendChild(progressContainer);

            attackerUsernameElement.parentNode.insertBefore(
              energyContainer,
              attackerUsernameElement.nextSibling,
            );
          } else {
            setTimeout(insertEnergyDisplay, 200);
          }
        }
        insertEnergyDisplay();
      })
      .catch((error) => {
        console.log("Network error in energy fetch - triggering API setup");
        showAPIKeySetup();
      });
  }

  // Initialize script if API key is available
  if (API_KEY) {
    initializeScript();
  }
})();