Greasy Fork is available in English.
70px UI, Sync with Native Icon CSS classes, Working Cancel
// ==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(); }
});
})();