您需要先安装一个扩展,例如 篡改猴、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 // @grant GM_addStyle // @version 1.2.0 // @author Szanti // @description Make FF visible, enable attack buttons, list target hp or remaining hosp time // ==/UserScript== const API_KEY = "###PDA-APIKEY###" const POLLING_INTERVAL = undefined const STALE_TIME = undefined const SHOW = undefined // Show.LEVEL // Show.RESPECT const USE_TORNPAL = undefined // Tornpal.YES // Tornpal.NO // Tornpal.WAIT_FOR_TT const UseTornPal = Object.freeze({ YES: "Trying TornPal then TornTools", NO: "Disabled TornPal, trying only TornTools", WAIT_FOR_TT: "Trying TornTools then TornPal" }) const Show = Object.freeze({ LEVEL: "Showing Level", RESPECT: "Showing Respect", RESP_UNAVAILABLE: "Can't show respect without fair fight estimation" }) {(function() { 'use strict' if(isPda()) { 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", API_KEY) // Amount of time between each API call let polling_interval = GM_getValue("polling-interval", POLLING_INTERVAL ?? 1000) // Least amount of time after which to update data let stale_time = GM_getValue("stale-time", STALE_TIME ?? 300_000) // Show level or respect let show_respect = loadEnum(Show, GM_getValue("show-respect", SHOW ?? Show.RESPECT)) // Torntools is definitely inaccessible on PDA dont bother waiting for it let use_tornpal = loadEnum( UseTornPal, GM_getValue("use-tornpal", USE_TORNPAL ?? (isPda() ? UseTornPal.YES : UseTornPal.WAIT_FOR_TT))) // How often to try to find a specific condition on the page const MAX_TRIES_UNTIL_REJECTION = 5 // How long to wait in between such tries const TRY_DELAY = 1000 // How long until we consider stop looking for the hospitaliztion after a possible attack const DEF_NOT_HOSPITAL = 15_000 // Time after which a target coming out of hospital is updated const OUT_OF_HOSP = 60_000 // It's ok to display stale data until it can get updated but not invalid data const INVALIDATION_TIME = Math.max(900_000, stale_time) // Our data cache let targets = GM_getValue("targets", {}) // In queue for profile data update, may need to be replaced with filtered array on unpause let profile_updates = [] // In queue for TornPal update const ff_updates = [] // Update attacked targets when regaining focus let attacked_targets = [] // If the api key can be used for tornpal, assume it works fail if not let can_tornpal = true // To TornTool or not to TornTool const torntools = !(document.documentElement.style.getPropertyValue("--tt-theme-color").length == 0) if(!torntools && use_tornpal == UseTornPal.NO) { console.warn("Couldn't find TornTools and TornPal is deactivated, FF estimation unavailable.") show_respect = Show.RESP_UNAVAILABLE } const icons = { "rock": "🪨", "paper": "📜", "scissors": "✂️" } const Debug = { API_LOOP: Symbol("Debug.API_LOOP"), UPDATE: Symbol("Debug.UPDATE") } /** * * ATTACH CSS FOR FLASH EFFECT * **/ GM_addStyle(` @keyframes green_flash { 0% {background-color: var(--default-bg-panel-color);} 50% {background-color: oklab(from var(--default-bg-panel-color) L -0.087 0.106); } 100% {background-color: var(--default-bg-panel-color);} } .flash_green { animation: green_flash 500ms ease-in-out; animation-iteration-count: 1; } `) /** * * 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?.length == 16) { GM_setValue("api-key", new_key) api_key = new_key can_tornpal = true for(const row of document.querySelector(".tableWrapper > ul").children) updateFf(row) } else { throw new Error("No valid key detected.") } }) } catch (e) { if(api_key.charAt(0) === "#") throw new Error("Please set the public or TornPal capable api key in the script manually on line 17.") } try { let menu_id = GM_registerMenuCommand( use_tornpal, function toggleTornPal() { use_tornpal = next_state() GM_setValue("use-tornpal", use_tornpal) menu_id = GM_registerMenuCommand( use_tornpal, toggleTornPal, {id: menu_id, autoClose: false} ) }, {autoClose: false}) function next_state() { if(use_tornpal == UseTornPal.WAIT_FOR_TT) return UseTornPal.YES if(use_tornpal == UseTornPal.YES) return UseTornPal.NO return UseTornPal.WAIT_FOR_TT } } catch(e) { if(USE_TORNPAL === undefined) console.warn("Please choose UseTornPal.YES, UseTornPal.NO or UseTornPal.WAIT_FOR_TT on line 22. (Default: UseTornPal.WAIT_FOR_TT)") } 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 === undefined) console.warn("Please set the api polling interval (in ms) on line 18. (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 300)?", 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 === undefined) console.warn("Please set the stale time (in ms) on line 19. (default 5 minutes)") } try { let menu_id = GM_registerMenuCommand( show_respect, function toggleRespect() { const old_show_respect = show_respect show_respect = next_state() try { for(const row of document.querySelector(".tableWrapper > ul").children) redrawFf(row) } catch(e) { // Maybe the user clicks it before fair fight is loaded show_respect = old_show_respect throw e } setFfColHeader() GM_setValue("show-respect", show_respect) menu_id = GM_registerMenuCommand( show_respect, toggleRespect, {id: menu_id, autoClose: false} ) }, {autoClose: false} ) function next_state() { if(use_tornpal == UseTornPal.NO || (!can_tornpal && !torntools)) return Show.RESP_UNAVAILABLE if(show_respect == Show.RESPECT) return Show.LEVEL return Show.RESPECT } } catch(e) { if(SHOW === undefined) console.warn("Please select if you want to see estimated respect Show.RESPECT or Show.LEVEL on line 20. (Default Show.RESPECT)") } /** * * SET UP SCRIPT * **/ waitForElement(".tableWrapper > ul") .then(function attachToTable(table) { const wrapper = table.parentNode const button = table.querySelector("[class*='buttonsGroup'] > button") if(button.getAttribute("data-is-tooltip-opened") != null) { const description = wrapper.querySelector("[class*=tableHead] > [class*=description___]") description.style.maxWidth = description.scrollWidth - button.scrollWidth + "px" } setFfColHeader() parseTable(table) new MutationObserver(records => records.forEach(r => r.addedNodes.forEach(n => { if(n.tagName === "UL") parseTable(n) }))) .observe(table.parentNode, {childList: true}) startLoop() }) function parseTable(table) { parseRows(table.children) new MutationObserver( records => records.forEach(r => parseRows(r.addedNodes)) ).observe(table, {childList: true}) function parseRows(rows) { for(const row of rows) { if(row.classList.contains("tornPreloader")) continue const id = getId(row) const target = targets[id] const level_from_page = Number(row.querySelector("[class*='level___']").textContent) const status_from_page = row.querySelector("[class*='status___'] > span").textContent reworkButtons() new MutationObserver(records => records.forEach(r => r.addedNodes.forEach(n => { if(n.className.includes("buttonsGroup")) reworkButtons() }))) .observe(row, {childList: true}) if(target?.timestamp + INVALIDATION_TIME > Date.now() && status_from_page === target?.status) { redrawStatus(row) updateStatus(row, target.timestamp + stale_time) } else { targets[id] = {level: level_from_page, status: status_from_page} if(status_from_page === "Hospital") updateUntilHospitalized(row) else updateStatus(row) } if(target?.fair_fight?.last_updated > target?.last_action) redrawFf(row) else updateFf(row) function reworkButtons() { const buttons_group = row.querySelector("[class*='buttonsGroup']") if(!buttons_group) return const sample_button = buttons_group.querySelector("button") const disabled_button = buttons_group.querySelector("[class*='disabled___']") const refresh_button = document.createElement("button") sample_button.classList.forEach(c => { if(c.charAt(0) !== 'd') refresh_button.classList.add(c) }) const refresh_icon = document.createElementNS("http://www.w3.org/2000/svg", "svg") refresh_icon.setAttribute("width", 16) refresh_icon.setAttribute("height", 15) refresh_icon.setAttribute("viewBox", "0 0 16 15") const refresh_icon_path = document.createElementNS("http://www.w3.org/2000/svg", "path") refresh_icon_path.setAttribute("d", "M9,0A7,7,0,0,0,2.09,6.83H0l3.13,3.5,3.13-3.5H3.83A5.22,5.22,0,1,1,9,12.25a5.15,5.15,0,0,1-3.08-1l-1.2,1.29A6.9,6.9,0,0,0,9,14,7,7,0,0,0,9,0Z") refresh_icon.append(refresh_icon_path) refresh_button.appendChild(refresh_icon) if(sample_button.getAttribute("data-is-tooltip-opened") == null) { refresh_button.append(document.createTextNode("Refresh")) } else { const description = row.querySelector("[class*=description___]") description.style.maxWidth = description.scrollWidth - sample_button.scrollWidth + "px" } buttons_group.prepend(refresh_button) refresh_button.addEventListener("click", () => updateStatus(row, Date.now(), true)) buttons_group.modified = true if(!disabled_button) { buttons_group.querySelector("a").addEventListener("click", () => attacked_targets.push(row)) return } const a = document.createElement("a") a.href = `/loader2.php?sid=getInAttack&user2ID=${id}` disabled_button.childNodes.forEach(n => a.appendChild(n)) disabled_button.classList.forEach(c => { if(c.charAt(0) !== 'd'){ a.classList.add(c)} }) disabled_button.parentNode.insertBefore(a, disabled_button) disabled_button.parentNode.removeChild(disabled_button) a.addEventListener("click", () => attacked_targets.push(row)) } } profile_updates.sort(prioritizeUpdates) function prioritizeUpdates(a, b) { return updateValue(b) - updateValue(a) function updateValue(row) { const target = targets[getId(row)] if(!target?.timestamp || target.timestamp + INVALIDATION_TIME < Date.now()) return Infinity if(target.life.current < target.life.maximum) return Date.now() + target.timestamp return target.timestamp } } } } function redrawStatus(row) { const target = targets[getId(row)] const status_element = row.querySelector("[class*='status___'] > span") setStatus() 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 updateUntilHospitalized(row) else updateStatus(row, target.hospital + OUT_OF_HOSP) /* 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) } }) } // 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)) 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 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 redrawFf(row) { const target = targets[getId(row)] const ff = target.fair_fight.value const text_element = row.querySelector("[class*='level___']") const respect = (1 + 0.005 * target.level) * Math.min(3, ff) if(show_respect == Show.RESPECT) text_element.textContent = formatNumber(respect) + " " + formatNumber(ff) else text_element.textContent = target.level + " " + formatNumber(ff) function formatNumber(x) { return Math.floor(x) + "." + String(Math.floor((x%1)*100)).padStart(2, '0') } } function updateStatus(row, when, fast_track) { log(Debug.UPDATE, "Going to update", getName(row), "at", new Date(when).toLocaleTimeString()) const requested_at = Date.now() const id = getId(row) if(fast_track && !row.fast_tracked) { row.updating = true row.fast_tracked = true profile_updates.unshift(row) return } setTimeout(() => { if(row.updating || targets[id]?.timestamp > requested_at) { if(row.updating) log(Debug.UPDATE, "Already marked for update", getName(row)) else log(Debug.UPDATE, "Already updated", getName(row)) return } row.updating = true profile_updates.push(row) }, when - Date.now()) } function updateFf(row) { /** * UseTornPal | can_tornpal | torntools | case | action * ------------+---------------+-------------+------+-------- * YES | YES | N/A | a | ff_updates.push * YES | NO | YES | e | try_tt (error when can_tornpal got set), fail silently * YES | NO | NO | b | fail silently (error whet can_tornpal got set) * NO | N/A | YES | d | try_tt, fail with error * NO | N/A | NO | b | fail silently (warn when torntools got set) * WAIT_FOR_TT | YES | YES | c | try_tt catch ff_updates.push * WAIT_FOR_TT | YES | NO | a | ff_updates.push * WAIT_FOR_TT | NO | YES | d | try_tt, fail with error * WAIT_FOR_TT | NO | NO | b | fail silently (error when can_tornpal got set) **/ /** Case a - Only TornPal **/ if((use_tornpal == UseTornPal.YES && can_tornpal) || (use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal && !torntools) ) { ff_updates.push(row) return } /** Case b - Neither TornPal nor Torntools **/ if(!torntools) return waitForElement(".tt-ff-scouter-indicator", row) .catch(function noTtFound(e) { /** Case c - TornTools failed so try TornPal next **/ if(use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal) ff_updates.push(row) /** Case d - TornTools failed but TornPal cannot be used**/ else if(use_tornpal == UseTornPal.NO || use_tornpal == UseTornPal.WAIT_FOR_TT) throw new Error("Cannot find fair fight estimation from tornpal or from torntools.") /** Case e - User has enabled TornPal but it failed already, likely because TornTools is not installed, but we tried it anyway. **/ }) .then(function ffFromTt(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 id = getId(row) Object.assign(targets[getId(row)], {fair_fight: {value: ff}}) redrawFf(row) }) } function updateUntilHospitalized(row, time_out_after = INVALIDATION_TIME) { const id = getId(row) const start = Date.now() updateStatus(row) const attack_updater = setInterval( function attackUpdater() { updateStatus(row) if((targets[id]?.hospital > Date.now()) || Date.now() > start + time_out_after) { clearInterval(attack_updater) return } }, polling_interval) } function startLoop() { const loop_id = crypto.randomUUID() let idle_start = undefined let run_if_locked = [] GM_setValue("main-loop", loop_id) GM_setValue("has-lock", loop_id) addEventListener("focus", function refocus() { GM_setValue("main-loop", loop_id) while(attacked_targets.length > 0) updateUntilHospitalized(attacked_targets.pop(), DEF_NOT_HOSPITAL) }) setInterval(mainLoop, polling_interval) function mainLoop() { const jobs_waiting = profile_updates.length > 0 || ff_updates.length > 0 || run_if_locked.length > 0 let has_lock = GM_getValue("has-lock") if(jobs_waiting && has_lock != loop_id && (has_lock === undefined || GM_getValue("main-loop") == loop_id)) { GM_setValue("has-lock", loop_id) log(Debug.API_LOOP, loop_id, "Setting lock and unpausing") has_lock = loop_id Object.assign(targets, GM_getValue("targets", {})) profile_updates = profile_updates .filter(row => { const t = targets[getId(row)] if(!t?.timestamp || t.timestamp < idle_start) return true finishUpdate(row) return false }) } else if(!jobs_waiting && has_lock == loop_id) { GM_setValue("has-lock", undefined) log(Debug.API_LOOP, loop_id, "Releasing lock") has_lock = undefined } if(has_lock != loop_id) { log(Debug.API_LOOP, loop_id, "Idling") idle_start = Date.now() return } log(Debug.API_LOOP, loop_id, "Running") while(run_if_locked.length > 0) run_if_locked.pop()() if(api_key.charAt(0) === "#") return /** * * TornPal updates * **/ if(ff_updates.length > 0) { const scouts = ff_updates.splice(0,250) GM_xmlhttpRequest({ url: `https://tornpal.com/api/v1/ffscoutergroup?comment=targetlisthelper&key=${api_key}&targets=${scouts.map(getId).join(",")}`, onload: function updateFf({responseText}) { const r = JSON.parse(responseText) if(!r.status) { if(r.error_code == 772) { can_tornpal = false if(!torntools) show_respect = Show.RESP_UNAVAILABLE } throw new Error("TornPal error: " + r.message) } run_if_locked.push(() => { Object.values(r.results) .forEach(({result}) => { if(result.status) targets[result.player_id].fair_fight = {last_updated: result.last_updated, value: result.value} }) GM_setValue("targets", targets) setTimeout(() => { scouts.forEach(row => { if(targets[getId(row)].fair_fight) redrawFf(row) }) }) }) } }) } /** * * Torn profile updates * **/ let row while(profile_updates.length > 0 && !row?.isConnected) row = profile_updates.shift() if(!row) return const id = getId(row) GM_xmlhttpRequest({ url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`, onload: function updateProfile({responseText}) { let r = undefined try { r = JSON.parse(responseText) // Can also throw on malformed response if(r.error) throw new Error("Torn error: " + r.error.error) } catch (e) { profile_updates.unshift(row) // Oh Fuck, Put It Back In throw e } const response_date = Date.now() run_if_locked.push(() => { if(targets[id]?.timestamp < response_date) { Object.assign(targets[id], { timestamp: response_time, 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, last_action: r.last_action.timestamp, level: r.level }) GM_setValue("targets", targets) } finishUpdate(row) }) } }) function finishUpdate(row) { row.updating = false row.fast_tracked = false setTimeout(() => { row.classList.add('flash_green'); setTimeout(() => row.classList.remove('flash_green'), 500) redrawStatus(row) updateStatus(row, targets[getId(row)].timestamp + stale_time) }) } } } function getId(row) { if(!row.player_id) row.player_id = row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0] return row.player_id } function getName(row) { return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text) } function setFfColHeader() { document .querySelector("[class*='level___'] > button") .childNodes[0] .data = show_respect == Show.RESPECT ? "R" : "Lvl" } function waitForCondition(condition, silent_fail, max_tries = MAX_TRIES_UNTIL_REJECTION) { return new Promise((resolve, reject) => { let tries = 0 const interval = setInterval( function conditionChecker() { const result = condition() tries += 1 if(!result && tries <= max_tries) return clearInterval(interval) if(result) resolve(result) else if(!silent_fail) reject(result) }, TRY_DELAY) }) } function waitForElement(query_string, element = document, silent_fail = false) { return waitForCondition(() => element.querySelector(query_string), silent_fail) } function isPda() { return window.navigator.userAgent.includes("com.manuito.tornpda") } /** Ugly as fuck because we cant save what cant be stringified :/ **/ function loadEnum(the_enum, loaded_value) { for(const [key,value] of Object.entries(the_enum)) { if(value === loaded_value) return the_enum[key] } return undefined } function log(type, ...message) { if(true) return else if(type == Debug.API_LOOP) console.log(new Date().toLocaleTimeString(), ...message)/**/ else if(type == Debug.UPDATE) console.log(new Date().toLocaleTimeString(), ...message)/**/ } })()}