Torn — Larger Chain Timer + Random Lvl 1 Finder (Ctrl+Click) v1.5

Enlarges the chain timer and lets you Ctrl+Click it to find a random Level 1 target. Menu options: profile vs attack loader, new tab vs same tab.

Mint 2025.11.01.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn — Larger Chain Timer + Random Lvl 1 Finder (Ctrl+Click) v1.5
// @namespace    [https://www.torn.com/](https://www.torn.com/)
// @version      1.6
// @description  Enlarges the chain timer and lets you Ctrl+Click it to find a random Level 1 target. Menu options: profile vs attack loader, new tab vs same tab.
// @author       Combined by ChatGPT (base scripts by Annosz & others)
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @connect      api.torn.com
// @license     GNU GPLv3
// ==/UserScript==

(function () {
    'use strict';

    // -------- waitForKeyElements inline --------
    function waitForKeyElements(selector, callback, waitOnce = true, interval = 300) {
        const alreadyFound = new Set();
        const observer = new MutationObserver(() => {
            document.querySelectorAll(selector).forEach(el => {
                if (!alreadyFound.has(el)) {
                    alreadyFound.add(el);
                    callback(el);
                }
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
        // also run initially in case elements already exist
        document.querySelectorAll(selector).forEach(el => {
            if (!alreadyFound.has(el)) {
                alreadyFound.add(el);
                callback(el);
            }
        });
    }

    // -------- storage keys & defaults --------
    const API_KEY_STORAGE = 'torn_random_api_key_v1';
    const CONFIG_STORAGE = 'torn_random_config_v1';
    const URL_MODE_STORAGE = 'torn_random_url_mode_v1';     // 'attack' or 'profile'
    const OPEN_MODE_STORAGE = 'torn_random_open_mode_v1';   // 'newtab' or 'sametab'
    const GLOW_DURATION_MS = 700;

    const DEFAULT_URL_MODE = 'attack';
    const DEFAULT_OPEN_MODE = 'newtab';

    function getApiKey() { return GM_getValue(API_KEY_STORAGE, null); }
    function setApiKey(k) { GM_setValue(API_KEY_STORAGE, k); }

    function getUrlMode() { return GM_getValue(URL_MODE_STORAGE, DEFAULT_URL_MODE); }
    function setUrlMode(m) { GM_setValue(URL_MODE_STORAGE, m); }
    function getOpenMode() { return GM_getValue(OPEN_MODE_STORAGE, DEFAULT_OPEN_MODE); }
    function setOpenMode(m) { GM_setValue(OPEN_MODE_STORAGE, m); }

    function getConfig() {
        const def = { maxId: 3972564, maxAttempts: 60, delayMs: 350 };
        try { return Object.assign(def, JSON.parse(GM_getValue(CONFIG_STORAGE, JSON.stringify(def)))); }
        catch { return def; }
    }
    function setConfig(cfg) { GM_setValue(CONFIG_STORAGE, JSON.stringify(cfg)); }

    // -------- menu commands --------
    GM_registerMenuCommand('Set Torn API Key', () => {
        const cur = getApiKey() || '';
        const k = prompt('Enter your Torn API key (16 chars):', cur);
        if (k !== null) setApiKey(k.trim());
    });

    GM_registerMenuCommand('Configure Finder (attempts/delay/maxId)', () => {
        const cfg = getConfig();
        const maxId = parseInt(prompt('Max user ID to sample (default ' + cfg.maxId + '):', cfg.maxId)) || cfg.maxId;
        const maxAttempts = parseInt(prompt('Max attempts per click (default ' + cfg.maxAttempts + '):', cfg.maxAttempts)) || cfg.maxAttempts;
        const delayMs = parseInt(prompt('Delay between API calls in ms (default ' + cfg.delayMs + '):', cfg.delayMs)) || cfg.delayMs;
        setConfig({ maxId, maxAttempts, delayMs });
        alert('Configuration saved.');
    });

    GM_registerMenuCommand('Set Target URL (profile / attack loader)', () => {
        const cur = getUrlMode();
        const choice = prompt(`Choose target URL mode (type exactly):\n- profile  -> profiles.php?XID=ID\n- attack   -> loader.php?sid=attack&user2ID=ID\n\nCurrent: ${cur}`, cur);
        if (choice === null) return;
        const normalized = choice.trim().toLowerCase();
        if (normalized === 'profile' || normalized === 'attack') {
            setUrlMode(normalized);
            alert('Saved. Now using: ' + (normalized === 'attack' ? 'attack loader' : 'profile'));
        } else {
            alert('Invalid choice. Enter "profile" or "attack".');
        }
    });

    GM_registerMenuCommand('Set Open Mode (new tab / same tab)', () => {
        const cur = getOpenMode();
        const choice = prompt(`Choose how to open the target (type exactly):\n- newtab  -> opens in a new tab (GM_openInTab)\n- sametab -> opens in the current tab\n\nCurrent: ${cur}`, cur);
        if (choice === null) return;
        const normalized = choice.trim().toLowerCase();
        if (normalized === 'newtab' || normalized === 'sametab') {
            setOpenMode(normalized);
            alert('Saved. Now opening in: ' + (normalized === 'newtab' ? 'new tab' : 'same tab'));
        } else {
            alert('Invalid choice. Enter "newtab" or "sametab".');
        }
    });

    // -------- make sure glow CSS is available --------
    (function injectGlowStyles(){
        if (document.getElementById('torn-random-glow-styles')) return;
        const css = `
        @keyframes tornRandomPulse {
          0% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); }
          30% { box-shadow: 0 0 12px 6px rgba(255, 215, 0, 0.85); }
          100% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); }
        }
        .torn-random-glow {
          animation: tornRandomPulse ${GLOW_DURATION_MS}ms ease-out;
          border-radius: 6px !important;
        }`;
        const s = document.createElement('style');
        s.id = 'torn-random-glow-styles';
        s.textContent = css;
        document.head.appendChild(s);
    })();

    // -------- API util --------
    const sleep = ms => new Promise(r => setTimeout(r, ms));

    function apiGetUser(id, apiKey) {
        return new Promise((resolve, reject) => {
            const url = `https://api.torn.com/user/${id}?selections=profile&key=${encodeURIComponent(apiKey)}`;
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: res => {
                    try { resolve(JSON.parse(res.responseText)); }
                    catch (e) { reject(e); }
                },
                onerror: err => reject(err)
            });
        });
    }

    // -------- open URL helpers --------
    function makeTargetUrl(id) {
        return getUrlMode() === 'profile'
            ? `https://www.torn.com/profiles.php?XID=${encodeURIComponent(id)}`
            : `https://www.torn.com/loader.php?sid=attack&user2ID=${encodeURIComponent(id)}`;
    }

    function openTargetUrl(url) {
        if (getOpenMode() === 'sametab') {
            window.location.href = url;
        } else {
            try { GM_openInTab(url, { active: true, insert: true }); }
            catch (e) { window.open(url, '_blank', 'noopener'); }
        }
    }

    // -------- Core: primary selector --------
    waitForKeyElements(".speed___dFP2B", primaryAction);

    function primaryAction(jNode) {
        try {
            const barStats = document.querySelector(".bar-stats___E_LqA") || document.querySelector("div[class*='bar-stats']");
            const timeLeft = document.querySelector(".bar-timeleft___B9RGV") || barStats?.querySelector("p[class*='bar-timeleft']") || null;
            const speed = document.querySelector(".speed___dFP2B") || null;
            const tickList = document.querySelector(".tick-list___McObN") || null;

            if (!barStats || !timeLeft) {
                const alt = Array.from(document.querySelectorAll("div[class*='bar-stats'], div.bar-stats"))
                                .find(n => n && n.textContent && n.textContent.includes("Chain:"));
                if (alt) attachToTimer(alt);
                return;
            }
            attachToTimer(barStats, timeLeft, speed, tickList);
        } catch (err) {
            console.error("[Torn Random Finder] primaryAction error:", err);
        }
    }

    waitForKeyElements("div[class*='bar-stats'], div.bar-stats", node => {
        if (node && node.textContent && node.textContent.includes("Chain:")) {
            const existing = node.querySelector("p[class*='bar-timeleft']");
            attachToTimer(node, existing);
        }
    });

    // Color update function for the timer text gradient green (#65A128) to red between 5:00 and 2:00
    function updateTimerColor(timeText, element) {
        const parts = timeText.split(':');
        if (parts.length !== 2) return;
        const minutes = parseInt(parts[0], 10);
        const seconds = parseInt(parts[1], 10);
        if (isNaN(minutes) || isNaN(seconds)) return;

        const totalSeconds = minutes * 60 + seconds;
        const startGreen = 5 * 60;  // 300 seconds
        const startRed = 2 * 60;    // 120 seconds

        if (totalSeconds >= startGreen) {
            element.style.color = '#65A128';  // fixed green color
        } else if (totalSeconds <= startRed) {
            element.style.color = 'red';
        } else {
            // interpolate between #65A128 (101,161,40) and red (255,0,0)
            const ratio = (totalSeconds - startRed) / (startGreen - startRed); // 0..1
            const r = Math.round(255 * (1 - ratio) + 101 * ratio);
            const g = Math.round(0 * (1 - ratio) + 161 * ratio);
            const b = Math.round(0 * (1 - ratio) + 40 * ratio);
            element.style.color = `rgb(${r},${g},${b})`;
        }
    }

    // Start continuous color updating on the timer element
    function startColorGradientTimer(timeLeftElem) {
        updateTimerColor(timeLeftElem.textContent.trim(), timeLeftElem);
        const intervalId = setInterval(() => {
            if (!document.body.contains(timeLeftElem)) {
                clearInterval(intervalId);
                return;
            }
            updateTimerColor(timeLeftElem.textContent.trim(), timeLeftElem);
        }, 500);
    }

    function attachToTimer(barStats, timeLeftElem=null, speedElem=null, tickListElem=null) {
        try {
            if (!timeLeftElem) {
                timeLeftElem = barStats.querySelector("p[class*='bar-timeleft']") || barStats.querySelector("p");
            }
            if (!timeLeftElem) return;
            if (timeLeftElem.dataset.tornRandomAttached) return;
            timeLeftElem.dataset.tornRandomAttached = '1';

            barStats.style.display = "block";
            timeLeftElem.style.fontSize = "60px";
            timeLeftElem.style.height = "62px";
            if (speedElem) speedElem.style.top = "unset";
            if (tickListElem) tickListElem.style.height = "8px";

            timeLeftElem.style.cursor = "pointer";
            timeLeftElem.title = "Ctrl+Click to find a random Level 1 target (menu options available)";
            timeLeftElem.style.transition = 'color 0.5s ease-in-out'; // smooth fade for color change

            // Start the color gradient update loop
            startColorGradientTimer(timeLeftElem);

            timeLeftElem.addEventListener('click', async (e) => {
                if (!e.ctrlKey) {
                    console.log("Tip: Hold Ctrl and click the timer to find a random Level 1 target.");
                    return;
                }
                e.preventDefault();
                console.log("[Torn Random Finder] Ctrl+Click detected — starting search.");
                timeLeftElem.style.opacity = "0.5";
                setTimeout(() => (timeLeftElem.style.opacity = ""), 300);
                await findRandomLevel1(timeLeftElem);
            });

            console.log("[Torn Random Finder] Attached to chain timer successfully.");
        } catch (err) {
            console.error("[Torn Random Finder] attachToTimer error:", err);
        }
    }

    // -------- finder logic --------
    async function findRandomLevel1(timeLeftElement = null) {
        let apiKey = getApiKey();
        if (!apiKey) {
            const want = confirm('No Torn API key found. Would you like to enter it now?');
            if (!want) return;
            const k = prompt('Enter your Torn API key:');
            if (!k) return alert('API key required.');
            setApiKey(k.trim());
            apiKey = getApiKey();
        }

        const cfg = getConfig();

        for (let attempt = 1; attempt <= cfg.maxAttempts; attempt++) {
            const id = Math.floor(Math.random() * cfg.maxId) + 1;
            console.log(`Attempt ${attempt}/${cfg.maxAttempts} — sampling ID ${id}`);

            try {
                const info = await apiGetUser(id, apiKey);

                if (info && info.error) {
                    if (info.error.error === "Incorrect ID") {
                        console.log(`Skipped invalid ID ${id}`);
                        continue;
                    } else {
                        alert(`❌ API Error: ${info.error.error || JSON.stringify(info.error)}`);
                        return;
                    }
                }

                const level = (typeof info.level !== 'undefined') ? Number(info.level)
                              : (info.profile && typeof info.profile.level !== 'undefined') ? Number(info.profile.level)
                              : null;

                let statusText = '';
                if (info.profile && info.profile.status) {
                    if (typeof info.profile.status === 'string') statusText = info.profile.status;
                    else if (info.profile.status.state) statusText = info.profile.status.state;
                    else statusText = JSON.stringify(info.profile.status);
                } else if (info.status) {
                    statusText = (typeof info.status === 'string') ? info.status : JSON.stringify(info.status);
                }

                const blockedStatuses = /hospital|jail|federal jail/i;
                const inBlockedState = blockedStatuses.test(String(statusText || ''));

                if (level === 1 && !inBlockedState) {
                    console.log('Found match — opening target', id);

                    // Removed glow effect confirmation on Ctrl+click

                    const targetUrl = makeTargetUrl(id);
                    openTargetUrl(targetUrl);
                    return;
                }

            } catch (e) {
                console.warn('Request error for ID', id, e);
            }

            await sleep(cfg.delayMs);
        }

        alert(`No matching Level 1 profile found after ${cfg.maxAttempts} attempts. Try increasing attempts or maxId in the script menu.`);
    }

})();