RoC Coordinator

War Coordination System - Fix Username on TornPDA

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         RoC Coordinator
// @namespace    http://tampermonkey.net/
// @version      13.2
// @description  War Coordination System - Fix Username on TornPDA
// @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
// ==/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;
    }

    // ==========================================
    // SERVER CONFIGURATION & UTILS
    // ==========================================
    const API_URL = "https://war.tdkv.io.vn";

    let activeTargets = {};
    let isSyncing = false;
    let processingIds = new Set();

    // Kiểm tra đúng 2 URL được cấp phép
    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");

    // Đã nâng cấp hàm lấy tên (Ưu tiên thẻ input JSON của Torn)
    function getMyUserName() {
        // 1. Lấy từ thẻ hidden input (Hoạt động hoàn hảo trên PDA/Mobile)
        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);
            }
        }

        // 2. Dự phòng 1: DOM trên PC
        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();
            }
        }

        // 3. Dự phòng 2: Link Profile trực tiếp
        let directA = document.querySelector('a[class*="menu-value"][href*="/profiles.php?XID="]');
        if (directA) return directA.innerText.trim();

        // 4. Trường hợp xấu nhất
        return "User_" + myUserId;
    }

    // ==========================================
    // 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-header {
                width: 60px !important; min-width: 60px !important; text-align: center; font-weight: bold;
                display: flex; align-items: center; justify-content: center;
                color: #ccc; font-size: 10px; letter-spacing: 0px;
                padding-right: 5px; flex-shrink: 0 !important;
            }
            .dibs-col {
                width: 60px !important; min-width: 60px !important; display: flex; flex-direction: column;
                justify-content: center; align-items: center; gap: 4px; padding: 2px;
                flex-shrink: 0 !important;
            }
        `;
        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; // Thay đổi kiểm tra URL tại đây
        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')) {
                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');
                }
            }
        }

        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;

            // Kiểm tra xem mục tiêu có link để attack không (phân biệt Attack sáng/mờ)
            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');
            }

            // Nếu đang trong viện HOẶC không thể tấn công (chữ mờ), không inject buttons
            if (isHospital || !isAttackable) {
                let stateName = isHospital ? 'hospital' : 'unattackable';
                let displayText = isHospital ? 'Hospital' : '-'; // Hiển thị '-' nếu không attack được

                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" style="background:#dc3545; color:white; border:none; padding:3px; border-radius:3px; cursor:pointer; font-weight:bold; font-size:11px; width:100%; letter-spacing: 0.5px;">ATTK</button>
                    <button class="btn-assist" style="background:#ffc107; color:black; border:none; padding:3px; border-radius:3px; cursor:pointer; font-weight:bold; font-size:11px; width:100%; letter-spacing: 0.5px;">ASST</button>
                `;
            } else if (targetData.status === 'dibbed') {
                if(targetData.user_id == myUserId) {
                    newState = 'my-dib';
                    newHTML = `
                        <span style="font-size:10px; background:#17a2b8; color:white; padding:3px; border-radius:3px; text-align:center; width:100%; box-sizing:border-box;">YOURS</span>
                        <button class="btn-assist" style="background:#ffc107; color:black; border:none; padding:3px; border-radius:3px; cursor:pointer; font-weight:bold; font-size:11px; width:100%; letter-spacing: 0.5px;">ESCL</button>
                    `;
                } else {
                    newState = `busy-${targetData.user_id}`;
                    newHTML = `<span style="font-size:10px; background:#6c757d; color:white; padding:4px 2px; border-radius:3px; text-align:center; width:100%; box-sizing:border-box; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${targetData.user_name}</span>`;
                }
            } else if (targetData.status === 'assist') {
                if (targetData.slots_filled >= targetData.slots_max) {
                    newState = `assist-full`;
                    newHTML = `<span style="font-size:11px; background:#343a40; color:white; padding:4px; border-radius:3px; text-align:center; font-weight:bold; width:100%; box-sizing:border-box;">${targetData.slots_filled}/${targetData.slots_max}</span>`;
                } else {
                    newState = `assist-${targetData.slots_filled}`;
                    newHTML = `<button class="btn-join" style="background:#28a745; color:white; border:none; padding:4px; border-radius:3px; cursor:pointer; font-weight:bold; font-size:11px; animation: blink 1s infinite; width:100%;">JOIN ${targetData.slots_filled}/${targetData.slots_max}</button>`;
                }
            }

            if (btnContainer.dataset.state !== newState) {
                btnContainer.innerHTML = newHTML;
                btnContainer.dataset.state = newState;
                attachEvents(btnContainer, targetId, newState);
            }
        });
    }

    // ==========================================
    // 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; // Thay đổi kiểm tra URL tại đây
        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});

})();