您需要先安装一个扩展,例如 篡改猴、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.0 // @author Szanti // @description Make FF visible, enable attack buttons, list target hp or remaining hosp time // ==/UserScript== (function() { 'use strict' let api_key = GM_getValue("api-key") 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 const INVALID_TIME = Math.max(900_000, stale_time) const targets = GM_getValue("targets", {}) const getApi = [] 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 17.") } 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(polling_interval == 1000) console.warn("Please set the api polling interval on line 18 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(stale_time == 900_000) console.warn("Please set the api polling interval on line 18 manually if you wish a different value from the default 1000ms.") } setInterval( function mainLoop() { if(api_key) { let row = getApi.shift() while(row && !row.isConnected) row = getApi.shift() if(row && row.isConnected) parseApi(row) } } , polling_interval) waitForElement(".tableWrapper > ul").then( function setUpTableHandler(table) { parseTable(table) new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(n => { if(n.tagType="UL") parseTable(n) })) ).observe(table.parentNode, {childList: true}) }) function parseApi(row) { const id = getId(row) GM_xmlhttpRequest({ url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`, onload: ({responseText}) => { const r = JSON.parse(responseText) if(r.error) { console.error("[Target list helper] Api error:", r.error.error) return } const icon = { "rock": "🪨", "paper": "📜", "scissors": "✂️" }[r.competition.status] targets[id] = { timestamp: Date.now(), icon: icon ?? r.competition.status, hospital: r.status.until*1000, hp: r.life.current, maxHp: r.life.maximum, status: r.status.state } GM_setValue("targets", targets) setStatus(row) } }) } function setStatus(row) { const id = getId(row) let status_element = row.querySelector("[class*='status___'] > span") let status = status_element.textContent let next_update = targets[id].timestamp + stale_time - Date.now() if(targets[id].status === "Okay") { if(Date.now() > targets[id].hospital + OUT_OF_HOSP) status_element.classList.replace("user-red-status", "user-green-status") status = targets[id].hp + "/" + targets[id].maxHp if(targets[id].hp < targets[id].maxHp) next_update = Math.min(next_update, 300000 - Date.now()%300000) } else if(targets[id].status === "Hospital") { status_element.classList.replace("user-green-status", "user-red-status") if(targets[id].hospital < Date.now()) { status = "Out" targets[id].status = "Okay" next_update = Math.min(next_update, targets[id].hospital + OUT_OF_HOSP - Date.now()) } else { status = formatTimeLeft(targets[id].hospital) setTimeout(() => setStatus(row), 1000-Date.now()%1000 + 1) next_update = next_update > 0 ? undefined : next_update } } if(next_update !== undefined) { setTimeout(() => getApi.push(row), next_update) } row.querySelector("[class*='status___'] > span").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) + '.' + (dec<10 ? "0" : "") + dec }) .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()) { getApi.push(row) } else if(row.querySelector("[class*='status___'] > span").textContent === "Hospital") { setStatus(row) getApi.push(row) } else { setStatus(row) } } function formatTimeLeft(until) { const time_left = until - Date.now() const min = Math.floor(time_left/60000) const min_pad = min < 10 ? "0" : "" const sec = Math.floor((time_left/1000)%60) const sec_pad = sec < 10 ? "0" : "" return min_pad + min + ":" + sec_pad + sec } 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)) } })()