Target list helper

Make FF visible, enable attack buttons, list target hp or remaining hosp time

As of 2025-03-14. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name Target list helper
  3. // @namespace szanti
  4. // @license GPL
  5. // @match https://www.torn.com/page.php?sid=list&type=targets*
  6. // @grant GM_xmlhttpRequest
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_registerMenuCommand
  10. // @version 1.1.3
  11. // @author Szanti
  12. // @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict'
  17.  
  18. let api_key = GM_getValue("api-key", "###PDA-APIKEY###")
  19. let polling_interval = GM_getValue("polling-interval", 1000)
  20. let stale_time = GM_getValue("stale-time", 600_000)
  21.  
  22. const MAX_TRIES_UNTIL_REJECTION = 5
  23. const TRY_DELAY = 1000
  24. const OUT_OF_HOSP = 60_000
  25. // It's ok to display stale data until it can get updated but not invalid data
  26. const INVALID_TIME = Math.max(900_000, stale_time)
  27.  
  28. // Torn PDA returns as string and not JSON
  29. const targets_preloaded = GM_getValue("targets", {})
  30. const targets = targets_preloaded instanceof String ? JSON.parse(targets_preloaded) : targets_preloaded
  31. const getApi = []
  32.  
  33. const icons =
  34. { "rock": "🪨",
  35. "paper": "📜",
  36. "scissors": "✂️" }
  37.  
  38. /**
  39. *
  40. * REGISTER MENU COMMANDS
  41. *
  42. **/
  43. try {
  44. GM_registerMenuCommand('Set Api Key', function setApiKey() {
  45. const new_key = prompt("Please enter a public api key", api_key);
  46. if (new_key && new_key.length == 16) {
  47. api_key = new_key;
  48. GM_setValue("api-key", new_key);
  49. } else {
  50. throw new Error("No valid key detected.");
  51. }
  52. })
  53. } catch (e) {
  54. if(!api_key)
  55. throw new Error("Please set the public api key in the script manually on line 20.")
  56. }
  57.  
  58. try {
  59. GM_registerMenuCommand('Api polling interval', function setPollingInterval() {
  60. const new_polling_interval = prompt("How often in ms should the api be called (default 1000)?",polling_interval);
  61. if (Number.isFinite(new_polling_interval)) {
  62. polling_interval = new_polling_interval;
  63. GM_setValue("polling-interval", new_polling_interval);
  64. } else {
  65. throw new Error("Please enter a numeric polling interval.");
  66. }
  67. });
  68. } catch (e) {
  69. if(!GM_getValue("polling-interval"))
  70. console.warn("Please set the api polling interval on line 21 manually if you wish a different value from the default 1000ms.")
  71. }
  72.  
  73. try {
  74. GM_registerMenuCommand('Set Stale Time', function setStaleTime() {
  75. const new_stale_time = prompt("After how many seconds should data about a target be considered stale (default 900)?", stale_time/1000);
  76. if (Number.isFinite(new_stale_time)) {
  77. stale_time = new_stale_time;
  78. GM_setValue("stale-time", new_stale_time*1000);
  79. } else {
  80. throw new Error("Please enter a numeric stale time.");
  81. }
  82. })
  83. } catch (e) {
  84. if(!GM_getValue("stale-time"))
  85. console.warn("Please set the stale time on line 22 manually if you wish a different value from the default 5 minutes.")
  86. }
  87.  
  88. /**
  89. *
  90. * SET UP SCRIPT
  91. *
  92. **/
  93.  
  94. setInterval(
  95. function mainLoop() {
  96. if(api_key === "###PDA-APIKEY###")
  97. return
  98.  
  99. let row = getApi.shift()
  100. while(row && !row.isConnected)
  101. row = getApi.shift()
  102.  
  103. if(!row)
  104. return
  105.  
  106. const id = getId(row)
  107.  
  108. GM_xmlhttpRequest({
  109. url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
  110. onload: function parseAPI({responseText}) {
  111. let r = undefined
  112. try {
  113. r = JSON.parse(responseText) // Can also throw on malformed response
  114. if(r.error)
  115. throw new Error("Api error:", r.error.error)
  116. } catch (e) {
  117. getApi.unshift(row) // Oh Fuck, Put It Back In
  118. throw e
  119. }
  120. targets[id] = {
  121. timestamp: Date.now(),
  122. icon: icons[r.competition.status] ?? r.competition.status,
  123. hospital: r.status.until == 0 ? Math.min(targets[id]?.hospital ?? 0, Date.now()) : r.status.until*1000,
  124. life: r.life,
  125. status: r.status.state
  126. }
  127. GM_setValue("targets", targets)
  128. updateTarget(row)
  129. }
  130. })
  131. }, polling_interval)
  132.  
  133. waitForElement(".tableWrapper > ul").then(
  134. function setUpTableHandler(table) {
  135. new MutationObserver((records) =>
  136. records.forEach(r => r.addedNodes.forEach(n => { if(n.tagName === "UL") parseTable(n) }))
  137. ).observe(table.parentNode, {childList: true})
  138.  
  139. parseTable(table)
  140. })
  141.  
  142. function updateTarget(row) {
  143. const id = getId(row)
  144. const status_element = row.querySelector("[class*='status___'] > span")
  145.  
  146. setStatus(row)
  147. setTimeout(() => getApi.push(row), targets[id].timestamp + stale_time - Date.now())
  148.  
  149. if(targets[id].status === "Okay" && Date.now() > targets[id].hospital + OUT_OF_HOSP) {
  150. status_element.classList.replace("user-red-status", "user-green-status")
  151. } else if(targets[id].status === "Hospital") {
  152. status_element.classList.replace("user-green-status", "user-red-status")
  153. if(targets[id].hospital < Date.now()) // Defeated but not yet selected where to put
  154. setTimeout(() => getApi.push(row), 5000)
  155. else
  156. setTimeout(() => getApi.push(row), targets[id].hospital + OUT_OF_HOSP - Date.now())
  157.  
  158. /* To make sure we dont run two timers on the same row in parallel, *
  159. * we make the sure that a row has at most one timer id. */
  160. let last_timer = row.timer =
  161. setTimeout(function updateTimer() {
  162. const time_left = targets[id].hospital - Date.now()
  163.  
  164. if(time_left > 0 && last_timer == row.timer) {
  165. row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row)
  166. last_timer = row.timer
  167. } else if(time_left <= 0) {
  168. targets[id].status = "Okay"
  169. }
  170. setStatus(row)
  171. })
  172. }
  173.  
  174. // Check if we need to register a healing tick in the interim
  175. if(row.health_update || targets[id].life.current == targets[id].life.maximum)
  176. return
  177.  
  178. let next_health_tick = targets[id].timestamp + targets[id].life.ticktime*1000
  179. while(next_health_tick < Date.now()) {
  180. targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)
  181. next_health_tick += targets[id].life.interval*1000
  182. }
  183.  
  184. row.health_update =
  185. setTimeout(function updateHealth() {
  186. targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)
  187.  
  188. if(targets[id].life.current < targets[id].life.maximum) {
  189. row.health_update = setTimeout(updateHealth, targets[id].life.interval*1000)
  190. } else {
  191. row.health_update = undefined
  192. targets[id].status = "Okay"
  193. }
  194.  
  195. setStatus(row)
  196. }, next_health_tick - Date.now())
  197. }
  198.  
  199. function setStatus(row) {
  200. const id = getId(row)
  201. const status_element = row.querySelector("[class*='status___'] > span")
  202. let status = status_element.textContent
  203.  
  204. if(targets[id].status === "Hospital") {
  205. const time_left = targets[id].hospital - Date.now()
  206. status = String(Math.floor(time_left/60_000)).padStart(2, '0')
  207. + ":"
  208. + String(Math.floor((time_left/1000)%60)).padStart(2, '0')
  209. } else if(targets[id].status === "Okay") {
  210. status = targets[id].life.current + "/" + targets[id].life.maximum
  211. }
  212.  
  213. status_element.textContent = status + " " + targets[id].icon
  214. }
  215.  
  216. function parseTable(table) {
  217. for(const row of table.children) parseRow(row)
  218. new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(parseRow))).observe(table, {childList: true})
  219. getApi.sort((a, b) => {
  220. const a_target = targets[getId(a)]
  221. const b_target = targets[getId(b)]
  222.  
  223. const calcValue = target =>
  224. (!target
  225. || target.status === "Hospital"
  226. || target.timestamp + INVALID_TIME < Date.now())
  227. ? Infinity : target.timestamp
  228.  
  229. return calcValue(b_target) - calcValue(a_target)
  230. })
  231. }
  232.  
  233. function parseRow(row) {
  234. if(row.classList.contains("tornPreloader"))
  235. return
  236.  
  237. waitForElement(".tt-ff-scouter-indicator", row)
  238. .then(el => {
  239. const ff_perc = el.style.getPropertyValue("--band-percent")
  240. const ff =
  241. (ff_perc < 33) ? ff_perc/33+1
  242. : (ff_perc < 66) ? 2*ff_perc/33
  243. : (ff_perc - 66)*4/34+4
  244.  
  245. const dec = Math.round((ff%1)*100)
  246. row.querySelector("[class*='level___']").textContent += " " + Math.floor(ff) + '.' + String(Math.round((ff%1)*100)).padStart(2, '0')
  247. })
  248. .catch(() => {console.warn("[Target list helper] No FF Scouter detected.")})
  249.  
  250. const button = row.querySelector("[class*='disabled___']")
  251.  
  252. if(button) {
  253. const a = document.createElement("a")
  254. a.href = `/loader2.php?sid=getInAttack&user2ID=${getId(row)}`
  255. button.childNodes.forEach(n => a.appendChild(n))
  256. button.classList.forEach(c => {
  257. if(c.charAt(0) != 'd')
  258. a.classList.add(c)
  259. })
  260. button.parentNode.insertBefore(a, button)
  261. button.parentNode.removeChild(button)
  262. }
  263.  
  264. const id = getId(row)
  265. if(targets[id]
  266. && targets[id].timestamp + INVALID_TIME > Date.now()
  267. && row.querySelector("[class*='status___'] > span").textContent === targets[id].status
  268. ) {
  269. updateTarget(row)
  270. } else {
  271. getApi.push(row)
  272. }
  273. }
  274.  
  275. function getId(row) {
  276. return row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0]
  277. }
  278.  
  279. function getName(row) {
  280. return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text)
  281. }
  282.  
  283. function waitForCondition(condition, silent_fail) {
  284. return new Promise((resolve, reject) => {
  285. let tries = 0
  286. const interval = setInterval(
  287. function conditionChecker() {
  288. const result = condition()
  289. tries += 1
  290.  
  291. if(!result && tries <= MAX_TRIES_UNTIL_REJECTION)
  292. return
  293.  
  294. clearInterval(interval)
  295.  
  296. if(result)
  297. resolve(result)
  298. else if(!silent_fail)
  299. reject(result)
  300. }, TRY_DELAY)
  301. })
  302. }
  303.  
  304. function waitForElement(query_string, element = document) {
  305. return waitForCondition(() => element.querySelector(query_string))
  306. }
  307. })()