Greasy Fork is available in English.

Target list helper

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

Verze ze dne 13. 03. 2025. Zobrazit nejnovější verzi.

  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.0
  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")
  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. const INVALID_TIME = Math.max(900_000, stale_time)
  26.  
  27. const targets = GM_getValue("targets", {})
  28. const getApi = []
  29.  
  30. try {
  31. GM_registerMenuCommand('Set Api Key', function setApiKey() {
  32. const new_key = prompt("Please enter a public api key", api_key);
  33. if (new_key && new_key.length == 16) {
  34. api_key = new_key;
  35. GM_setValue("api-key", new_key);
  36. } else {
  37. throw new Error("No valid key detected.");
  38. }
  39. })
  40. } catch (e) {
  41. if(!api_key)
  42. throw new Error("Please set the public api key in the script manually on line 17.")
  43. }
  44.  
  45. try {
  46. GM_registerMenuCommand('Api polling interval', function setPollingInterval() {
  47. const new_polling_interval = prompt("How often in ms should the api be called (default 1000)?",polling_interval);
  48. if (Number.isFinite(new_polling_interval)) {
  49. polling_interval = new_polling_interval;
  50. GM_setValue("polling-interval", new_polling_interval);
  51. } else {
  52. throw new Error("Please enter a numeric polling interval.");
  53. }
  54. });
  55. } catch (e) {
  56. if(polling_interval == 1000)
  57. console.warn("Please set the api polling interval on line 18 manually if you wish a different value from the default 1000ms.")
  58. }
  59.  
  60. try {
  61. GM_registerMenuCommand('Set Stale Time', function setStaleTime() {
  62. const new_stale_time = prompt("After how many seconds should data about a target be considered stale (default 900)?", stale_time/1000);
  63. if (Number.isFinite(new_stale_time)) {
  64. stale_time = new_stale_time;
  65. GM_setValue("stale-time", new_stale_time*1000);
  66. } else {
  67. throw new Error("Please enter a numeric stale time.");
  68. }
  69. })
  70. } catch (e) {
  71. if(stale_time == 900_000)
  72. console.warn("Please set the api polling interval on line 18 manually if you wish a different value from the default 1000ms.")
  73. }
  74.  
  75. setInterval(
  76. function mainLoop() {
  77. if(api_key) {
  78. let row = getApi.shift()
  79. while(row && !row.isConnected)
  80. row = getApi.shift()
  81. if(row && row.isConnected)
  82. parseApi(row)
  83. }
  84. }
  85. , polling_interval)
  86.  
  87. waitForElement(".tableWrapper > ul").then(
  88. function setUpTableHandler(table) {
  89. parseTable(table)
  90.  
  91. new MutationObserver((records) =>
  92. records.forEach(r => r.addedNodes.forEach(n => { if(n.tagType="UL") parseTable(n) }))
  93. ).observe(table.parentNode, {childList: true})
  94. })
  95.  
  96. function parseApi(row) {
  97. const id = getId(row)
  98.  
  99. GM_xmlhttpRequest({
  100. url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
  101. onload: ({responseText}) => {
  102. const r = JSON.parse(responseText)
  103. if(r.error) {
  104. console.error("[Target list helper] Api error:", r.error.error)
  105. return
  106. }
  107. const icon =
  108. {
  109. "rock": "🪨",
  110. "paper": "📜",
  111. "scissors": "✂️"
  112. }[r.competition.status]
  113. targets[id] = {
  114. timestamp: Date.now(),
  115. icon: icon ?? r.competition.status,
  116. hospital: r.status.until*1000,
  117. hp: r.life.current,
  118. maxHp: r.life.maximum,
  119. status: r.status.state
  120. }
  121. GM_setValue("targets", targets)
  122. setStatus(row)
  123. }
  124. })
  125. }
  126.  
  127. function setStatus(row) {
  128. const id = getId(row)
  129.  
  130. let status_element = row.querySelector("[class*='status___'] > span")
  131. let status = status_element.textContent
  132.  
  133. let next_update = targets[id].timestamp + stale_time - Date.now()
  134. if(targets[id].status === "Okay") {
  135. if(Date.now() > targets[id].hospital + OUT_OF_HOSP)
  136. status_element.classList.replace("user-red-status", "user-green-status")
  137. status = targets[id].hp + "/" + targets[id].maxHp
  138.  
  139. if(targets[id].hp < targets[id].maxHp)
  140. next_update = Math.min(next_update, 300000 - Date.now()%300000)
  141. } else if(targets[id].status === "Hospital") {
  142. status_element.classList.replace("user-green-status", "user-red-status")
  143.  
  144. if(targets[id].hospital < Date.now()) {
  145. status = "Out"
  146. targets[id].status = "Okay"
  147. next_update = Math.min(next_update, targets[id].hospital + OUT_OF_HOSP - Date.now())
  148. } else {
  149. status = formatTimeLeft(targets[id].hospital)
  150. setTimeout(() => setStatus(row), 1000-Date.now()%1000 + 1)
  151. next_update = next_update > 0 ? undefined : next_update
  152. }
  153. }
  154.  
  155. if(next_update !== undefined) {
  156. setTimeout(() => getApi.push(row), next_update)
  157. }
  158.  
  159. row.querySelector("[class*='status___'] > span").textContent = status + " " + targets[id].icon
  160. }
  161.  
  162. function parseTable(table) {
  163. for(const row of table.children) parseRow(row)
  164. new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(parseRow))).observe(table, {childList: true})
  165. getApi.sort((a, b) => {
  166. const a_target = targets[getId(a)]
  167. const b_target = targets[getId(b)]
  168.  
  169. const calcValue = target =>
  170. (!target
  171. || target.status === "Hospital"
  172. || target.timestamp + INVALID_TIME < Date.now())
  173. ? Infinity : target.timestamp
  174.  
  175. return calcValue(b_target) - calcValue(a_target)
  176. })
  177. }
  178.  
  179. function parseRow(row) {
  180. if(row.classList.contains("tornPreloader"))
  181. return
  182.  
  183. waitForElement(".tt-ff-scouter-indicator", row)
  184. .then(el => {
  185. const ff_perc = el.style.getPropertyValue("--band-percent")
  186. const ff =
  187. (ff_perc < 33) ? ff_perc/33+1
  188. : (ff_perc < 66) ? 2*ff_perc/33
  189. : (ff_perc - 66)*4/34+4
  190.  
  191. const dec = Math.round((ff%1)*100)
  192. row.querySelector("[class*='level___']").textContent += " " + Math.floor(ff) + '.' + (dec<10 ? "0" : "") + dec
  193. })
  194. .catch(() => {console.warn("[Target list helper] No FF Scouter detected.")})
  195.  
  196. const button = row.querySelector("[class*='disabled___']")
  197.  
  198. if(button) {
  199. const a = document.createElement("a")
  200. a.href = `/loader2.php?sid=getInAttack&user2ID=${getId(row)}`
  201. button.childNodes.forEach(n => a.appendChild(n))
  202. button.classList.forEach(c => {
  203. if(c.charAt(0) != 'd')
  204. a.classList.add(c)
  205. })
  206. button.parentNode.insertBefore(a, button)
  207. button.parentNode.removeChild(button)
  208. }
  209.  
  210. const id = getId(row)
  211. if(!targets[id] || targets[id].timestamp + INVALID_TIME < Date.now()) {
  212. getApi.push(row)
  213. } else if(row.querySelector("[class*='status___'] > span").textContent === "Hospital") {
  214. setStatus(row)
  215. getApi.push(row)
  216. } else {
  217. setStatus(row)
  218. }
  219. }
  220.  
  221. function formatTimeLeft(until) {
  222. const time_left = until - Date.now()
  223. const min = Math.floor(time_left/60000)
  224. const min_pad = min < 10 ? "0" : ""
  225. const sec = Math.floor((time_left/1000)%60)
  226. const sec_pad = sec < 10 ? "0" : ""
  227. return min_pad + min + ":" + sec_pad + sec
  228. }
  229.  
  230. function getId(row) {
  231. return row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0]
  232. }
  233.  
  234. function getName(row) {
  235. return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text)
  236. }
  237.  
  238. function waitForCondition(condition, silent_fail) {
  239. return new Promise((resolve, reject) => {
  240. let tries = 0
  241. const interval = setInterval(
  242. function conditionChecker() {
  243. const result = condition()
  244. tries += 1
  245.  
  246. if(!result && tries <= MAX_TRIES_UNTIL_REJECTION)
  247. return
  248.  
  249. clearInterval(interval)
  250.  
  251. if(result)
  252. resolve(result)
  253. else if(!silent_fail)
  254. reject(result)
  255. }, TRY_DELAY)
  256. })
  257. }
  258.  
  259. function waitForElement(query_string, element = document) {
  260. return waitForCondition(() => element.querySelector(query_string))
  261. }
  262. })()