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