RoC Coordinator

War Coordination System - Compact Square Buttons UI with Dynamic YATA Dual Range Stat Filter & Cache

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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});

})();