Target list helper

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

От 14.03.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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