Greasy Fork is available in English.
War Coordination System - Fix Username on TornPDA
// ==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});
})();