// ==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.1.4
// @author Szanti
// @description Make FF visible, enable attack buttons, list target hp or remaining hosp time
// ==/UserScript==
(function() {
'use strict'
if(window.navigator.userAgent.includes("com.manuito.tornpda")) {
GM_getValue = (key, default_value) => JSON.parse(GM_getValue(key, default_value))
GM_setValue = (key, value) => GM_setValue(key, JSON.stringify(value))
}
let api_key = GM_getValue("api-key", "###PDA-APIKEY###")
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
// It's ok to display stale data until it can get updated but not invalid data
const INVALID_TIME = Math.max(900_000, stale_time)
const targets = GM_getValue("targets", {})
const getApi = []
const icons =
{ "rock": "🪨",
"paper": "📜",
"scissors": "✂️" }
/**
*
* 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 && 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 25.")
}
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(!GM_getValue("polling-interval"))
console.warn("Please set the api polling interval on line 26 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(!GM_getValue("stale-time"))
console.warn("Please set the stale time on line 27 manually if you wish a different value from the default 5 minutes.")
}
/**
*
* SET UP SCRIPT
*
**/
setInterval(
function mainLoop() {
if(api_key === "###PDA-APIKEY###")
return
let row = getApi.shift()
while(row && !row.isConnected)
row = getApi.shift()
if(!row)
return
const id = getId(row)
GM_xmlhttpRequest({
url: `https://api.torn.com/user/${id}?key=${api_key}&selections=profile`,
onload: function parseAPI({responseText}) {
let r = undefined
try {
r = JSON.parse(responseText) // Can also throw on malformed response
if(r.error)
throw new Error("Api error:", r.error.error)
} catch (e) {
getApi.unshift(row) // Oh Fuck, Put It Back In
throw e
}
targets[id] = {
timestamp: Date.now(),
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
}
GM_setValue("targets", targets)
updateTarget(row)
}
})
}, polling_interval)
waitForElement(".tableWrapper > ul").then(
function setUpTableHandler(table) {
new MutationObserver((records) =>
records.forEach(r => r.addedNodes.forEach(n => { if(n.tagName === "UL") parseTable(n) }))
).observe(table.parentNode, {childList: true})
parseTable(table)
})
function updateTarget(row) {
const id = getId(row)
const status_element = row.querySelector("[class*='status___'] > span")
setStatus(row)
setTimeout(() => getApi.push(row), targets[id].timestamp + stale_time - Date.now())
if(targets[id].status === "Okay" && Date.now() > targets[id].hospital + OUT_OF_HOSP) {
status_element.classList.replace("user-red-status", "user-green-status")
} else if(targets[id].status === "Hospital") {
status_element.classList.replace("user-green-status", "user-red-status")
if(targets[id].hospital < Date.now()) // Defeated but not yet selected where to put
setTimeout(() => getApi.push(row), 5000)
else
setTimeout(() => getApi.push(row), targets[id].hospital + OUT_OF_HOSP - Date.now())
/* 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 = targets[id].hospital - Date.now()
if(time_left > 0 && last_timer == row.timer) {
row.timer = setTimeout(updateTimer,1000 - Date.now()%1000, row)
last_timer = row.timer
} else if(time_left <= 0) {
targets[id].status = "Okay"
}
setStatus(row)
})
}
// Check if we need to register a healing tick in the interim
if(row.health_update || targets[id].life.current == targets[id].life.maximum)
return
let next_health_tick = targets[id].timestamp + targets[id].life.ticktime*1000
while(next_health_tick < Date.now()) {
targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)
next_health_tick += targets[id].life.interval*1000
}
row.health_update =
setTimeout(function updateHealth() {
targets[id].life.current = Math.min(targets[id].life.maximum, targets[id].life.current + targets[id].life.increment)
if(targets[id].life.current < targets[id].life.maximum) {
row.health_update = setTimeout(updateHealth, targets[id].life.interval*1000)
} else {
row.health_update = undefined
targets[id].status = "Okay"
}
setStatus(row)
}, next_health_tick - Date.now())
}
function setStatus(row) {
const id = getId(row)
const status_element = row.querySelector("[class*='status___'] > span")
let status = status_element.textContent
if(targets[id].status === "Hospital") {
const time_left = targets[id].hospital - Date.now()
status = String(Math.floor(time_left/60_000)).padStart(2, '0')
+ ":"
+ String(Math.floor((time_left/1000)%60)).padStart(2, '0')
} else if(targets[id].status === "Okay") {
status = targets[id].life.current + "/" + targets[id].life.maximum
}
status_element.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) + '.' + String(Math.round((ff%1)*100)).padStart(2, '0')
})
.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()
&& row.querySelector("[class*='status___'] > span").textContent === targets[id].status
) {
updateTarget(row)
} else {
getApi.push(row)
}
}
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))
}
})()