您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
show tornstats spies on faction wall page
// ==UserScript== // @name dont-touch-my-wall-battlestats // @namespace akm.torn.dont-touch-my-battlestats // @version 0.3 // @description show tornstats spies on faction wall page // @author Anonknee Moose // @license GNU GPLv3 // @run-at document-end // @match https://www.torn.com/factions.php* // @require https://update.greasyfork.org/scripts/493751/1368224/dont-touch-my-apikey-config.js // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect tornstats.com // ==/UserScript== // License information: https://www.gnu.org/licenses/gpl-3.0.html // License summary: You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. // Usage request: Under this license you are not required to request access to use this software. You are free to use it as you see fit. // Warranty: This software is provided as-is with no warranty or guarantee of support. Use at your own risk. // The why: When this script was originally copied it was in the public domain under the same license // as the original author's other scripts. The original author has been actively trying to remove // their scripts from the internet. This script is being maintained to keep it available for users // who still find it useful. If the original author would like this script removed, please contact // Greasy Fork with a proper reason, and they will remove it if they see fit. // If you are the original author and would like to take over maintenance of this script, please // contact Greasy Fork, and they will transfer ownership to you with my prior consent. // If you are the original author and would like to discuss the license or any other matter, please // contact me through Greasy Fork and I will respond as soon as possible. // Changes: This script has been modified to use the API key configuration script to allow for easier // configuration of the API key. The original script had the API key hardcoded in the script. // The original script had a bug where the API key was not being saved to local storage. // The original script had a bug where the faction sort was not being saved to local storage. // The original script had a bug where the faction filter from and to values were not being saved to local storage. // The original script had a bug where the faction filter from and to values were not being properly formatted. // The original script used the term "finally" which is a reserved word in JavaScript. This has been changed to "dtmb". let tornapiKeyHere = "Enter your TORN API key here"; /* * ------------------------------------------------------------------------- * | DO NOT MODIFY BELOW | * ------------------------------------------------------------------------- */ let apiKey = tornapiKeyHere?.length == 16 ? tornapiKeyHere : localStorage["dtmb.torn.api"]; if (!apiKey) { alert('Error: Please enter your API key in the script.'); } localStorage.setItem("dtmb.torn.api", apiKey || ""); let bsCache = JSONparse(localStorage["dtmb.torn.bs"]) || {}; let hospTime = {}; let previousSort = parseInt(localStorage.getItem("dtmb.torn.factionSort")) || 1; let filterFrom = parseInt(localStorage.getItem("dtmb.torn.factionFilterFrom")) || undefined; let filterTo = parseInt(localStorage.getItem("dtmb.torn.factionFilterTo")) || undefined; let loadTSFactionLock = false; let loadTSFactionBacklog = []; let loadTSFactionDone = []; let hospLoopCounter = 0; const hospNodes = []; function JSONparse(str) { try { return JSON.parse(str); } catch (e) { console.log(e); } return null; } function loadTSFactionsDone() { loadTSFactionLock = false; loadTSFactions(); } function loadTSFactions(id) { if (loadTSFactionLock) { if ( id && loadTSFactionDone.indexOf(id) === -1 && loadTSFactionBacklog.indexOf(id) === -1 ) loadTSFactionBacklog.push(id); return; } if (!id && loadTSFactionBacklog.length == 0) { showStatsAll(); return; } loadTSFactionLock = true; id = id || loadTSFactionBacklog.shift(); loadTSFactionDone.push(id); GM_xmlhttpRequest({ method: "GET", url: `https://www.tornstats.com/api/v2/${apiKey}/spy/faction/${id}`, onload: (r) => { let j = JSONparse(r.responseText); if (!j || !j.status || !j.faction) { loadTSFactionsDone(); return; } Object.keys(j.faction.members).forEach((k) => addSpy(k, j.faction.members[k].spy) ); localStorage["dtmb.torn.bs"] = JSON.stringify(bsCache); loadTSFactionsDone(); }, onabort: () => loadTSFactionsDone(), onerror: () => loadTSFactionsDone(), ontimeout: () => loadTSFactionsDone(), }); } function loadFactions() { let factionIds = Array.from( document.querySelectorAll("[href^='/factions.php?step=profile&ID=']") ) .map((a) => a.href.replace(/.*?ID=(\d+)$/, "$1")) .filter((v, i, a) => a.indexOf(v) === i); factionIds.forEach((id) => loadTSFactions(id)); } function sortStats(node, sort) { if (!node) node = document.querySelector(".f-war-list .members-list"); if (!node) return; let sortIcon = node.parentNode.querySelector(".bs > [class*='sortIcon']"); if (sort) node.dtmbSort = sort; else if (node.dtmbSort == undefined) node.dtmbSort = 2; else if (++node.dtmbSort > 2) node.dtmbSort = sortIcon ? 1 : 0; if (sortIcon) { if (node.dtmbSort > 0) { let active = node.parentNode.querySelector( "[class*='activeIcon']:not([class*='dtmb-bs-activeIcon'])" ); if (active) { let activeClass = active.className.match( /(?:\s|^)(activeIcon(?:[^\s|$]+))(?:\s|$)/ )[1]; active.classList.remove(activeClass); } sortIcon.classList.add("dtmb-bs-activeIcon"); if (node.dtmbSort == 1) { sortIcon.classList.remove("dtmb-bs-desc"); sortIcon.classList.add("dtmb-bs-asc"); } else { sortIcon.classList.remove("dtmb-bs-asc"); sortIcon.classList.add("dtmb-bs-desc"); } } else { sortIcon.classList.remove("dtmb-bs-activeIcon"); } } let nodes = Array.from( node.querySelectorAll( ".table-body > .table-row, .your:not(.row-animation-new), .enemy:not(.row-animation-new)" ) ); for (let i = 0; i < nodes.length; i++) if (nodes[i].dtmbPos == undefined) nodes[i].dtmbPos = i; nodes = nodes.sort((a, b) => { let posA = a.dtmbPos; let idA = a .querySelector('a[href*="XID"]') .href.replace(/.*?XID=(\d+)/i, "$1"); let totalA = (bsCache[idA] && typeof bsCache[idA].total == "number" && bsCache[idA].total) || posA; let posB = b.dtmbPos; let idB = b .querySelector('a[href*="XID"]') .href.replace(/.*?XID=(\d+)/i, "$1"); let totalB = (bsCache[idB] && typeof bsCache[idB].total == "number" && bsCache[idB].total) || posB; let type = node.dtmbSort; switch (node.dtmbSort) { case 1: if (totalA <= 100 && totalB <= 100) return totalB > totalA ? 1 : -1; return totalA > totalB ? 1 : -1; case 2: return totalB > totalA ? 1 : -1; default: return posA > posB ? 1 : -1; } }); for (let i = 0; i < nodes.length; i++) nodes[i].parentNode.appendChild(nodes[i]); if (!sort) { document.querySelectorAll(".members-list").forEach((e) => { if (node != e) sortStats(e, node.dtmbSort); }); } } function addSpy(id, spy) { if (!spy) return; bsCache[id] = spy; } function updateStats(id, node, parentNode) { if (!node) return; let stats = ["N/A", "N/A", "N/A", "N/A", "N/A"]; let time = ""; if (bsCache[id]) { if ( (filterFrom && bsCache[id].total <= filterFrom) || (filterTo && bsCache[id].total >= filterTo) ) { parentNode.style.display = "none"; } else { parentNode.style.display = ""; } stats[0] = bsCache[id].total; stats[1] = bsCache[id].strength; stats[2] = bsCache[id].defense; stats[3] = bsCache[id].speed; stats[4] = bsCache[id].dexterity; let difference = new Date().getTime() / 1000 - bsCache[id].timestamp; if (difference < 0) { delete bsCache[id]; localStorage["dtmb.torn.bs"] = JSON.stringify(bsCache); return; } if (difference > 365 * 24 * 60 * 60) time = Math.floor(difference / (365 * 24 * 60 * 60)) + " years ago"; else if (difference > 30 * 24 * 60 * 60) time = Math.floor(difference / (30 * 24 * 60 * 60)) + " months ago"; else if (difference > 24 * 60 * 60) time = Math.floor(difference / (24 * 60 * 60)) + " days ago"; else if (difference > 60 * 60) time = Math.floor(difference / (60 * 60)) + " hours ago"; else if (difference > 60) time = Math.floor(difference / 60) + " minutes ago"; else time = Math.floor(difference) + " seconds ago"; } let units = ["K", "M", "B", "T", "Q"]; for (let i = 0; i < stats.length; i++) { let stat = Number.parseInt(stats[i]); if (Number.isNaN(stat) || stat == 0) continue; for (let j = 0; j < units.length; j++) { stat = stat / 1000; if (stat > 1000) continue; stat = stat.toFixed(i == 0 ? (stat >= 100 ? 0 : 1) : 2); stats[i] = `${stat}${units[j]}`; break; } } node.innerHTML = stats[0]; node.title = ` <div class="dtmb-bs-stat"> <b>STR</b> <span class="dtmb-bs-stat">${stats[1]}</span><br/> <b>DEF</b> <span class="dtmb-bs-stat">${stats[2]}</span><br/> <b>SPD</b> <span class="dtmb-bs-stat">${stats[3]}</span><br/> <b>DEX</b> <span class="dtmb-bs-stat">${stats[4]}</span><br/> ${time} </div>`; } function updateHospTimers() { for (let i = 0, n = hospNodes.length; i < n; i++) { const hospNode = hospNodes[i]; const id = hospNode[0]; const node = hospNode[1]; if (!node) continue; if (!hospTime[id]) continue; let totalSeconds = hospTime[id] - new Date().getTime() / 1000; if (!totalSeconds || totalSeconds <= 0) continue; else if (totalSeconds >= 10 * 60 && hospLoopCounter % 10 != 0) continue; else if ( totalSeconds < 10 * 60 && totalSeconds >= 5 * 60 && hospLoopCounter % 5 != 0 ) continue; let hours = Math.floor(totalSeconds / 3600); totalSeconds %= 3600; let minutes = Math.floor(totalSeconds / 60); let seconds = Math.floor(totalSeconds % 60); node.textContent = `${hours.toString().padLeft(2, "0")}:${minutes .toString() .padLeft(2, "0")}:${seconds.toString().padLeft(2, "0")}`; } if (hospNodes.length > 0) hospLoopCounter++; setTimeout(updateHospTimers, 1000); } function updateStatus(id, node) { if (!node) return; if (hospNodes.find((h) => h[0] == id)) return; hospNodes.push([id, node]); } function showStats(node) { if (!node) return; let id = node .querySelector('a[href*="XID"]') .href.replace(/.*?XID=(\d+)/i, "$1"); let bsNode = node.querySelector(".bs") || document.createElement("div"); let statusNode = node.querySelector(".status"); updateStats(id, bsNode, node); updateStatus(id, statusNode); if (bsNode.classList.contains("bs")) { return; } bsNode.className = "table-cell bs level lvl left iconShow dtmb-bs-col"; let iconsNode = node.querySelector(".user-icons, .member-icons, .points"); iconsNode.parentNode.insertBefore(bsNode, iconsNode); let isMobile = false; bsNode.addEventListener("touchstart", () => (isMobile = true)); bsNode.addEventListener("click", () => { if (isMobile) return; window.open(`loader.php?sid=attack&user2ID=${id}`, "_newtab"); }); bsNode.addEventListener("dblclick", () => { window.open(`loader.php?sid=attack&user2ID=${id}`, "_newtab"); }); } function showStatsAll(node) { if (!node) node = Array.from( document.querySelectorAll(".f-war-list .members-list, .members-list") ); if (!node) return; if (!(node instanceof Array)) { node = [node]; } node.forEach((n) => n .querySelectorAll( ".your:not(.row-animation-new), .enemy:not(.row-animation-new), .table-body > .table-row" ) .forEach((e) => showStats(e)) ); } function watchWall(observeNode) { if (!observeNode) return; loadFactions(); let parentNode = observeNode.parentNode.parentNode.parentNode; let factionNames = parentNode.querySelector(".faction-names"); if (factionNames && !factionNames.querySelector(".dtmb-bs-swap")) { let swapNode = document.createElement("div"); swapNode.className = "dtmb-bs-swap"; swapNode.innerHTML = "<>"; factionNames.appendChild(swapNode); swapNode.addEventListener("click", () => { parentNode .querySelectorAll( ".name.left, .name.right, .tab-menu-cont.right, .tab-menu-cont.left" ) .forEach((e) => { if (e.classList.contains("left")) { e.classList.remove("left"); e.classList.add("right"); } else { e.classList.remove("right"); e.classList.add("left"); } }); }); let filterNode = document.createElement("div"); filterNode.className = "dtmb-bs-filter input-money-group no-max-value"; let filterFromInput = document.createElement("input"); filterFromInput.className = "input-money"; filterFromInput.placeholder = "Filter BS from"; filterFromInput.value = localStorage.getItem("dtmb.torn.factionFilterFrom") || ""; let filterToInput = document.createElement("input"); filterToInput.className = "input-money"; filterToInput.placeholder = "Filter BS to"; filterToInput.value = localStorage.getItem("dtmb.torn.factionFilterTo") || ""; filterNode.appendChild(filterFromInput); filterNode.appendChild(filterToInput); factionNames.appendChild(filterNode); function filterFromTo() { function formatInput(input) { let value = input.value.toLowerCase(); let valueNum = value.replace(/[^\d]/g, ""); let multiplier = 1; if (value.indexOf("k") !== -1) multiplier = 1000; else if (value.indexOf("m") !== -1) multiplier = 1000000; else if (value.indexOf("b") !== -1) multiplier = 1000000000; else if (value.indexOf("t") !== -1) multiplier = 1000000000000; valueNum *= multiplier; input.value = valueNum > 0 ? valueNum.toLocaleString("en-US") : ""; return valueNum; } filterFrom = formatInput(filterFromInput); filterTo = formatInput(filterToInput); localStorage.setItem("dtmb.torn.factionFilterFrom", filterFrom || ""); localStorage.setItem("dtmb.torn.factionFilterTo", filterTo || ""); showStatsAll(); } filterFromTo(); filterFromInput.addEventListener("keyup", filterFromTo); filterToInput.addEventListener("keyup", filterFromTo); } let titleNode = observeNode.parentNode.querySelector(".title, .c-pointer"); let lvNode = titleNode.querySelector(".level"); lvNode.childNodes[0].nodeValue = "Lv"; if (!titleNode.querySelector(".bs")) { let bsNode = lvNode.cloneNode(true); bsNode.classList.add("bs"); bsNode.childNodes[0].nodeValue = "BS"; titleNode.insertBefore( bsNode, titleNode.querySelector(".user-icons, .points") ); if (bsNode.childNodes.length > 1) { let orderClass = bsNode.childNodes[1].className.match( /(?:\s|^)((?:asc|desc)(?:[^\s|$]+))(?:\s|$)/ )[1]; bsNode.childNodes[1].classList.remove(orderClass); for (let i = 0; i < titleNode.children.length; i++) { titleNode.children[i].addEventListener("click", (e) => { setTimeout(() => { let sort = i + 1; let sortIcon = e.target.querySelector("[class*='sortIcon']"); let desc = sortIcon ? sortIcon.className.indexOf("desc") === -1 : false; sort = desc ? sort : -sort; localStorage.setItem("dtmb.torn.factionSort", sort); if (!e.target.classList.contains("bs")) document .querySelectorAll("[class*='dtmb-bs-activeIcon']") .forEach((e) => e.classList.remove("dtmb-bs-activeIcon")); //if (Math.abs(sort) != 3) document.querySelectorAll("[class*='dtmb-bs-activeIcon']").forEach((e) => e.classList.remove("dtmb-bs-activeIcon")); }, 100); }); } bsNode.addEventListener("click", () => { sortStats(observeNode); }); let title = titleNode.children[Math.abs(previousSort) - 1]; let sortIcon = title.querySelector("[class*='sortIcon']"); let desc = sortIcon ? sortIcon.className.indexOf("desc") !== -1 : false; let active = sortIcon ? sortIcon.className.indexOf("activeIcon") !== -1 : false; let x = 0; if (title.classList.contains("bs") && observeNode.querySelector(".enemy")) x = 0; //funny edge case, dont ask :) //if (Math.abs(previousSort) == 3 && observeNode.querySelector(".enemy")) x = 0; //funny edge case, dont ask :) else if (!active && previousSort < 0) x = 1; else if (!active) x = 2; else if (previousSort < 0 && !desc) x = 1; else if (previousSort > 0 && desc) x = 1; for (; x > 0; x--) { title.click(); } } } showStatsAll(observeNode); let prevSortCheck = ""; const mo = new MutationObserver((mutations) => { mutations.forEach((mutation) => { for (const node of mutation.addedNodes) { if ( node.classList && (node.classList.contains("your") || node.classList.contains("enemy")) ) { showStats(node); } } }); let sort = Array.from(observeNode.querySelectorAll('a[href*="XID"]')) .map((a) => a.href) .join(","); if ( prevSortCheck != sort && observeNode.parentNode.querySelector(".dtmb-bs-activeIcon") ) { mo.disconnect(); sortStats(observeNode, observeNode.dtmbSort); prevSortCheck = Array.from(observeNode.querySelectorAll('a[href*="XID"]')) .map((a) => a.href) .join(","); mo.takeRecords(); mo.observe(observeNode, { childList: true, subtree: true }); } }); mo.observe(observeNode, { childList: true, subtree: true }); } function watchWalls(observeNode) { if (!observeNode) return; observeNode.querySelectorAll(".members-list").forEach((e) => watchWall(e)); new MutationObserver((mutations) => { mutations.forEach((mutation) => { for (const node of mutation.addedNodes) { node.querySelector && node.querySelectorAll(".members-list").forEach((w) => watchWall(w)); } }); }).observe(observeNode, { childList: true, subtree: true }); } function memberList(observeNode) { if (!observeNode) return; loadFactions(); let titleNode = observeNode.querySelector(".table-header"); if (!titleNode || titleNode.querySelector(".bs")) return; let bsNode = document.createElement("li"); bsNode.className = "table-cell bs torn-divider divider-vertical"; bsNode.innerHTML = "BS"; titleNode.insertBefore(bsNode, titleNode.querySelector(".member-icons")); for (let i = 0; i < titleNode.children.length; i++) { titleNode.children[i].addEventListener("click", (e) => { let sort = i + 1; sort = e.target.querySelector("[class*='asc']") ? -sort : sort; localStorage.setItem("dtmb.torn.factionSort", sort); }); } bsNode.addEventListener("click", () => { sortStats(observeNode); }); if (previousSort >= 0) { titleNode.children[previousSort - 1].click(); titleNode.children[previousSort - 1].click(); } else if (previousSort < 0) titleNode.children[-previousSort - 1].click(); observeNode .querySelectorAll(".table-body > .table-row") .forEach((e) => showStats(e)); } updateHospTimers(); memberList(document.querySelector(".members-list")); watchWalls(document.querySelector(".f-war-list")); new MutationObserver((mutations) => { mutations.forEach((mutation) => { for (const node of mutation.addedNodes) { memberList(node.querySelector && node.querySelector(".members-list")); watchWalls(node.querySelector && node.querySelector(".f-war-list")); } }); }).observe(document.body, { childList: true, subtree: true }); const oldFetch = unsafeWindow.fetch; unsafeWindow.fetch = async (url, init) => { if ( !url.includes("step=getwarusers") && !url.includes("step=getProcessBarRefreshData") ) return oldFetch(url, init); let response = await oldFetch(url, init); let clone = response.clone(); clone.json().then((json) => { let members = null; if (json.warDesc) members = json.warDesc.members; else if (json.userStatuses) members = json.userStatuses; else return; Object.keys(members).forEach((id) => { let status = members[id].status || members[id]; id = members[id].userID || id; if (status.text == "Hospital") hospTime[id] = status.updateAt; else delete hospTime[id]; }); showStatsAll(); }); return response; }; const oldWebSocket = unsafeWindow.WebSocket; unsafeWindow.WebSocket = function (...args) { const socket = new oldWebSocket(...args); socket.addEventListener("message", (event) => { let json = JSONparse(event.data); if ( !json?.result?.data?.data?.message?.namespaces?.users?.actions ?.updateStatus?.status ) return; // console.log(json); let id = json.result.data.data.message.namespaces.users.actions.updateStatus .userId; let status = json.result.data.data.message.namespaces.users.actions.updateStatus .status; if (status.text == "Hospital") hospTime[id] = status.updateAt; else delete hospTime[id]; showStatsAll(); }); return socket; }; GM_addStyle(` @media screen and (max-width: 1000px) { .members-cont .bs { display: none; } } .members-cont .level { width: 27px !important; } .members-cont .id { padding-left: 5px !important; width: 28px !important; } .members-cont .points { width: 42px !important; } .dtmb-bs-stat { font-family: monospace; } .dtmb-bs-stat > span { display: inline-block; width: 55px; text-align: right; } .faction-names { position: relative; } .dtmb-bs-filter { position: absolute !important; top: 25px !important; left: 0; right: 0; margin-left: auto; margin-right: auto; width: 120px; cursor: pointer; } .dtmb-bs-filter > input { display: block !important; width: 100px; } .dtmb-bs-swap { position: absolute; top: 0px; left: 0; right: 0; margin-left: auto; margin-right: auto; width: 100px; cursor: pointer; } .dtmb-bs-activeIcon { display: block !important; } .dtmb-bs-asc { border-bottom: 6px solid var(--sort-arrow-color); border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 0 solid transparent; height: 0; top: -8px; width: 0; } .dtmb-bs-desc { border-bottom: 0 solid transparent; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid var(--sort-arrow-border-color); height: 0; top: -1px; width: 0; } .dtmb-bs-col { text-overflow: clip !important; } .raid-members-list .level:not(.bs) { width: 16px !important; } div.desc-wrap:not([class*='warDesc']) .dtmb-bs-swap { display: none; } div.desc-wrap:not([class*='warDesc']) .faction-names { padding-top: 100px !important; } .re_spy_title, .re_spy_col { display: none !important; } `);