Target list helper

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

Versione datata 14/03/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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.1
// @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", "###PDA-APIKEY###")
  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
  // It's ok to display stale data until it can get updated but not invalid data
  const INVALID_TIME = Math.max(900_000, stale_time)

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

  const icons =
        { "rock": "🪨",
          "paper": "📜",
          "scissors": "✂️" }

  /**
   *
   * REGISTER MENU COMMANDS
   *
   **/
  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 20.")
  }

  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(!GM_getValue("polling-interval"))
      console.warn("Please set the api polling interval on line 21 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(!GM_getValue("stale-time"))
      console.warn("Please set the stale time on line 22 manually if you wish a different value from the default 5 minutes.")
  }

  /**
   *
   * SET UP SCRIPT
   *
   **/

  setInterval(
    function mainLoop() {
      if(!api_key)
        return

      let row = getApi.shift()
      while(row && !row.isConnected)
        row = getApi.shift()

      if(!row)
        return

      const id = getId(row)

      GM_xmlhttpRequest({
        url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
        onload: function parseAPI({responseText}) {
          let r = undefined
          try {
            r = JSON.parse(responseText) // Can also throw on malformed response
            if(r.error)
              throw new Error("Api error:", r.error.error)
          } catch (e) {
            getApi.unshift(row) // Oh Fuck, Put It Back In
            throw e
          }
          targets[id] = {
            timestamp: Date.now(),
            icon: icons[r.competition.status] ?? r.competition.status,
            hospital: r.status.until == 0 ? Math.min(targets[id]?.hospital ?? 0, Date.now()) : r.status.until*1000,
            life: r.life,
            status: r.status.state
          }
          GM_setValue("targets", targets)
          updateTarget(row)
        }
      })
    }, polling_interval)

  waitForElement(".tableWrapper > ul").then(
    function setUpTableHandler(table) {
      new MutationObserver((records) =>
        records.forEach(r => r.addedNodes.forEach(n => { if(n.tagName === "UL") parseTable(n) }))
      ).observe(table.parentNode, {childList: true})

      parseTable(table)
  })

  function updateTarget(row) {
    const id = getId(row)
    const status_element = row.querySelector("[class*='status___'] > span")

    setStatus(row)
    setTimeout(() => getApi.push(row), targets[id].timestamp + stale_time - Date.now())

    if(targets[id].status === "Okay" && Date.now() > targets[id].hospital + OUT_OF_HOSP) {
      status_element.classList.replace("user-red-status", "user-green-status")
    } else if(targets[id].status === "Hospital") {
      status_element.classList.replace("user-green-status", "user-red-status")
      if(targets[id].hospital < Date.now()) // Defeated but not yet selected where to put
        setTimeout(() => getApi.push(row), 5000)
      else
        setTimeout(() => getApi.push(row), targets[id].hospital + OUT_OF_HOSP - Date.now())

      /* To make sure we dont run two timers on the same row in parallel, *
       * we make the sure that a row has at most one timer id.            */
      let last_timer = row.timer =
        setTimeout(function updateTimer() {
          const time_left = targets[id].hospital - Date.now()

          if(time_left > 0 && last_timer == row.timer) {
            row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row)
            last_timer = row.timer
          } else if(time_left <= 0) {
            targets[id].status = "Okay"
          }
          setStatus(row)
        })
    }

    // Check if we need to register a healing tick in the interim
    if(row.health_update || targets[id].life.current == targets[id].life.maximum)
      return

    let next_health_tick = targets[id].timestamp + targets[id].life.ticktime*1000
    while(next_health_tick < Date.now()) {
      targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)
      next_health_tick += targets[id].life.interval*1000
    }

    row.health_update =
      setTimeout(function updateHealth() {
        targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)

        if(targets[id].life.current < targets[id].life.maximum) {
          row.health_update = setTimeout(updateHealth, targets[id].life.interval*1000)
        } else {
          row.health_update = undefined
          targets[id].status = "Okay"
        }

        setStatus(row)
      }, next_health_tick - Date.now())
  }

  function setStatus(row) {
    const id = getId(row)
    const status_element = row.querySelector("[class*='status___'] > span")
    let status = status_element.textContent

    if(targets[id].status === "Hospital")
      status = String(Math.floor(time_left/60_000)).padStart(2, '0')
                + ":"
                + String(Math.floor((time_left/1000)%60)).padStart(2, '0')
    else if(targets[id].status === "Okay")
      status = targets[id].life.current + "/" + targets[id].life.maximum

    status_element.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) + '.' + String(Math.round((ff%1)*100)).padStart(2, '0')
    })
    .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()
      && row.querySelector("[class*='status___'] > span").textContent === targets[id].status
    ) {
      updateTarget(row)
    } else {
      getApi.push(row)
    }
  }

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