Target list helper

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

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

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