Target list helper

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

As of 13.03.2025. See ბოლო ვერსია.

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!)

// ==UserScript==
// @name        Target list helper
// @namespace   szanti
// @license     GPL
// @match       https://www.torn.com/page.php?sid=list&type=targets*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @version     1.0
// @author      Szanti
// @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
// ==/UserScript==

(function() {
  'use strict'

  let api_key = GM_getValue("api-key")
  let polling_interval = GM_getValue("polling-interval") ?? 1000
  let stale_time = GM_getValue("stale-time") ?? 600_000

  const MAX_TRIES_UNTIL_REJECTION = 5
  const TRY_DELAY = 1000
  const OUT_OF_HOSP = 60_000
  const INVALID_TIME = Math.max(900_000, stale_time)

  const targets = GM_getValue("targets", {})
  const getApi = []

  try {
    GM_registerMenuCommand('Set Api Key', function setApiKey() {
      const new_key = prompt("Please enter a public api key", api_key);
      if (new_key && new_key.length == 16) {
        api_key = new_key;
        GM_setValue("api-key", new_key);
      } else {
        throw new Error("No valid key detected.");
      }
    })
  } catch (e) {
    if(!api_key)
      throw new Error("Please set the public api key in the script manually on line 17.")
  }

  try {
    GM_registerMenuCommand('Api polling interval', function setPollingInterval() {
      const new_polling_interval = prompt("How often in ms should the api be called (default 1000)?",polling_interval);
      if (Number.isFinite(new_polling_interval)) {
        polling_interval = new_polling_interval;
        GM_setValue("polling-interval", new_polling_interval);
      } else {
        throw new Error("Please enter a numeric polling interval.");
      }
    });
  } catch (e) {
    if(polling_interval == 1000)
      console.warn("Please set the api polling interval on line 18 manually if you wish a different value from the default 1000ms.")
  }

  try {
    GM_registerMenuCommand('Set Stale Time', function setStaleTime() {
      const new_stale_time = prompt("After how many seconds should data about a target be considered stale (default 900)?", stale_time/1000);
      if (Number.isFinite(new_stale_time)) {
        stale_time = new_stale_time;
        GM_setValue("stale-time", new_stale_time*1000);
      } else {
        throw new Error("Please enter a numeric stale time.");
      }
    })
  } catch (e) {
    if(stale_time == 900_000)
      console.warn("Please set the api polling interval on line 18 manually if you wish a different value from the default 1000ms.")
  }

  setInterval(
    function mainLoop() {
      if(api_key) {
        let row = getApi.shift()
        while(row && !row.isConnected)
          row = getApi.shift()
        if(row)
          console.log(getName(row))
        if(row && row.isConnected)
          parseApi(row)
      }
    }
    , polling_interval)

  waitForElement(".tableWrapper > ul").then(
    function setUpTableHandler(table) {
      parseTable(table)

      new MutationObserver((records) =>
        records.forEach(r => r.addedNodes.forEach(n => { if(n.tagType="UL") parseTable(n) }))
      ).observe(table.parentNode, {childList: true})
  })

  function parseApi(row) {
    const id = getId(row)

    GM_xmlhttpRequest({
      url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
      onload: ({responseText}) => {
        const r = JSON.parse(responseText)
        if(r.error) {
          console.error("[Target list helper] Api error:", r.error.error)
          return
        }
        const icon =
              {
                "rock": "🪨",
                "paper": "📜",
                "scissors": "✂️"
              }[r.competition.status]
        targets[id] = {
          timestamp: Date.now(),
          icon: icon ?? r.competition.status,
          hospital: r.status.until*1000,
          hp: r.life.current,
          maxHp: r.life.maximum,
          status: r.status.state
        }
        GM_setValue("targets", targets)
        setStatus(row)
      }
    })
  }

  function setStatus(row) {
    const id = getId(row)

    let status_element = row.querySelector("[class*='status___'] > span")
    let status = status_element.textContent

    let next_update = targets[id].timestamp + stale_time - Date.now()
    if(targets[id].status === "Okay") {
      if(Date.now() > targets[id].hospital + OUT_OF_HOSP)
      status_element.classList.replace("user-red-status", "user-green-status")
      status = targets[id].hp + "/" + targets[id].maxHp

      if(targets[id].hp < targets[id].maxHp)
        next_update = Math.min(next_update, 300000 - Date.now()%300000)
    } else if(targets[id].status === "Hospital") {
      status_element.classList.replace("user-green-status", "user-red-status")

      if(targets[id].hospital < Date.now()) {
        status = "Out"
        targets[id].status = "Okay"
        next_update = Math.min(next_update, targets[id].hospital + OUT_OF_HOSP - Date.now())
      } else {
        status = formatTimeLeft(targets[id].hospital)
        setTimeout(() => setStatus(row), 1000-Date.now()%1000 + 1)
        next_update = next_update > 0 ? undefined : next_update
      }
    }

    if(next_update !== undefined) {
      setTimeout(() => getApi.push(row), next_update)
    }

    row.querySelector("[class*='status___'] > span").textContent = status + " " + targets[id].icon
  }

  function parseTable(table) {
    for(const row of table.children) parseRow(row)
    new MutationObserver((records) => records.forEach(r => r.addedNodes.forEach(parseRow))).observe(table, {childList: true})
    getApi.sort((a, b) => {
      const a_target = targets[getId(a)]
      const b_target = targets[getId(b)]

      const calcValue = target =>
        (!target
         || target.status === "Hospital"
         || target.timestamp + INVALID_TIME < Date.now())
        ? Infinity : target.timestamp

      return calcValue(b_target) - calcValue(a_target)
    })
  }

  function parseRow(row) {
    if(row.classList.contains("tornPreloader"))
      return

    waitForElement(".tt-ff-scouter-indicator", row)
    .then(el => {
      const ff_perc = el.style.getPropertyValue("--band-percent")
      const ff =
        (ff_perc < 33) ? ff_perc/33+1
        : (ff_perc < 66) ? 2*ff_perc/33
        : (ff_perc - 66)*4/34+4

      const dec = Math.round((ff%1)*100)
      row.querySelector("[class*='level___']").textContent += " " + Math.floor(ff) + '.' + (dec<10 ? "0" : "") + dec
    })
    .catch(() => {console.warn("[Target list helper] No FF Scouter detected.")})

    const button = row.querySelector("[class*='disabled___']")

    if(button) {
      const a = document.createElement("a")
      a.href = `/loader2.php?sid=getInAttack&user2ID=${getId(row)}`
      button.childNodes.forEach(n => a.appendChild(n))
      button.classList.forEach(c => {
        if(c.charAt(0) != 'd')
          a.classList.add(c)
      })
      button.parentNode.insertBefore(a, button)
      button.parentNode.removeChild(button)
    }

    const id = getId(row)
    if(!targets[id] || targets[id].timestamp + INVALID_TIME < Date.now()) {
      getApi.push(row)
    } else if(row.querySelector("[class*='status___'] > span").textContent === "Hospital") {
      setStatus(row)
      getApi.push(row)
    } else {
      setStatus(row)
    }
  }

  function formatTimeLeft(until) {
      const time_left = until - Date.now()
      const min = Math.floor(time_left/60000)
      const min_pad = min < 10 ? "0" : ""
      const sec = Math.floor((time_left/1000)%60)
      const sec_pad = sec < 10 ? "0" : ""
      return min_pad + min + ":" + sec_pad + sec
  }

  function getId(row) {
    return row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0]
  }

  function getName(row) {
    return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text)
  }

  function waitForCondition(condition, silent_fail) {
    return new Promise((resolve, reject) => {
      let tries = 0
      const interval = setInterval(
        function conditionChecker() {
          const result = condition()
          tries += 1

          if(!result && tries <= MAX_TRIES_UNTIL_REJECTION)
            return

          clearInterval(interval)

          if(result)
            resolve(result)
          else if(!silent_fail)
            reject(result)
      }, TRY_DELAY)
    })
  }

  function waitForElement(query_string, element = document) {
    return waitForCondition(() => element.querySelector(query_string))
  }
})()