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 15. 03. 2025. Zobrazit nejnovější verzi.

// ==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.5
// @author      Szanti
// @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
// ==/UserScript==

(function() {
  'use strict'

  if(window.navigator.userAgent.includes("com.manuito.tornpda")) {
    GM_getValue = (key, default_value) => GM.getValue(key) ? JSON.parse(GM.getValue(key)): default_value
    GM_setValue = (key, value) => GM.setValue(key, JSON.stringify(value))
  }

  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 = []
  let main_loop = undefined

  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);
        startLoop()
      } else {
        throw new Error("No valid key detected.");
      }
    })
  } catch (e) {
    if(api_key.charAt(0) === "#")
      throw new Error("Please set the public api key in the script manually on line 25.")
  }

  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 26 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 27 manually if you wish a different value from the default 5 minutes.")
  }

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

  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)

      if(api_key.charAt(0) === "#")
        throw new Error("Need public API key.")
      else
        startLoop()
  })

  function startLoop() {
    if(!main_loop)
      main_loop = setInterval(mainLoop, polling_interval)

    function mainLoop() {
      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: parseApi
      })

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

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

    setStatus()
    let next_update = target.timestamp + stale_time - Date.now()

    if(target.status === "Okay" && Date.now() > target.hospital + OUT_OF_HOSP) {
      status_element.classList.replace("user-red-status", "user-green-status")
    } else if(target.status === "Hospital") {
      status_element.classList.replace("user-green-status", "user-red-status")
      if(target.hospital < Date.now()) // Defeated but not yet selected where to put
        next_update = Math.min(next_update, 5000)
      else
        next_update = Math.min(next_update, target.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 = target.hospital - Date.now()

          if(time_left > 0 && last_timer == row.timer) {
            status_element.textContent = formatHospTime(time_left) + " " + target.icon
            last_timer = row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row)
          } else if(time_left <= 0) {
            target.status = "Okay"
            setStatus(row)
          }
        })
    }

    setTimeout(() => getApi.push(row), next_update)

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

    let next_health_tick = target.timestamp + target.life.ticktime*1000
    if(next_health_tick < Date.now()) {
      const health_ticks = Math.ceil((Date.now() - next_health_tick)/(target.life.interval * 1000))
      console.log(getName(row), new Date(target.timestamp).toLocaleTimeString(), target.life.ticktime, health_ticks)
      target.life.current = Math.min(target.life.maximum, target.life.current + health_ticks * target.life.increment)
      next_health_tick = next_health_tick + health_ticks * target.life.interval * 1000
      console.log(getName(row), new Date(next_health_tick).toLocaleTimeString())
      target.life.ticktime = next_health_tick - target.timestamp
      setStatus(row)
    }

    row.health_update =
      setTimeout(function updateHealth() {
        target.life.current = Math.min(target.life.maximum, target.life.current + target.life.increment)
        target.ticktime = Date.now() + target.life.interval*1000 - target.timestamp

        if(target.life.current < target.life.maximum)
          row.health_update = setTimeout(updateHealth, target.life.interval*1000)
        else
          row.health_update = undefined

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

    function setStatus() {
      let status = status_element.textContent

      if(target.status === "Hospital")
        status = formatHospTime(target.hospital - Date.now())
      else if(target.status === "Okay")
        status = target.life.current + "/" + target.life.maximum

      status_element.textContent = status + " " + target.icon
    }

    function formatHospTime(time_left) {
      return String(Math.floor(time_left/60_000)).padStart(2, '0')
              + ":"
              + String(Math.floor((time_left/1000)%60)).padStart(2, '0')
    }
  }

  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(function apiSorter(a, b) {

      return updateValue(b) - updateValue(a)

      function updateValue(row) {
        const target = targets[getId(row)]
        if(!target
           || target.timestamp + INVALID_TIME < Date.now()
           || row.querySelector("[class*='status___'] > span").textContent !== target.status
          )
          return Infinity

        if(target.life.current < target.life.maximum)
          return Date.now() + target.timestamp

        return target.timestamp
      }
    })
  }

  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 respect = (1 + 0.005*Number(row.querySelector("[class*='level___']").textContent )) * Math.min(3, ff)

      row.querySelector("[class*='level___']").textContent += " " + formatNumber(ff)
      // row.querySelector("[class*='level___']").textContent = formatNumber(respect) + " " + formatNumber(ff)
    })
    .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 target = targets[getId(row)]
    if(target
      && target.timestamp + INVALID_TIME > Date.now()
      && row.querySelector("[class*='status___'] > span").textContent === target.status
    )
      updateTarget(row)
    else
      getApi.push(row)

    function formatNumber(x) {
      return Math.floor(x) + "." + String(Math.floor((x%1)*100)).padStart(2, '0')
    }
  }

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