Deadman Watcher

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">&times;</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();
})();