- // ==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
- // @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", "###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 20.")
- }
-
- 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 21 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 22 manually if you wish a different value from the default 5 minutes.")
- }
-
- /**
- *
- * SET UP SCRIPT
- *
- **/
-
- setInterval(
- function mainLoop() {
- if(!api_key)
- 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")
- 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))
- }
- })()