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();

})();