Revive Helper

Hospital revive helper with percentage checking

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Revive Helper
// @namespace    revive-helper.zero.nao
// @version      1.3
// @description  Hospital revive helper with percentage checking
// @author       nao [2669774]
// @match        https://www.torn.com/hospitalview.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  // Constants
  const DEFAULT_THRESHOLD = localStorage.reviveThreshold || 90;
  const CACHE_KEY = 'reviveHelperCache';
  const CACHE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour

  // State management
  const state = {
    queue: [],
    revivableCount: 0,
    currentIndex: 0,
    isProcessing: false,
    threshold: DEFAULT_THRESHOLD,
    lastCheckedUser: null,
    lastChance: null,
    lastConfirmUrl: null,
    lastEnergy: null,
    awaitingConfirmation: false,
    cacheData: null,
    cacheModified: false,
  };

  // DOM Elements cache
  const elements = {};

  // Utility functions
  const getRFC = () => {
    const match = document.cookie.match(/rfc_v=([^;]+)/);
    return match ? match[1] : null;
  };

  // Debounce utility to prevent rapid clicks
  const debounce = (func, wait) => {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  };

  // Cache helpers - batch operations for performance
  const loadCache = () => {
    try {
      const cache = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
      const now = Date.now();
      // Filter out expired entries
      const validCache = {};
      for (const [userId, entry] of Object.entries(cache)) {
        if (now - entry.timestamp <= CACHE_EXPIRY_MS) {
          validCache[userId] = entry;
        }
      }
      state.cacheData = validCache;
      state.cacheModified = false;
      return validCache;
    } catch (e) {
      console.error('[Revive Helper] Cache load error:', e);
      state.cacheData = {};
      return {};
    }
  };

  const getCachedPercentage = (userId) => {
    if (!state.cacheData) loadCache();
    const entry = state.cacheData[userId];
    if (!entry) return null;
    return entry.percentage;
  };

  const setCachedPercentage = (userId, percentage) => {
    if (!state.cacheData) loadCache();
    state.cacheData[userId] = { percentage, timestamp: Date.now() };
    state.cacheModified = true;
  };

  const saveCache = () => {
    if (!state.cacheModified || !state.cacheData) return;
    try {
      localStorage.setItem(CACHE_KEY, JSON.stringify(state.cacheData));
      state.cacheModified = false;
    } catch (e) {
      console.error('[Revive Helper] Cache save error:', e);
    }
  };

  const updateUI = {
    reviveButton: (text, color = null) => {
      if (elements.reviveButton) {
        elements.reviveButton.textContent = text;
        if (color) {
          elements.reviveButton.style.background = color;
        }
      }
    },
    count: (text) => {
      if (elements.count) elements.count.textContent = text;
    },
    status: (text, color = "white") => {
      if (elements.status) {
        elements.status.textContent = text;
        elements.status.style.color = color;
      }
    },
    currentUser: (name, percentage, energy) => {
      if (elements.currentUser) {
        if (name) {
          let html = `Name: <span>${name}</span>`;
          if (percentage !== undefined && percentage !== null) {
            html += ` | Percentage: <span>${percentage}%</span>`;
          }
          if (energy !== undefined && energy !== null) {
            html += ` | Energy: <span>${energy}</span>`;
          }
          elements.currentUser.innerHTML = html;
        } else {
          elements.currentUser.innerHTML = "";
        }
      }
    },
  };

  // CSS injection
  const injectStyles = () => {
    if (document.getElementById("revive-helper-styles")) return;

    const style = document.createElement("style");
    style.id = "revive-helper-styles";
    style.textContent = `
            .revive-helper-container {
                display: flex;
                align-items: center;
                gap: 12px;
                margin-left: 20px;
                flex-wrap: wrap;
            }

            .revive-btn, .force-revive-btn {
                color: white;
                border: none;
                padding: 12px 16px;
                border-radius: 6px;
                font-size: 14px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.2s ease;
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                width: 110px;
                min-width: 110px;
                max-width: 110px;
                height: 40px;
                min-height: 40px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                flex-shrink: 0;
                text-align: center;
                display: inline-flex;
                align-items: center;
                justify-content: center;
            }

            .revive-btn {
                background-color: transparent !important;
                border: 2px solid #667eea !important;
            }

            .revive-btn:hover {
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(0,0,0,0.15);
            }

            .revive-btn:active {
                transform: translateY(0);
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }

            .revive-btn.ready {
                background-color: transparent !important;
                border: 2px solid #48bb78 !important;
            }

            .force-revive-btn {
                background-color: transparent !important;
                border: 2px solid #f56565 !important;
            }

            .force-revive-btn:hover {
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(0,0,0,0.15);
            }

            .force-revive-btn:active {
                transform: translateY(0);
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }

            #revive-threshold {
                background: rgba(0, 0, 0, 0.3);
                border: 1px solid rgba(255,255,255,0.2);
                border-radius: 6px;
                padding: 8px 12px;
                color: white;
                font-size: 14px;
                width: 80px;
                transition: all 0.2s ease;
            }

            #revive-threshold:focus {
                outline: none;
                border-color: #667eea;
                box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
                background: rgba(255,255,255,0.15);
            }

            #revive-threshold::placeholder {
                color: rgba(255,255,255,0.6);
            }

            .revive-count {
                color: #a0aec0;
                font-size: 13px;
                font-weight: 500;
                width: 100px;
                min-width: 100px;
                max-width: 100px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                flex-shrink: 0;
                background: rgba(0, 0, 0, 0.3);
                padding: 6px 10px;
                border-radius: 4px;
            }

            .revive-status {
                color: #a0aec0;
                font-size: 13px;
                font-weight: 500;
                min-width: 200px;
                max-width: 400px;
                white-space: nowrap;
                flex-shrink: 0;
                background: rgba(0, 0, 0, 0.3);
                padding: 6px 10px;
                border-radius: 4px;
            }

            .revive-current-user {
                color: #a0aec0;
                font-size: 13px;
                font-weight: 500;
                min-width: 250px;
                max-width: 500px;
                white-space: nowrap;
                flex-shrink: 0;
                background: rgba(0, 0, 0, 0.3);
                padding: 6px 10px;
                border-radius: 4px;
            }

            .revive-current-user span {
                color: #667eea;
                font-weight: 600;
            }
        `;
    document.head.appendChild(style);
  };

  // UI insertion
  const insertUI = () => {
    const contentTitle = document.querySelector(".content-title");
    if (!contentTitle) {
      setTimeout(insertUI, 1000);
      return;
    }

    const container = document.createElement("div");
    container.className = "revive-helper-container";
    container.innerHTML = `
            <button id="revive-btn" class="revive-btn">Check %</button>
            <button id="force-revive-btn" class="force-revive-btn">YOLO Revive</button>
            <input type="number" id="revive-threshold" placeholder="%" value="${DEFAULT_THRESHOLD}" min="0" max="100">
            <span class="revive-current-user" id="revive-current-user"></span>
            <span class="revive-count" id="revive-count">Revivable: 0</span>
            <span class="revive-status" id="revive-status">Ready</span>
        `;

    contentTitle.appendChild(container);

    // Cache elements
    elements.reviveButton = document.getElementById("revive-btn");
    elements.forceReviveButton = document.getElementById("force-revive-btn");
    elements.threshold = document.getElementById("revive-threshold");
    elements.currentUser = document.getElementById("revive-current-user");
    elements.count = document.getElementById("revive-count");
    elements.status = document.getElementById("revive-status");

    // Event listeners with debouncing
    elements.reviveButton.addEventListener("click", debounce(handleRevive, 300));
    elements.forceReviveButton.addEventListener("click", debounce(handleForceRevive, 300));
    elements.threshold.addEventListener("input", (e) => {
      let value = parseInt(e.target.value);
      // Validate input: must be between 0 and 100
      if (isNaN(value) || value < 0) {
        value = 0;
        e.target.value = 0;
      } else if (value > 100) {
        value = 100;
        e.target.value = 100;
      }
      state.threshold = value;
      localStorage.reviveThreshold = value;
    });
  };

  // Extract user IDs from hospital data
  const extractRevivableUsers = (data) => {
    console.log(data);
    const users = [];
    console.log("[Revive Helper] Processing data:", data);

    if (!data || !data.players) {
      console.warn("[Revive Helper] No players found in data");
      return users;
    }

    // Parse the players list
    const players = data.players;
    players.forEach((player) => {
      // Check if player is revivable
      if (player && player.user_id && player.isReviveAvailable === true) {
        let name = player.name;
        if (!name && player.print_name) {
          // Extract name from print_name HTML: <a ...>Name</a>
          const match = player.print_name.match(/>([^<]+)</);
          if (match) name = match[1];
        }

        users.push({
          id: player.user_id,
          name: name || "Unknown",
          level: player.level || 0,
        });
      }
    });

    console.log(`[Revive Helper] Extracted ${users.length} users`);
    return users;
  };

  // Get revive chance percentage and confirm URL
  const getReviveChance = async (userId) => {
    const rfc = getRFC();
    if (!rfc) throw new Error("RFC token not found");

    try {
      const response = await $.ajax({
        url: `/revive.php?action=revive&ID=${userId}&text_response=1&rfcv=${rfc}`,
        method: "GET",
        timeout: 10000, // 10 second timeout
      });
      let content = response;
      try {
        const json = JSON.parse(response);
        if (json.msg) content = json.msg;
      } catch (e) {
        // Not JSON, use raw text
      }

      // Extract percentage from response like "has a <b>67.02%</b> chance"
      const percentMatch = content.match(/(\d+\.?\d*)%/);
      // Extract confirm URL from action-yes link
      const urlMatch = content.match(
        /href=(revive\.php\?action=revive&step=revive&ID=\d+)/,
      );
      // Extract energy cost
      const energyMatch = content.match(/use <b>(\d+) energy<\/b>/);

      console.log("[Revive Helper] getReviveChance response:", {
        contentLength: content.length,
        percentMatch: percentMatch ? percentMatch[1] : null,
        urlMatch: urlMatch ? urlMatch[1] : null,
        energyMatch: energyMatch ? energyMatch[1] : null,
      });

      if (percentMatch) {
        return {
          percentage: parseFloat(percentMatch[1]),
          confirmUrl: urlMatch ? urlMatch[1] : null,
          energy: energyMatch ? parseInt(energyMatch[1]) : null,
        };
      } else {
        return { percentage: 0, confirmUrl: null, energy: null };
      }
    } catch (error) {
      console.error("[Revive Helper] getReviveChance request failed:", error);
      throw new Error(`Failed to check revive chance: ${error.statusText || "Network error"}`);
    }
  };

  // Perform revive using confirm URL from check response
  const performRevive = async (confirmUrl) => {
    const rfc = getRFC();
    console.log("[Revive Helper] performRevive called with URL:", confirmUrl);
    if (!confirmUrl.includes("rfcv=")) {
      confirmUrl += `&rfcv=${rfc}`;
    }
    console.log("[Revive Helper] Final URL:", confirmUrl);

    try {
      const response = await $.ajax({
        url: `/${confirmUrl}`,
        method: "GET",
        timeout: 10000, // 10 second timeout
      });
      console.log("[Revive Helper] Revive response:", response.substring(0, 200));

      // Parse the response
      try {
        const result = JSON.parse(response);
        return result;
      } catch (e) {
        // Not JSON, return raw response
        return { raw: response };
      }
    } catch (error) {
      console.error("[Revive Helper] Revive request failed:", error);
      throw new Error(`Request failed: ${error.statusText || "Network error"}`);
    }
  };

  // Handle Revive button click
  const handleRevive = async () => {
    console.log("[Revive Helper] handleRevive clicked", {
      isProcessing: state.isProcessing,
      awaitingConfirmation: state.awaitingConfirmation,
      lastCheckedUser: state.lastCheckedUser?.name,
    });

    if (state.isProcessing) {
      console.log("[Revive Helper] BLOCKED: isProcessing is true");
      return;
    }

    // If we're awaiting confirmation, perform the revive
    if (state.awaitingConfirmation && state.lastCheckedUser) {
      console.log("[Revive Helper] PERFORM REVIVE branch");
      console.log(
        "[Revive Helper] Performing revive for",
        state.lastCheckedUser.name,
      );
      state.isProcessing = true;
      updateUI.status("Reviving...", "yellow");

      if (!state.lastConfirmUrl) {
        console.log("[Revive Helper] ERROR: lastConfirmUrl is null!");
        updateUI.status("Error: No confirm URL", "red");
        state.isProcessing = false;
        return;
      }

      try {
        console.log(
          "[Revive Helper] Calling performRevive with URL:",
          state.lastConfirmUrl,
        );
        const result = await performRevive(state.lastConfirmUrl);
        console.log("[Revive Helper] Revive result:", result);

        // Check if revive was successful based on color
        if (result.color === "green") {
          updateUI.status(
            `Successful - Revived ${state.lastCheckedUser.name}! (-${state.lastEnergy}e)`,
            "green",
          );
        } else if (result.color === "red") {
          const reason = result.msg || "Unknown error";
          updateUI.status(`Failed - ${reason}`, "red");
        }

        // Remove from queue using splice to avoid index shift
        state.queue.splice(state.currentIndex, 1);

        // Update current user info or clear if no more
        const remaining = state.queue.length - state.currentIndex;
        if (remaining > 0 && state.queue[state.currentIndex]) {
          const nextUser = state.queue[state.currentIndex];
          updateUI.currentUser(nextUser.name, null, null);
        } else {
          updateUI.currentUser(null, null, null);
        }
        updateUI.count(`Revivable: ${remaining}`);
      } catch (error) {
        console.log("[Revive Helper] Revive error:", error);
        updateUI.status(`Error: ${error.message}`, "red");
      }

      state.awaitingConfirmation = false;
      updateUI.reviveButton("Check %");
      elements.reviveButton.classList.remove("ready");
      state.lastCheckedUser = null;
      state.lastChance = null;
      state.lastConfirmUrl = null;
      state.lastEnergy = null;
      state.isProcessing = false;
      saveCache();
      return;
    }

    // CHECK NEXT USER branch
    console.log("[Revive Helper] CHECK NEXT USER branch");
    // Otherwise, check the next user
    if (state.queue.length === 0 || state.currentIndex >= state.queue.length) {
      // Trigger AJAX update by changing hash
      updateUI.status("Scanning for new users...", "yellow");
      const currentHash = location.hash;
      if (currentHash.includes("start=")) {
        location.hash = currentHash.includes(".0")
          ? currentHash.replace(".0", "")
          : currentHash + ".0";
      } else {
        location.hash = "#start=0";
      }
      return;
    }

    state.isProcessing = true;
    let user = state.queue[state.currentIndex];

    // Skip users with cached percentages below threshold
    while (user) {
      const cachedPercentage = getCachedPercentage(user.id);
      if (cachedPercentage !== null && cachedPercentage < state.threshold) {
        console.log(`[Revive Helper] Skipping ${user.name} - cached ${cachedPercentage}% below threshold`);
        updateUI.status(`Skipping ${user.name} (cached ${cachedPercentage}%)`, "orange");
        state.currentIndex++;
        user = state.queue[state.currentIndex];
      } else {
        break;
      }
    }

    // Check if we've run out of users after skipping
    if (!user || state.currentIndex >= state.queue.length) {
      updateUI.status("No more users (all cached below threshold)", "white");
      updateUI.currentUser(null, null, null);
      state.isProcessing = false;
      saveCache();
      return;
    }

    updateUI.status(`Checking ${user.name}...`, "yellow");

    try {
      const result = await getReviveChance(user.id);
      // Cache the percentage for this user
      setCachedPercentage(user.id, result.percentage);

      state.lastCheckedUser = user;
      state.lastChance = result.percentage;
      state.lastConfirmUrl = result.confirmUrl;
      state.lastEnergy = result.energy;

      if (result.percentage >= state.threshold) {
        // Good chance, await confirmation
        state.awaitingConfirmation = true;
        updateUI.status("Click Revive to confirm", "green");
        updateUI.currentUser(user.name, result.percentage, result.energy);
        elements.reviveButton.classList.add("ready");
        updateUI.reviveButton("Revive");
        // Allow clicks again for confirmation
        state.isProcessing = false;
      } else {
        // Too low, show next user without checking
        updateUI.status(`${result.percentage}% (too low)`, "orange");
        // Move to next user - clear lastCheckedUser so next click starts fresh
        state.currentIndex++;
        state.lastCheckedUser = null;
        state.isProcessing = false;
        updateUI.reviveButton("Check %");
        elements.reviveButton.classList.remove("ready");
        // Update current user to next user
        const remaining = state.queue.length - state.currentIndex;
        if (remaining > 0 && state.queue[state.currentIndex]) {
          const nextUser = state.queue[state.currentIndex];
          updateUI.currentUser(nextUser.name, null, null);
          updateUI.status(`Ready: ${nextUser.name}`, "white");
        } else {
          updateUI.currentUser(null, null, null);
          updateUI.status("No more users", "white");
        }
        updateUI.count(`Revivable: ${remaining}`);
      }
      saveCache();
    } catch (error) {
      updateUI.status(`Error checking ${user.name}: ${error.message}`, "red");
      state.currentIndex++;
      state.isProcessing = false;
      updateUI.reviveButton("Check %");
      elements.reviveButton.classList.remove("ready");
      saveCache();
    }
  };

  // Handle Force Revive button click
  const handleForceRevive = async () => {
    if (state.isProcessing) return;

    if (state.queue.length === 0 || state.currentIndex >= state.queue.length) {
      updateUI.status("No users in queue. Click Revive to scan.", "orange");
      return;
    }

    const user = state.queue[state.currentIndex];
    state.isProcessing = true;

    updateUI.status(`Force reviving ${user.name}...`, "yellow");

    try {
      const rfc = getRFC();
      const reviveUrl = `revive.php?action=revive&step=revive&ID=${user.id}&rfcv=${rfc}`;
      const result = await performRevive(reviveUrl);

      // Check if revive was successful based on color
      if (result.color === "green") {
        updateUI.status(`Successful - YOLOed ${user.name}!`, "green");
      } else if (result.color === "red") {
        const reason = result.msg || "Unknown error";
        updateUI.status(`Failed - ${reason}`, "red");
      }

      // Remove from queue using splice to avoid index shift
      state.queue.splice(state.currentIndex, 1);

      // Update current user info or clear if no more
      const remaining = state.queue.length - state.currentIndex;
      if (remaining > 0 && state.queue[state.currentIndex]) {
        const nextUser = state.queue[state.currentIndex];
        updateUI.currentUser(nextUser.name, null, null);
      } else {
        updateUI.currentUser(null, null, null);
      }
      updateUI.count(`Revivable: ${remaining}`);
    } catch (error) {
      updateUI.status(`Error: ${error.message}`, "red");
      state.currentIndex++;
    } finally {
      state.isProcessing = false;
      updateUI.reviveButton("Check %");
      elements.reviveButton.classList.remove("ready");
    }
  };

  // AJAX interception for hospital data
  const interceptAjax = () => {
    const originalAjax = window.jQuery?.ajax;
    if (!originalAjax) {
      setTimeout(interceptAjax, 100);
      return;
    }

    window.jQuery.ajax = function (options) {
      if (options.url?.includes("hospitalview.php")) {
        const originalSuccess = options.success;
        options.success = function (data, textStatus, jqXHR) {
          try {
            const responseData = JSON.parse(data);
            console.log(responseData);

            if (responseData.success && responseData.data) {
              let users = extractRevivableUsers(responseData.data);
              // Load cache once for this batch
              loadCache();
              // Filter out users with cached percentages below threshold
              const originalCount = users.length;
              users = users.filter(user => {
                const cachedPercentage = getCachedPercentage(user.id);
                return cachedPercentage === null || cachedPercentage >= state.threshold;
              });
              const filteredCount = originalCount - users.length;
              if (users.length > 0) {
                state.queue = users;
                state.currentIndex = 0;
                state.revivableCount = users.length;
                updateUI.count(`Revivable: ${users.length}`);
                if (filteredCount > 0) {
                  updateUI.status(`Loaded ${users.length} users (${filteredCount} cached below threshold)`, "white");
                } else {
                  updateUI.status(`Loaded ${users.length} users`, "white");
                }
                // Update current user section with first user
                updateUI.currentUser(users[0].name, null, null);
              } else if (originalCount > 0) {
                // All users were filtered out
                state.queue = [];
                state.currentIndex = 0;
                state.revivableCount = 0;
                updateUI.count(`Revivable: 0`);
                updateUI.status(`All ${originalCount} users cached below threshold`, "orange");
                updateUI.currentUser(null, null, null);
              }
            }
          } catch (e) {
            console.warn("[Revive Helper] Failed to process hospital data:", e);
          }

          if (originalSuccess) {
            originalSuccess(data, textStatus, jqXHR);
          }
        };
      }
      return originalAjax.call(this, options);
    };
  };

  // Initialize
  const init = () => {
    injectStyles();
    insertUI();
    interceptAjax();
  };

  // Start when DOM is ready
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();