RoC Coordinator

War Coordination System - Fix Username on TornPDA

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();