Target list helper

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

От 23.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
// @grant       GM_addStyle
// @version     1.2.0
// @author      Szanti
// @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
// ==/UserScript==

const API_KEY = "###PDA-APIKEY###"
const POLLING_INTERVAL = undefined
const STALE_TIME = undefined
const SHOW = undefined // Show.LEVEL // Show.RESPECT
const USE_TORNPAL = undefined // Tornpal.YES // Tornpal.NO // Tornpal.WAIT_FOR_TT

const UseTornPal = Object.freeze({
  YES: "Trying TornPal then TornTools",
  NO: "Disabled TornPal, trying only TornTools",
  WAIT_FOR_TT: "Trying TornTools then TornPal"
})

const Show = Object.freeze({
  LEVEL: "Showing Level",
  RESPECT: "Showing Respect",
  RESP_UNAVAILABLE: "Can't show respect without fair fight estimation"
})

{(function() {
  'use strict'

  if(isPda()) {
    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", API_KEY)
  // Amount of time between each API call
  let polling_interval = GM_getValue("polling-interval", POLLING_INTERVAL ?? 1000)
  // Least amount of time after which to update data
  let stale_time = GM_getValue("stale-time", STALE_TIME ?? 300_000)
  // Show level or respect
  let show_respect = loadEnum(Show, GM_getValue("show-respect", SHOW ?? Show.RESPECT))
  // Torntools is definitely inaccessible on PDA dont bother waiting for it
  let use_tornpal =
      loadEnum(
        UseTornPal,
        GM_getValue("use-tornpal", USE_TORNPAL ?? (isPda() ? UseTornPal.YES : UseTornPal.WAIT_FOR_TT)))

  // How often to try to find a specific condition on the page
  const MAX_TRIES_UNTIL_REJECTION = 5
  // How long to wait in between such tries
  const TRY_DELAY = 1000
  // How long until we consider stop looking for the hospitaliztion after a possible attack
  const DEF_NOT_HOSPITAL = 15_000
  // Time after which a target coming out of hospital is updated
  const OUT_OF_HOSP = 60_000
  // It's ok to display stale data until it can get updated but not invalid data
  const INVALIDATION_TIME = Math.max(900_000, stale_time)

  // Our data cache
  let targets = GM_getValue("targets", {})
  // In queue for profile data update, may need to be replaced with filtered array on unpause
  let profile_updates = []
  // In queue for TornPal update
  const ff_updates = []
  // Update attacked targets when regaining focus
  let attacked_targets = []
  // If the api key can be used for tornpal, assume it works fail if not
  let can_tornpal = true
  // To TornTool or not to TornTool
  const torntools = !(document.documentElement.style.getPropertyValue("--tt-theme-color").length == 0)
  if(!torntools && use_tornpal == UseTornPal.NO) {
    console.warn("Couldn't find TornTools and TornPal is deactivated, FF estimation unavailable.")
    show_respect = Show.RESP_UNAVAILABLE
  }

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

  const Debug = {
    API_LOOP: Symbol("Debug.API_LOOP"),
    UPDATE: Symbol("Debug.UPDATE")
  }

  /**
   *
   * ATTACH CSS FOR FLASH EFFECT
   *
   **/
  GM_addStyle(`
    @keyframes green_flash {
      0% {background-color: var(--default-bg-panel-color);}
      50% {background-color: oklab(from var(--default-bg-panel-color) L -0.087 0.106); }
      100% {background-color: var(--default-bg-panel-color);}
    }
    .flash_green {
      animation: green_flash 500ms ease-in-out;
      animation-iteration-count: 1;
    }
  `)

  /**
   *
   * 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?.length == 16) {
        GM_setValue("api-key", new_key)
        api_key = new_key
        can_tornpal = true
        for(const row of document.querySelector(".tableWrapper > ul").children) updateFf(row)
      } else {
        throw new Error("No valid key detected.")
      }
    })
  } catch (e) {
    if(api_key.charAt(0) === "#")
      throw new Error("Please set the public or TornPal capable api key in the script manually on line 17.")
  }

  try {
    let menu_id = GM_registerMenuCommand(
      use_tornpal,
      function toggleTornPal() {
        use_tornpal = next_state()
        GM_setValue("use-tornpal", use_tornpal)
        menu_id = GM_registerMenuCommand(
          use_tornpal,
          toggleTornPal,
          {id: menu_id, autoClose: false}
        )
      },
      {autoClose: false})

    function next_state() {
      if(use_tornpal == UseTornPal.WAIT_FOR_TT)
        return UseTornPal.YES
      if(use_tornpal == UseTornPal.YES)
        return UseTornPal.NO
      return UseTornPal.WAIT_FOR_TT
    }
  } catch(e) {
    if(USE_TORNPAL === undefined)
      console.warn("Please choose UseTornPal.YES, UseTornPal.NO or UseTornPal.WAIT_FOR_TT on line 22. (Default: UseTornPal.WAIT_FOR_TT)")
  }

  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 === undefined)
      console.warn("Please set the api polling interval (in ms) on line 18. (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 300)?", 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 === undefined)
      console.warn("Please set the stale time (in ms) on line 19. (default 5 minutes)")
  }

  try {
    let menu_id = GM_registerMenuCommand(
      show_respect,
      function toggleRespect() {
        const old_show_respect = show_respect
        show_respect = next_state()
        try {
          for(const row of document.querySelector(".tableWrapper > ul").children) redrawFf(row)
        } catch(e) { // Maybe the user clicks it before fair fight is loaded
          show_respect = old_show_respect
          throw e
        }
        setFfColHeader()
        GM_setValue("show-respect", show_respect)
        menu_id = GM_registerMenuCommand(
          show_respect,
          toggleRespect,
          {id: menu_id, autoClose: false}
        )
      },
      {autoClose: false}
    )

    function next_state() {
      if(use_tornpal == UseTornPal.NO || (!can_tornpal && !torntools))
        return Show.RESP_UNAVAILABLE
      if(show_respect == Show.RESPECT)
        return Show.LEVEL
      return Show.RESPECT
    }
  } catch(e) {
    if(SHOW === undefined)
      console.warn("Please select if you want to see estimated respect Show.RESPECT or Show.LEVEL on line 20. (Default Show.RESPECT)")
  }

  /**
   *
   * SET UP SCRIPT
   *
   **/
  waitForElement(".tableWrapper > ul")
  .then(function attachToTable(table) {
    const wrapper = table.parentNode

    const button = table.querySelector("[class*='buttonsGroup'] > button")
    if(button.getAttribute("data-is-tooltip-opened") != null) {
      const description = wrapper.querySelector("[class*=tableHead] > [class*=description___]")
      description.style.maxWidth = description.scrollWidth - button.scrollWidth + "px"
    }

    setFfColHeader()

    parseTable(table)

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

    startLoop()
  })

  function parseTable(table) {
    parseRows(table.children)

    new MutationObserver(
      records => records.forEach(r => parseRows(r.addedNodes))
    ).observe(table, {childList: true})

    function parseRows(rows) {
      for(const row of rows) {
        if(row.classList.contains("tornPreloader"))
          continue

        const id = getId(row)
        const target = targets[id]
        const level_from_page = Number(row.querySelector("[class*='level___']").textContent)
        const status_from_page = row.querySelector("[class*='status___'] > span").textContent

        reworkButtons()

        new MutationObserver(records =>
          records.forEach(r =>
            r.addedNodes.forEach(n => {
              if(n.className.includes("buttonsGroup")) reworkButtons()
        })))
        .observe(row, {childList: true})

        if(target?.timestamp + INVALIDATION_TIME > Date.now() && status_from_page === target?.status) {
          redrawStatus(row)
          updateStatus(row, target.timestamp + stale_time)
        } else {
          targets[id] = {level: level_from_page, status: status_from_page}
          if(status_from_page === "Hospital")
            updateUntilHospitalized(row)
          else
            updateStatus(row)
        }

        if(target?.fair_fight?.last_updated > target?.last_action)
          redrawFf(row)
        else
          updateFf(row)

        function reworkButtons() {
          const buttons_group = row.querySelector("[class*='buttonsGroup']")
          if(!buttons_group)
            return

          const sample_button = buttons_group.querySelector("button")
          const disabled_button = buttons_group.querySelector("[class*='disabled___']")
          const refresh_button = document.createElement("button")

          sample_button.classList.forEach(c => {
            if(c.charAt(0) !== 'd')
              refresh_button.classList.add(c)
          })

          const refresh_icon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
          refresh_icon.setAttribute("width", 16)
          refresh_icon.setAttribute("height", 15)
          refresh_icon.setAttribute("viewBox", "0 0 16 15")

          const refresh_icon_path = document.createElementNS("http://www.w3.org/2000/svg", "path")
          refresh_icon_path.setAttribute("d", "M9,0A7,7,0,0,0,2.09,6.83H0l3.13,3.5,3.13-3.5H3.83A5.22,5.22,0,1,1,9,12.25a5.15,5.15,0,0,1-3.08-1l-1.2,1.29A6.9,6.9,0,0,0,9,14,7,7,0,0,0,9,0Z")

          refresh_icon.append(refresh_icon_path)
          refresh_button.appendChild(refresh_icon)

          if(sample_button.getAttribute("data-is-tooltip-opened") == null) {
            refresh_button.append(document.createTextNode("Refresh"))
          } else {
            const description = row.querySelector("[class*=description___]")
            description.style.maxWidth = description.scrollWidth - sample_button.scrollWidth + "px"
          }

          buttons_group.prepend(refresh_button)

          refresh_button.addEventListener("click", () => updateStatus(row, Date.now(), true))
          buttons_group.modified = true


          if(!disabled_button) {
            buttons_group.querySelector("a").addEventListener("click", () => attacked_targets.push(row))
            return
          }
          const a = document.createElement("a")
          a.href = `/loader2.php?sid=getInAttack&user2ID=${id}`
          disabled_button.childNodes.forEach(n => a.appendChild(n))
          disabled_button.classList.forEach(c => {
            if(c.charAt(0) !== 'd'){
              a.classList.add(c)}
          })
          disabled_button.parentNode.insertBefore(a, disabled_button)
          disabled_button.parentNode.removeChild(disabled_button)
          a.addEventListener("click",  () => attacked_targets.push(row))
        }
      }

      profile_updates.sort(prioritizeUpdates)

      function prioritizeUpdates(a, b) {
        return updateValue(b) - updateValue(a)

        function updateValue(row) {
          const target = targets[getId(row)]
          if(!target?.timestamp || target.timestamp + INVALIDATION_TIME < Date.now())
            return Infinity

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

          return target.timestamp
        }
      }
    }
  }

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

    setStatus()

    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
        updateUntilHospitalized(row)
      else
        updateStatus(row,  target.hospital + OUT_OF_HOSP)

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

    // 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))
      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
      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 redrawFf(row) {
    const target = targets[getId(row)]
    const ff = target.fair_fight.value

    const text_element = row.querySelector("[class*='level___']")
    const respect = (1 + 0.005 * target.level) * Math.min(3, ff)
    if(show_respect == Show.RESPECT)
      text_element.textContent = formatNumber(respect) + " " + formatNumber(ff)
    else
      text_element.textContent = target.level + " " + formatNumber(ff)

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

  function updateStatus(row, when, fast_track) {
    log(Debug.UPDATE, "Going to update", getName(row), "at", new Date(when).toLocaleTimeString())
    const requested_at = Date.now()
    const id = getId(row)
    if(fast_track && !row.fast_tracked) {
      row.updating = true
      row.fast_tracked = true
      profile_updates.unshift(row)
      return
    }
    setTimeout(() => {
      if(row.updating || targets[id]?.timestamp > requested_at) {
        if(row.updating)
          log(Debug.UPDATE, "Already marked for update", getName(row))
        else
          log(Debug.UPDATE, "Already updated", getName(row))
        return
      }

      row.updating = true
        profile_updates.push(row)
    }, when - Date.now())
  }

  function updateFf(row) {
    /**
     *    UseTornPal |  can_tornpal  |  torntools  | case | action
     *   ------------+---------------+-------------+------+--------
     *           YES |           YES |         N/A |    a | ff_updates.push
     *           YES |            NO |         YES |    e | try_tt (error when can_tornpal got set), fail silently
     *           YES |            NO |          NO |    b | fail silently (error whet can_tornpal got set)
     *            NO |           N/A |         YES |    d | try_tt, fail with error
     *            NO |           N/A |          NO |    b | fail silently (warn when torntools got set)
     *   WAIT_FOR_TT |           YES |         YES |    c | try_tt catch ff_updates.push
     *   WAIT_FOR_TT |           YES |          NO |    a | ff_updates.push
     *   WAIT_FOR_TT |            NO |         YES |    d | try_tt, fail with error
     *   WAIT_FOR_TT |            NO |          NO |    b | fail silently (error when can_tornpal got set)
     **/
    /** Case a - Only TornPal **/
    if((use_tornpal == UseTornPal.YES && can_tornpal)
       || (use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal && !torntools)
    ) {
      ff_updates.push(row)
      return
    }

    /** Case b - Neither TornPal nor Torntools **/
    if(!torntools)
      return

    waitForElement(".tt-ff-scouter-indicator", row)
    .catch(function noTtFound(e) {
      /** Case c - TornTools failed so try TornPal next **/
      if(use_tornpal == UseTornPal.WAIT_FOR_TT && can_tornpal)
        ff_updates.push(row)
      /** Case d - TornTools failed but TornPal cannot be used**/
      else if(use_tornpal == UseTornPal.NO || use_tornpal == UseTornPal.WAIT_FOR_TT)
        throw new Error("Cannot find fair fight estimation from tornpal or from torntools.")
      /** Case e - User has enabled TornPal but it failed already, likely because TornTools is not installed, but we tried it anyway. **/
    })
    .then(function ffFromTt(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 id = getId(row)
      Object.assign(targets[getId(row)], {fair_fight: {value: ff}})
      redrawFf(row)
    })
  }

  function updateUntilHospitalized(row, time_out_after = INVALIDATION_TIME) {
    const id = getId(row)
    const start = Date.now()
    updateStatus(row)
    const attack_updater = setInterval(
      function attackUpdater() {
        updateStatus(row)
        if((targets[id]?.hospital > Date.now()) || Date.now() > start + time_out_after) {
          clearInterval(attack_updater)
          return
        }
      }, polling_interval)
  }

  function startLoop() {
    const loop_id = crypto.randomUUID()
    let idle_start = undefined
    let run_if_locked = []
    GM_setValue("main-loop", loop_id)
    GM_setValue("has-lock", loop_id)

    addEventListener("focus", function refocus() {
      GM_setValue("main-loop", loop_id)
      while(attacked_targets.length > 0)
        updateUntilHospitalized(attacked_targets.pop(), DEF_NOT_HOSPITAL)
    })

    setInterval(mainLoop, polling_interval)

    function mainLoop() {
      const jobs_waiting = profile_updates.length > 0 || ff_updates.length > 0 || run_if_locked.length > 0
      let has_lock = GM_getValue("has-lock")

      if(jobs_waiting && has_lock != loop_id && (has_lock === undefined || GM_getValue("main-loop") == loop_id)) {
        GM_setValue("has-lock", loop_id)
        log(Debug.API_LOOP, loop_id, "Setting lock and unpausing")
        has_lock = loop_id

        Object.assign(targets, GM_getValue("targets", {}))
        profile_updates =
          profile_updates
          .filter(row => {
            const t = targets[getId(row)]
            if(!t?.timestamp || t.timestamp < idle_start)
              return true
            finishUpdate(row)
            return false
          })
      } else if(!jobs_waiting && has_lock == loop_id) {
        GM_setValue("has-lock", undefined)
        log(Debug.API_LOOP, loop_id, "Releasing lock")
        has_lock = undefined
      }

      if(has_lock != loop_id) {
        log(Debug.API_LOOP, loop_id, "Idling")
        idle_start = Date.now()
        return
      }

      log(Debug.API_LOOP, loop_id, "Running")
      while(run_if_locked.length > 0)
        run_if_locked.pop()()

      if(api_key.charAt(0) === "#")
        return

      /**
       *
       * TornPal updates
       *
       **/
      if(ff_updates.length > 0) {
        const scouts = ff_updates.splice(0,250)
        GM_xmlhttpRequest({
          url: `https://tornpal.com/api/v1/ffscoutergroup?comment=targetlisthelper&key=${api_key}&targets=${scouts.map(getId).join(",")}`,

          onload: function updateFf({responseText}) {

            const r = JSON.parse(responseText)

            if(!r.status) {
              if(r.error_code == 772) {
                can_tornpal = false
                if(!torntools)
                  show_respect = Show.RESP_UNAVAILABLE
              }
              throw new Error("TornPal error: " + r.message)
            }

            run_if_locked.push(() => {
              Object.values(r.results)
              .forEach(({result}) => {
                if(result.status)
                  targets[result.player_id].fair_fight = {last_updated: result.last_updated, value: result.value}
              })

              GM_setValue("targets", targets)

              setTimeout(() => {
                scouts.forEach(row => {
                  if(targets[getId(row)].fair_fight)
                    redrawFf(row)
                })
              })
            })
          }
        })
      }

      /**
       *
       * Torn profile updates
       *
       **/
      let row
      while(profile_updates.length > 0 && !row?.isConnected)
        row = profile_updates.shift()

      if(!row)
        return

      const id = getId(row)

      GM_xmlhttpRequest({
        url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
        onload: function updateProfile({responseText}) {
          let r = undefined
          try {
            r = JSON.parse(responseText) // Can also throw on malformed response
            if(r.error)
              throw new Error("Torn error: " + r.error.error)
          } catch (e) {
            profile_updates.unshift(row) // Oh Fuck, Put It Back In
            throw e
          }

          const response_date = Date.now()

          run_if_locked.push(() => {
            if(targets[id]?.timestamp < response_date) {
              Object.assign(targets[id], {
                timestamp: response_time,
                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,
                last_action: r.last_action.timestamp,
                level: r.level
              })

              GM_setValue("targets", targets)
            }
            finishUpdate(row)
          })
        }
      })

      function finishUpdate(row) {
        row.updating = false
        row.fast_tracked = false

        setTimeout(() => {
          row.classList.add('flash_green');
          setTimeout(() => row.classList.remove('flash_green'), 500)

          redrawStatus(row)
          updateStatus(row, targets[getId(row)].timestamp + stale_time)
        })
      }
    }
  }

  function getId(row) {
    if(!row.player_id)
      row.player_id = row.querySelector("[class*='honorWrap___'] > a").href.match(/\d+/)[0]
    return row.player_id
  }

  function getName(row) {
    return row.querySelectorAll(".honor-text").values().reduce((text, node) => node.textContent ?? text)
  }

  function setFfColHeader() {
    document
    .querySelector("[class*='level___'] > button")
    .childNodes[0]
    .data = show_respect == Show.RESPECT ? "R" : "Lvl"
  }

  function waitForCondition(condition, silent_fail, max_tries = MAX_TRIES_UNTIL_REJECTION) {
    return new Promise((resolve, reject) => {
      let tries = 0
      const interval = setInterval(
        function conditionChecker() {
          const result = condition()
          tries += 1

          if(!result && tries <= max_tries)
            return

          clearInterval(interval)

          if(result)
            resolve(result)
          else if(!silent_fail)
            reject(result)
      }, TRY_DELAY)
    })
  }

  function waitForElement(query_string, element = document, silent_fail = false) {
    return waitForCondition(() => element.querySelector(query_string), silent_fail)
  }

  function isPda() {
    return window.navigator.userAgent.includes("com.manuito.tornpda")
  }

  /** Ugly as fuck because we cant save what cant be stringified :/ **/
  function loadEnum(the_enum, loaded_value) {
    for(const [key,value] of Object.entries(the_enum)) {
      if(value === loaded_value)
        return the_enum[key]
    }
    return undefined
  }

  function log(type, ...message) {
    if(true)
      return
    else if(type == Debug.API_LOOP)
      console.log(new Date().toLocaleTimeString(), ...message)/**/
    else if(type == Debug.UPDATE)
      console.log(new Date().toLocaleTimeString(), ...message)/**/
  }
})()}