Target list helper

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

Verzia zo dňa 13.03.2025. Pozri najnovšiu verziu.

// ==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 && 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))
  }
})()
长期地址
遇到问题?请前往 GitHub 提 Issues。