Target list helper

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

  1. // ==UserScript==
  2. // @name Target list helper
  3. // @namespace szanti
  4. // @license GPL-3.0-or-later
  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_deleteValue
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_addStyle
  12. // @version 2.0.3
  13. // @author Szanti
  14. // @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
  15. // ==/UserScript==
  16.  
  17. const API_KEY = "###PDA-APIKEY###"
  18. const POLLING_INTERVAL = undefined
  19. const STALE_TIME = undefined
  20. const SHOW = undefined // Show.LEVEL // Show.RESPECT
  21. const USE_TORNPAL = undefined // Tornpal.YES // Tornpal.NO // Tornpal.WAIT_FOR_TT
  22.  
  23. const UseTornPal = Object.freeze({
  24. YES: "Trying TornPal then TornTools",
  25. NO: "Disabled TornPal, trying only TornTools",
  26. WAIT_FOR_TT: "Trying TornTools then TornPal"
  27. })
  28.  
  29. const Show = Object.freeze({
  30. LEVEL: "Showing Level",
  31. RESPECT: "Showing Respect",
  32. RESP_UNAVAILABLE: "Can't show respect without fair fight estimation"
  33. })
  34.  
  35. {(async function() {
  36. 'use strict'
  37.  
  38. if(isPda()) {
  39. // On TornPDA resorting the list leads to the entire script being reloaded
  40. if(window.target_list_helper_loaded)
  41. return
  42. window.target_list_helper_loaded = true
  43.  
  44. GM.xmlHttpRequest = GM.xmlhttpRequest
  45. GM_getValue = (key, default_value) => {
  46. const value = GM.getValue(key)
  47. return value ? JSON.parse(value) : default_value
  48. }
  49.  
  50. GM_setValue = (key, value) => GM.setValue(key, JSON.stringify(value))
  51. }
  52.  
  53. let api_key = GM_getValue("api-key", API_KEY)
  54. // Amount of time between each API call
  55. let polling_interval = GM_getValue("polling-interval", POLLING_INTERVAL ?? 1000)
  56. // Least amount of time after which to update data
  57. let stale_time = GM_getValue("stale-time", STALE_TIME ?? 300_000)
  58. // Show level or respect
  59. let show_respect = loadEnum(Show, GM_getValue("show-respect", SHOW ?? Show.RESPECT))
  60. // Torntools is definitely inaccessible on PDA dont bother waiting for it
  61. let use_tornpal =
  62. loadEnum(
  63. UseTornPal,
  64. GM_getValue("use-tornpal", USE_TORNPAL ?? (isPda() ? UseTornPal.YES : UseTornPal.WAIT_FOR_TT)))
  65.  
  66. // How long until we stop looking for the hospitalization after a possible attack
  67. const CONSIDER_ATTACK_FAILED = 15_000
  68. // Time after which a target coming out of hospital is updated
  69. const OUT_OF_HOSP = 60_000
  70. // It's ok to display stale data until it can get updated but not invalid data
  71. const INVALIDATION_TIME = Math.max(900_000, stale_time)
  72.  
  73. // Our data cache
  74. let targets = GM_getValue("targets", {})
  75. // In queue for profile data update, may need to be replaced with a filtered array on unpause
  76. let profile_updates = []
  77. // In queue for TornPal update
  78. const ff_updates = []
  79. // Update attacked targets when regaining focus
  80. let attacked_targets = []
  81. // If the api key can be used for tornpal, assume it works, fail if not
  82. let can_tornpal = true
  83. // To TornTool or not to TornTool
  84. const torntools = !(document.documentElement.style.getPropertyValue("--tt-theme-color").length == 0)
  85. if(!torntools && use_tornpal == UseTornPal.NO) {
  86. console.warn("[Target list helper] Couldn't find TornTools and TornPal is deactivated, FF estimation unavailable.")
  87. show_respect = Show.RESP_UNAVAILABLE
  88. }
  89.  
  90. const ff_format = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2 , maximumFractionDigits: 2 })
  91. const bs_format = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 })
  92. const timer_format = new Intl.DurationFormat("en-US", { style: "digital", fractionalDigits: 0, hoursDisplay: "auto"})
  93.  
  94. // This is how to fill in react input values so they register
  95. const native_input_value_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
  96.  
  97. const icons =
  98. { "rock": "🪨",
  99. "paper": "📜",
  100. "scissors": "✂️" }
  101.  
  102. const Debug = {
  103. API_LOOP: Symbol("Debug.API_LOOP"),
  104. UPDATE: Symbol("Debug.UPDATE")
  105. }
  106.  
  107. /**
  108. *
  109. * ATTACH CSS FOR FLASH EFFECT
  110. *
  111. **/
  112. GM_addStyle(`
  113. @keyframes green_flash {
  114. 0% {background-color: var(--default-bg-panel-color);}
  115. 50% {background-color: oklab(from var(--default-bg-panel-color) L -0.087 0.106); }
  116. 100% {background-color: var(--default-bg-panel-color);}
  117. }
  118. .flash_green {
  119. animation: green_flash 500ms ease-in-out;
  120. animation-iteration-count: 1;
  121. }
  122. `)
  123.  
  124. /**
  125. *
  126. * ASSETS
  127. *
  128. **/
  129. const refresh_button =
  130. (function makeRefreshButton(){
  131. const button = document.createElement("button")
  132. const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  133. icon.setAttribute("width", 16)
  134. icon.setAttribute("height", 15)
  135. icon.setAttribute("viewBox", "0 0 16 15")
  136. const icon_path = document.createElementNS("http://www.w3.org/2000/svg", "path")
  137. icon_path.setAttribute("d", "M9,0A7,7,0,0,0,2.09,6.83H0l3.13,3.5,3.13-3.5H3.83A5.22,5.22,0,1,1,9,12.25a5.15,5.15,0,0,1-3.08-1l-1.2,1.29A6.9,6.9,0,0,0,9,14,7,7,0,0,0,9,0Z")
  138. icon.appendChild(icon_path)
  139. button.appendChild(icon)
  140. return button
  141. })()
  142.  
  143. const copy_bss_button =
  144. (function makeCopyBssButton(){
  145. const button = document.createElement("button")
  146. const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  147. icon.setAttribute("width", 16)
  148. icon.setAttribute("height", 13)
  149. icon.setAttribute("viewBox", "0 0 16 13")
  150. const icon_path_1 = document.createElementNS("http://www.w3.org/2000/svg", "path")
  151. icon_path_1.setAttribute("d", "M16,13S14.22,4.41,6.42,4.41V1L0,6.7l6.42,5.9V8.75c4.24,0,7.37.38,9.58,4.25")
  152. icon.append(icon_path_1)
  153. const icon_path_2 = document.createElementNS("http://www.w3.org/2000/svg", "path")
  154. icon_path_2.setAttribute("d", "M16,12S14.22,3.41,6.42,3.41V0L0,5.7l6.42,5.9V7.75c4.24,0,7.37.38,9.58,4.25")
  155. icon.append(icon_path_2)
  156. button.appendChild(icon)
  157. return button
  158. })()
  159.  
  160. /**
  161. *
  162. * REGISTER MENU COMMANDS
  163. *
  164. **/
  165. {
  166. try {
  167. GM_registerMenuCommand('Set Api Key', function setApiKey() {
  168. const new_key = prompt("Please enter a public api key", api_key)
  169. if (new_key?.length == 16) {
  170. GM_setValue("api-key", new_key)
  171. api_key = new_key
  172. can_tornpal = true
  173. for(const row of document.querySelector(".tableWrapper > ul").childNodes) updateFf(row)
  174. } else {
  175. throw new Error("No valid key detected.")
  176. }
  177. })
  178. } catch (e) {
  179. if(api_key.charAt(0) === "#")
  180. throw new Error("Please set the public or TornPal capable api key in the script manually on line 18.")
  181. }
  182.  
  183. try {
  184. let menu_id = GM_registerMenuCommand(
  185. use_tornpal,
  186. function toggleTornPal() {
  187. use_tornpal = next_state()
  188. GM_setValue("use-tornpal", use_tornpal)
  189. menu_id = GM_registerMenuCommand(
  190. use_tornpal,
  191. toggleTornPal,
  192. {id: menu_id, autoClose: false}
  193. )
  194. },
  195. {autoClose: false})
  196.  
  197. function next_state() {
  198. if(use_tornpal == UseTornPal.WAIT_FOR_TT)
  199. return UseTornPal.YES
  200. if(use_tornpal == UseTornPal.YES)
  201. return UseTornPal.NO
  202. return UseTornPal.WAIT_FOR_TT
  203. }
  204. } catch(e) {
  205. if(USE_TORNPAL === undefined)
  206. console.warn("[Target list helper] Please choose UseTornPal.YES, UseTornPal.NO or UseTornPal.WAIT_FOR_TT on line 22. (Default: UseTornPal.WAIT_FOR_TT)")
  207. }
  208.  
  209. try {
  210. GM_registerMenuCommand('Api polling interval', function setPollingInterval() {
  211. const new_polling_interval = prompt("How often in ms should the api be called (default 1000)?",polling_interval)
  212. if (Number.isFinite(new_polling_interval)) {
  213. polling_interval = new_polling_interval
  214. GM_setValue("polling-interval", new_polling_interval)
  215. } else {
  216. throw new Error("Please enter a numeric polling interval.")
  217. }
  218. })
  219. } catch (e) {
  220. if(POLLING_INTERVAL === undefined)
  221. console.warn("[Target list helper] Please set the api polling interval (in ms) on line 19. (default 1000ms)")
  222. }
  223.  
  224. try {
  225. GM_registerMenuCommand('Set Stale Time', function setStaleTime() {
  226. const new_stale_time = prompt("After how many seconds should data about a target be considered stale (default 300)?", stale_time/1000)
  227. if (Number.isFinite(new_stale_time)) {
  228. stale_time = new_stale_time
  229. GM_setValue("stale-time", new_stale_time*1000)
  230. } else {
  231. throw new Error("Please enter a numeric stale time.")
  232. }
  233. })
  234. } catch (e) {
  235. if(STALE_TIME === undefined)
  236. console.warn("[Target list helper] Please set the stale time (in ms) on line 20. (default 5 minutes)")
  237. }
  238.  
  239. try {
  240. let menu_id = GM_registerMenuCommand(
  241. show_respect,
  242. function toggleRespect() {
  243. const old_show_respect = show_respect
  244. show_respect = next_state()
  245. try {
  246. for(const row of document.querySelector(".tableWrapper > ul").childNodes) redrawFf(row)
  247. } catch(e) { // Maybe the user clicks it before fair fight is loaded
  248. show_respect = old_show_respect
  249. throw e
  250. }
  251. setFfColHeader()
  252. if(show_respect != Show.RESP_UNAVAILABLE)
  253. GM_setValue("show-respect", show_respect)
  254. menu_id = GM_registerMenuCommand(
  255. show_respect,
  256. toggleRespect,
  257. {id: menu_id, autoClose: false}
  258. )
  259. },
  260. {autoClose: false}
  261. )
  262.  
  263. function next_state() {
  264. if((use_tornpal == UseTornPal.NO || !can_tornpal) && !torntools)
  265. return Show.RESP_UNAVAILABLE
  266. if(show_respect == Show.RESPECT)
  267. return Show.LEVEL
  268. return Show.RESPECT
  269. }
  270. } catch(e) {
  271. if(SHOW === undefined)
  272. console.warn("[Target list helper] Please select if you want to see estimated respect Show.RESPECT or Show.LEVEL on line 21. (Default Show.RESPECT)")
  273. }
  274. }
  275.  
  276. /**
  277. *
  278. * THE SCRIPT PROPER
  279. *
  280. **/
  281. const row_list = await waitForElement(".tableWrapper > ul", document.getElementById("users-list-root"))
  282.  
  283. const table = row_list.parentNode
  284. const table_head = table.querySelector("[class*=tableHead]")
  285. const description_header = table_head.querySelector("[class*=description___]")
  286. waitForElement("[aria-label='Remove player from the list']", row_list)
  287. .then(button => {
  288. if(button.getAttribute("data-is-tooltip-opened") != null)
  289. description_header.style.maxWidth = (description_header.scrollWidth - button.scrollWidth) + "px"
  290. })
  291.  
  292. setFfColHeader()
  293. table_head.insertBefore(description_header, table_head.querySelector("[class*=level___]"))
  294.  
  295. parseTable(row_list)
  296.  
  297. // Observe changes after resorting
  298. new MutationObserver(records => {
  299. records.forEach(r =>
  300. r.addedNodes.forEach(n => {
  301. if(n.tagName === "UL") parseTable(n)
  302. }))})
  303. .observe(table, {childList: true})
  304.  
  305. const loop_id = crypto.randomUUID()
  306. let idle_start = undefined
  307. let process_responses = []
  308. GM_setValue("main-loop", loop_id)
  309. GM_setValue("has-lock", loop_id)
  310.  
  311. addEventListener("focus", function refocus() {
  312. GM_setValue("main-loop", loop_id)
  313. while(attacked_targets.length > 0)
  314. updateUntilHospitalized(attacked_targets.pop(), CONSIDER_ATTACK_FAILED)
  315. })
  316.  
  317. setInterval(mainLoop, polling_interval)
  318.  
  319. function mainLoop() {
  320. const jobs_waiting = profile_updates.length > 0 || ff_updates.length > 0 || process_responses.length > 0
  321. let has_lock = GM_getValue("has-lock")
  322.  
  323. if(jobs_waiting && has_lock != loop_id && (has_lock === undefined || GM_getValue("main-loop") == loop_id)) {
  324. GM_setValue("has-lock", loop_id)
  325. has_lock = loop_id
  326.  
  327. Object.assign(targets, GM_getValue("targets", {}))
  328. profile_updates =
  329. profile_updates
  330. .filter(row => {
  331. const t = targets[getId(row)]
  332. if(!t?.timestamp || t.timestamp < idle_start)
  333. return true
  334. finishUpdate(row)
  335. return false
  336. })
  337. } else if(!jobs_waiting && has_lock == loop_id) {
  338. GM_deleteValue("has-lock", undefined)
  339. has_lock = undefined
  340. }
  341.  
  342. if(has_lock != loop_id) {
  343. idle_start = Date.now()
  344. return
  345. }
  346.  
  347. while(process_responses.length > 0)
  348. process_responses.pop()()
  349.  
  350. GM_setValue("targets", targets)
  351.  
  352. if(api_key.charAt(0) === "#")
  353. return
  354.  
  355. /**
  356. *
  357. * TornPal updates
  358. *
  359. **/
  360. if(ff_updates.length > 0) {
  361. const scouts = ff_updates.splice(0,250)
  362. GM.xmlHttpRequest({
  363. url: `https://ffscouter.com/api/v1/get-stats?key=${api_key}&targets=${scouts.map(getId).join(",")}`,
  364. onload: function updateFf({responseText}) {
  365. const r = JSON.parse(responseText)
  366. if(r.error) {
  367. can_tornpal = false
  368. if(!torntools)
  369. show_respect = Show.RESP_UNAVAILABLE
  370. throw new Error("TornPal error: " + r.error)
  371. }
  372. process_responses.push(() => {
  373. r.forEach((result) => {
  374. if(result.fair_fight !== null)
  375. targets[result.player_id].fair_fight =
  376. {
  377. last_updated: result.last_updated*1000,
  378. fair_fight: result.fair_fight,
  379. bs_estimate: result.bs_estimate
  380. }
  381. })
  382. setTimeout(() => {
  383. scouts.forEach(row => {
  384. if(targets[getId(row)].fair_fight)
  385. redrawFf(row)
  386. })
  387. })
  388. })
  389. }
  390. })
  391. }
  392.  
  393. /**
  394. *
  395. * Torn profile updates
  396. *
  397. **/
  398. let row
  399. while(profile_updates.length > 0 && !row?.isConnected)
  400. row = profile_updates.shift()
  401.  
  402. if(!row)
  403. return
  404.  
  405. const id = getId(row)
  406.  
  407. GM.xmlHttpRequest({
  408. url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
  409. onload: function updateProfile({responseText}) {
  410. let r = undefined
  411. try {
  412. r = JSON.parse(responseText) // Can also throw on malformed response
  413. if(r.error)
  414. throw new Error("Torn error: " + r.error.error)
  415. } catch (e) {
  416. profile_updates.unshift(row) // Oh Fuck, Put It Back In
  417. throw e
  418. }
  419.  
  420. const response_date = Date.now()
  421.  
  422. process_responses.push(() => {
  423. if(targets[id].timestamp === undefined || targets[id].timestamp <= response_date) {
  424. Object.assign(targets[id], {
  425. timestamp: response_date,
  426. icon: icons[r.competition.status] ?? r.competition.status ?? "",
  427. hospital: r.status.until == 0 ? Math.min(targets[id]?.hospital ?? 0, Date.now()) : r.status.until*1000,
  428. life: r.life,
  429. status: r.status.state,
  430. last_action: r.last_action.timestamp*1000,
  431. level: r.level
  432. })
  433. }
  434. finishUpdate(row)
  435. })
  436. }
  437. })
  438.  
  439. function finishUpdate(row) {
  440. row.updating = false
  441. row.fast_tracked = false
  442.  
  443. setTimeout(() => {
  444. row.classList.add('flash_green');
  445. setTimeout(() => row.classList.remove('flash_green'), 500)
  446.  
  447. redrawStatus(row)
  448. updateStatus(row, targets[getId(row)].timestamp + stale_time)
  449. })
  450. }
  451. }
  452.  
  453. function parseTable(table) {
  454. parseRows(table.childNodes)
  455.  
  456. // Observe new rows getting added
  457. new MutationObserver(
  458. records => records.forEach(r => parseRows(r.addedNodes))
  459. ).observe(table, {childList: true})
  460.  
  461. function parseRows(rows) {
  462. for(const row of rows) {
  463. if(row.classList.contains("tornPreloader"))
  464. continue
  465.  
  466. const id = getId(row)
  467. const target = targets[id]
  468. const level_from_page = Number(row.querySelector("[class*='level___']").textContent)
  469. const status_from_page = row.querySelector("[class*='status___'] > span").textContent
  470.  
  471. reworkRow()
  472.  
  473. new MutationObserver(records =>
  474. records.forEach(r =>
  475. r.addedNodes.forEach(n => {
  476. if(n.className.includes("buttonsGroup")) reworkRow()
  477. })))
  478. .observe(row, {childList: true})
  479.  
  480. if(target?.timestamp + INVALIDATION_TIME > Date.now() && status_from_page === target?.status) {
  481. redrawStatus(row)
  482. updateStatus(row, target.timestamp + stale_time)
  483. } else {
  484. targets[id] = {level: level_from_page, status: status_from_page, fair_fight: target?.fair_fight}
  485. if(status_from_page === "Hospital")
  486. updateUntilHospitalized(row)
  487. else
  488. updateStatus(row)
  489. }
  490.  
  491. if(target?.fair_fight?.last_updated > target?.last_action)
  492. redrawFf(row)
  493. else
  494. updateFf(row)
  495.  
  496. function reworkRow() {
  497. // Switch description and Ff column
  498. const description = row.querySelector("[class*=description___]")
  499. const ff = row.querySelector("[class*='level___']")
  500. row.querySelector("[class*='contentGroup___']").insertBefore(description, ff)
  501.  
  502. const buttons_group = row.querySelector("[class*='buttonsGroup']")
  503. if(!buttons_group)
  504. return
  505.  
  506. const sample_button = buttons_group.querySelector("button:not([class*='disabled___'])")
  507. const disabled_button = buttons_group.querySelector("[class*='disabled___']")
  508. const edit_button = row.querySelector("[aria-label='Edit user descripton'], [aria-label='Edit player']")
  509. const wide_mode = sample_button.getAttribute("data-is-tooltip-opened") !== null
  510.  
  511. const new_refresh_button = refresh_button.cloneNode(true)
  512. sample_button.classList.forEach(c => new_refresh_button.classList.add(c))
  513. if(!wide_mode)
  514. new_refresh_button.append(document.createTextNode("Refresh"))
  515. buttons_group.prepend(new_refresh_button)
  516. new_refresh_button.addEventListener("click", () => updateStatus(row, Date.now(), true))
  517.  
  518. // Fix description width
  519. if(wide_mode)
  520. description.style.maxWidth = (description.scrollWidth - new_refresh_button.scrollWidth) + "px"
  521.  
  522. // Add BSS button
  523. edit_button?.addEventListener(
  524. "click",
  525. async function addBssButton() {
  526. const faction_el = row.querySelector("[class*='factionImage___']")
  527. const faction =
  528. faction_el?.getAttribute("alt") !== ""
  529. ? faction_el?.getAttribute("alt")
  530. : faction_el.parentNode.getAttribute("href").match(/[0-9]+/g)[0]
  531. const bss_str =
  532. "BS: " + bs_format.format(targets[id].fair_fight.bs_estimate) + (faction ? " - " + faction : "")
  533.  
  534. // "BSS: " + String(Math.round(((targets[id].fair_fight.fair_fight - 1)*3*getBss())/8)).padStart(6, ' ')
  535. // + (faction ? " - " + faction : "")
  536.  
  537. const new_copy_bss_button = copy_bss_button.cloneNode(true)
  538.  
  539. const wrapper = await waitForElement("[class*='wrapper___']", row)
  540. wrapper.childNodes[1].classList.forEach(c => new_copy_bss_button.classList.add(c))
  541. wrapper.append(new_copy_bss_button)
  542.  
  543. new_copy_bss_button.addEventListener("click", (e) => {
  544. e.stopPropagation()
  545. native_input_value_setter.call(wrapper.childNodes[0], bss_str)
  546. wrapper.childNodes[0].dispatchEvent(new Event('input', { bubbles: true }))
  547. })
  548. if(wide_mode)
  549. waitForElement("[aria-label='Edit user descripton']", row)
  550. .then(button => { button.addEventListener("click", addBssButton) })
  551. })
  552.  
  553. // Enable attack buttons and make them report if they're clicked
  554. if(disabled_button) {
  555. const a = document.createElement("a")
  556. a.href = `/loader2.php?sid=getInAttack&user2ID=${id}`
  557. disabled_button.childNodes.forEach(n => a.appendChild(n))
  558. disabled_button.classList.forEach(c => {
  559. if(c.charAt(0) !== 'd')
  560. a.classList.add(c)
  561. })
  562. disabled_button.parentNode.insertBefore(a, disabled_button)
  563. disabled_button.parentNode.removeChild(disabled_button)
  564. }
  565. (disabled_button ?? buttons_group.querySelector("a")).addEventListener("click", () => attacked_targets.push(row))
  566. }
  567. }
  568.  
  569. profile_updates.sort(
  570. function prioritizeUpdates(a, b) {
  571. return updateValue(b) - updateValue(a)
  572.  
  573. function updateValue(row) {
  574. const target = targets[getId(row)]
  575. if(!target?.timestamp || target.timestamp + INVALIDATION_TIME < Date.now())
  576. return Infinity
  577.  
  578. if(target.life.current < target.life.maximum)
  579. return Date.now() + target.timestamp
  580.  
  581. return target.timestamp
  582. }
  583. })
  584. }
  585. }
  586.  
  587. function redrawStatus(row) {
  588. const target = targets[getId(row)]
  589. const status_element = row.querySelector("[class*='status___'] > span")
  590.  
  591. setStatus()
  592.  
  593. if(target.status === "Okay" && Date.now() > target.hospital + OUT_OF_HOSP) {
  594. status_element.classList.replace("user-red-status", "user-green-status")
  595. } else if(target.status === "Hospital") {
  596. status_element.classList.replace("user-green-status", "user-red-status")
  597. if(target.hospital < Date.now()) // Defeated but not yet selected where to put
  598. updateUntilHospitalized(row)
  599. else
  600. updateStatus(row, target.hospital + OUT_OF_HOSP)
  601.  
  602. /* To make sure we dont run two timers on the same row in parallel, *
  603. * we make the sure that a row has at most one timer id. */
  604. let last_timer = row.timer =
  605. setTimeout(function updateTimer() {
  606. const time_left = target.hospital - Date.now()
  607.  
  608. if(time_left > 0 && last_timer == row.timer) {
  609. status_element.textContent =
  610. timer_format.format({minutes: Math.trunc(time_left/60_000), seconds: Math.trunc(time_left/1000%60)})
  611. + " " + target.icon
  612. last_timer = row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row)
  613. } else if(time_left <= 0) {
  614. target.status = "Okay"
  615. setStatus(row)
  616. }
  617. })
  618. }
  619.  
  620. // Check if we need to register a healing tick in the interim
  621. if(row.health_update || target.life.current == target.life.maximum)
  622. return
  623.  
  624. let next_health_tick = target.timestamp + target.life.ticktime*1000
  625. if(next_health_tick < Date.now()) {
  626. const health_ticks = Math.ceil((Date.now() - next_health_tick)/(target.life.interval * 1000))
  627. target.life.current = Math.min(target.life.maximum, target.life.current + health_ticks * target.life.increment)
  628. next_health_tick = next_health_tick + health_ticks * target.life.interval * 1000
  629. target.life.ticktime = next_health_tick - target.timestamp
  630. setStatus(row)
  631. }
  632.  
  633. row.health_update =
  634. setTimeout(function updateHealth() {
  635. target.life.current = Math.min(target.life.maximum, target.life.current + target.life.increment)
  636. target.ticktime = Date.now() + target.life.interval*1000 - target.timestamp
  637.  
  638. if(target.life.current < target.life.maximum)
  639. row.health_update = setTimeout(updateHealth, target.life.interval*1000)
  640. else
  641. row.health_update = undefined
  642.  
  643. setStatus(row)
  644. }, next_health_tick - Date.now())
  645.  
  646. function setStatus() {
  647. let status = status_element.textContent
  648.  
  649. if(target.status === "Okay")
  650. status = target.life.current + "/" + target.life.maximum
  651.  
  652. status_element.textContent = status + " " + target.icon
  653. }
  654. }
  655.  
  656. function redrawFf(row) {
  657. const target = targets[getId(row)]
  658. const ff = target.fair_fight.fair_fight
  659.  
  660. const text_element = row.querySelector("[class*='level___']")
  661. const respect = (1 + 0.005 * target.level) * Math.min(3, ff)
  662.  
  663. if(show_respect == Show.RESPECT)
  664. text_element.textContent = ff_format.format(respect) + " " + ff_format.format(ff)
  665. else
  666. text_element.textContent = target.level + " " + ff_format.format(ff)
  667. }
  668.  
  669. function updateStatus(row, when, fast_track) {
  670. const requested_at = Date.now()
  671. const id = getId(row)
  672. if(fast_track && !row.fast_tracked) {
  673. row.updating = true
  674. row.fast_tracked = true
  675. profile_updates.unshift(row)
  676. return
  677. }
  678. setTimeout(() => {
  679. if(row.updating || targets[id]?.timestamp > requested_at)
  680. return
  681.  
  682. row.updating = true
  683. profile_updates.push(row)
  684. }, when - Date.now())
  685. }
  686.  
  687. function updateFf(row) {
  688. /**
  689. * UseTornPal | can_tornpal | torntools | case | action
  690. * ------------+---------------+-------------+------+--------
  691. * YES | YES | N/A | a | ff_updates.push
  692. * YES | NO | YES | e | try_tt (error when can_tornpal got set), fail silently
  693. * YES | NO | NO | b | fail silently (error whet can_tornpal got set)
  694. * NO | N/A | YES | d | try_tt, fail with error
  695. * NO | N/A | NO | b | fail silently (warn when torntools got set)
  696. * WAIT_FOR_TT | YES | YES | c | try_tt catch ff_updates.push
  697. * WAIT_FOR_TT | YES | NO | a | ff_updates.push
  698. * WAIT_FOR_TT | NO | YES | d | try_tt, fail with error
  699. * WAIT_FOR_TT | NO | NO | b | fail silently (error when can_tornpal got set)
  700. **/
  701. /** Case a - Only TornPal **/
  702. if((use_tornpal == UseTornPal.YES && can_tornpal)
  703. || (use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal && !torntools)
  704. ) {
  705. ff_updates.push(row)
  706. return
  707. }
  708.  
  709. /** Case b - Neither TornPal nor Torntools **/
  710. if(!torntools)
  711. return
  712.  
  713. waitForElement(".tt-ff-scouter-indicator", row, 5000)
  714. .then(function ffFromTt(el) {
  715. const ff_perc = el.style.getPropertyValue("--band-percent")
  716. const ff =
  717. (ff_perc < 33) ? ff_perc/33+1
  718. : (ff_perc < 66) ? 2*ff_perc/33
  719. : (ff_perc - 66)*4/34+4
  720. const id = getId(row)
  721. Object.assign(targets[getId(row)], {fair_fight: {fair_fight: ff}})
  722. redrawFf(row)
  723. })
  724. .catch(function noTtFound(e) {
  725. /** Case c - TornTools failed so try TornPal next **/
  726. if(use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal)
  727. ff_updates.push(row)
  728. /** Case d - TornTools failed but TornPal cannot be used**/
  729. else if(use_tornpal == UseTornPal.NO || use_tornpal == UseTornPal.WAIT_FOR_TT)
  730. console.error("[Target list helper] No fair fight estimation from TornPal or torntools for target " + getName(row) + " found. Is FF Scouter enabled?")
  731. /** Case e - User has enabled TornPal, likely because TornTools is not installed, but we tried it anyway. **/
  732. })
  733. }
  734.  
  735. function updateUntilHospitalized(row, time_out_after = INVALIDATION_TIME) {
  736. const id = getId(row)
  737. const start = Date.now()
  738. updateStatus(row)
  739. const attack_updater = setInterval(
  740. function attackUpdater() {
  741. updateStatus(row)
  742. if((targets[id]?.hospital > Date.now()) || Date.now() > start + time_out_after) {
  743. clearInterval(attack_updater)
  744. return
  745. }
  746. }, polling_interval)
  747. }
  748.  
  749. function getId(row) {
  750. if(!row.player_id)
  751. row.player_id = row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0]
  752. return row.player_id
  753. }
  754.  
  755. function getName(row) {
  756. return row.querySelector(".honor-text-wrap > img").alt
  757. }
  758.  
  759. function setFfColHeader() {
  760. document
  761. .querySelector("[class*='level___'] > button")
  762. .childNodes[0]
  763. .data = show_respect == Show.RESPECT ? "R" : "Lvl"
  764. }
  765.  
  766. const {getBss} =
  767. (function bss() {
  768. let bss = undefined
  769.  
  770. GM.xmlHttpRequest({ url: `https://api.torn.com/user/?key=${api_key}&selections=battlestats` })
  771. .then(function setBss(response) {
  772. let r = undefined
  773. try {
  774. r = JSON.parse(response.responseText)
  775. if(r.error) throw Error(r.error.error)
  776. } catch(e) {
  777. console.error("Error getting battlestat score:", e)
  778. }
  779. bss = Math.sqrt(r.strength) + Math.sqrt(r.speed) + Math.sqrt(r.dexterity) + Math.sqrt(r.defense)
  780. })
  781.  
  782. function getBss() {
  783. return bss
  784. }
  785.  
  786. return {getBss}
  787. })()
  788.  
  789. function waitForElement(query_string, element = document, fail_after) {
  790. const el = element.querySelector(query_string)
  791. if(el)
  792. return Promise.resolve(el)
  793.  
  794. return new Promise((resolve, reject) => {
  795. let resolved = false
  796.  
  797. const observer = new MutationObserver(
  798. function checkElement() {
  799. observer.takeRecords()
  800. const el = element.querySelector(query_string)
  801. if(el) {
  802. resolved = true
  803. observer.disconnect()
  804. resolve(el)
  805. }
  806. })
  807.  
  808. observer.observe(element, {childList: true, subtree: true})
  809.  
  810. if(Number.isFinite(fail_after))
  811. setTimeout(() => {
  812. if(!resolved){
  813. observer.disconnect()
  814. reject(query_string + " not found.")
  815. }
  816. }, fail_after)
  817. })
  818. }
  819.  
  820. function isPda() {
  821. return window.navigator.userAgent.includes("com.manuito.tornpda")
  822. }
  823.  
  824. /** Ugly as fuck because we cant save what cant be stringified :/ **/
  825. function loadEnum(the_enum, loaded_value) {
  826. for(const [key,value] of Object.entries(the_enum)) {
  827. if(value === loaded_value)
  828. return the_enum[key]
  829. }
  830. return undefined
  831. }
  832. })()}