Torn Quick Deposit

Quick Deposit cash to Ghost Trade / Company Vault / Faction Vault

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn Quick Deposit
// @namespace    http://tampermonkey.net/
// @version      1.3.4
// @description  Quick Deposit cash to Ghost Trade / Company Vault / Faction Vault
// @author       e7cf09 [3441977]
// @icon         https://editor.torn.com/cd385b6f-7625-47bf-88d4-911ee9661b52-3441977.png
// @match        https://www.torn.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // CONFIG & STATE
    // USAGE: Set PANIC_THRESHOLD for auto-triggers. Map physical keys in KEYS object.
    // EXAMPLES: Empty: [], Single: ["KeyA"], Multiple: ["ControlLeft", "KeyA"]
    const CONFIG = {
        PANIC_THRESHOLD: 20000000, // Panic Threshold: Only triggers if balance exceeds this
        STRICT_GHOST_MODE: true, // Relaxed matching for Ghost trades (if false, any trade found is used)
        KEYS: {
            FACTION: [],
            GHOST: [],
            COMPANY: [],
            RESET: [],
            PANIC: ["KeyP"],
            EXECUTE: [] // Auto-deposit / Panic Execute
        },
        DEAD_SIGNALS: ["no trade was found", "trade has been accepted", "declined", "cancelled", "locked"]
    };

    const STATE = {
        balance: 0,
        panic: localStorage.getItem('torn_tactical_panic_enabled') !== 'false',
        ghostID: localStorage.getItem('torn_tactical_ghost_id'),
        locks: { panic: false, sending: false, jumping: false, dead: false },
        els: { overlay: null, btn: null }
    };

    // UTILS
    const qs = (s, p=document) => p.querySelector(s);
    const qsa = (s, p=document) => p.querySelectorAll(s);
    const fmt = (n) => "$" + parseInt(n).toLocaleString('en-US');
    const setStyle = (el, css) => Object.assign(el.style, css);

    // NETWORK INTERCEPT
    const intercept = (proto, method, handler) => {
        const orig = proto[method];
        proto[method] = function(...args) {
            handler(this, args);
            return orig.apply(this, args);
        };
    };

    intercept(XMLHttpRequest.prototype, 'open', (xhr, args) => xhr._url = args[1]);
    intercept(XMLHttpRequest.prototype, 'send', (xhr) => {
        xhr.addEventListener('load', () => {
            if (xhr._url?.includes('trade.php')) checkDeadTrade(xhr.responseText, xhr._url);
        });
    });

    const origFetch = window.fetch;
    window.fetch = async (...args) => {
        const res = await origFetch(...args);
        const url = args[0]?.toString() || '';
        if (url.match(/sidebar|user/)) {
            res.clone().json().then(d => updateBalance(d?.user?.money || d?.sidebarData?.user?.money)).catch(()=>{});
        }
        return res;
    };

    // WEBSOCKET INTERCEPT
    const OrigWS = window.WebSocket;
    window.WebSocket = function(url, protocols) {
        const ws = new OrigWS(url, protocols);
        ws.addEventListener('message', e => {
            if (typeof e.data !== 'string') return;
            if (e.data.includes('attack-effects')) triggerPanic();
            const m = e.data.match(/"money"\s*:\s*(\d+)/);
            if (m) updateBalance(m[1]);
        });
        return ws;
    };
    window.WebSocket.prototype = OrigWS.prototype;

    // LOGIC
    function checkDeadTrade(text, url) {
        if (!text || text.length < 20) return;
        if (CONFIG.DEAD_SIGNALS.some(s => text.toLowerCase().includes(s))) {
            let id = null;
            if (url && url.includes('ID=')) {
                const match = url.match(/ID=(\d+)/);
                if (match) id = match[1];
            } else {
                const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
                id = params.get('ID');
            }

            if (STATE.ghostID && id === STATE.ghostID) {
                localStorage.removeItem('torn_tactical_ghost_id');
                STATE.ghostID = null;
                injectBtn();
            }
        }
    }

    function getStatus() {
        const icons = qsa('ul[class*="status-icons"] > li');
        if (!icons.length) return { faction: true, ghost: true, company: false, reason: "LOADING" }; // Aggressive default

        let s = { faction: true, ghost: true, company: false, reason: "" };
        let isHosp = false, isTravel = false;

        icons.forEach(li => {
            if (li.className.match(/icon1[56]/)) isHosp = true;
            if (li.className.includes('icon71')) isTravel = true;
            if (li.className.includes('icon73')) s.company = true;
        });

        if (isHosp) { s.faction = s.company = false; s.reason = "HOSP/JAIL"; }
        else if (isTravel) { s.faction = s.ghost = s.company = false; s.reason = "TRAVEL"; }

        // Special check for Faction: Ensure we are in a faction
        const sidebarFaction = qs('a[href^="/factions.php"]');
        if (!sidebarFaction) s.faction = false;

        return s;
    }

    function updateBalance(val) {
        if (!val) return;
        const num = parseInt(typeof val === 'object' ? val.value : val);
        if (isNaN(num)) return;

        STATE.balance = num;
        if (STATE.balance <= 0) {
            STATE.locks.sending = false;
            dismissPanic();
        } else if (STATE.balance >= CONFIG.PANIC_THRESHOLD && STATE.panic) {
            const s = getStatus();
            if (STATE.ghostID ? s.ghost : s.faction) triggerPanic();
        } else if (STATE.locks.panic) {
            dismissPanic();
        }
        if (STATE.locks.panic) updateOverlay();
    }

    function triggerPanic() {
        if (!STATE.panic || STATE.balance < CONFIG.PANIC_THRESHOLD) return;
        const s = getStatus();
        if (s.reason === 'LOADING') return; // Wait for icons to load
        if (!(STATE.ghostID && s.ghost) && !s.company && !s.faction) return;

        STATE.locks.panic = true;
        updateOverlay();
    }

    function dismissPanic() {
        STATE.locks.panic = false;
        STATE.locks.sending = false;
        if (STATE.els.overlay) STATE.els.overlay.style.display = 'none';
    }

    async function executeDeposit(mode = 'auto') {
        // 1. Handle Confirmation Boxes immediately
        const box = Array.from(qsa('.cash-confirm')).find(b => b.offsetParent && b.style.display !== 'none');
        if (box) {
            if (box.querySelector('.yes')) {
                if (STATE.els.overlay) STATE.els.overlay.innerHTML = "DEPOSITING...";
                box.querySelector('.yes').click();
                STATE.locks.sending = false;
                return;
            }
        }

        // Reset sending lock if confirmation box is gone (User clicked No/Cancel)
        if (!box && STATE.locks.sending) {
            // Check if we are stuck in sending state without a box
            // This happens if user clicked No, or closed the modal
            STATE.locks.sending = false;
        }

        if (STATE.locks.sending) return;
        const status = getStatus();

        // Determine Target Mode
        let target = mode;
        if (mode === 'auto') {
            if (STATE.ghostID && status.ghost) target = 'ghost';
            else if (status.company) target = 'company';
            else if (status.faction) target = 'faction';
            else return;
        }

        // Validate Target with strict mode checks
        if (target === 'ghost') {
            // Strict check: Must have Ghost ID AND allow ghost status
            if (!STATE.ghostID || !status.ghost) {
                 if (mode === 'ghost') return; // Explicit request fails
                 // Fallback for auto: Try Company -> Faction
                 target = status.company ? 'company' : (status.faction ? 'faction' : null);
            }
        }
        if (target === 'company' && !status.company) {
            if (mode !== 'auto') return; // Strict fail for manual mode
            target = 'faction';
        }
        if (!target || (target === 'faction' && !status.faction)) return;

        // EXECUTION
        const setVal = (input, val) => {
            input.value = val;
            ['input', 'change', 'blur'].forEach(e => input.dispatchEvent(new Event(e, { bubbles: true })));
        };

        const jump = (url, msg) => {
            if (STATE.locks.jumping) return;
            STATE.locks.jumping = true;
            if (STATE.els.overlay) STATE.els.overlay.innerHTML = msg;
            setTimeout(() => STATE.locks.jumping = false, 1500);
            window.location.href = url;
        };

        if (target === 'ghost') {
            const url = window.location.href;
            // Relaxed check: Just need to be on trade.php and have the correct ID
            // We'll let the element check determine if we are on the "add money" view
            const onPageWithID = url.includes('trade.php') && url.includes(STATE.ghostID);

            // Check if the specific "Add Money" input exists
            const input = qs('.input-money[type="text"]');

            if (!onPageWithID || !input) {
                return jump(`https://www.torn.com/trade.php#step=addmoney&ID=${STATE.ghostID}`, "JUMPING<br>TO GHOST");
            }

            STATE.locks.sending = true;
            if (STATE.els.overlay) STATE.els.overlay.innerHTML = "DEPOSITING<br>TO GHOST";
            setVal(input, input.getAttribute('data-money') || STATE.balance);

            const btn = input.form?.querySelector('input[type="submit"], button[type="submit"]') || qs('input[type="submit"][value="Change"], button[type="submit"][value="Change"]');
            if (btn) {
                btn.disabled = false;
                btn.classList.remove('disabled');
                btn.click();
                STATE.locks.jumping = false;
            } else STATE.locks.sending = false;

        } else if (target === 'company') {
            const url = window.location.href;
            const onPage = url.includes('companies.php') && url.includes('option=funds');

            // Precise selector for Deposit Input (vs Withdraw)
            const input = qs('input[aria-labelledby="deposit-label"][type="text"]');

            if (!input) {
                if (!onPage) return jump(`https://www.torn.com/companies.php?step=your&type=1#/option=funds`, "JUMPING<br>TO COMPANY");
                return;
            }

            STATE.locks.sending = true;
            if (STATE.els.overlay) STATE.els.overlay.innerHTML = "DEPOSITING<br>TO COMPANY";
            setVal(input, input.getAttribute('data-money') || STATE.balance);

            const container = input.closest('.funds-cont');
            const btn = container ? container.querySelector('.deposit.btn-wrap button') : null;

            if (btn) {
                btn.disabled = false;
                btn.click();
            } else STATE.locks.sending = false;

        } else { // Faction
            const form = qs('.give-money-form') || qs('.input-money')?.closest('form');
            const onPage = window.location.href.includes('factions.php') && window.location.href.includes('tab=armoury');

            if (!form) {
                if (!onPage) return jump(`https://www.torn.com/factions.php?step=your&type=1#/tab=armoury`, "JUMPING<br>TO FACTION");
                qs('a[href*="tab=armoury"]')?.click();
                return;
            }

            const input = form.querySelector('.input-money');
            if (!input) return;

            STATE.locks.sending = true;
            if (STATE.els.overlay) STATE.els.overlay.innerHTML = "DEPOSITING<br>TO FACTION";

            let amt = input.getAttribute('data-money');
            if (!amt) {
                const txt = form.querySelector('.i-have')?.innerText.replace(/[$,]/g, '');
                if (txt && !isNaN(txt)) amt = txt;
            }
            setVal(input, amt || STATE.balance);

            const btn = form.querySelector('button.torn-btn');
            if (btn) {
                btn.disabled = false;
                btn.click();
            } else STATE.locks.sending = false;
        }
    }

    // UI COMPONENTS
    function showToast(html) {
        const t = document.createElement('div');
        t.innerHTML = `<div style="font-size:11px;color:#aaa;margin-bottom:4px;text-transform:uppercase;">Quick Deposit</div>${html}`;
        setStyle(t, {
            position: 'fixed', top: '15%', left: '50%', transform: 'translate(-50%, -50%)',
            zIndex: 2147483647, background: 'rgba(0,0,0,0.85)', color: 'white',
            padding: '12px 24px', borderRadius: '8px', pointerEvents: 'none',
            opacity: 0, transition: 'opacity 0.3s', textAlign: 'center', border: '1px solid #ffffff33'
        });
        document.body.appendChild(t);
        requestAnimationFrame(() => t.style.opacity = '1');
        setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 2000);
    }

    function updateOverlay() {
        if (!STATE.locks.panic || STATE.balance <= 0) return dismissPanic();

        if (!STATE.els.overlay) {
            const d = document.createElement('div');
            d.id = 'torn-panic-overlay';
            let css = `position: fixed; z-index: 2147483647; background: rgba(255, 0, 0, 0.9); color: white;
                font-family: 'Arial Black', sans-serif; font-size: 14px; text-align: center;
                padding: 10px 20px; border-radius: 30px; border: 3px solid white;
                box-shadow: 0 0 20px red; cursor: pointer; white-space: nowrap; user-select: none;`;

            // Default to off-screen, will be moved by mousemove
            css += `top: -999px; left: -999px; transform: translate(-50%, -50%);`;

            d.style.cssText = css;
            d.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); executeDeposit('auto'); });
            document.addEventListener('mousemove', e => {
                if (STATE.locks.panic) {
                    d.style.left = e.clientX + 'px';
                    d.style.top = e.clientY + 'px';
                }
            });
            document.body.appendChild(d);
            STATE.els.overlay = d;
        }

        STATE.els.overlay.style.display = 'block';
        if (STATE.locks.sending || STATE.locks.jumping) return;

        // Determine Text
        let txt = "JUMP TO<br>FACTION";
        if (qs('.cash-confirm')) {
            txt = "CONFIRM<br>DEPOSIT";
        } else {
            const s = getStatus();

            if (STATE.ghostID && s.ghost) {
                // Ghost Logic
                const hashParams = new URLSearchParams(window.location.hash.substring(1));
                const searchParams = new URLSearchParams(window.location.search);
                const currentStep = hashParams.get('step') || searchParams.get('step');
                const currentID = hashParams.get('ID') || searchParams.get('ID');

                const onGhostPage = window.location.href.includes('trade.php') &&
                                    currentStep === 'addmoney' &&
                                    currentID === STATE.ghostID;

                txt = onGhostPage ? `DEPOSIT<br>${fmt(STATE.balance)}<br><span style='font-size:10px'>GHOST</span>` : "JUMP TO<br>GHOST";

            } else if (s.company) {
                // Company Logic
                const onCompanyPage = window.location.href.includes('companies.php') && window.location.href.includes('option=funds');
                const container = qs('.funds-cont');
                // We are "on page" if URL matches AND the funds container is present (meaning loaded)
                // OR if URL matches and we are just waiting for load (to prevent flickering JUMP TO)
                const ready = onCompanyPage && (container || document.readyState === 'loading');

                txt = ready ? `DEPOSIT<br>${fmt(STATE.balance)}<br><span style='font-size:10px'>COMPANY</span>` : "JUMP TO<br>COMPANY";

            } else {
                // Faction Logic
                const current = window.location.href;
                const onFactionPage = current.includes('factions.php') && current.includes('step=your') && current.includes('type=1') && current.includes('tab=armoury');
                const form = qs('.give-money-form');

                // Show DEPOSIT if form exists OR if we are on the correct URL (even if form hasn't loaded yet)
                const ready = form || onFactionPage;

                txt = ready ? `DEPOSIT<br>${fmt(STATE.balance)}` : "JUMP TO<br>FACTION";
            }
        }

        if (STATE.els.overlay.innerHTML !== txt) STATE.els.overlay.innerHTML = txt;
    }

    function injectBtn() {
        const moneyEl = document.getElementById('user-money');
        if (!moneyEl) return;

        if (!STATE.els.btn) {
            const b = document.createElement('a');
            b.id = 'torn-tactical-deposit';
            b.href = '#';
            b.style.marginLeft = '10px';
            b.style.cursor = 'pointer';

            // Hardcoded style to match [use] button in sidebar
            // Based on user feedback: class="use___wM1PI"
            b.className = 'use___wM1PI';

            // Fallback inline styles only if class fails to apply styles
            // We set marginLeft because original element might have margin defined in CSS
            b.style.marginLeft = '10px';

            b.addEventListener('click', (e) => { e.preventDefault(); executeDeposit('auto'); });
            STATE.els.btn = b;
        }

        if (moneyEl.parentNode && moneyEl.nextSibling !== STATE.els.btn) {
            moneyEl.parentNode.insertBefore(STATE.els.btn, moneyEl.nextSibling);
        }

        const s = getStatus();
        let txt, title;
        if (STATE.ghostID) {
            txt = '[ghost]';
            title = `Ghost ID: ${STATE.ghostID}`;
        } else {
            txt = '[deposit]';
            title = s.company ? "Company Vault" : "Faction Vault";
        }

        if (STATE.els.btn.innerText !== txt) STATE.els.btn.innerText = txt;
        if (STATE.els.btn.title !== title) STATE.els.btn.title = title;
    }

    // EVENT LISTENERS
    window.addEventListener('keydown', e => {
        if (!e.isTrusted || e.target.matches('input, textarea') || e.target.isContentEditable) return;
        const k = e.code;

        // Helper to check if key matches any in the list
        const isKey = (type) => CONFIG.KEYS[type] && CONFIG.KEYS[type].includes(k);

        // Prevent default if it's any of our keys
        if (Object.values(CONFIG.KEYS).flat().includes(k)) e.preventDefault();

        if (isKey('FACTION')) executeDeposit('faction');
        if (isKey('GHOST') && STATE.ghostID) executeDeposit('ghost');
        if (isKey('COMPANY')) executeDeposit('company');
        if (isKey('EXECUTE')) executeDeposit('auto');
        if (isKey('RESET')) {
            localStorage.removeItem('torn_tactical_ghost_id');
            STATE.ghostID = null;
            injectBtn();
            showToast("GHOST ID CLEARED");
        }
        if (isKey('PANIC')) {
            STATE.panic = !STATE.panic;
            localStorage.setItem('torn_tactical_panic_enabled', STATE.panic);
            showToast(`PANIC MODE <span style="color:${STATE.panic?'#4dff4d':'#ff4d4d'}">${STATE.panic?'ON':'OFF'}</span>`);
            if (STATE.panic) updateBalance(STATE.balance); // Trigger check
            else dismissPanic();
        }
    });

    window.addEventListener('hashchange', () => {
        if (STATE.locks.jumping && STATE.ghostID && window.location.href.includes(STATE.ghostID)) STATE.locks.jumping = false;
        if (STATE.locks.panic) updateOverlay();
    });

    // INIT
    const scanTrades = () => {
        if (!window.location.href.includes('trade.php')) return;

        // Handle both Hash (Vue/React router) and Search (Legacy) params
        const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
        const currentID = params.get('ID');

        // Check for dead signals on page load
        if (qs('.info-msg, .error-msg')?.innerText.match(new RegExp(CONFIG.DEAD_SIGNALS.join('|'), 'i'))) {
            if (STATE.ghostID && currentID === STATE.ghostID) {
                STATE.ghostID = null; localStorage.removeItem('torn_tactical_ghost_id'); injectBtn(); return;
            }
        }

        // Capture from Log (Priority: Check chat logs on Trade View page)
        if (currentID) {
            const logs = qsa('ul.log .msg');
            let isGhost = false;
            
            if (!CONFIG.STRICT_GHOST_MODE) {
                // If not strict, ANY trade we visit is considered valid
                isGhost = true;
            } else {
                isGhost = Array.from(logs).some(msg => {
                    const clone = msg.cloneNode(true);
                    clone.querySelector('a')?.remove(); // Remove username
                    return clone.innerText.toLowerCase().includes('ghost');
                });
            }

            if (isGhost && !STATE.ghostID) {
                STATE.ghostID = currentID;
                localStorage.setItem('torn_tactical_ghost_id', currentID);
                injectBtn();
            }
        }

        // Capture from List (Only if not locked, and only if we are not on a specific trade page)
        if (!currentID && !STATE.ghostID) {
            qsa('ul.trade-list-container > li').forEach(li => {
                // Check content excluding username
                const clone = li.cloneNode(true);
                clone.querySelector('.user.name')?.remove();

                if (!CONFIG.STRICT_GHOST_MODE || clone.innerText.toLowerCase().includes('ghost')) {
                    const mid = li.querySelector('a.btn-wrap')?.href.match(/ID=(\d+)/);
                    if (mid) { STATE.ghostID = mid[1]; localStorage.setItem('torn_tactical_ghost_id', mid[1]); injectBtn(); }
                }
            });
        }
    };

    let init = false;
    const start = () => {
        if (init) return;
        init = true;

        // Initial Balance Check from DOM
        const moneyEl = document.getElementById('user-money');
        if (moneyEl) {
            const val = moneyEl.getAttribute('data-money') || moneyEl.innerText.replace(/[^\d]/g, '');
            if (val) updateBalance(val);
        }

        // Observers
        new MutationObserver((mutations) => {
            let ignore = true;
            for (const m of mutations) {
                if (!m.target.id?.includes('torn-') &&
                    !m.target.closest?.('#torn-tactical-deposit') &&
                    !m.target.closest?.('#torn-panic-overlay')) {
                    ignore = false;
                    break;
                }
            }
            if (ignore) return;

            injectBtn();
            scanTrades();
            // Removed updateBalance(STATE.balance) to prevent loop
        }).observe(document, { childList: true, subtree: true });

        // Early Init
        if (moneyEl) injectBtn();
    };

    if (document.readyState === 'loading') {
        // Fast track
        const early = new MutationObserver(() => {
            if (qs('#user-money')) { injectBtn(); }
            if (document.body) { start(); early.disconnect(); }
        });
        early.observe(document.documentElement, { childList: true, subtree: true });
        document.addEventListener('DOMContentLoaded', start);
    } else start();

})();