Greasy Fork 还支持 简体中文。

Target list helper

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

目前為 2025-03-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.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))
  }
})()