Greasy Fork is available in English.

Target list helper

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

Tính đến 14-03-2025. Xem phiên bản mới nhất.

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

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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
  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. })()