Greasy Fork is available in English.
Hospital revive helper with percentage checking
// ==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();
}
})();