Level Progress Estimation

Adds a fractional level pill to the PDA header tray.

2026/04/16のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Level Progress Estimation
// @namespace    http://tampermonkey.net/
// @version      4.0.0
// @description  Adds a fractional level pill to the PDA header tray.
// @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;

  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";
      if (level <= 1) return level.toString();

      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 level.toFixed(2);

      const fraction = Math.max(0, Math.min(0.99, (lowerAnchor.position - rank) / (lowerAnchor.position - currentAnchor.position)));
      return (level + fraction).toFixed(2);
    } catch (e) {
      return null;
    }
  }

  function injectIcon(val) {
    const existing = document.getElementById('acc-lvl-val');
    if (existing) {
      existing.textContent = val;
      return;
    }

    const header = document.querySelector('#header-root') || document.querySelector('.header-wrapper');
    if (!header) return;

    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;";
    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 = () => window.location.href = "/halloffame.php#/type=level";
    tray.prepend(pill);
  }

  async function run() {
    let currentVal = await getAccurateLevel();
    if (!currentVal) return;

    injectIcon(currentVal);
    setInterval(async () => {
      const updated = await getAccurateLevel();
      if (updated) injectIcon(currentVal = updated);
    }, 600000);
    
    setInterval(() => { if (!document.getElementById('acc-lvl-pill')) injectIcon(currentVal); }, 2000);
  }

  if (document.readyState === "complete") run();
  else window.addEventListener("load", run);
})();