Revive Helper

Hospital revive helper with percentage checking

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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();
  }
})();