- // ==UserScript==
- // @name Target list helper
- // @namespace szanti
- // @license GPL-3.0-or-later
- // @match https://www.torn.com/page.php?sid=list&type=targets*
- // @grant GM.xmlHttpRequest
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // @grant GM_addStyle
- // @version 2.0.2
- // @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"
- })
-
- {(async function() {
- 'use strict'
-
- if(isPda()) {
- // On TornPDA resorting the list leads to the entire script being reloaded
- if(window.target_list_helper_loaded)
- return
- window.target_list_helper_loaded = true
-
- GM.xmlHttpRequest = GM.xmlhttpRequest
- GM_getValue = (key, default_value) => {
- const value = GM.getValue(key)
- return value ? JSON.parse(value) : 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 long until we stop looking for the hospitalization after a possible attack
- const CONSIDER_ATTACK_FAILED = 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 a 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("[Target list helper] Couldn't find TornTools and TornPal is deactivated, FF estimation unavailable.")
- show_respect = Show.RESP_UNAVAILABLE
- }
-
- const number_format = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2 , maximumFractionDigits: 2 })
- const timer_format = new Intl.DurationFormat("en-US", { style: "digital", fractionalDigits: 0, hoursDisplay: "auto"})
-
- // This is how to fill in react input values so they register
- const native_input_value_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
-
- 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;
- }
- `)
-
- /**
- *
- * ASSETS
- *
- **/
- const refresh_button =
- (function makeRefreshButton(){
- const button = document.createElement("button")
- const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- icon.setAttribute("width", 16)
- icon.setAttribute("height", 15)
- icon.setAttribute("viewBox", "0 0 16 15")
- const icon_path = document.createElementNS("http://www.w3.org/2000/svg", "path")
- 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")
- icon.appendChild(icon_path)
- button.appendChild(icon)
- return button
- })()
-
- const copy_bss_button =
- (function makeCopyBssButton(){
- const button = document.createElement("button")
- const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- icon.setAttribute("width", 16)
- icon.setAttribute("height", 13)
- icon.setAttribute("viewBox", "0 0 16 13")
- const icon_path_1 = document.createElementNS("http://www.w3.org/2000/svg", "path")
- icon_path_1.setAttribute("d", "M16,13S14.22,4.41,6.42,4.41V1L0,6.7l6.42,5.9V8.75c4.24,0,7.37.38,9.58,4.25")
- icon.append(icon_path_1)
- const icon_path_2 = document.createElementNS("http://www.w3.org/2000/svg", "path")
- icon_path_2.setAttribute("d", "M16,12S14.22,3.41,6.42,3.41V0L0,5.7l6.42,5.9V7.75c4.24,0,7.37.38,9.58,4.25")
- icon.append(icon_path_2)
- button.appendChild(icon)
- return button
- })()
-
- /**
- *
- * 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").childNodes) 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 18.")
- }
-
- 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("[Target list helper] 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("[Target list helper] Please set the api polling interval (in ms) on line 19. (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("[Target list helper] Please set the stale time (in ms) on line 20. (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").childNodes) redrawFf(row)
- } catch(e) { // Maybe the user clicks it before fair fight is loaded
- show_respect = old_show_respect
- throw e
- }
- setFfColHeader()
- if(show_respect != Show.RESP_UNAVAILABLE)
- 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("[Target list helper] Please select if you want to see estimated respect Show.RESPECT or Show.LEVEL on line 21. (Default Show.RESPECT)")
- }
- }
-
- /**
- *
- * THE SCRIPT PROPER
- *
- **/
- const row_list = await waitForElement(".tableWrapper > ul", document.getElementById("users-list-root"))
-
- const table = row_list.parentNode
- const table_head = table.querySelector("[class*=tableHead]")
- const description_header = table_head.querySelector("[class*=description___]")
- waitForElement("[aria-label='Remove player from the list']", row_list)
- .then(button => {
- if(button.getAttribute("data-is-tooltip-opened") != null)
- description_header.style.maxWidth = (description_header.scrollWidth - button.scrollWidth) + "px"
- })
-
- setFfColHeader()
- table_head.insertBefore(description_header, table_head.querySelector("[class*=level___]"))
-
- parseTable(row_list)
-
- // Observe changes after resorting
- new MutationObserver(records => {
- records.forEach(r =>
- r.addedNodes.forEach(n => {
- if(n.tagName === "UL") parseTable(n)
- }))})
- .observe(table, {childList: true})
-
- const loop_id = crypto.randomUUID()
- let idle_start = undefined
- let process_responses = []
- 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(), CONSIDER_ATTACK_FAILED)
- })
-
- setInterval(mainLoop, polling_interval)
-
- function mainLoop() {
- const jobs_waiting = profile_updates.length > 0 || ff_updates.length > 0 || process_responses.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)
- 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_deleteValue("has-lock", undefined)
- has_lock = undefined
- }
-
- if(has_lock != loop_id) {
- idle_start = Date.now()
- return
- }
-
- while(process_responses.length > 0)
- process_responses.pop()()
-
- GM_setValue("targets", targets)
-
- 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)
- }
-
- process_responses.push(() => {
- Object.values(r.results)
- .forEach((result) => {
- if(result.status)
- targets[result.result.player_id].fair_fight = {last_updated: result.result.last_updated*1000, value: result.result.value}
- })
- 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()
-
- process_responses.push(() => {
- if(targets[id].timestamp === undefined || targets[id].timestamp <= response_date) {
- Object.assign(targets[id], {
- timestamp: response_date,
- 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*1000,
- level: r.level
- })
- }
- 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 parseTable(table) {
- parseRows(table.childNodes)
-
- // Observe new rows getting added
- 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
-
- reworkRow()
-
- new MutationObserver(records =>
- records.forEach(r =>
- r.addedNodes.forEach(n => {
- if(n.className.includes("buttonsGroup")) reworkRow()
- })))
- .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, fair_fight: target?.fair_fight}
- 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 reworkRow() {
- // Switch description and Ff column
- const description = row.querySelector("[class*=description___]")
- const ff = row.querySelector("[class*='level___']")
- row.querySelector("[class*='contentGroup___']").insertBefore(description, ff)
-
- const buttons_group = row.querySelector("[class*='buttonsGroup']")
- if(!buttons_group)
- return
-
- const sample_button = buttons_group.querySelector("button:not([class*='disabled___'])")
- const disabled_button = buttons_group.querySelector("[class*='disabled___']")
- const edit_button = row.querySelector("[aria-label='Edit user descripton'], [aria-label='Edit player']")
- const wide_mode = sample_button.getAttribute("data-is-tooltip-opened") !== null
-
- const new_refresh_button = refresh_button.cloneNode(true)
- sample_button.classList.forEach(c => new_refresh_button.classList.add(c))
- if(!wide_mode)
- new_refresh_button.append(document.createTextNode("Refresh"))
- buttons_group.prepend(new_refresh_button)
- new_refresh_button.addEventListener("click", () => updateStatus(row, Date.now(), true))
-
- // Fix description width
- if(wide_mode)
- description.style.maxWidth = (description.scrollWidth - new_refresh_button.scrollWidth) + "px"
-
- // Add BSS button
- edit_button?.addEventListener(
- "click",
- async function addBssButton() {
- const faction_el = row.querySelector("[class*='factionImage___']")
- const faction =
- faction_el?.getAttribute("alt") !== ""
- ? faction_el?.getAttribute("alt")
- : faction_el.parentNode.getAttribute("href").match(/[0-9]+/g)[0]
- const bss_str =
- "BSS: " + String(Math.round(((targets[id].fair_fight.value - 1)*3*getBss())/8)).padStart(6, ' ')
- + (faction ? " - " + faction : "")
-
- const new_copy_bss_button = copy_bss_button.cloneNode(true)
-
- const wrapper = await waitForElement("[class*='wrapper___']", row)
- wrapper.childNodes[1].classList.forEach(c => new_copy_bss_button.classList.add(c))
- wrapper.append(new_copy_bss_button)
-
- new_copy_bss_button.addEventListener("click", (e) => {
- e.stopPropagation()
- native_input_value_setter.call(wrapper.childNodes[0], bss_str)
- wrapper.childNodes[0].dispatchEvent(new Event('input', { bubbles: true }))
- })
- if(wide_mode)
- waitForElement("[aria-label='Edit user descripton']", row)
- .then(button => { button.addEventListener("click", addBssButton) })
- })
-
- // Enable attack buttons and make them report if they're clicked
- if(disabled_button) {
- 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)
- }
- (disabled_button ?? buttons_group.querySelector("a")).addEventListener("click", () => attacked_targets.push(row))
- }
- }
-
- profile_updates.sort(
- 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 =
- timer_format.format({minutes: Math.trunc(time_left/60_000), seconds: Math.trunc(time_left/1000%60)})
- + " " + 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 === "Okay")
- status = target.life.current + "/" + target.life.maximum
-
- status_element.textContent = status + " " + target.icon
- }
- }
-
- 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 = number_format.format(respect) + " " + number_format.format(ff)
- else
- text_element.textContent = target.level + " " + number_format.format(ff)
- }
-
- function updateStatus(row, when, fast_track) {
- 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)
- 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, 5000)
- .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)
- })
- .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)
- console.error("[Target list helper] No fair fight estimation from TornPal or torntools for target " + getName(row) + " found. Is FF Scouter enabled?")
- /** Case e - User has enabled TornPal, likely because TornTools is not installed, but we tried it anyway. **/
- })
- }
-
- 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 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.querySelector(".honor-text-wrap > img").alt
- }
-
- function setFfColHeader() {
- document
- .querySelector("[class*='level___'] > button")
- .childNodes[0]
- .data = show_respect == Show.RESPECT ? "R" : "Lvl"
- }
-
- const {getBss} =
- (function bss() {
- let bss = undefined
-
- GM.xmlHttpRequest({ url: `https://api.torn.com/user/?key=${api_key}&selections=battlestats` })
- .then(function setBss(response) {
- let r = undefined
- try {
- r = JSON.parse(response.responseText)
- if(r.error) throw Error(r.error.error)
- } catch(e) {
- console.error("Error getting battlestat score:", e)
- }
- bss = Math.sqrt(r.strength) + Math.sqrt(r.speed) + Math.sqrt(r.dexterity) + Math.sqrt(r.defense)
- })
-
- function getBss() {
- return bss
- }
-
- return {getBss}
- })()
-
- function waitForElement(query_string, element = document, fail_after) {
- const el = element.querySelector(query_string)
- if(el)
- return Promise.resolve(el)
-
- return new Promise((resolve, reject) => {
- let resolved = false
-
- const observer = new MutationObserver(
- function checkElement() {
- observer.takeRecords()
- const el = element.querySelector(query_string)
- if(el) {
- resolved = true
- observer.disconnect()
- resolve(el)
- }
- })
-
- observer.observe(element, {childList: true, subtree: true})
-
- if(Number.isFinite(fail_after))
- setTimeout(() => {
- if(!resolved){
- observer.disconnect()
- reject(query_string + " not found.")
- }
- }, fail_after)
- })
- }
-
- 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
- }
- })()}