您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Make FF visible, enable attack buttons, list target hp or remaining hosp time
当前为
// ==UserScript== // @name Target list helper // @namespace szanti // @license GPL // @match https://www.torn.com/page.php?sid=list&type=targets* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @version 1.1.4 // @author Szanti // @description Make FF visible, enable attack buttons, list target hp or remaining hosp time // ==/UserScript== (function() { 'use strict' if(window.navigator.userAgent.includes("com.manuito.tornpda")) { GM_getValue = (key, default_value) => JSON.parse(GM.getValue(key, default_value)) GM_setValue = (key, value) => GM.setValue(key, JSON.stringify(value)) } let api_key = GM_getValue("api-key", "###PDA-APIKEY###") let polling_interval = GM_getValue("polling-interval", 1000) let stale_time = GM_getValue("stale-time", 600_000) const MAX_TRIES_UNTIL_REJECTION = 5 const TRY_DELAY = 1000 const OUT_OF_HOSP = 60_000 // It's ok to display stale data until it can get updated but not invalid data const INVALID_TIME = Math.max(900_000, stale_time) const targets = GM_getValue("targets", {}) const getApi = [] const icons = { "rock": "🪨", "paper": "📜", "scissors": "✂️" } /** * * REGISTER MENU COMMANDS * **/ try { GM_registerMenuCommand('Set Api Key', function setApiKey() { const new_key = prompt("Please enter a public api key", api_key); if (new_key && new_key.length == 16) { api_key = new_key; GM_setValue("api-key", new_key); } else { throw new Error("No valid key detected."); } }) } catch (e) { if(!api_key) throw new Error("Please set the public api key in the script manually on line 25.") } try { GM_registerMenuCommand('Api polling interval', function setPollingInterval() { const new_polling_interval = prompt("How often in ms should the api be called (default 1000)?",polling_interval); if (Number.isFinite(new_polling_interval)) { polling_interval = new_polling_interval; GM_setValue("polling-interval", new_polling_interval); } else { throw new Error("Please enter a numeric polling interval."); } }); } catch (e) { if(!GM_getValue("polling-interval")) console.warn("Please set the api polling interval on line 26 manually if you wish a different value from the default 1000ms.") } try { GM_registerMenuCommand('Set Stale Time', function setStaleTime() { const new_stale_time = prompt("After how many seconds should data about a target be considered stale (default 900)?", stale_time/1000); if (Number.isFinite(new_stale_time)) { stale_time = new_stale_time; GM_setValue("stale-time", new_stale_time*1000); } else { throw new Error("Please enter a numeric stale time."); } }) } catch (e) { if(!GM_getValue("stale-time")) console.warn("Please set the stale time on line 27 manually if you wish a different value from the default 5 minutes.") } /** * * SET UP SCRIPT * **/ setInterval( function mainLoop() { if(api_key === "###PDA-APIKEY###") return let row = getApi.shift() while(row && !row.isConnected) row = getApi.shift() if(!row) return const id = getId(row) GM_xmlhttpRequest({ url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`, onload: function parseAPI({responseText}) { let r = undefined try { r = JSON.parse(responseText) // Can also throw on malformed response if(r.error) throw new Error("Api error:", r.error.error) } catch (e) { getApi.unshift(row) // Oh Fuck, Put It Back In throw e } targets[id] = { timestamp: Date.now(), icon: icons[r.competition.status] ?? r.competition.status, hospital: r.status.until == 0 ? Math.min(targets[id]?.hospital ?? 0, Date.now()) : r.status.until*1000, life: r.life, status: r.status.state } GM_setValue("targets", targets) updateTarget(row) } }) }, polling_interval) waitForElement(".tableWrapper > ul").then( function setUpTableHandler(table) { new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(n => { if(n.tagName === "UL") parseTable(n) })) ).observe(table.parentNode, {childList: true}) parseTable(table) }) function updateTarget(row) { const id = getId(row) const status_element = row.querySelector("[class*='status___'] > span") setStatus(row) setTimeout(() => getApi.push(row), targets[id].timestamp + stale_time - Date.now()) if(targets[id].status === "Okay" && Date.now() > targets[id].hospital + OUT_OF_HOSP) { status_element.classList.replace("user-red-status", "user-green-status") } else if(targets[id].status === "Hospital") { status_element.classList.replace("user-green-status", "user-red-status") if(targets[id].hospital < Date.now()) // Defeated but not yet selected where to put setTimeout(() => getApi.push(row), 5000) else setTimeout(() => getApi.push(row), targets[id].hospital + OUT_OF_HOSP - Date.now()) /* To make sure we dont run two timers on the same row in parallel, * * we make the sure that a row has at most one timer id. */ let last_timer = row.timer = setTimeout(function updateTimer() { const time_left = targets[id].hospital - Date.now() if(time_left > 0 && last_timer == row.timer) { row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row) last_timer = row.timer } else if(time_left <= 0) { targets[id].status = "Okay" } setStatus(row) }) } // Check if we need to register a healing tick in the interim if(row.health_update || targets[id].life.current == targets[id].life.maximum) return let next_health_tick = targets[id].timestamp + targets[id].life.ticktime*1000 while(next_health_tick < Date.now()) { targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment) next_health_tick += targets[id].life.interval*1000 } row.health_update = setTimeout(function updateHealth() { targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment) if(targets[id].life.current < targets[id].life.maximum) { row.health_update = setTimeout(updateHealth, targets[id].life.interval*1000) } else { row.health_update = undefined targets[id].status = "Okay" } setStatus(row) }, next_health_tick - Date.now()) } function setStatus(row) { const id = getId(row) const status_element = row.querySelector("[class*='status___'] > span") let status = status_element.textContent if(targets[id].status === "Hospital") { const time_left = targets[id].hospital - Date.now() status = String(Math.floor(time_left/60_000)).padStart(2, '0') + ":" + String(Math.floor((time_left/1000)%60)).padStart(2, '0') } else if(targets[id].status === "Okay") { status = targets[id].life.current + "/" + targets[id].life.maximum } status_element.textContent = status + " " + targets[id].icon } function parseTable(table) { for(const row of table.children) parseRow(row) new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(parseRow))).observe(table, {childList: true}) getApi.sort((a, b) => { const a_target = targets[getId(a)] const b_target = targets[getId(b)] const calcValue = target => (!target || target.status === "Hospital" || target.timestamp + INVALID_TIME < Date.now()) ? Infinity : target.timestamp return calcValue(b_target) - calcValue(a_target) }) } function parseRow(row) { if(row.classList.contains("tornPreloader")) return waitForElement(".tt-ff-scouter-indicator", row) .then(el => { const ff_perc = el.style.getPropertyValue("--band-percent") const ff = (ff_perc < 33) ? ff_perc/33+1 : (ff_perc < 66) ? 2*ff_perc/33 : (ff_perc - 66)*4/34+4 const dec = Math.round((ff%1)*100) row.querySelector("[class*='level___']").textContent += " " + Math.floor(ff) + '.' + String(Math.round((ff%1)*100)).padStart(2, '0') }) .catch(() => {console.warn("[Target list helper] No FF Scouter detected.")}) const button = row.querySelector("[class*='disabled___']") if(button) { const a = document.createElement("a") a.href = `/loader2.php?sid=getInAttack&user2ID=${getId(row)}` button.childNodes.forEach(n => a.appendChild(n)) button.classList.forEach(c => { if(c.charAt(0) != 'd') a.classList.add(c) }) button.parentNode.insertBefore(a, button) button.parentNode.removeChild(button) } const id = getId(row) if(targets[id] && targets[id].timestamp + INVALID_TIME > Date.now() && row.querySelector("[class*='status___'] > span").textContent === targets[id].status ) { updateTarget(row) } else { getApi.push(row) } } function getId(row) { return row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0] } function getName(row) { return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text) } function waitForCondition(condition, silent_fail) { return new Promise((resolve, reject) => { let tries = 0 const interval = setInterval( function conditionChecker() { const result = condition() tries += 1 if(!result && tries <= MAX_TRIES_UNTIL_REJECTION) return clearInterval(interval) if(result) resolve(result) else if(!silent_fail) reject(result) }, TRY_DELAY) }) } function waitForElement(query_string, element = document) { return waitForCondition(() => element.querySelector(query_string)) } })()