RoC Coordinator

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();