Fractional level estimation with status bar toggle and value persistence
Verze ze dne
// ==UserScript==
// @name Level Progress Estimation
// @namespace http://tampermonkey.net/
// @version 13.2
// @description Fractional level estimation with status bar toggle and value persistence
// @author Pint-Shot-Riot
// @match https://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const STORAGE = {
apiKey: "torn_api_key",
cache: "level_progress_cache_v2",
rateWindowStart: "level_progress_rate_window_start_v2",
rateCount: "level_progress_rate_count_v2",
lastCallAt: "level_progress_last_call_at_v2",
visible: "pda_level_visible"
};
const PAGE_SIZE = 100;
const CACHE_DURATION_MS = 12 * 60 * 60 * 1000;
const INACTIVE_THRESHOLD_SECONDS = 365 * 24 * 60 * 60;
const RATE_LIMIT = 60;
const RATE_WINDOW_MS = 60 * 1000;
const MIN_INTERVAL_MS = 1100;
const MAX_SCAN_PAGES = 20;
let isVisible = localStorage.getItem(STORAGE.visible) !== 'false';
let lastKnownValue = "--";
const styleSheet = document.createElement("style");
styleSheet.innerText = `
.pda-lvl-status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 16px;
opacity: 0.75;
transition: opacity 0.15s, transform 0.15s;
filter: grayscale(1);
}
.pda-lvl-status-icon:hover { opacity: 1; transform: scale(1.15); }
.pda-lvl-active { filter: grayscale(0) !important; opacity: 1 !important; }
`;
document.head.appendChild(styleSheet);
async function getApiKey() {
let key = localStorage.getItem("APIKey") || await GM_getValue(STORAGE.apiKey, "");
if (!key || key.length < 10) {
key = prompt("Please enter your Torn API Key:");
if (key) await GM_setValue(STORAGE.apiKey, key.trim());
}
return key ? key.trim() : null;
}
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
async function waitForApiSlot() {
while (true) {
const now = Date.now();
let windowStart = await GM_getValue(STORAGE.rateWindowStart, 0);
let callCount = await GM_getValue(STORAGE.rateCount, 0);
let lastCallAt = await GM_getValue(STORAGE.lastCallAt, 0);
if (!windowStart || now - windowStart >= RATE_WINDOW_MS) {
windowStart = now;
callCount = 0;
await GM_setValue(STORAGE.rateWindowStart, windowStart);
await GM_setValue(STORAGE.rateCount, callCount);
}
const waitMs = Math.max(MIN_INTERVAL_MS - (now - lastCallAt), callCount >= RATE_LIMIT ? RATE_WINDOW_MS - (now - windowStart) : 0);
if (waitMs <= 0) {
await GM_setValue(STORAGE.lastCallAt, now);
await GM_setValue(STORAGE.rateCount, callCount + 1);
return;
}
await sleep(waitMs + 25);
}
}
function fetchTorn(url) {
return new Promise(async (resolve, reject) => {
await waitForApiSlot();
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.error) reject(data.error.error);
else resolve(data);
} catch (e) { reject("JSON Error"); }
},
onerror: (err) => reject(err)
});
});
}
async function findInactiveAnchors(apiKey, userLevel, userRank) {
const lowerLevel = userLevel - 1;
let currentAnchor = null, lowerAnchor = null;
const startOffset = Math.max(0, Math.floor((userRank - 1) / PAGE_SIZE) * PAGE_SIZE);
for (let distance = 0; distance < MAX_SCAN_PAGES; distance++) {
const offsets = [startOffset - (distance * PAGE_SIZE), startOffset + (distance * PAGE_SIZE)];
for (let offset of offsets) {
if (offset < 0) continue;
try {
const data = await fetchTorn(`https://api.torn.com/v2/torn/hof?limit=${PAGE_SIZE}&offset=${offset}&cat=level&key=${apiKey}`);
const players = data.hof || [];
for (const p of players) {
const isInactive = (Math.floor(Date.now() / 1000) - p.last_action) >= INACTIVE_THRESHOLD_SECONDS;
if (!isInactive) continue;
if (p.level === userLevel && p.position < userRank) {
if (!currentAnchor || p.position > currentAnchor.position) currentAnchor = p;
}
if (p.level === lowerLevel && p.position > userRank) {
if (!lowerAnchor || p.position < lowerAnchor.position) lowerAnchor = p;
}
}
if (currentAnchor && lowerAnchor) return { currentAnchor, lowerAnchor };
} catch (e) { continue; }
}
}
return { currentAnchor, lowerAnchor };
}
async function getAccurateLevel() {
const key = await getApiKey();
if (!key) return null;
const cachedRaw = await GM_getValue(STORAGE.cache, null);
let cached = cachedRaw ? JSON.parse(cachedRaw) : null;
try {
const user = await fetchTorn(`https://api.torn.com/v2/user/hof?key=${key}`);
const lvl = user.hof.level.value;
const rank = user.hof.level.rank;
if (lvl >= 100) return "100.00";
if (cached && Date.now() - cached.updatedAt < CACHE_DURATION_MS && cached.level === lvl && cached.currentAnchor && cached.lowerAnchor) {
const progress = (cached.lowerAnchor.position - rank) / (cached.lowerAnchor.position - cached.currentAnchor.position);
return (lvl + Math.max(0, Math.min(0.99, progress))).toFixed(2);
}
const { currentAnchor, lowerAnchor } = await findInactiveAnchors(key, lvl, rank);
let finalVal = lvl;
if (currentAnchor && lowerAnchor) {
const progress = (lowerAnchor.position - rank) / (lowerAnchor.position - currentAnchor.position);
finalVal = lvl + Math.max(0, Math.min(0.99, progress));
await GM_setValue(STORAGE.cache, JSON.stringify({ level: lvl, currentAnchor, lowerAnchor, updatedAt: Date.now() }));
}
return finalVal.toFixed(2);
} catch (e) {
return cached ? cached.level.toFixed(2) : null;
}
}
function syncUI() {
const pill = document.getElementById('acc-lvl-pill');
const icon = document.getElementById('pda-lvl-status-btn');
if (pill) pill.style.display = isVisible ? 'inline-flex' : 'none';
if (icon) isVisible ? icon.classList.add('pda-lvl-active') : icon.classList.remove('pda-lvl-active');
}
function handleToggle() {
isVisible = !isVisible;
localStorage.setItem(STORAGE.visible, isVisible);
syncUI();
}
function injectUI(val) {
if (val) lastKnownValue = val;
const statusUl = document.querySelector('ul[class*="status-icons"]');
if (statusUl && !document.getElementById('pda-lvl-status-btn')) {
const li = document.createElement('li');
li.style.display = "inline-flex";
const a = document.createElement('a');
a.id = 'pda-lvl-status-btn';
a.className = 'pda-lvl-status-icon';
a.innerHTML = '📈';
a.onclick = (e) => { e.preventDefault(); handleToggle(); };
li.appendChild(a);
statusUl.appendChild(li);
}
const pill = document.getElementById('acc-lvl-pill');
if (!pill) {
const header = document.querySelector('#header-root') || document.querySelector('.header-wrapper');
const tray = header?.querySelector('[class*="right_"]') || header?.querySelector('[class*="header-buttons"]');
if (tray) {
const newPill = document.createElement('div');
newPill.id = 'acc-lvl-pill';
newPill.style = "display: inline-flex; align-items: center; background: #222; border: 1px solid #444; border-radius: 10px; padding: 2px 8px; margin: 0 6px; height: 22px; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.4); flex-shrink: 0; transition: border-color 0.2s;";
newPill.onclick = () => window.location.href = "/halloffame.php#/type=level";
newPill.innerHTML = `<span style="color: #85b200; font-size: 10px; font-weight: bold; margin-right: 5px; font-family: sans-serif; pointer-events: none;">LV</span>
<span id="acc-lvl-val" style="color: #fff; font-size: 11px; font-family: 'Courier New', monospace; font-weight: bold; pointer-events: none;">${lastKnownValue}</span>`;
tray.prepend(newPill);
}
} else {
document.getElementById('acc-lvl-val').textContent = lastKnownValue;
}
syncUI();
}
async function update() {
const val = await getAccurateLevel();
injectUI(val);
}
update();
setInterval(update, 1800000);
setInterval(() => injectUI(), 3000);
})();