您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Monitors a user's profile and provides desktop and Discord notifications when they are out of the hospital. Features a watchlist and per-user notification settings.
// ==UserScript== // @name Deadman Watcher // @namespace http://tampermonkey.net/ // @version 1.1 // @description Monitors a user's profile and provides desktop and Discord notifications when they are out of the hospital. Features a watchlist and per-user notification settings. // @author HeyItzWerty [3626448] // @match https://www.torn.com/profiles.php?XID=* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @connect discord.com // @license MIT // @supportURL https://www.torn.com/messages.php#/p=compose&XID=3626448 // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // --- CONFIGURATION & GLOBALS --- const SCRIPT_PREFIX = 'dmw_'; const TORN_GREEN = '#85ea2d'; const TORN_GREY = '#d4d4d4'; const ICONS = { gravestone: `<svg xmlns="http://www.w3.org/2000/svg" class="default___XXAGt profileButtonIcon" width="46" height="46" viewBox="0 0 46 46"><path d="M23,5 C17.48,5 13,9.48 13,15 L13,25 C13,26.1 13.9,27 15,27 L15,37 C15,38.1 15.9,39 17,39 L29,39 C30.1,39 31,38.1 31,37 L31,27 C32.1,27 33,26.1 33,25 L33,15 C33,9.48 28.52,5 23,5 Z M21,14 L25,14 L25,22 L27,22 L27,24 L19,24 L19,22 L21,22 L21,14 Z" /></svg>`, settings: `<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 24 24" width="20" fill="currentColor"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61-.25-1.17-.59-1.69-.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>` }; let originalTitle = document.title; let originalActionText = "What would you like to do?"; let flashInterval = null; let statusObserver = null; let titleUpdateInterval = null; let hospitalStartTime = 0; let hospitalInitialSeconds = 0; // --- SETTINGS MANAGEMENT --- const DEFAULT_MESSAGES = [ "**%USERNAME%** has risen from the dead! Attack now: https://www.torn.com/loader2.php?sid=getInAttack&user2ID=%USERID%", "Looks like **%USERNAME%** is back on their feet. Go put them back down: https://www.torn.com/loader2.php?sid=getInAttack&user2ID=%USERID%", "The doctors did their job on **%USERNAME%**. Now you do yours: https://www.torn.com/loader2.php?sid=getInAttack&user2ID=%USERID%" ]; const DEFAULT_SETTINGS = { autoAttack: false, webhookUrl: '', webhookMessage: '' }; async function loadSettings() { const saved = await GM_getValue(SCRIPT_PREFIX + 'settings', {}); return { ...DEFAULT_SETTINGS, ...saved }; } async function saveSettings() { const newSettings = { autoAttack: document.getElementById(`${SCRIPT_PREFIX}auto-attack`).checked, webhookUrl: document.getElementById(`${SCRIPT_PREFIX}webhook-url`).value.trim(), webhookMessage: document.getElementById(`${SCRIPT_PREFIX}webhook-message`).value.trim() }; await GM_setValue(SCRIPT_PREFIX + 'settings', newSettings); alert('Settings saved!'); } // --- WATCHLIST MANAGEMENT --- async function getWatchedUsers() { return await GM_getValue(SCRIPT_PREFIX + 'watched_users', {}); } async function saveWatchedUsers(users) { await GM_setValue(SCRIPT_PREFIX + 'watched_users', users); } async function toggleUserInWatchlist(userId, userName) { const users = await getWatchedUsers(); if (users[userId]) { delete users[userId]; } else { users[userId] = { name: userName, discord: false }; } await saveWatchedUsers(users); return !!users[userId]; } // --- SETTINGS UI --- async function renderWatchlist(container) { if (!container) return; try { const users = await getWatchedUsers(); if (Object.keys(users).length === 0) { container.innerHTML = `<p class="${SCRIPT_PREFIX}no-users">No users in watchlist. Visit a profile and click the watch button to add them.</p>`; return; } let listHtml = '<ul>'; for (const [id, data] of Object.entries(users)) { listHtml += `<li data-id="${id}"><a href="/profiles.php?XID=${id}" target="_blank">${data.name || 'Unknown User'}</a><div class="controls"><label>Discord Alerts: <input type="checkbox" class="${SCRIPT_PREFIX}discord-toggle" ${data.discord ? 'checked' : ''}></label><button class="${SCRIPT_PREFIX}remove-btn">Remove</button></div></li>`; } listHtml += '</ul>'; container.innerHTML = listHtml; container.querySelectorAll(`.${SCRIPT_PREFIX}remove-btn`).forEach(btn => { btn.onclick = async () => { const id = btn.closest('li').dataset.id; const currentUsers = await getWatchedUsers(); delete currentUsers[id]; await saveWatchedUsers(currentUsers); await renderWatchlist(container); if (getUserIdFromPage() === id) { await updateButtonState(); startStopPageWatcher(false); } }; }); container.querySelectorAll(`.${SCRIPT_PREFIX}discord-toggle`).forEach(chk => { chk.onchange = async () => { const id = chk.closest('li').dataset.id; const currentUsers = await getWatchedUsers(); if (currentUsers[id]) { currentUsers[id].discord = chk.checked; await saveWatchedUsers(currentUsers); } }; }); } catch (error) { console.error('[DMW] Error rendering watchlist:', error); container.innerHTML = `<p class="${SCRIPT_PREFIX}no-users">Error loading watchlist.</p>`; } } async function populateSettingsModal(modal) { try { const settings = await loadSettings(); modal.querySelector(`#${SCRIPT_PREFIX}auto-attack`).checked = settings.autoAttack; modal.querySelector(`#${SCRIPT_PREFIX}webhook-url`).value = settings.webhookUrl || ''; modal.querySelector(`#${SCRIPT_PREFIX}webhook-message`).value = settings.webhookMessage || ''; await renderWatchlist(modal.querySelector(`#${SCRIPT_PREFIX}watchlist-container`)); } catch (e) { console.error('[DMW] Failed to populate settings modal:', e); modal.querySelector(`#${SCRIPT_PREFIX}watchlist-container`).innerHTML = `<p class="${SCRIPT_PREFIX}no-users">Error loading settings.</p>`; } } function createSettingsModal() { document.getElementById(`${SCRIPT_PREFIX}settings-modal`)?.remove(); const modal = document.createElement('div'); modal.id = `${SCRIPT_PREFIX}settings-modal`; modal.className = `${SCRIPT_PREFIX}modal`; modal.style.display = 'block'; modal.innerHTML = ` <div class="${SCRIPT_PREFIX}modal-content"> <span class="${SCRIPT_PREFIX}close-button">×</span> <h2 class="${SCRIPT_PREFIX}modal-header">${ICONS.settings} Deadman Watcher Settings</h2> <div class="${SCRIPT_PREFIX}section"> <h3>Alerts & Actions</h3> <div class="${SCRIPT_PREFIX}form-group ${SCRIPT_PREFIX}checkbox-group"> <input type="checkbox" id="${SCRIPT_PREFIX}auto-attack"> <label for="${SCRIPT_PREFIX}auto-attack">Open attack page when a watched target is alive</label> </div> <div class="${SCRIPT_PREFIX}form-group"> <label for="${SCRIPT_PREFIX}webhook-url">Discord Webhook URL:</label> <input type="text" id="${SCRIPT_PREFIX}webhook-url"> </div> <div class="${SCRIPT_PREFIX}form-group"> <label for="${SCRIPT_PREFIX}webhook-message">Custom Webhook Message (use %USERNAME% and %USERID%):</label> <textarea id="${SCRIPT_PREFIX}webhook-message" rows="3" placeholder="Leave blank for a random default message."></textarea> </div> </div> <div class="${SCRIPT_PREFIX}section"> <h3>Watchlist</h3> <div id="${SCRIPT_PREFIX}watchlist-container"><div class="${SCRIPT_PREFIX}no-users">Loading...</div></div> </div> <div class="${SCRIPT_PREFIX}footer"> <a href="https://www.torn.com/profiles.php?XID=3626448" target="_blank">Made by HeyItzWerty [3626448]</a> <button id="${SCRIPT_PREFIX}save-button">Save Settings</button> </div> </div>`; document.body.appendChild(modal); modal.querySelector(`.${SCRIPT_PREFIX}close-button`).onclick = () => modal.remove(); modal.querySelector(`#${SCRIPT_PREFIX}save-button`).onclick = async () => { await saveSettings(); modal.remove(); }; populateSettingsModal(modal).catch(console.error); } GM_registerMenuCommand("Deadman Watcher Settings", createSettingsModal); // --- CORE FUNCTIONALITY --- function sendWebhookNotification(userName, userId, settings, watchedUser) { if (!settings.webhookUrl || !watchedUser.discord) return; let messageContent = settings.webhookMessage; if (!messageContent) messageContent = DEFAULT_MESSAGES[Math.floor(Math.random() * DEFAULT_MESSAGES.length)]; const content = messageContent.replace(/%USERNAME%/g, userName).replace(/%USERID%/g, userId); GM_xmlhttpRequest({ method: "POST", url: settings.webhookUrl, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ content }), onload: () => console.log(`[DMW] Webhook notification for ${userName}.`), onerror: (e) => console.error(`[DMW] Webhook failed for ${userName}:`, e) }); } // --- TITLE AND NOTIFICATION LOGIC --- function stopAllTimers() { if (flashInterval) clearInterval(flashInterval); if (titleUpdateInterval) clearInterval(titleUpdateInterval); flashInterval = null; titleUpdateInterval = null; } function startFlashing(name) { stopAllTimers(); let state = false; const flashTitle1 = `🟢 ${name} is alive! 🟢`; const flashTitle2 = `✅ ATTACK NOW! ✅`; flashInterval = setInterval(() => { document.title = state ? flashTitle1 : flashTitle2; state = !state; }, 800); } function parseHospitalTime(timeString) { let totalSeconds = 0; const hourMatch = timeString.match(/(\d+)\s+hours?/); const minuteMatch = timeString.match(/(\d+)\s+minutes?/); const secondMatch = timeString.match(/(\d+)\s+seconds?/); // Added to handle seconds as well if (hourMatch) totalSeconds += parseInt(hourMatch[1], 10) * 3600; if (minuteMatch) totalSeconds += parseInt(minuteMatch[1], 10) * 60; if (secondMatch) totalSeconds += parseInt(secondMatch[1], 10); return totalSeconds; } function formatSecondsToHMS(secs) { const hours = Math.floor(secs / 3600); const minutes = Math.floor((secs % 3600) / 60); const seconds = Math.floor(secs % 60); return [hours, minutes, seconds].map(v => v.toString().padStart(2, '0')).join(':'); } // --- THIS FUNCTION CONTAINS THE FIX --- function updateTimerTitle() { const elapsed = (Date.now() - hospitalStartTime) / 1000; // FIX: Calculate remaining time by subtracting elapsed time from the initial duration. let remainingSeconds = hospitalInitialSeconds - elapsed; // Prevent the timer from showing negative numbers if there's a slight delay. if (remainingSeconds < 0) { remainingSeconds = 0; } const formattedTime = formatSecondsToHMS(remainingSeconds); const userName = getUserNameFromPage(); document.title = `🔴 ${userName} [${formattedTime}] 🔴`; } async function handleStatusChange(targetNode) { const isHospitalized = targetNode.classList.contains('hospital'); const wasHospitalized = (targetNode.dataset.lastStatus === 'hospital'); const userName = getUserNameFromPage(); const userId = getUserIdFromPage(); stopAllTimers(); // Clear any previous timers before deciding what to do next if (isHospitalized) { const statusDescElement = targetNode.querySelector('.description .main-desc'); if (statusDescElement) { hospitalInitialSeconds = parseHospitalTime(statusDescElement.textContent); hospitalStartTime = Date.now(); updateTimerTitle(); // Set initial title titleUpdateInterval = setInterval(updateTimerTitle, 1000); } } else { document.title = `🟢 ${userName} [Alive] 🟢`; if (wasHospitalized) { console.log(`[DMW] ${userName} is now alive!`); const settings = await loadSettings(); const watchedUsers = await getWatchedUsers(); const watchedUser = watchedUsers[userId]; if (!watchedUser) return; startFlashing(userName); sendWebhookNotification(userName, userId, settings, watchedUser); if (settings.autoAttack) { GM_openInTab(`https://www.torn.com/loader2.php?sid=getInAttack&user2ID=${userId}`, { active: true }); } } } targetNode.dataset.lastStatus = isHospitalized ? 'hospital' : 'ok'; } function startStopPageWatcher(isWatching) { if (statusObserver) statusObserver.disconnect(); stopAllTimers(); if (isWatching) { const statusDiv = document.querySelector('.profile-status'); if (statusDiv) { statusObserver = new MutationObserver((mutations) => { mutations.forEach(m => (m.type === 'attributes' && m.attributeName === 'class') && handleStatusChange(m.target)); }); statusObserver.observe(statusDiv, { attributes: true }); handleStatusChange(statusDiv); // Initial check } } else { document.title = originalTitle; } } // --- UI INJECTION & EVENT HANDLING --- async function updateButtonState() { const watchButton = document.getElementById(`${SCRIPT_PREFIX}watch-button`); if (!watchButton) return; const isWatched = !!(await getWatchedUsers())[getUserIdFromPage()]; const icon = watchButton.querySelector('svg path'); watchButton.classList.toggle('active', isWatched); if (icon) icon.style.fill = isWatched ? TORN_GREEN : TORN_GREY; } function injectButton(actionsContainer) { const descContainer = document.getElementById('profile-container-description'); if (!descContainer) return; originalActionText = descContainer.textContent; const userId = getUserIdFromPage(); const userName = getUserNameFromPage(); const watchButton = document.createElement('a'); watchButton.id = `${SCRIPT_PREFIX}watch-button`; watchButton.href = '#'; watchButton.className = 'profile-button profile-button-watch'; watchButton.innerHTML = ICONS.gravestone; watchButton.addEventListener('click', async (e) => { e.preventDefault(); const isNowWatching = await toggleUserInWatchlist(userId, userName); startStopPageWatcher(isNowWatching); updateButtonState(); }); watchButton.addEventListener('mouseover', async () => { const watchedUsers = await getWatchedUsers(); descContainer.textContent = watchedUsers[userId] ? `Disable Deadman Watcher for ${userName}` : `Enable Deadman Watcher for ${userName}`; }); watchButton.addEventListener('mouseout', () => { descContainer.textContent = originalActionText; }); actionsContainer.appendChild(watchButton); } // --- INITIALIZATION --- async function initialize() { try { if (!window.location.pathname.startsWith('/profiles.php')) return; GM_addStyle(` .profile-button-watch svg path { fill: ${TORN_GREY}; transition: fill 0.2s ease-in-out; } .profile-button-watch.active svg path { fill: ${TORN_GREEN}; } .${SCRIPT_PREFIX}modal { display: none; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); font-family: 'Signika', sans-serif; } .${SCRIPT_PREFIX}modal-content { background-color: #333; color: #d4d4d4; margin: 10% auto; padding: 25px; border: 1px solid #444; width: 90%; max-width: 600px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); position: relative; } .${SCRIPT_PREFIX}close-button { color: #aaa; float: right; font-size: 28px; font-weight: bold; line-height: 1; } .${SCRIPT_PREFIX}close-button:hover, .${SCRIPT_PREFIX}close-button:focus { color: #fff; text-decoration: none; cursor: pointer; } .${SCRIPT_PREFIX}modal-header { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 15px; display: flex; align-items: center; gap: 10px; } .${SCRIPT_PREFIX}section { border: 1px solid #444; border-radius: 5px; padding: 15px; margin-bottom: 20px; background-color: #2d2d2d; } .${SCRIPT_PREFIX}section h3 { margin-top: 0; color: #eee; } .${SCRIPT_PREFIX}form-group { margin-bottom: 15px; } .${SCRIPT_PREFIX}form-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #ccc; } .${SCRIPT_PREFIX}form-group input[type="text"], .${SCRIPT_PREFIX}form-group textarea { width: 100%; padding: 10px; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; background-color: #222; color: #d4d4d4; } .${SCRIPT_PREFIX}form-group textarea { resize: vertical; } .${SCRIPT_PREFIX}checkbox-group { display: flex; align-items: center; margin-bottom: 10px; } .${SCRIPT_PREFIX}checkbox-group input { margin-right: 10px; width: 18px; height: 18px; flex-shrink: 0; accent-color: ${TORN_GREEN}; } .${SCRIPT_PREFIX}checkbox-group label { margin-bottom: 0; } .${SCRIPT_PREFIX}footer { display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid #555; } .${SCRIPT_PREFIX}footer a { color: #999; text-decoration: none; font-size: 0.9em; } .${SCRIPT_PREFIX}footer a:hover { text-decoration: underline; } #${SCRIPT_PREFIX}save-button { background-color: #555; color: white; padding: 10px 18px; border: 1px solid #777; border-radius: 4px; cursor: pointer; font-size: 16px; } #${SCRIPT_PREFIX}save-button:hover { background-color: #666; } #${SCRIPT_PREFIX}watchlist-container ul { list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; } #${SCRIPT_PREFIX}watchlist-container li { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #444; } #${SCRIPT_PREFIX}watchlist-container li:last-child { border-bottom: none; } #${SCRIPT_PREFIX}watchlist-container a { color: ${TORN_GREEN}; text-decoration: none; } #${SCRIPT_PREFIX}watchlist-container .controls { display: flex; align-items: center; gap: 15px; } #${SCRIPT_PREFIX}watchlist-container .controls label { display: flex; align-items: center; gap: 5px; cursor: pointer; font-size: 0.9em; } #${SCRIPT_PREFIX}watchlist-container .controls input[type="checkbox"] { accent-color: ${TORN_GREEN}; } .${SCRIPT_PREFIX}remove-btn { background: #800; color: #fff; border: 1px solid #a00; border-radius: 3px; padding: 3px 8px; cursor: pointer; font-size: 0.8em; } .${SCRIPT_PREFIX}remove-btn:hover { background: #a00; } .${SCRIPT_PREFIX}no-users { color: #999; font-style: italic; padding: 10px; text-align: center; } `); const observer = new MutationObserver(async (mutations, obs) => { const actionsContainer = document.querySelector('.profile-action .buttons-list'); if (actionsContainer && !document.getElementById(`${SCRIPT_PREFIX}watch-button`)) { obs.disconnect(); injectButton(actionsContainer); const watchedUsers = await getWatchedUsers(); if (watchedUsers[getUserIdFromPage()]) { startStopPageWatcher(true); } await updateButtonState(); } }); const profileRoot = document.getElementById('profileroot'); if (profileRoot) { observer.observe(profileRoot, { childList: true, subtree: true }); } } catch (e) { console.error('[DMW] A critical error occurred during initialization:', e); } } function getUserIdFromPage() { try { return new URLSearchParams(window.location.search).get('XID'); } catch (e) { return null; } } function getUserNameFromPage() { const nameElement = document.querySelector('h4#skip-to-content'); if (!nameElement) return 'User'; return nameElement.textContent.split(' [')[0].trim(); } initialize(); })();