RoC Banking system

70px UI, Sync with Native Icon CSS classes, Working Cancel

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         RoC Banking system
// @namespace    http://tampermonkey.net/
// @version      2.12
// @description  70px UI, Sync with Native Icon CSS classes, Working Cancel
// @author       Gemini
// @license      RoC
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @connect      api.torn.com
// @connect      war.tdkv.io.vn
// @run-at       document-end

// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. STORAGE & FORMATTING
    // ==========================================
    const storage = {
        set: (k, v) => (typeof GM_setValue !== 'undefined' ? GM_setValue(k, v) : localStorage.setItem(k, v)),
        get: (k, d) => {
            let v = (typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k));
            return (v === null || v === undefined) ? d : v;
        }
    };

    const API_KEY = storage.get("limited_key", "");
    const SERVER_URL = 'https://war.tdkv.io.vn/bank_api.php';

    function formatM(num) {
        if (!num) return "0";
        if (num >= 1000000) {
            let m = num / 1000000;
            return (m % 1 === 0 ? m : m.toFixed(2)) + 'm';
        }
        if (num >= 1000) {
            let k = num / 1000;
            return (k % 1 === 0 ? k : k.toFixed(1)) + 'k';
        }
        return num.toString();
    }

    // ==========================================
    // 2. CSS STYLES
    // ==========================================
    const style = document.createElement('style');
    style.innerHTML = `
        #torn-bank-container {
            position: fixed; width: 70px; box-sizing: border-box;
            background: rgba(22, 22, 22, 0.95); color: #fff; padding: 10px; border-radius: 8px; z-index: 999999;
            box-shadow: 0 4px 15px rgba(0,0,0,0.8); border: 1px solid #444; font-family: sans-serif;
            backdrop-filter: blur(5px); transition: box-shadow 0.3s, border-color 0.3s;
        }

        @keyframes pulseGreenBox {
            0% { box-shadow: 0 0 5px #1a1a1a, 0 0 0px #198754; border-color: #444; }
            50% { box-shadow: 0 0 5px #1a1a1a, 0 0 15px #198754; border-color: #20c997; }
            100% { box-shadow: 0 0 5px #1a1a1a, 0 0 0px #198754; border-color: #444; }
        }
        .notify-blink { animation: pulseGreenBox 1.5s infinite !important; border-color: #20c997 !important; }

        @keyframes pulseGreenIcon {
            0% { text-shadow: 0 0 2px #198754; opacity: 1; transform: scale(1); }
            50% { text-shadow: 0 0 10px #20c997, 0 0 20px #20c997; opacity: 0.8; transform: scale(1.1); }
            100% { text-shadow: 0 0 2px #198754; opacity: 1; transform: scale(1); }
        }
        .notify-blink-icon a { animation: pulseGreenIcon 1.2s infinite !important; }

        .bank-header {
            font-size: 13px; font-weight: bold; color: #f0b90b; text-align: center; margin-bottom: 8px;
            border-bottom: 1px solid #444; padding-bottom: 6px; cursor: move; user-select: none;
            display: flex; justify-content: space-between; align-items: center;
        }
        .drag-icon { color: #888; font-size: 10px; }
        .min-btn { cursor: pointer; color: #bbb; padding: 0 2px; font-size: 12px; line-height: 1; }
        .min-btn:hover { color: #fff; }

        .bank-btn { width: 100%; padding: 6px 0; margin: 4px 0; border: none; border-radius: 4px; background: #333; color: white; cursor: pointer; font-size: 11px; font-weight: bold; transition: 0.2s; text-align: center; }
        .bank-btn:hover:not(:disabled) { background: #444; }
        .bank-btn:disabled { opacity: 0.4; cursor: not-allowed; }
        .btn-check { background: #0d6efd !important; font-size: 14px; }
        .btn-pay { background: #198754 !important; margin-top: 6px; font-size: 14px; }

        #bank-msg { font-size: 10px; text-align: center; margin-top: 5px; min-height: 12px; transition: 0.3s; }
        .msg-success { color: #20c997; } .msg-error { color: #dc3545; }

        #banker-panel { display: none; margin-top: 10px; border-top: 1px dashed #555; padding-top: 8px; }
        .banker-title { font-size: 14px; color: #0dcaf0; font-weight: bold; margin-bottom: 6px; text-align: center; }
        .req-item { display: flex; flex-direction: column; background: #222; padding: 4px; margin-bottom: 5px; border-radius: 4px; border: 1px solid #444; align-items: center; }
        .req-name { color: #fff; font-size: 9px; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 48px; margin-bottom: 2px;}
        .req-amt { color: #f0b90b; font-size: 10px; font-weight: bold; margin-bottom: 3px; }
        .btn-banker-pay { background: #198754; border: none; color: white; padding: 4px 0; border-radius: 3px; cursor: pointer; font-size: 12px; width: 100%; font-weight: bold; }
        .btn-banker-pay:disabled { background: #555; cursor: not-allowed; opacity: 0.6; }
    `;
    document.head.appendChild(style);

    // ==========================================
    // 3. CORE LOGIC & STATE
    // ==========================================
    let factionBalance = 0;
    let requestedAmount = 0;
    let pollInterval = null;
    let isBanker = false;
    let bankerInterval = null;
    let isMinimized = storage.get("bank_ui_minimized", "false") === "true";
    let hasPendingRequests = false;
    let iconInjectAttempts = 0;

    const container = document.createElement('div');
    container.id = 'torn-bank-container';

    let savedX = storage.get("bank_ui_x", "");
    let savedY = storage.get("bank_ui_y", "");
    if (savedX && savedY) {
        container.style.left = savedX; container.style.top = savedY;
    } else {
        container.style.top = "15%"; container.style.right = "15px";
    }
    document.body.appendChild(container);

    function getUserInfo() {
        try {
            const data = JSON.parse(document.getElementById('torn-user').value);
            return { id: data.id, name: data.playername };
        } catch (e) { return { id: null, name: "Unknown" }; }
    }

    function renderUI() {
        container.style.display = isMinimized ? 'none' : 'block';

        container.innerHTML = `
            <div class="bank-header" id="drag-header">
                <span class="drag-icon">⠿</span>
                <span>🏦</span>
                <span class="min-btn" id="btn-minimize" title="Minimize">❌</span>
            </div>

            <div id="bank-body">
                <button class="bank-btn btn-check" id="btn-fetch" title="Check Balance">🔄</button>

                <div id="bank-tools" style="display: none; margin-top: 8px;">
                    <div style="text-align:center; font-size:10px; color:#20c997; font-weight:bold;" id="val-bal">$0</div>
                    <div id="val-req" style="font-size:14px; font-weight:bold; text-align:center; margin:8px 0; color:#fff;">$0</div>

                    <button class="bank-btn" id="add-1">+1m</button>
                    <button class="bank-btn" id="add-10">+10m</button>
                    <button class="bank-btn" id="add-all">MAX</button>
                    <button class="bank-btn" id="btn-clr" style="color:#ff6b6b; background:#2a1a1a;" title="Clear">🗑️</button>

                    <button class="bank-btn btn-pay" id="btn-send" disabled title="Send Request">📤</button>

                    <div id="cancel-container" style="text-align:center; margin-top:4px; display:none;">
                        <a href="javascript:void(0);" id="btn-cancel-bank" style="color:#888; font-size:12px; text-decoration:none; display:inline-block; width:100%;" title="Cancel Request">❌</a>
                    </div>
                    <div id="bank-msg"></div>
                </div>

                <div id="banker-panel">
                    <div class="banker-title">👨‍💼</div>
                    <div id="banker-list" style="max-height: 150px; overflow-y: auto; overflow-x: hidden;">
                        <div style="font-size:10px; text-align:center; color:#888;">...</div>
                    </div>
                </div>
            </div>
        `;

        document.getElementById('btn-minimize').onclick = toggleMinimize;
        document.getElementById('btn-fetch').onclick = fetchBalance;
        document.getElementById('add-1').onclick = () => modifyAmount(1000000);
        document.getElementById('add-10').onclick = () => modifyAmount(10000000);
        document.getElementById('add-all').onclick = () => { requestedAmount = factionBalance; updateDisplay(); };
        document.getElementById('btn-clr').onclick = () => { requestedAmount = 0; updateDisplay(); };
        document.getElementById('btn-send').onclick = sendRequest;
        document.getElementById('btn-cancel-bank').onclick = cancelRequest;

        makeDraggable(container, document.getElementById('drag-header'));
        checkBankerStatus();

        const activeId = storage.get("active_bank_id", null);
        if (activeId) {
            updateDisplay();
            startPolling(activeId);
        }

        setInterval(injectStatusIcon, 1000);
    }

    function cancelRequest() {
        const activeId = storage.get("active_bank_id", null);
        const btn = document.getElementById('btn-cancel-bank');

        if (activeId) {
            if(btn) { btn.innerText = "⏳"; btn.style.pointerEvents = "none"; }

            GM_xmlhttpRequest({
                method: "GET", url: `${SERVER_URL}?action=delete_request&id=${activeId}`,
                onload: (res) => {
                    stopWaiting();
                    showInternalMsg("Canceled!", "error");
                    if(btn) { btn.innerText = "❌"; btn.style.pointerEvents = "auto"; }
                },
                onerror: () => {
                    stopWaiting();
                    if(btn) { btn.innerText = "❌"; btn.style.pointerEvents = "auto"; }
                }
            });
        } else {
            stopWaiting();
        }
    }

    // ==========================================
    // 5. NATIVE STATUS ICON INJECTION (UPDATED)
    // ==========================================
    function injectStatusIcon() {
        if (document.getElementById('bank-status-icon-li')) return;

        const statusUl = document.querySelector('ul[class*="status-icons"]');
        if (!statusUl) {
            iconInjectAttempts++;
            if (iconInjectAttempts > 10 && isMinimized) {
                isMinimized = false;
                storage.set("bank_ui_minimized", "false");
                container.style.display = 'block';
            }
            return;
        }

        const li = document.createElement('li');
        li.id = 'bank-status-icon-li';

        // Cố gắng copy class của một icon đang có sẵn để kế thừa toàn bộ CSS của Torn
        const existingIcon = statusUl.querySelector('li');
        if (existingIcon && existingIcon.className) {
            li.className = existingIcon.className;
        } else {
            // Fallback nếu người chơi đang không có icon trạng thái nào khác
            li.style.cssText = "display: inline-block; vertical-align: top; width: 17px; height: 17px;";
        }

        // Thêm margin-right 10px như bạn yêu cầu, và cursor pointer
        li.style.marginRight = "10px";
        li.style.cursor = "pointer";

        // Dùng Flexbox cho thẻ a để đảm bảo Emoji luông nằm chính giữa LI (cho dù kích thước LI native là bao nhiêu)
        li.innerHTML = `
            <a href="javascript:void(0);" aria-label="Faction Bank" title="Open Bank"
               style="text-decoration: none; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 13px; filter: grayscale(${isMinimized ? '0%' : '100%'});">
               🏦
            </a>`;

        li.onclick = (e) => {
            e.preventDefault();
            toggleMinimize();
        };

        statusUl.prepend(li); // Vẫn ép nó lên vị trí đầu tiên
        updateBlinkState();
    }

    function toggleMinimize() {
        isMinimized = !isMinimized;
        storage.set("bank_ui_minimized", isMinimized.toString());
        container.style.display = isMinimized ? 'none' : 'block';

        const iconA = document.querySelector('#bank-status-icon-li a');
        if (iconA) {
            iconA.style.filter = isMinimized ? 'grayscale(0%)' : 'grayscale(100%)';
        }
    }

    function updateBlinkState() {
        const iconLi = document.getElementById('bank-status-icon-li');
        if (hasPendingRequests) {
            container.classList.add('notify-blink');
            if (iconLi) iconLi.classList.add('notify-blink-icon');
        } else {
            container.classList.remove('notify-blink');
            if (iconLi) iconLi.classList.remove('notify-blink-icon');
        }
    }

    function makeDraggable(elmnt, header) {
        var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        header.onmousedown = dragMouseDown; header.ontouchstart = dragMouseDown;
        function dragMouseDown(e) {
            if (e.target.id === 'btn-minimize') return;
            e = e || window.event;
            pos3 = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
            pos4 = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
            document.onmouseup = closeDragElement; document.ontouchend = closeDragElement;
            document.onmousemove = elementDrag; document.ontouchmove = elementDrag;
        }
        function elementDrag(e) {
            e = e || window.event; e.preventDefault();
            let clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
            let clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
            pos1 = pos3 - clientX; pos2 = pos4 - clientY; pos3 = clientX; pos4 = clientY;
            let newTop = elmnt.offsetTop - pos2; let newLeft = elmnt.offsetLeft - pos1;
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - elmnt.offsetHeight));
            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - elmnt.offsetWidth));
            elmnt.style.top = newTop + "px"; elmnt.style.left = newLeft + "px"; elmnt.style.right = 'auto';
        }
        function closeDragElement() {
            document.onmouseup = null; document.onmousemove = null;
            document.ontouchend = null; document.ontouchmove = null;
            storage.set("bank_ui_x", elmnt.style.left); storage.set("bank_ui_y", elmnt.style.top);
        }
    }

    function showInternalMsg(text, type = 'success') {
        const msgBox = document.getElementById('bank-msg');
        if (msgBox) {
            msgBox.innerText = text;
            msgBox.className = type === 'success' ? 'msg-success' : 'msg-error';
            setTimeout(() => { if(msgBox) msgBox.innerText = ''; }, 4000);
        }
    }

    function fetchBalance() {
        if (!API_KEY) return showInternalMsg("No Key", "error");
        const btn = document.getElementById('btn-fetch');
        btn.innerText = "⏳";
        GM_xmlhttpRequest({
            method: "GET", url: `https://api.torn.com/v2/user/money?key=${API_KEY}`,
            onload: (res) => {
                const data = JSON.parse(res.responseText || res);
                factionBalance = data.money.faction.money || 0;
                document.getElementById('bank-tools').style.display = 'block';
                document.getElementById('val-bal').innerText = '$' + formatM(factionBalance);
                btn.innerText = "🔄";
                updateDisplay();
            }
        });
    }

    function modifyAmount(amt) {
        if (requestedAmount + amt <= factionBalance) requestedAmount += amt;
        else requestedAmount = factionBalance;
        updateDisplay();
    }

    function updateDisplay() {
        const activeId = storage.get("active_bank_id", null);
        const reqEl = document.getElementById('val-req');
        const sendBtn = document.getElementById('btn-send');
        const cancelBox = document.getElementById('cancel-container');
        const toolBtns = ['add-1', 'add-10', 'add-all', 'btn-clr'];

        if (reqEl) reqEl.innerText = '$' + formatM(requestedAmount);

        if (activeId) {
            if (sendBtn) { sendBtn.disabled = true; sendBtn.innerHTML = "⏳"; sendBtn.style.background = "#555"; }
            if (cancelBox) cancelBox.style.display = 'block';
            toolBtns.forEach(id => { let el = document.getElementById(id); if(el) el.disabled = true; });
        } else {
            if (sendBtn) { sendBtn.disabled = (requestedAmount <= 0); sendBtn.innerHTML = "📤"; sendBtn.style.background = ""; }
            if (cancelBox) cancelBox.style.display = 'none';
            toolBtns.forEach(id => { let el = document.getElementById(id); if(el) el.disabled = false; });
        }
    }

    function sendRequest() {
        const user = getUserInfo();
        const btn = document.getElementById('btn-send');
        btn.disabled = true; btn.innerHTML = "⏳";

        GM_xmlhttpRequest({
            method: "POST", url: `${SERVER_URL}?action=create_request`,
            headers: { "Content-Type": "application/json" },
            data: JSON.stringify({ user_id: user.id, username: user.name, amount: requestedAmount }),
            onload: (res) => {
                const data = JSON.parse(res.responseText || res);
                if (data.request_id) {
                    storage.set("active_bank_id", data.request_id);
                    requestedAmount = 0; updateDisplay(); startPolling(data.request_id);
                }
            }
        });
    }

    function startPolling(id) {
        if (pollInterval) clearInterval(pollInterval);
        pollInterval = setInterval(() => {
            GM_xmlhttpRequest({
                method: "GET", url: `${SERVER_URL}?action=check_status&id=${id}`,
                onload: (res) => {
                    const data = JSON.parse(res.responseText || res);
                    if (data.status === 'paid') {
                        stopWaiting(); showInternalMsg("✅", "success"); fetchBalance();
                    } else if (data.status === 'none') {
                        stopWaiting(); alert("❌ Your request was CANCELED! (Insufficient Balance)"); fetchBalance();
                    }
                }
            });
        }, 2000);
    }

    function stopWaiting() {
        if (pollInterval) clearInterval(pollInterval);
        storage.set("active_bank_id", "");
        requestedAmount = 0;
        updateDisplay();
    }

    function checkBankerStatus() {
        if(!API_KEY) return;
        GM_xmlhttpRequest({
            method: "GET", url: `https://api.torn.com/v2/faction/balance?cat=current&key=${API_KEY}`,
            onload: (res) => {
                const data = JSON.parse(res.responseText || res);
                if (!data.error && data.balance) {
                    isBanker = true;
                    document.getElementById('banker-panel').style.display = 'block';
                    loadPendingRequests();
                    if(!bankerInterval) bankerInterval = setInterval(loadPendingRequests, 2000);
                }
            }
        });
    }

    function loadPendingRequests() {
        if(!isBanker) return;
        GM_xmlhttpRequest({
            method: "GET", url: `${SERVER_URL}?action=get_pending`,
            onload: (res) => {
                try {
                    const data = JSON.parse(res.responseText || res);
                    const listEl = document.getElementById('banker-list');

                    if (data.status === 'success') {
                        hasPendingRequests = (data.data.length > 0);
                        updateBlinkState();

                        if(data.data.length === 0) {
                            listEl.innerHTML = '<div style="font-size:10px; text-align:center; color:#888;">...</div>';
                            return;
                        }

                        listEl.innerHTML = '';
                        data.data.forEach(req => {
                            const item = document.createElement('div');
                            item.className = 'req-item';
                            item.innerHTML = `
                                <span class="req-name" title="${req.username}">${req.username}</span>
                                <span class="req-amt">$${formatM(parseInt(req.amount))}</span>
                                <button class="btn-banker-pay" id="pay-${req.id}">💸</button>
                            `;
                            listEl.appendChild(item);
                            document.getElementById(`pay-${req.id}`).onclick = function() { processBankerPayment(this, req); };
                        });
                    }
                } catch(e) {}
            }
        });
    }

    function processBankerPayment(btnEl, req) {
        btnEl.disabled = true; btnEl.innerText = '⏳';

        GM_xmlhttpRequest({
            method: "GET", url: `https://api.torn.com/v2/faction/balance?cat=current&key=${API_KEY}`,
            onload: (res) => {
                const data = JSON.parse(res.responseText || res);
                if(data.error) {
                    btnEl.disabled = false; btnEl.innerText = '💸';
                    return showInternalMsg("Error", "error");
                }
                const targetUser = data.balance.members.find(m => m.id == req.user_id);

                if(!targetUser || targetUser.money < req.amount) {
                    GM_xmlhttpRequest({
                        method: "GET", url: `${SERVER_URL}?action=delete_request&id=${req.id}`,
                        onload: () => {
                            alert(`❌ User ${req.username} Insufficient Balance!\nReq: $${formatM(parseInt(req.amount))}\nCurr: $${targetUser ? formatM(targetUser.money) : 0}\n\n🗑️ Request Auto-Deleted!`);
                            loadPendingRequests();
                        }
                    });
                    return;
                }

                GM_xmlhttpRequest({
                    method: "GET", url: `${SERVER_URL}?action=process_pay&id=${req.id}&ajax=1`,
                    onload: (resp) => {
                        try {
                            const sData = JSON.parse(resp.responseText || resp);
                            if(sData.status === 'success') {
                                if (typeof GM_openInTab === 'function') GM_openInTab(sData.url, { active: true });
                                else window.location.href = sData.url;
                                loadPendingRequests();
                            } else {
                                alert(sData.msg || "Server error!");
                                btnEl.disabled = false; btnEl.innerText = '💸';
                            }
                        } catch (e) {
                            const fallbackUrl = `https://www.torn.com/factions.php?step=your#/tab=controls&option=give-to-user&giveMoneyTo=${req.user_id}&money=${req.amount}`;
                            if (typeof GM_openInTab === 'function') GM_openInTab(fallbackUrl, { active: true });
                            else window.location.href = fallbackUrl;
                        }
                    }
                });
            }
        });
    }

    renderUI();

    GM_registerMenuCommand("🔑 Set API Key", () => {
        let key = prompt("Enter Limited API Key:", storage.get("limited_key", ""));
        if (key) { storage.set("limited_key", key); location.reload(); }
    });
})();