Fitbit-style level progress tracker plus dynamic live Baldr targets.
// ==UserScript==
// @name Torn Leveling Helper
// @namespace Torn Leveling Helper
// @version 2.0.8
// @description Fitbit-style level progress tracker plus dynamic live Baldr targets.
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @connect api.torn.com
// @connect raw.githubusercontent.com
// @connect oran.pw
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const KEY_VERSION = '2.0.8';
const XP_CACHE_SCHEMA = 'hof-progress-v2';
const HOF_CACHE_MS = 5 * 60 * 1000;
const TARGET_REFRESH_MS = 25 * 1000;
const LIVE_TARGET_CACHE_MS = 90 * 1000;
const DISPLAYED_TARGET_RECHECK_MS = 20 * 1000;
const TARGET_STATUS_DELAY_MS = 1500;
const BALDR_RAW_CACHE_MS = 6 * 60 * 60 * 1000;
const BALDR_DATA_URLS = [
'https://raw.githubusercontent.com/OranWeb/tc-baldrs-levelling-list/master/data.json',
'https://oran.pw/baldrstargets/data.json'
];
const MAX_TARGETS_SHOWN = 6;
const CANDIDATES_TO_CHECK = 8;
const MIN_TOTAL_SAFETY_RATIO = 2.0;
const MAX_TARGET_SPEED_RATIO = 1.0;
const BOX_ID = 'xpPctBox';
const STYLE_ID = 'xpPctStyle';
let lastUrl = location.href;
let mountTimer = null;
let initRunning = false;
let appStarted = false;
let activeApiKey = '';
let targetScanRunning = false;
let displayedRecheckRunning = false;
let nextTargetRefreshAt = 0;
let lastHospitalDetectedId = '';
if (GM_getValue('keyVersion') !== KEY_VERSION) {
GM_deleteValue('xpPctCache');
GM_deleteValue('baldrRawCache');
GM_deleteValue('liveTargetCache');
GM_setValue('keyVersion', KEY_VERSION);
}
addStyle(`
#xpPctBox {
margin: 6px 0;
padding: 12px;
background: radial-gradient(circle at top, #1f3b2b, #07110b 70%);
border: 1px solid rgba(139,195,74,.55);
border-radius: 18px;
color: #dfffd2;
font-size: 12px;
max-width: 285px;
z-index: 999999;
box-shadow:
0 0 16px rgba(139,195,74,.35),
inset 0 0 18px rgba(0,0,0,.85);
font-family: Arial, sans-serif;
}
#xpPctBox b {
color: #b8ff7a;
text-transform: uppercase;
letter-spacing: .6px;
}
#xpPctPercent {
margin-top: 4px;
font-size: 28px;
font-weight: bold;
color: #9cff57;
text-shadow: 0 0 10px rgba(156,255,87,.8);
}
#xpRefreshText,
#targetRefreshText {
opacity: .75;
font-size: 11px;
margin-top: 3px;
color: #c9ffbd;
}
#xpPctBarOuter {
height: 12px;
background: #102017;
border-radius: 999px;
overflow: hidden;
margin-top: 7px;
border: 1px solid rgba(139,195,74,.25);
box-shadow: inset 0 0 6px rgba(0,0,0,.8);
}
#xpPctBarInner {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #39ff88, #b8ff3d);
box-shadow: 0 0 12px rgba(57,255,136,.85);
border-radius: 999px;
transition: width 1.2s ease-out;
animation: xpPulse 1.8s ease-in-out infinite;
}
@keyframes xpPulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.35); }
}
.xpHeartbeat {
margin: 7px 0 6px;
height: 28px;
overflow: hidden;
border-radius: 10px;
background: rgba(0,0,0,.28);
border: 1px solid rgba(139,195,74,.18);
}
.xpHeartbeat svg {
width: 220px;
height: 28px;
animation: xpHeartMove var(--heart-speed, 1.6s) linear infinite;
}
.xpHeartbeat path {
fill: none;
stroke: #39ff88;
stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(57,255,136,.9));
}
.xpHeartbeat.dead {
border-color: rgba(255,70,70,.35);
}
.xpHeartbeat.dead svg {
animation: none;
}
.xpHeartbeat.dead path {
stroke: #ff4545;
filter: drop-shadow(0 0 5px rgba(255,69,69,.9));
}
@keyframes xpHeartMove {
from { transform: translateX(0); }
to { transform: translateX(-110px); }
}
#xpTargetsBox {
margin-top: 11px;
padding-top: 8px;
border-top: 1px solid rgba(139,195,74,.25);
}
#xpTargetsHeader {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
margin-bottom: 5px;
}
.xpTargetRow {
display: grid;
grid-template-columns: 1fr auto;
gap: 6px;
align-items: center;
background: rgba(255,255,255,.035);
border-radius: 10px;
padding: 6px;
margin-top: 5px;
border: 1px solid rgba(139,195,74,.10);
}
.xpTargetName {
color: #f0ffe8;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 170px;
}
.xpTargetMeta {
opacity: .78;
font-size: 10px;
margin-top: 1px;
line-height: 1.25;
color: #d6ffc9;
}
.xpAttackBtn {
text-decoration: none;
background: linear-gradient(180deg, #53d769, #1f8f3a);
color: #041006 !important;
border-radius: 999px;
padding: 4px 8px;
font-size: 11px;
font-weight: bold;
box-shadow: 0 0 8px rgba(83,215,105,.45);
}
.xpAttackBtn:hover {
filter: brightness(1.15);
}
.xpTiny {
opacity: .68;
font-size: 10px;
margin-top: 4px;
color: #d6ffc9;
}
`);
installNavigationWatcher();
setInterval(watchAttackPageForHospitalizedTarget, 1500);
scheduleInit();
function scheduleInit() {
clearTimeout(mountTimer);
mountTimer = setTimeout(() => init(), 300);
}
async function init() {
if (initRunning) return;
initRunning = true;
try {
let key = GM_getValue('tornApiKey', '');
if (!key) {
key = prompt('Enter your Torn API key:');
if (!key) return;
GM_setValue('tornApiKey', key.trim());
}
activeApiKey = key;
const cached = GM_getValue('xpPctCache', null);
if (isValidXpCache(cached)) {
renderPercent(cached.percent, cached.time, true);
startTargetLoop(key);
appStarted = true;
} else {
GM_deleteValue('xpPctCache');
if (!document.querySelector(`#${BOX_ID}`)) {
injectBox('Loading progress...');
}
}
if (isValidXpCache(cached) && Date.now() - cached.time < HOF_CACHE_MS) {
return;
}
await refreshHofPercent(key, cached);
startTargetLoop(key);
appStarted = true;
} catch (err) {
const cached = GM_getValue('xpPctCache', null);
if (isValidXpCache(cached)) {
renderPercent(cached.percent, cached.time, true, `HoF update failed: ${err.message}`);
} else {
injectBox(`Progress unavailable<br><small>${escapeHtml(err.message)}</small>`);
}
} finally {
initRunning = false;
}
}
function isValidXpCache(cached) {
return (
cached &&
cached.schema === XP_CACHE_SCHEMA &&
typeof cached.percent === 'number' &&
Number.isFinite(cached.percent) &&
cached.percent >= 0 &&
cached.percent < 100 &&
cached.time
);
}
async function refreshHofPercent(key, oldCache) {
updateProgressText(isValidXpCache(oldCache)
? 'Showing cached progress while updating HoF...'
: 'Loading HoF progress...'
);
const profile = await apiV1('/user/?selections=profile', key);
const userId = profile.player_id || profile.user_id || profile.id;
const level = profile.level || profile.Level;
if (!userId || !level) {
throw new Error('Could not read player ID or level.');
}
updateProgressText('Reading level HoF rank...');
const hof = await apiV2(`/user/${userId}/hof`, key);
const myRank =
hof.hof?.level?.rank ||
hof.level?.rank ||
hof.level_rank ||
hof.rank;
if (!myRank) {
throw new Error('Could not read level HoF rank.');
}
updateProgressText('Calculating HoF progress...');
const topRank = await findBoundary(key, Number(level), Number(myRank), 'top');
const bottomRank = await findBoundary(key, Number(level), Number(myRank), 'bottom');
if (!Number.isFinite(topRank) || !Number.isFinite(bottomRank) || bottomRank <= topRank) {
throw new Error('Could not calculate valid HoF boundary.');
}
const linear = (bottomRank - Number(myRank)) / (bottomRank - topRank);
let percent = (Math.log1p(linear * 9) / Math.log1p(9)) * 100;
if (!Number.isFinite(percent)) {
throw new Error('HoF percentage calculation returned invalid data.');
}
percent = Math.max(0, Math.min(99.99, percent));
const cacheTime = Date.now();
GM_setValue('xpPctCache', {
schema: XP_CACHE_SCHEMA,
time: cacheTime,
percent
});
renderPercent(percent, cacheTime, false);
}
async function findBoundary(key, level, myRank, direction) {
let step = 100;
let low = myRank;
let high = myRank;
while (step <= 500000) {
const testRank =
direction === 'top'
? Math.max(1, myRank - step)
: myRank + step;
const testLevel = await getLevelAtRank(key, testRank);
if (direction === 'top') {
if (testLevel > level || testRank === 1) {
low = testRank;
high = myRank;
break;
}
} else {
if (testLevel < level || testLevel === null) {
low = myRank;
high = testRank;
break;
}
}
step *= 2;
}
while (low < high) {
const mid = Math.floor((low + high) / 2);
const midLevel = await getLevelAtRank(key, mid);
if (direction === 'top') {
if (midLevel >= level) high = mid;
else low = mid + 1;
} else {
if (midLevel >= level) low = mid + 1;
else high = mid;
}
}
return direction === 'top' ? low : low - 1;
}
async function getLevelAtRank(key, rank) {
const offset = Math.max(0, rank - 1);
const data = await apiV2(
`/torn/hof?cat=level&limit=1&offset=${offset}`,
key
);
const row =
data.hof?.[0] ||
data.hof?.level?.[0] ||
data.level?.[0] ||
Object.values(data.hof || {})[0];
if (!row) return null;
return Number(row.value || row.level || row.score || row.amount || 0);
}
function startTargetLoop(key) {
clearInterval(window.xpTargetRefreshTimer);
clearInterval(window.xpDisplayedTargetRecheckTimer);
clearInterval(window.xpTargetCountdownTimer);
nextTargetRefreshAt = Date.now() + TARGET_REFRESH_MS;
loadTargetsPanel(key, true);
window.xpTargetRefreshTimer = setInterval(() => {
nextTargetRefreshAt = Date.now() + TARGET_REFRESH_MS;
if (!document.querySelector(`#${BOX_ID}`)) {
scheduleInit();
return;
}
recheckDisplayedTargets(key, true);
}, TARGET_REFRESH_MS);
window.xpDisplayedTargetRecheckTimer = setInterval(() => {
if (!document.querySelector(`#${BOX_ID}`)) return;
recheckDisplayedTargets(key, false);
}, DISPLAYED_TARGET_RECHECK_MS);
window.xpTargetCountdownTimer = setInterval(() => {
updateTargetCountdownText();
}, 1000);
}
async function loadTargetsPanel(key, forceScan = false) {
const panel = document.querySelector('#xpTargetsContent');
if (!panel || !key) return;
const cached = GM_getValue('liveTargetCache', null);
if (
!forceScan &&
cached &&
Date.now() - cached.time < LIVE_TARGET_CACHE_MS
) {
renderTargets(cached.targets || [], cached.myStats || emptyStats());
updateTargetRefreshText(cached.totalRawTargets || 0, true);
return;
}
if (targetScanRunning) return;
targetScanRunning = true;
panel.innerHTML = `
${renderHeartbeat(0)}
Checking target pulse...
`;
try {
const myStats = await getMyBattleStats(key);
const rawTargets = await getBaldrTargets();
const candidates = rawTargets
.map(normalizeTarget)
.filter(t => t.id && t.level)
.filter(t =>
t.total <= 0 ||
myStats.total >= t.total * MIN_TOTAL_SAFETY_RATIO
)
.filter(t =>
t.speed <= 0 ||
t.speed <= myStats.speed * MAX_TARGET_SPEED_RATIO
)
.sort(sortMaxPotentialXp)
.slice(0, CANDIDATES_TO_CHECK);
const liveTargets = [];
for (const target of candidates) {
if (liveTargets.length >= MAX_TARGETS_SHOWN) break;
await sleep(TARGET_STATUS_DELAY_MS);
const live = await getTargetLiveStatus(key, target.id);
target.name = live.name || target.name;
target.level = live.level || target.level;
target.statusState = live.statusState;
target.statusText = live.statusText;
if (!isAliveAvailable(live.statusState, live.statusText)) {
continue;
}
liveTargets.push(target);
}
GM_setValue('liveTargetCache', {
time: Date.now(),
targets: liveTargets,
myStats,
totalRawTargets: rawTargets.length
});
renderTargets(liveTargets, myStats);
updateTargetRefreshText(rawTargets.length, false);
} catch (err) {
const cached = GM_getValue('liveTargetCache', null);
if (cached?.targets) {
renderTargets(cached.targets, cached.myStats || emptyStats());
updateTargetRefreshText(cached.totalRawTargets || 0, true, 'API busy');
} else {
panel.innerHTML =
`${renderHeartbeat(0)}
<span style="color:#f99;">Targets unavailable</span><br>
<small>${escapeHtml(err.message)}</small>`;
}
} finally {
targetScanRunning = false;
}
}
async function recheckDisplayedTargets(key, forceDeepIfEmpty = false) {
if (displayedRecheckRunning || targetScanRunning || !key) return;
const cached = GM_getValue('liveTargetCache', null);
if (!cached?.targets?.length) {
if (forceDeepIfEmpty) {
await loadTargetsPanel(key, true);
}
return;
}
displayedRecheckRunning = true;
try {
const checked = [];
for (const target of cached.targets) {
await sleep(TARGET_STATUS_DELAY_MS);
try {
const live = await getTargetLiveStatus(key, target.id);
if (isAliveAvailable(live.statusState, live.statusText)) {
checked.push({
...target,
name: live.name || target.name,
level: live.level || target.level,
statusState: live.statusState,
statusText: live.statusText
});
}
} catch {
checked.push(target);
}
}
cached.targets = checked;
cached.time = Date.now();
GM_setValue('liveTargetCache', cached);
renderTargets(checked, cached.myStats || emptyStats());
updateTargetRefreshText(cached.totalRawTargets || 0, true, 'fresh live check');
if (!checked.length && forceDeepIfEmpty) {
await loadTargetsPanel(key, true);
}
} finally {
displayedRecheckRunning = false;
}
}
function removeTargetFromCache(id) {
const cached = GM_getValue('liveTargetCache', null);
if (!cached?.targets) return;
cached.targets = cached.targets.filter(
t => Number(t.id) !== Number(id)
);
cached.time = Date.now();
GM_setValue('liveTargetCache', cached);
renderTargets(cached.targets, cached.myStats || emptyStats());
updateTargetRefreshText(cached.totalRawTargets || 0, true, 'removed target');
}
function watchAttackPageForHospitalizedTarget() {
let attackedId = '';
try {
const url = new URL(location.href);
attackedId =
url.searchParams.get('user2ID') ||
url.searchParams.get('userID') ||
url.searchParams.get('XID') ||
'';
} catch {
return;
}
if (!attackedId) return;
const text = document.body?.innerText?.toLowerCase() || '';
const targetDowned =
text.includes('hospitalized') ||
text.includes('hospitalised') ||
text.includes('you hospitalized') ||
text.includes('you hospitalised') ||
text.includes('is in hospital') ||
text.includes('left them in hospital') ||
text.includes('mugged') ||
text.includes('attacked and left');
if (!targetDowned) return;
if (lastHospitalDetectedId === `${attackedId}:${location.href}`) return;
lastHospitalDetectedId = `${attackedId}:${location.href}`;
removeTargetFromCache(attackedId);
const key = activeApiKey || GM_getValue('tornApiKey', '');
if (key) {
GM_deleteValue('liveTargetCache');
nextTargetRefreshAt = Date.now();
loadTargetsPanel(key, true);
}
}
function renderHeartbeat(aliveCount) {
const dead = aliveCount <= 0;
const speed =
dead
? 0
: Math.max(0.45, 1.8 - aliveCount * 0.22);
return `
<div
class="xpHeartbeat ${dead ? 'dead' : ''}"
style="--heart-speed:${speed}s"
title="${dead ? 'No live targets' : `${aliveCount} live targets`}"
>
<svg viewBox="0 0 220 28" preserveAspectRatio="none">
<path d="${
dead
? 'M0 14 H220'
: 'M0 14 H25 L32 14 L38 5 L47 23 L56 14 H82 L89 14 L95 7 L104 22 L113 14 H140 L147 14 L153 5 L162 23 L171 14 H220'
}"></path>
</svg>
</div>
`;
}
function renderTargets(targets, myStats) {
const panel = document.querySelector('#xpTargetsContent');
if (!panel) return;
if (!targets.length) {
panel.innerHTML = `
${renderHeartbeat(0)}
No alive available easy targets right now.
<div class="xpTiny">
Next live check in ${formatCountdown(nextTargetRefreshAt - Date.now())}.
</div>
`;
return;
}
panel.innerHTML =
renderHeartbeat(targets.length) +
targets.map(t => `
<div class="xpTargetRow">
<div>
<div class="xpTargetName">${escapeHtml(t.name)}</div>
<div class="xpTargetMeta">
Lv ${Number(t.level).toLocaleString()}
· ${t.total ? `${shortNumber(t.total)} total` : 'stats ?'}
${t.speed ? `· ${shortNumber(t.speed)} spd` : ''}
<br>
${escapeHtml(t.statusState || 'Okay')}
· ${escapeHtml(t.listName)}
</div>
</div>
<a
class="xpAttackBtn"
data-target-id="${encodeURIComponent(t.id)}"
href="https://www.torn.com/page.php?sid=attack&user2ID=${encodeURIComponent(t.id)}"
target="_blank"
rel="noopener noreferrer"
>Go</a>
</div>
`).join('') +
`
<div class="xpTiny">
Total ${shortNumber(myStats.total)}
· SPD ${shortNumber(myStats.speed)}
· max XP mode
</div>
`;
document.querySelectorAll('.xpAttackBtn').forEach(btn => {
btn.onclick = () => {
const id = btn.getAttribute('data-target-id');
if (id) removeTargetFromCache(id);
};
});
}
function renderPercent(percent, cacheTime, cached = false, warning = '') {
const safePercent =
Math.max(0, Math.min(99.99, Number(percent)));
injectBox(`
<div><b>Level Progress</b></div>
<div id="xpPctPercent">
${safePercent.toFixed(2)}%
</div>
<div id="xpRefreshText">
${cached ? 'Cached HoF while syncing' : 'HoF synced'}
· <span id="xpRefreshCountdown">${formatCountdown(getRemainingMs(cacheTime))}</span>
${warning ? `<br><small>${escapeHtml(warning)}</small>` : ''}
</div>
<div id="xpPctBarOuter">
<div id="xpPctBarInner"></div>
</div>
<div id="xpTargetsBox">
<div id="xpTargetsHeader">
<b>Target Pulse</b>
</div>
<div id="targetRefreshText">
Next live check in ${formatCountdown(TARGET_REFRESH_MS)}
</div>
<div id="xpTargetsContent">
Loading targets...
</div>
</div>
`);
requestAnimationFrame(() => {
const bar =
document.querySelector('#xpPctBarInner');
if (bar) {
bar.style.width = `${safePercent}%`;
}
});
startRefreshCountdown(cacheTime);
}
function updateProgressText(text) {
const el = document.querySelector('#xpRefreshText');
if (el) el.innerHTML = escapeHtml(text);
}
function startRefreshCountdown(cacheTime) {
clearInterval(window.xpRefreshCountdownTimer);
window.xpRefreshCountdownTimer = setInterval(() => {
const el =
document.querySelector('#xpRefreshCountdown');
if (!el) return;
const remainingMs = getRemainingMs(cacheTime);
el.textContent = formatCountdown(remainingMs);
if (remainingMs <= 0) {
clearInterval(window.xpRefreshCountdownTimer);
scheduleInit();
}
}, 1000);
}
function updateTargetRefreshText(totalRawTargets, cached = false, note = '') {
const el =
document.querySelector('#targetRefreshText');
if (!el) return;
const now = new Date();
const remaining = Math.max(0, nextTargetRefreshAt - Date.now());
el.textContent =
`${cached ? 'Cached' : 'Updated'} ${now.toLocaleTimeString()} · ${totalRawTargets} targets · next live check in ${formatCountdown(remaining)}${note ? ` · ${note}` : ''}`;
}
function updateTargetCountdownText() {
const el = document.querySelector('#targetRefreshText');
const cached = GM_getValue('liveTargetCache', null);
if (!el) return;
const remaining = Math.max(0, nextTargetRefreshAt - Date.now());
if (cached?.targets) {
el.textContent =
`Showing ${cached.targets.length} live targets · next live check in ${formatCountdown(remaining)}`;
} else {
el.textContent =
`Next live check in ${formatCountdown(remaining)}`;
}
const tiny = document.querySelector('#xpTargetsContent .xpTiny');
if (tiny && !cached?.targets?.length) {
tiny.textContent = `Next live check in ${formatCountdown(remaining)}.`;
}
}
async function getBaldrTargets() {
const cached = GM_getValue('baldrRawCache', null);
if (
cached &&
Date.now() - cached.time < BALDR_RAW_CACHE_MS
) {
return cached.targets;
}
for (const url of BALDR_DATA_URLS) {
try {
const data = await httpJson(url);
const targets = normalizeBaldrTargets(data);
if (targets.length) {
GM_setValue('baldrRawCache', {
time: Date.now(),
targets
});
return targets;
}
} catch {}
}
throw new Error('Could not load Baldr targets.');
}
async function getMyBattleStats(key) {
const data =
await apiV1('/user/?selections=battlestats', key);
const strength = Number(data.strength || 0);
const defense = Number(data.defense || data.defence || 0);
const speed = Number(data.speed || 0);
const dexterity = Number(data.dexterity || 0);
return {
strength,
defense,
speed,
dexterity,
total:
strength + defense + speed + dexterity
};
}
async function getTargetLiveStatus(key, id) {
const data =
await apiV1(`/user/${id}?selections=profile`, key);
const statusObj = data.status || {};
return {
name: data.name,
level: Number(data.level || 0),
statusState:
statusObj.state ||
data.status_state ||
'',
statusText:
statusObj.description ||
statusObj.details ||
data.status ||
''
};
}
function normalizeBaldrTargets(data) {
const out = [];
const seen = new Set();
function walk(value, listName = '') {
if (Array.isArray(value)) {
value.forEach(v => walk(v, listName));
return;
}
if (!value || typeof value !== 'object') return;
const id =
value.id ||
value.ID ||
value.user_id ||
value.userid ||
value.player_id ||
value.torn_id ||
value.uid ||
value.UserID;
const level =
value.level ||
value.Level ||
value.lvl ||
value.LVL;
if (id && level) {
const numericId = Number(id);
if (!seen.has(numericId)) {
seen.add(numericId);
out.push({
...value,
_listName: listName || 'Baldr'
});
}
return;
}
for (const [key, child] of Object.entries(value)) {
walk(child, key || listName);
}
}
walk(data, 'Baldr');
return out;
}
function normalizeTarget(t) {
const id = Number(
t.id ||
t.ID ||
t.user_id ||
t.userid ||
t.player_id ||
t.torn_id ||
t.uid ||
t.UserID ||
0
);
return {
id,
name: String(t.name || t.Name || t.username || `User ${id}`),
level: Number(t.level || t.Level || t.lvl || 0),
speed: parseStat(t.speed || t.Speed || 0),
total: parseStat(
t.total ||
t.Total ||
t.total_stats ||
t.stats ||
t.Stats ||
0
),
listName: String(t._listName || 'Baldr')
};
}
function sortMaxPotentialXp(a, b) {
if (b.level !== a.level) {
return b.level - a.level;
}
return (a.total || 0) - (b.total || 0);
}
function isAliveAvailable(statusState, statusText) {
const combined =
`${statusState} ${statusText}`.toLowerCase();
if (combined.includes('dead')) return false;
if (combined.includes('hospital')) return false;
if (combined.includes('jail')) return false;
if (combined.includes('travel')) return false;
if (combined.includes('abroad')) return false;
if (combined.includes('federal')) return false;
return combined.includes('okay') || combined.trim() === '';
}
function emptyStats() {
return {
strength: 0,
defense: 0,
speed: 0,
dexterity: 0,
total: 0
};
}
function getRemainingMs(cacheTime) {
return Math.max(
0,
HOF_CACHE_MS - (Date.now() - cacheTime)
);
}
function formatCountdown(ms) {
const totalSeconds =
Math.ceil(Math.max(0, ms) / 1000);
const mins = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${mins}:${String(secs).padStart(2, '0')}`;
}
function parseStat(value) {
if (typeof value === 'number') return value;
const str =
String(value)
.toLowerCase()
.replace(/,/g, '')
.trim();
const n = parseFloat(str);
if (!Number.isFinite(n)) return 0;
if (str.endsWith('k')) return n * 1_000;
if (str.endsWith('m')) return n * 1_000_000;
if (str.endsWith('b')) return n * 1_000_000_000;
return n;
}
function shortNumber(num) {
num = Number(num || 0);
if (num >= 1_000_000_000) {
return `${(num / 1_000_000_000).toFixed(1)}b`;
}
if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}m`;
}
if (num >= 1_000) {
return `${(num / 1_000).toFixed(1)}k`;
}
return String(Math.round(num));
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function apiV1(path, key) {
return api(
`https://api.torn.com${path}${path.includes('?') ? '&' : '?'}key=${encodeURIComponent(key)}`
);
}
function apiV2(path, key) {
return api(
`https://api.torn.com/v2${path}${path.includes('?') ? '&' : '?'}key=${encodeURIComponent(key)}`
);
}
function api(url) {
return httpJson(url);
}
function httpJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
timeout: 20000,
onload: res => {
try {
const json = JSON.parse(res.responseText);
if (json.error) {
reject(
new Error(
json.error.error || json.error
)
);
} else {
resolve(json);
}
} catch {
reject(new Error('Bad API response.'));
}
},
onerror: () => reject(new Error('Request failed.')),
ontimeout: () => reject(new Error('Request timed out.'))
});
});
}
function installNavigationWatcher() {
const observer = new MutationObserver(() => {
watchAttackPageForHospitalizedTarget();
const urlChanged = location.href !== lastUrl;
if (urlChanged) {
lastUrl = location.href;
scheduleInit();
return;
}
if (
appStarted &&
!document.querySelector(`#${BOX_ID}`)
) {
scheduleInit();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
['pushState', 'replaceState'].forEach(fn => {
const original = history[fn];
history[fn] = function () {
const result =
original.apply(this, arguments);
scheduleInit();
return result;
};
});
window.addEventListener('popstate', scheduleInit);
window.addEventListener('hashchange', scheduleInit);
}
function getInjectTarget() {
return (
document.querySelector('#sidebarroot') ||
document.querySelector('.content-title') ||
document.querySelector('#mainContainer') ||
document.body
);
}
function injectBox(html) {
let box =
document.querySelector(`#${BOX_ID}`);
const target = getInjectTarget();
if (!target) return;
if (!box) {
box = document.createElement('div');
box.id = BOX_ID;
}
if (
!box.isConnected ||
box.parentElement !== target
) {
target.prepend(box);
}
box.innerHTML = html;
}
function addStyle(css) {
if (document.querySelector(`#${STYLE_ID}`)) {
return;
}
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, s => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[s]));
}
})();