Fractional level estimation
Pada tanggal
// ==UserScript==
// @name Level Progress Estimation
// @namespace http://tampermonkey.net/
// @version 4.3.2
// @description Fractional level estimation
// @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 INACTIVE_THRESHOLD = 31536000;
const PAGE_SIZE = 100;
const COOLDOWN_MS = 30000;
let lastKnownValue = GM_getValue("last_lvl_est", null);
let isRefreshing = false;
let lastRefreshTime = 0;
function getTimeUntilReset() {
const now = new Date();
const tctReset = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0));
const diff = tctReset - now;
const hours = Math.floor(diff / 3600000);
const mins = Math.floor((diff % 3600000) / 60000);
return `${hours}h ${mins}m`;
}
async function getApiKey() {
let key = localStorage.getItem("APIKey") || await GM_getValue("torn_api_key", "");
if (!key || key.length < 10) {
key = prompt("Enter your Torn API Key:");
if (key) await GM_setValue("torn_api_key", key.trim());
}
return key ? key.trim() : null;
}
async function fetchTorn(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
data.error ? reject(data.error.error) : resolve(data);
} catch (e) { reject("JSON Error"); }
},
onerror: reject
});
});
}
async function getAccurateLevel() {
const key = await getApiKey();
if (!key) return null;
try {
const user = await fetchTorn(`https://api.torn.com/v2/user/hof?key=${key}`);
const { value: level, rank } = user.hof.level;
if (level >= 100) return "100.00";
const offset = Math.max(0, Math.floor((rank - 50) / PAGE_SIZE) * PAGE_SIZE);
const hofData = await fetchTorn(`https://api.torn.com/v2/torn/hof?limit=${PAGE_SIZE}&offset=${offset}&cat=level&key=${key}`);
const now = Date.now() / 1000;
const filterAnchor = (p, lvl) => p.level === lvl && (now - p.last_action) > INACTIVE_THRESHOLD;
let currentAnchor = hofData.hof.filter(p => filterAnchor(p, level) && p.position < rank).sort((a, b) => b.position - a.position)[0];
let lowerAnchor = hofData.hof.filter(p => filterAnchor(p, level - 1) && p.position > rank).sort((a, b) => a.position - b.position)[0];
if (!lowerAnchor) {
const nextHof = await fetchTorn(`https://api.torn.com/v2/torn/hof?limit=${PAGE_SIZE}&offset=${offset + PAGE_SIZE}&cat=level&key=${key}`);
lowerAnchor = nextHof.hof.filter(p => filterAnchor(p, level - 1)).sort((a, b) => a.position - b.position)[0];
}
if (!currentAnchor || !lowerAnchor) return lastKnownValue || level.toFixed(2);
const fraction = Math.max(0, Math.min(0.99, (lowerAnchor.position - rank) / (lowerAnchor.position - currentAnchor.position)));
const result = (level + fraction).toFixed(2);
lastKnownValue = result;
GM_setValue("last_lvl_est", result);
GM_setValue("last_lvl_time", new Date().toLocaleTimeString());
return result;
} catch (e) {
return lastKnownValue;
}
}
function injectIcon(val) {
if (!val) return;
const existingVal = document.getElementById('acc-lvl-val');
const pill = document.getElementById('acc-lvl-pill');
if (existingVal) {
existingVal.textContent = val;
if (pill) pill.title = `Next HoF Reset: ${getTimeUntilReset()}\nLast Update: ${GM_getValue("last_lvl_time", "Unknown")}`;
return;
}
const header = document.querySelector('#header-root') || document.querySelector('.header-wrapper');
const tray = header?.querySelector('[class*="right_"]') || header?.querySelector('[class*="header-buttons"]') || header?.querySelector('.header-navigation');
if (!tray) return;
const newPill = document.createElement('div');
newPill.id = 'acc-lvl-pill';
newPill.title = `Next HoF Reset: ${getTimeUntilReset()}\nLast Update: ${GM_getValue("last_lvl_time", "Unknown")}`;
newPill.style = "display: inline-flex; align-items: center; background: #333; border: 1px solid #444; border-radius: 10px; padding: 2px 8px; margin: 0 4px; height: 22px; vertical-align: middle; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.5); flex-shrink: 0; z-index: 999; transition: transform 0.1s;";
newPill.innerHTML = `
<span style="color: #85b200; font-size: 10px; font-weight: bold; margin-right: 4px; 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;">${val}</span>
`;
newPill.onclick = async () => {
const now = Date.now();
if (isRefreshing || (now - lastRefreshTime < COOLDOWN_MS)) {
newPill.style.transform = "scale(0.9)";
setTimeout(() => newPill.style.transform = "scale(1)", 100);
return;
}
isRefreshing = true;
lastRefreshTime = now;
const valSpan = document.getElementById('acc-lvl-val');
valSpan.textContent = "...";
const updated = await getAccurateLevel();
valSpan.textContent = updated || lastKnownValue;
newPill.title = `Next HoF Reset: ${getTimeUntilReset()}\nLast Update: ${GM_getValue("last_lvl_time", "Just now")}`;
isRefreshing = false;
};
tray.prepend(newPill);
}
async function run() {
if (lastKnownValue) injectIcon(lastKnownValue);
const initialFetch = await getAccurateLevel();
if (initialFetch) injectIcon(initialFetch);
setInterval(async () => {
const updated = await getAccurateLevel();
if (updated) injectIcon(updated);
}, 600000);
setInterval(() => {
if (!document.getElementById('acc-lvl-pill') && lastKnownValue) {
injectIcon(lastKnownValue);
}
}, 2000);
}
if (document.readyState === "complete") run();
else window.addEventListener("load", run);
})();