您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork镜像 is available in English.
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.5 // @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) => GM.getValue(key) ? 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 = [] let main_loop = undefined 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); startLoop() } else { throw new Error("No valid key detected."); } }) } catch (e) { if(api_key.charAt(0) === "#") 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 * **/ 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) if(api_key.charAt(0) === "#") throw new Error("Need public API key.") else startLoop() }) function startLoop() { if(!main_loop) main_loop = setInterval(mainLoop, polling_interval) function mainLoop() { 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: parseApi }) 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) } } } function updateTarget(row) { const target = targets[getId(row)] const status_element = row.querySelector("[class*='status___'] > span") setStatus() let next_update = target.timestamp + stale_time - Date.now() if(target.status === "Okay" && Date.now() > target.hospital + OUT_OF_HOSP) { status_element.classList.replace("user-red-status", "user-green-status") } else if(target.status === "Hospital") { status_element.classList.replace("user-green-status", "user-red-status") if(target.hospital < Date.now()) // Defeated but not yet selected where to put next_update = Math.min(next_update, 5000) else next_update = Math.min(next_update, target.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 = target.hospital - Date.now() if(time_left > 0 && last_timer == row.timer) { status_element.textContent = formatHospTime(time_left) + " " + target.icon last_timer = row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row) } else if(time_left <= 0) { target.status = "Okay" setStatus(row) } }) } setTimeout(() => getApi.push(row), next_update) // Check if we need to register a healing tick in the interim if(row.health_update || target.life.current == target.life.maximum) return let next_health_tick = target.timestamp + target.life.ticktime*1000 if(next_health_tick < Date.now()) { const health_ticks = Math.ceil((Date.now() - next_health_tick)/(target.life.interval * 1000)) console.log(getName(row), new Date(target.timestamp).toLocaleTimeString(), target.life.ticktime, health_ticks) target.life.current = Math.min(target.life.maximum, target.life.current + health_ticks * target.life.increment) next_health_tick = next_health_tick + health_ticks * target.life.interval * 1000 console.log(getName(row), new Date(next_health_tick).toLocaleTimeString()) target.life.ticktime = next_health_tick - target.timestamp setStatus(row) } row.health_update = setTimeout(function updateHealth() { target.life.current = Math.min(target.life.maximum, target.life.current + target.life.increment) target.ticktime = Date.now() + target.life.interval*1000 - target.timestamp if(target.life.current < target.life.maximum) row.health_update = setTimeout(updateHealth, target.life.interval*1000) else row.health_update = undefined setStatus(row) }, next_health_tick - Date.now()) function setStatus() { let status = status_element.textContent if(target.status === "Hospital") status = formatHospTime(target.hospital - Date.now()) else if(target.status === "Okay") status = target.life.current + "/" + target.life.maximum status_element.textContent = status + " " + target.icon } function formatHospTime(time_left) { return String(Math.floor(time_left/60_000)).padStart(2, '0') + ":" + String(Math.floor((time_left/1000)%60)).padStart(2, '0') } } 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(function apiSorter(a, b) { return updateValue(b) - updateValue(a) function updateValue(row) { const target = targets[getId(row)] if(!target || target.timestamp + INVALID_TIME < Date.now() || row.querySelector("[class*='status___'] > span").textContent !== target.status ) return Infinity if(target.life.current < target.life.maximum) return Date.now() + target.timestamp return target.timestamp } }) } 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 respect = (1 + 0.005*Number(row.querySelector("[class*='level___']").textContent )) * Math.min(3, ff) row.querySelector("[class*='level___']").textContent += " " + formatNumber(ff) // row.querySelector("[class*='level___']").textContent = formatNumber(respect) + " " + formatNumber(ff) }) .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 target = targets[getId(row)] if(target && target.timestamp + INVALID_TIME > Date.now() && row.querySelector("[class*='status___'] > span").textContent === target.status ) updateTarget(row) else getApi.push(row) function formatNumber(x) { return Math.floor(x) + "." + String(Math.floor((x%1)*100)).padStart(2, '0') } } 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)) } })()