War Coordination System - Compact Square Buttons UI with Dynamic YATA Dual Range Stat Filter & Cache
// ==UserScript==
// @name RoC Coordinator
// @namespace http://tampermonkey.net/
// @version 13.16
// @description War Coordination System - Compact Square Buttons UI with Dynamic YATA Dual Range Stat Filter & Cache
// @author You
// @license RoC
// @match https://www.torn.com/factions.php*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @connect war.tdkv.io.vn
// @connect yata.yt
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// TORN PDA COMPATIBILITY BLOCK
// ==========================================
var rD_xmlhttpRequest;
var rD_setValue;
var rD_getValue;
var rD_listValues;
var rD_deleteValue;
var rD_registerMenuCommand;
// DO NOT CHANGE THIS
var apikey = "###PDA-APIKEY###";
// DO NOT CHANGE THIS
if (apikey[0] != "#") {
console.log("[RoC Coord] Adding modifications to support TornPDA");
rD_xmlhttpRequest = function (details) {
if (details.method.toLowerCase() == "get") {
return PDA_httpGet(details.url)
.then(res => {
let normalizedRes = typeof res === 'string' ? { responseText: res } : res;
if (details.onload) details.onload(normalizedRes);
})
.catch(details.onerror ?? ((e) => console.error("[RoC Coord] PDA GET Error: ", e)));
} else if (details.method.toLowerCase() == "post") {
return PDA_httpPost(
details.url,
details.headers ?? {},
details.body ?? details.data ?? ""
)
.then(res => {
let normalizedRes = typeof res === 'string' ? { responseText: res } : res;
if (details.onload) details.onload(normalizedRes);
})
.catch(details.onerror ?? ((e) => console.error("[RoC Coord] PDA POST Error: ", e)));
}
};
rD_setValue = function (name, value) { return localStorage.setItem(name, value); };
rD_getValue = function (name, defaultValue) { return localStorage.getItem(name) ?? defaultValue; };
rD_listValues = function () {
const keys = [];
for (const key in localStorage) { if (localStorage.hasOwnProperty(key)) keys.push(key); }
return keys;
};
rD_deleteValue = function (name) { return localStorage.removeItem(name); };
rD_registerMenuCommand = function () { console.log("[RoC Coord] Disabling GM_registerMenuCommand in PDA"); };
rD_setValue("limited_key", apikey);
} else {
rD_xmlhttpRequest = GM_xmlhttpRequest;
rD_setValue = GM_setValue;
rD_getValue = GM_getValue;
rD_listValues = GM_listValues;
rD_deleteValue = GM_deleteValue;
rD_registerMenuCommand = GM_registerMenuCommand;
apikey = rD_getValue("limited_key", "");
}
rD_registerMenuCommand("Enter Limited API Key", () => {
let userInput = prompt(
"[RoC Coord]: Enter Limited API Key",
rD_getValue("limited_key", ""),
);
if (userInput !== null) {
rD_setValue("limited_key", userInput);
window.location.reload();
}
});
// ==========================================
// SERVER CONFIGURATION & UTILS
// ==========================================
const API_URL = "https://war.tdkv.io.vn";
let activeTargets = {};
let isSyncing = false;
let processingIds = new Set();
// ==========================================
// YATA STAT FILTER VARIABLES & UTILS
// ==========================================
let yataStats = {};
let currentFactionId = null;
let isFetchingYata = false;
// Tải cấu hình filter cũ lên (lưu dưới dạng chuỗi để tương thích cả PDA và PC)
let savedMinStr = rD_getValue("roc_filter_min", "0");
let savedMaxStr = rD_getValue("roc_filter_max", "Infinity");
let statFilterMin = savedMinStr === "Infinity" ? Infinity : parseFloat(savedMinStr);
let statFilterMax = savedMaxStr === "Infinity" ? Infinity : parseFloat(savedMaxStr);
const statSteps = [
0, 1000, 10000, 50000, 100000, 250000, 500000,
1000000, 2500000, 5000000, 10000000, 25000000, 50000000,
100000000, 250000000, 500000000, 1000000000, 2500000000, 5000000000,
10000000000, 25000000000, 50000000000, 100000000000, Infinity
];
let dynamicMaxIdx = statSteps.length - 1; // Default to Infinity
function formatNumberStr(val) {
if (val === Infinity) return "All";
if (val === 0) return "0";
if (val >= 1e9) return (val / 1e9).toFixed(val % 1e9 !== 0 ? 1 : 0).replace('.0', '') + "b";
if (val >= 1e6) return (val / 1e6).toFixed(val % 1e6 !== 0 ? 1 : 0).replace('.0', '') + "m";
if (val >= 1e3) return (val / 1e3).toFixed(val % 1e3 !== 0 ? 1 : 0).replace('.0', '') + "k";
return val.toString();
}
function isAllowedUrl() {
const url = window.location.href;
return url.includes('factions.php?step=your&type=1#/war/rank') ||
url.includes('factions.php?step=profile&ID=23188#/war/rank');
}
function getCookie(name) {
let match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : "Unknown";
}
const myUserId = getCookie("uid");
function getMyUserName() {
let tornUserInp = document.getElementById('torn-user');
if (tornUserInp && tornUserInp.value) {
try {
let userData = JSON.parse(tornUserInp.value);
if (userData && userData.playername) {
return userData.playername.trim();
}
} catch (e) {
console.error("[RoC Coord] Error parsing #torn-user JSON:", e);
}
}
let nameRowList = document.querySelectorAll('p[class*="menu-info-row"]');
for (let row of nameRowList) {
let span = row.querySelector('span[class*="menu-name"]');
let a = row.querySelector('a[class*="menu-value"]');
if (span && span.innerText.includes('Name:') && a) {
return a.innerText.trim();
}
}
let directA = document.querySelector('a[class*="menu-value"][href*="/profiles.php?XID="]');
if (directA) return directA.innerText.trim();
return "User_" + myUserId;
}
// ==========================================
// YATA DATA FUNCTIONS
// ==========================================
function getOpponentFactionId() {
let link = document.querySelector('a.opponentFactionName___vhESM[href*="ID="]');
if (link) {
let match = link.href.match(/ID=(\d+)/);
if (match) return match[1];
}
return null;
}
function processYataData() {
// Tìm Max Total trong data
let maxTotal = 0;
for (let id in yataStats) {
if (yataStats[id].total && yataStats[id].total > maxTotal) {
maxTotal = yataStats[id].total;
}
}
// Chọn index mốc giới hạn cho thanh kéo
let idx = statSteps.findIndex(val => val >= maxTotal);
if (idx === -1 || maxTotal === 0) idx = statSteps.length - 1;
dynamicMaxIdx = idx;
let minSlider = document.getElementById('roc-filter-min');
let maxSlider = document.getElementById('roc-filter-max');
if (minSlider && maxSlider) {
minSlider.max = dynamicMaxIdx;
maxSlider.max = dynamicMaxIdx;
// Chuyển mức stat đã lưu trước đó thành index của slider hiện hành
let minIdx = statSteps.findIndex(v => v >= statFilterMin);
if (minIdx === -1 || minIdx > dynamicMaxIdx) minIdx = 0;
let maxIdx = statSteps.findIndex(v => v >= statFilterMax);
if (maxIdx === -1 || maxIdx > dynamicMaxIdx) maxIdx = dynamicMaxIdx;
minSlider.value = minIdx;
maxSlider.value = maxIdx;
// Kích hoạt event để render UI slider text
minSlider.dispatchEvent(new Event('input'));
}
applyStatFilter();
}
async function fetchYataStats(factionId) {
if (!apikey || apikey === "" || apikey.includes("PDA-APIKEY")) {
console.warn("[RoC Coord] Valid API Key needed for YATA stats filtering.");
let statusSpan = document.getElementById('roc-filter-status');
if(statusSpan) {
statusSpan.innerText = "No API Key";
statusSpan.style.color = "#dc3545";
}
return;
}
// KIỂM TRA CACHE TRƯỚC (Hạn 3 ngày = 3 * 24 * 60 * 60 * 1000 = 259200000 ms)
let cachedData = rD_getValue("roc_yata_cache", null);
if (cachedData) {
try {
let parsed = JSON.parse(cachedData);
if (parsed && parsed.factionId === factionId && (Date.now() - parsed.timestamp) < 259200000) {
yataStats = parsed.data;
processYataData();
return; // Nếu lấy được cache hợp lệ thì dừng, không gọi web nữa
}
} catch(e) {
console.error("[RoC Coord] Error parsing cache:", e);
}
}
if (isFetchingYata) return;
isFetchingYata = true;
try {
let url = `https://yata.yt/api/v1/spies/?key=${apikey}&faction=${factionId}`;
let res = await new Promise((resolve, reject) => {
rD_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) { resolve(response); },
onerror: function(err) { reject(err); }
});
});
if (res && res.responseText) {
let json = JSON.parse(res.responseText);
if (json && json.spies) {
yataStats = json.spies;
// LƯU CACHE MỚI
rD_setValue("roc_yata_cache", JSON.stringify({
factionId: factionId,
timestamp: Date.now(),
data: yataStats
}));
processYataData();
}
}
} catch(e) {
console.error("[RoC Coord] YATA fetch error:", e);
}
isFetchingYata = false;
}
function injectFilterUI(headerRow) {
if (document.getElementById('roc-stat-filter')) return;
let filterContainer = document.createElement('div');
filterContainer.id = 'roc-stat-filter';
filterContainer.style.cssText = 'display: flex; align-items: center; justify-content: flex-start; gap: 6px; padding: 4px 6px; background: #222; border-bottom: 1px solid #444; color: #ddd; font-size: 11px; margin-bottom: 4px; border-radius: 4px; width: 100%; box-sizing: border-box;';
// Lấy index ban đầu dựa theo thông số lưu (trong trường hợp cache chưa load xong)
let initMinIdx = statSteps.findIndex(v => v >= statFilterMin);
if(initMinIdx === -1) initMinIdx = 0;
let initMaxIdx = statSteps.findIndex(v => v >= statFilterMax);
if(initMaxIdx === -1) initMaxIdx = dynamicMaxIdx;
filterContainer.innerHTML = `
<strong>Stat:</strong>
<div style="min-width: 65px; text-align: center; background: #111; padding: 2px 4px; border-radius: 4px; font-weight: bold; color: #fff;">
<span id="roc-slider-val">${formatNumberStr(statSteps[initMinIdx])} - ${formatNumberStr(statSteps[initMaxIdx])}</span>
</div>
<div class="roc-dual-slider-container">
<div class="roc-slider-track"></div>
<input type="range" id="roc-filter-min" min="0" max="${dynamicMaxIdx}" value="${initMinIdx}">
<input type="range" id="roc-filter-max" min="0" max="${dynamicMaxIdx}" value="${initMaxIdx}">
</div>
<span id="roc-filter-status" style="margin-left: auto; color: #aaa; font-size: 10px; white-space: nowrap;">Wait...</span>
`;
headerRow.parentNode.insertBefore(filterContainer, headerRow);
let minSlider = document.getElementById('roc-filter-min');
let maxSlider = document.getElementById('roc-filter-max');
let displayVal = document.getElementById('roc-slider-val');
function updateSliderUI() {
let minIdx = parseInt(minSlider.value);
let maxIdx = parseInt(maxSlider.value);
if (minIdx > maxIdx) {
let tmp = minIdx;
minIdx = maxIdx;
maxIdx = tmp;
minSlider.value = minIdx;
maxSlider.value = maxIdx;
}
displayVal.innerText = formatNumberStr(statSteps[minIdx]) + " - " + formatNumberStr(statSteps[maxIdx]);
}
function onSliderChange() {
let minIdx = parseInt(minSlider.value);
let maxIdx = parseInt(maxSlider.value);
statFilterMin = statSteps[minIdx];
statFilterMax = statSteps[maxIdx];
// Lưu cài đặt kéo thả ngay lập tức
rD_setValue("roc_filter_min", statFilterMin.toString());
rD_setValue("roc_filter_max", statFilterMax.toString());
applyStatFilter();
}
minSlider.addEventListener('input', updateSliderUI);
maxSlider.addEventListener('input', updateSliderUI);
minSlider.addEventListener('change', onSliderChange);
maxSlider.addEventListener('change', onSliderChange);
}
function applyStatFilter() {
let enemyRows = document.querySelectorAll('li.enemy');
let statusSpan = document.getElementById('roc-filter-status');
let hiddenCount = 0;
let hasData = Object.keys(yataStats).length > 0;
if (statusSpan) {
if (hasData) {
let cacheDateCheck = rD_getValue("roc_yata_cache", null);
let isCached = false;
if(cacheDateCheck) {
try { isCached = JSON.parse(cacheDateCheck).factionId === currentFactionId; } catch(e){}
}
statusSpan.innerText = isCached ? "Cache Loaded" : "YATA Loaded";
statusSpan.style.color = "#28a745";
}
}
enemyRows.forEach(li => {
let targetLink = li.querySelector('a[href*="user2ID="]');
if (!targetLink) return;
let targetId = new URLSearchParams(targetLink.href.split('?')[1]).get('user2ID');
let shouldHide = false;
if (statFilterMin > 0 || statFilterMax < Infinity) {
if (yataStats && yataStats[targetId] && yataStats[targetId].total !== undefined) {
let totalStat = yataStats[targetId].total;
if (totalStat < statFilterMin || totalStat > statFilterMax) {
shouldHide = true;
}
} else {
shouldHide = true;
}
}
// Tối ưu để không gọi style liên tục làm chớp giao diện (Flicker Fix)
if (shouldHide) {
hiddenCount++;
if (li.style.display !== 'none') {
li.style.setProperty('display', 'none', 'important');
}
} else {
if (li.style.display === 'none') {
li.style.removeProperty('display');
}
}
});
if (statusSpan && (statFilterMin > 0 || statFilterMax < Infinity)) {
statusSpan.innerText = `${hiddenCount} hidden`;
statusSpan.style.color = "#ffc107";
}
}
// ==========================================
// CSS INJECTION (Responsive Mobile/PC)
// ==========================================
if (!document.getElementById('coord-styles')) {
const style = document.createElement('style');
style.id = 'coord-styles';
style.innerHTML = `
@keyframes blink { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.05); } 100% { opacity: 1; transform: scale(1); } }
@media screen and (min-width: 784px) {
.enemy-faction {
max-width: calc(50% - 4px) !important;
box-sizing: border-box !important;
}
}
.dibs-flex-row {
display: flex !important;
flex-wrap: nowrap !important;
width: 100% !important;
box-sizing: border-box !important;
}
.dibs-flex-row > div {
flex-shrink: 1 !important;
min-width: 0 !important;
}
.dibs-flex-row .member, .dibs-flex-row .status {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.dibs-flex-row > .clear,
.dibs-flex-row > .tt-stats-estimate,
.dibs-flex-row > div[class*="tt-stats"] {
display: none !important;
width: 0 !important;
height: 0 !important;
position: absolute !important;
opacity: 0 !important;
pointer-events: none !important;
}
.dibs-flex-row .level {
width: 32px !important;
min-width: 32px !important;
flex-basis: 32px !important;
padding: 0 2px !important;
text-align: center !important;
}
.white-grad.dibs-flex-row {
height: 34px !important;
min-height: 34px !important;
align-items: center !important;
}
.dibs-header {
width: 68px !important; min-width: 68px !important; flex-basis: 68px !important;
text-align: center;
display: flex; align-items: center; justify-content: center;
color: yellow !important;
padding-right: 5px; flex-shrink: 0 !important;
}
.dibs-col {
width: 68px !important; min-width: 68px !important; flex-basis: 68px !important;
display: flex; flex-direction: row !important; flex-wrap: nowrap !important;
justify-content: center; align-items: center; gap: 2px !important; padding: 2px 0;
flex-shrink: 0 !important;
}
.coord-btn-sq {
width: 28px !important;
height: 28px !important;
min-width: 28px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important; margin: 0 !important;
border: none !important; border-radius: 4px !important;
font-weight: bold !important; font-size: 10px !important;
cursor: pointer !important; box-sizing: border-box !important;
}
.coord-btn-full {
width: 100% !important;
height: 28px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 4px !important; margin: 0 !important;
border: none !important; border-radius: 4px !important;
font-weight: bold !important; font-size: 10px !important;
cursor: pointer !important; box-sizing: border-box !important;
overflow: hidden !important; text-overflow: ellipsis !important;
white-space: nowrap !important;
}
/* Expanded Dual Slider Custom CSS */
.roc-dual-slider-container {
position: relative;
flex-grow: 1;
min-width: 60px;
margin: 0 8px;
height: 20px;
display: flex;
align-items: center;
}
.roc-dual-slider-container input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
position: absolute;
top: 0;
height: 20px;
background: transparent;
pointer-events: none;
margin: 0;
outline: none;
}
.roc-dual-slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
pointer-events: all;
width: 12px;
height: 12px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
position: relative;
z-index: 2;
margin-top: 4px;
border: 1px solid #fff;
}
.roc-dual-slider-container input[type="range"]::-moz-range-thumb {
pointer-events: all;
width: 12px;
height: 12px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
position: relative;
z-index: 2;
border: 1px solid #fff;
}
.roc-slider-track {
position: absolute;
top: 8px;
left: 0;
width: 100%;
height: 4px;
background: #555;
border-radius: 2px;
z-index: 1;
}
`;
document.head.appendChild(style);
}
// ==========================================
// API FUNCTIONS
// ==========================================
function apiRequest(method, endpoint, data = null) {
return new Promise((resolve, reject) => {
let options = {
method: method,
url: `${API_URL}/${endpoint}`,
headers: { "Content-Type": "application/json" },
onload: function(response) {
let rawText = (response && response.responseText) ? response.responseText.trim() : "";
if (!rawText) return resolve({status: 'success', data: []});
try {
let json = JSON.parse(rawText);
resolve(json);
} catch (e) {
reject("JSON Parse Error");
}
},
onerror: function(err) {
reject("Server connection error");
}
};
if (data) options.data = JSON.stringify(data);
rD_xmlhttpRequest(options);
});
}
async function sendAction(action, target_id, slots = 1) {
try {
return await apiRequest('POST', 'action.php', {
action, target_id, user_id: myUserId, user_name: getMyUserName(), slots
});
} catch(e) {
return {status: 'error', message: e};
}
}
async function syncData() {
if (isSyncing || !isAllowedUrl()) return;
isSyncing = true;
try {
let res = await apiRequest('GET', `sync.php?t=${Date.now()}`);
if(res && res.status === 'success' && res.data) {
activeTargets = {};
res.data.forEach(t => { activeTargets[t.target_id] = t; });
renderUI();
}
} catch(e) {}
isSyncing = false;
}
// ==========================================
// RENDER UI
// ==========================================
function renderUI() {
let enemyRows = document.querySelectorAll('li.enemy');
if (enemyRows.length === 0) return;
let enemyList = document.querySelector('ul.members-list:has(li.enemy)');
if (enemyList) {
let headerRow = enemyList.previousElementSibling;
if (headerRow && headerRow.classList.contains('white-grad') && !headerRow.querySelector('.dibs-header')) {
injectFilterUI(headerRow);
let attackHeader = headerRow.querySelector('.attack, [class*="attack"]');
if (attackHeader) {
let dibsHeader = document.createElement('div');
dibsHeader.className = 'dibs-header left';
dibsHeader.innerText = 'COORD';
headerRow.insertBefore(dibsHeader, attackHeader);
headerRow.classList.add('dibs-flex-row');
}
}
}
let oppFactionId = getOpponentFactionId();
if (oppFactionId && oppFactionId !== currentFactionId) {
currentFactionId = oppFactionId;
fetchYataStats(oppFactionId);
}
enemyRows.forEach(li => {
let targetLink = li.querySelector('a[href*="user2ID="]');
if(!targetLink) return;
let targetId = new URLSearchParams(targetLink.href.split('?')[1]).get('user2ID');
let statusDiv = li.querySelector('.status, [class*="status"]');
let attackCell = li.querySelector('.attack, [class*="attack"]');
if(!statusDiv || !attackCell) return;
let isAttackable = attackCell.querySelector('a') !== null;
if (processingIds.has(targetId)) return;
let isHospital = statusDiv.innerText.trim().toLowerCase().includes('hospital');
if(isHospital && activeTargets[targetId]) {
sendAction('clear', targetId);
delete activeTargets[targetId];
}
let btnContainer = li.querySelector('.dibs-col');
if(!btnContainer) {
btnContainer = document.createElement('div');
btnContainer.className = 'dibs-col coord-btns left';
li.insertBefore(btnContainer, attackCell);
li.classList.add('dibs-flex-row');
}
if (isHospital || !isAttackable) {
let stateName = isHospital ? 'hospital' : 'unattackable';
let displayText = isHospital ? 'Hospital' : '-';
if (btnContainer.dataset.state !== stateName) {
btnContainer.innerHTML = `<span style="font-size:10px; color:#555;">${displayText}</span>`;
btnContainer.dataset.state = stateName;
}
return;
}
let targetData = activeTargets[targetId];
let newState = '';
let newHTML = '';
if(!targetData) {
newState = 'idle';
newHTML = `
<button class="btn-dibs coord-btn-sq" style="background:#dc3545; color:white;">ATK</button>
<button class="btn-assist coord-btn-sq" style="background:#ffc107; color:black;">AST</button>
`;
} else if (targetData.status === 'dibbed') {
if(targetData.user_id == myUserId) {
newState = 'my-dib';
newHTML = `
<span class="coord-btn-sq" style="background:#17a2b8; color:white; font-size:9px;">YOU</span>
<button class="btn-assist coord-btn-sq" style="background:#ffc107; color:black; font-size:9px;">ESC</button>
`;
} else {
newState = `busy-${targetData.user_id}`;
newHTML = `<span class="coord-btn-full" style="background:#6c757d; color:white;">${targetData.user_name}</span>`;
}
} else if (targetData.status === 'assist') {
if (targetData.slots_filled >= targetData.slots_max) {
newState = `assist-full`;
newHTML = `<span class="coord-btn-full" style="background:#343a40; color:white;">${targetData.slots_filled}/${targetData.slots_max}</span>`;
} else {
newState = `assist-${targetData.slots_filled}`;
newHTML = `<button class="btn-join coord-btn-full" style="background:#28a745; color:white; animation: blink 1s infinite;">JOIN ${targetData.slots_filled}/${targetData.slots_max}</button>`;
}
}
if (btnContainer.dataset.state !== newState) {
btnContainer.innerHTML = newHTML;
btnContainer.dataset.state = newState;
attachEvents(btnContainer, targetId, newState);
}
});
applyStatFilter();
}
// ==========================================
// EVENTS
// ==========================================
function attachEvents(container, targetId, state) {
if (state === 'idle') {
container.querySelector('.btn-dibs').onclick = async (e) => {
e.preventDefault();
if (processingIds.has(targetId)) return;
processingIds.add(targetId);
container.innerHTML = `<span style="font-size:10px; color:gray;">Wait...</span>`;
let res = await sendAction('dibs', targetId);
processingIds.delete(targetId);
if(res && res.status === 'success') {
window.open(`https://www.torn.com/loader.php?sid=attack&user2ID=${targetId}`, '_blank');
syncData();
} else { alert(res ? res.message : "Error connecting to server."); syncData(); }
};
container.querySelector('.btn-assist').onclick = async (e) => {
e.preventDefault();
let slots = prompt("How many assist slots do you need?", "2");
if(slots && !isNaN(slots) && parseInt(slots) > 0) {
if (processingIds.has(targetId)) return;
processingIds.add(targetId);
container.innerHTML = `<span style="font-size:10px; color:gray;">Wait...</span>`;
let res = await sendAction('request_assist', targetId, parseInt(slots));
processingIds.delete(targetId);
if(res && res.status === 'success') {
window.open(`https://www.torn.com/loader.php?sid=attack&user2ID=${targetId}`, '_blank');
syncData();
} else { alert(res ? res.message : "Error connecting to server."); syncData(); }
}
};
} else if (state === 'my-dib') {
container.querySelector('.btn-assist').onclick = async (e) => {
e.preventDefault();
let slots = prompt("Escalate: How many more assist slots?", "2");
if(slots && !isNaN(slots)) {
if (processingIds.has(targetId)) return;
processingIds.add(targetId);
await sendAction('request_assist', targetId, parseInt(slots));
processingIds.delete(targetId);
syncData();
}
};
} else if (state.startsWith('assist-') && state !== 'assist-full') {
container.querySelector('.btn-join').onclick = async (e) => {
e.preventDefault();
if (processingIds.has(targetId)) return;
processingIds.add(targetId);
let res = await sendAction('join_assist', targetId);
processingIds.delete(targetId);
if(res && res.status === 'success') {
window.open(`https://www.torn.com/loader.php?sid=attack&user2ID=${targetId}`, '_blank');
syncData();
} else { alert(res ? res.message : "Error or slots are full."); syncData(); }
};
}
}
// ==========================================
// INITIALIZATION & OBSERVERS
// ==========================================
setInterval(syncData, 1500);
let renderTimeout;
const observer = new MutationObserver((mutations) => {
if(!isAllowedUrl()) return;
let isOwnMutation = mutations.every(m => m.target.classList && m.target.classList.contains('coord-btns'));
if (isOwnMutation) return;
clearTimeout(renderTimeout);
renderTimeout = setTimeout(renderUI, 300);
});
observer.observe(document.body, {childList: true, subtree: true});
})();