Level Progress Estimation

Adds a fractional level estimate pill with manual refresh

Verze ze dne 16. 04. 2026. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

Advertisement:

// ==UserScript==
// @name         Level Progress Estimation
// @namespace    http://tampermonkey.net/
// @version      4.1.0
// @description  Adds a fractional level estimate pill with manual refresh
// @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;

  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);
      return result;
    } catch (e) {
      return lastKnownValue;
    }
  }

  function injectIcon(val) {
    if (!val) return;
    const existingVal = document.getElementById('acc-lvl-val');
    if (existingVal) {
      existingVal.textContent = val;
      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 pill = document.createElement('div');
    pill.id = 'acc-lvl-pill';
    pill.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: opacity 0.2s;";
    pill.innerHTML = `
      <span style="color: #85b200; font-size: 10px; font-weight: bold; margin-right: 4px; font-family: sans-serif;">LV</span>
      <span id="acc-lvl-val" style="color: #fff; font-size: 11px; font-family: 'Courier New', monospace; font-weight: bold;">${val}</span>
    `;

    pill.onclick = async () => {
        const now = Date.now();
        if (isRefreshing || (now - lastRefreshTime < COOLDOWN_MS)) {
            pill.style.opacity = "0.5";
            setTimeout(() => pill.style.opacity = "1", 500);
            return;
        }

        isRefreshing = true;
        lastRefreshTime = now;
        const valSpan = document.getElementById('acc-lvl-val');
        const oldVal = valSpan.textContent;
        valSpan.textContent = "...";
        
        const updated = await getAccurateLevel();
        valSpan.textContent = updated || oldVal;
        isRefreshing = false;
    };

    tray.prepend(pill);
  }

  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);
})();