RoC Coordinator

War Coordination System - Fix Username on TornPDA

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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

})();