Torn - Level Progress Shower

Show your level progress on sidebar.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn - Level Progress Shower
// @namespace    http://tampermonkey.net/
// @version      1.01
// @description  Show your level progress on sidebar.
// @author       e7cf09 [3441977]
// @icon         https://editor.torn.com/33b77f1c-dcd9-4d96-867f-5b578631d137-3441977.png
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let apiKey = GM_getValue('torn_api_key', '');
    let cachedData = GM_getValue('cached_level_data', null);
    let cacheTime = GM_getValue('cache_timestamp', 0);
    let lastCall = GM_getValue('last_api_call_time', 0);
    let callCount = GM_getValue('api_call_count', 0);
    let callTime = GM_getValue('api_call_timestamp', 0);
    let lastLevel = GM_getValue('last_seen_level', 0);

    const CACHE_DURATION = 24 * 60 * 60 * 1000;
    const INACTIVE_THRESHOLD = 365 * 24 * 60 * 60;
    const RATE_LIMIT = 60;
    const RATE_WINDOW = 60 * 1000;
    const MIN_INTERVAL = 1000; 

    let processing = false;
    let displayLevel = null;
    let status = '';

    function canCall() {
        const now = Date.now();

        if (now - callTime > RATE_WINDOW) {
            callCount = 0;
            callTime = now;
            GM_setValue('api_call_count', callCount);
            GM_setValue('api_call_timestamp', callTime);
        }

        if (callCount >= RATE_LIMIT) {
            return false;
        }

        if (now - lastCall < MIN_INTERVAL) {
            return false;
        }

        return true;
    }

    function recordCall() {
        const now = Date.now();
        callCount++;
        lastCall = now;

        GM_setValue('api_call_count', callCount);
        GM_setValue('last_api_call_time', lastCall);

        if (now - callTime > RATE_WINDOW) {
            callTime = now;
            GM_setValue('api_call_timestamp', callTime);
        }
    }

    function makeRequest(url) {
        return new Promise((resolve, reject) => {
            if (!canCall()) {
                reject(new Error('API rate limit exceeded. Please wait.'));
                return;
            }

            recordCall();

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.error) {
                                reject(new Error(data.error.error || 'API Error'));
                            } else {
                                resolve(data);
                            }
                        } catch (e) {
                            reject(new Error('Invalid JSON response'));
                        }
                    } else {
                        reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Network error'));
                }
            });
        });
    }

    async function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function getUserLevel() {
        const url = `https://api.torn.com/v2/user/hof?key=${apiKey}`;
        const data = await makeRequest(url);
        return {
            level: data.hof.level.value,
            rank: data.hof.level.rank
        };
    }

    async function getHOF(limit = 100, offset = 0) {
        const url = `https://api.torn.com/v2/torn/hof?limit=${limit}&offset=${offset}&cat=level&key=${apiKey}`;
        const data = await makeRequest(url);
        return data.hof;
    }

    async function getUserHOF(userId) {
        const url = `https://api.torn.com/v2/user/${userId}/hof?key=${apiKey}`;
        const data = await makeRequest(url);
        return data.hof.level.rank;
    }

    async function findInactivePos(targetLevel) {
        const currentTime = Math.floor(Date.now() / 1000);
        
        const level1Pos = await getUserHOF(1364774);
        await sleep(MIN_INTERVAL);
        
        let left = 1;
        let right = level1Pos;
        let position = -1;
        
        while (left + 100 <= right) {
            const mid = Math.floor((left + right) / 2 - 50);
            
            try {
                const players = await getHOF(100, mid);
                await sleep(MIN_INTERVAL);
                
                let found = false;
                let higher = false;
                let lower = false;

                for (let i = 0; i < 100; i++) {
                    if (players[i].level === targetLevel) {
                        found = true;
                    }
                    if (players[i].level > targetLevel) {
                        higher = true;
                    } else if (players[i].level < targetLevel) {
                        lower = true;
                    }
                }
                
                if (higher && !found && !lower) {
                    left = mid + 100;
                } else if (higher && found && !lower) {
                    position = mid;
                    break;
                } else {
                    right = mid - 1;
                }
            } catch (error) {
                console.error('Error in binary search:', error);
                throw error;
            }
        }
        
        if (position === -1) {
            position = left;
        }
        
        let bestPos = -1;
        let bestId = null;
        let bestAction = null;
        let found = false;
        
        let currentOffset = position;
        while (1) {
            
            try {
                const players = await getHOF(100, currentOffset);
                await sleep(MIN_INTERVAL);
                
                for (let i = 0; i < players.length; i++) {
                    const player = players[i];
                        if (player.level === targetLevel) {
                        const daysSince = (currentTime - player.last_action) / (24 * 60 * 60);
                        if (daysSince > 365) {
                            bestPos = player.position;
                            bestId = player.id;
                            bestAction = player.last_action;
                            found = true;
                            break;
                        }
                    }
                }
                if (found) {
                    break;
                }
                currentOffset += 100;

            } catch (error) {
                console.error(`Error fetching data at offset ${currentOffset}:`, error);
                currentOffset += 100;
            }
        }
        
        if (!found) {
            return null;
        }
        
        return {
            position: bestPos,
            playerId: bestId,
            lastAction: bestAction,
            fetchTime: currentTime
        };
    }

    function validateInactive(playerData) {
        const currentTime = Math.floor(Date.now() / 1000);
        return playerData.lastAction < playerData.fetchTime;
    }

    async function calculateRelativePos() {
        try {
            const userLevel = await getUserLevel();
            await sleep(MIN_INTERVAL);

            GM_setValue('last_seen_level', userLevel.level);

            GM_setValue('process_step', 'user_level_fetched');
            GM_setValue('process_user_level', userLevel.level);
            GM_setValue('process_user_rank', userLevel.rank);
            GM_setValue('process_timestamp', Date.now());

            if (userLevel.level >= 100) {
                const result = {
                    level: userLevel.level,
                    rank: userLevel.rank,
                    relativePosition: 0,
                    finalLevel: 100.00,
                    lowerLevelInactiveData: null,
                    currentLevelInactiveData: null
                };

                GM_setValue('cached_level_data', result);
                GM_setValue('cache_timestamp', Date.now());
                GM_setValue('process_step', 'completed');

                return 100.00;
            }

            const lowerInactive = await findInactivePos(userLevel.level - 1);

            GM_setValue('process_step', 'lower_level_fetched');
            GM_setValue('process_lower_level_inactive_data', JSON.stringify(lowerInactive));

            const currentInactive = await findInactivePos(userLevel.level);

            GM_setValue('process_step', 'current_level_fetched');
            GM_setValue('process_current_level_inactive_data', JSON.stringify(currentInactive));

            if (!validateInactive(lowerInactive) || !validateInactive(currentInactive)) {
                throw new Error('Inactive player validation failed - need to refetch data');
            }

            if (lowerInactive.position === -1 || currentInactive.position === -1) {
                return userLevel.level;
            }

            const userRank = userLevel.rank;
            const lastCurrentPos = lowerInactive.position;
            const currentPos = currentInactive.position;

            const relativePos = (lastCurrentPos - userRank) / (lastCurrentPos - currentPos);
            let roundedRelative = Math.round(relativePos * 100) / 100;

            if (userLevel.level < 100) {
                roundedRelative = Math.min(roundedRelative, 0.99);
            }

            const finalLevel = userLevel.level + roundedRelative;

            const result = {
                level: userLevel.level,
                rank: userLevel.rank,
                relativePosition: roundedRelative,
                finalLevel: finalLevel,
                lowerLevelInactiveData: lowerInactive,
                currentLevelInactiveData: currentInactive
            };

            GM_setValue('cached_level_data', result);
            GM_setValue('cache_timestamp', Date.now());

            GM_setValue('process_step', 'completed');
            GM_setValue('process_final_level', finalLevel);

            return finalLevel;
        } catch (error) {

            GM_setValue('process_step', 'error');
            GM_setValue('process_error', error.message);

            if (error.message.includes('API') || error.message.includes('key')) {
                return null;
            }

            return 0;
        }
    }

    async function validateKey(key) {
        try {
            const url = `https://api.torn.com/v2/key/info?key=${key}`;
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: function(response) {
                        if (response.status === 200) {
                            try {
                                const data = JSON.parse(response.responseText);
                                resolve(data);
                            } catch (e) {
                                reject(new Error('Invalid JSON response'));
                            }
                        } else {
                            reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                        }
                    },
                    onerror: function(error) {
                        reject(new Error('Network error'));
                    }
                });
            });

            if (response.info && response.info.access && response.info.access.level >= 1) {
                return { valid: true, level: response.info.access.level };
            } else {
                return { valid: false, error: 'incorrect key' };
            }
        } catch (error) {
            return { valid: false, error: error.message };
        }
    }

    function showConfig() {
        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;';

        const modal = document.createElement('div');
        modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;border:2px solid #333;z-index:10000;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);';
        modal.innerHTML = `
            <h3>Torn API Configuration</h3>
            <p>Enter your public Torn API key:</p>
            <input type="text" id="keyInput" value="${apiKey}" placeholder="e.g., HhmiKs9LjgBN2UV7" style="width:300px;padding:8px;margin:10px 0;border:1px solid #ccc;border-radius:4px;">
            <br>
            <div id="validMsg" style="margin:10px 0;color:red;font-size:12px;"></div>
            <button id="saveKey" style="background:#007bff;color:white;border:none;padding:8px 16px;margin:5px;border-radius:4px;cursor:pointer;">Validate & Save</button>
            <button id="cancelKey" style="background:#007bff;color:white;border:none;padding:8px 16px;margin:5px;border-radius:4px;cursor:pointer;">Cancel</button>
            <button id="clearCache" style="background:#007bff;color:white;border:none;padding:8px 16px;margin:5px;border-radius:4px;cursor:pointer;">Clear Cache</button>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        document.getElementById('saveKey').onclick = async function() {
            const newKey = document.getElementById('keyInput').value.trim();
            const validDiv = document.getElementById('validMsg');
            const saveBtn = document.getElementById('saveKey');

            if (!newKey) {
                validDiv.textContent = 'Please enter an API key';
                validDiv.style.color = 'red';
                return;
            }

            validDiv.textContent = 'Validating API key...';
            validDiv.style.color = 'blue';
            saveBtn.disabled = true;
            saveBtn.textContent = 'Validating...';

            const validation = await validateKey(newKey);

            if (validation.valid) {
                validDiv.textContent = 'API key saved successfully!';
                validDiv.style.color = 'green';

                apiKey = newKey;
                GM_setValue('torn_api_key', apiKey);
                GM_setValue('needs_full_recalc', true);

                GM_setValue('cached_level_data', null);
                GM_setValue('cache_timestamp', 0);
                displayLevel = null;
                processing = false;

                setTimeout(() => {
                    document.body.removeChild(overlay);
                }, 1500);
            } else {
                validDiv.textContent = 'Invalid API key';
                validDiv.style.color = 'red';
                saveBtn.disabled = false;
                saveBtn.textContent = 'Validate & Save';
            }
        };

        document.getElementById('cancelKey').onclick = function() {
            document.body.removeChild(overlay);
        };

        document.getElementById('clearCache').onclick = function() {
            GM_setValue('cached_level_data', null);
            GM_setValue('cache_timestamp', 0);
            clearProcess();
            displayLevel = null;
            processing = false;
        };

        overlay.onclick = function(e) {
            if (e.target === overlay) {
                document.body.removeChild(overlay);
            }
        };
    }

    async function checkNeedsRecalc(cached) {
        try {
            const userLevel = await getUserLevel();
            await sleep(MIN_INTERVAL);
            
            if (userLevel.level !== cached.level) {
                return true;
            }
            
            if (cached.lowerLevelInactiveData && cached.currentLevelInactiveData) {
                const currentTime = Math.floor(Date.now() / 1000);
                
                if (cached.lowerLevelInactiveData.lastAction > cached.lowerLevelInactiveData.fetchTime ||
                    cached.currentLevelInactiveData.lastAction > cached.currentLevelInactiveData.fetchTime) {
                    return true;
                }
            }
            
            return false;
        } catch (error) {
            console.error('Error checking if full recalculation needed:', error);
            return true;
        }
    }

    async function updateRank(cached) {
        try {
            
            const userLevel = await getUserLevel();
            await sleep(MIN_INTERVAL);
            
            const userRank = userLevel.rank;
            const lastCurrentPos = cached.lowerLevelInactiveData.position;
            const currentPos = cached.currentLevelInactiveData.position;
            
            const relativePos = (lastCurrentPos - userRank) / (lastCurrentPos - currentPos);
            let roundedRelative = Math.round(relativePos * 100) / 100;
            
            if (cached.level < 100) {
                roundedRelative = Math.min(roundedRelative, 0.99);
            }
            
            const finalLevel = cached.level + roundedRelative;
            
            cached.rank = userRank;
            cached.relativePosition = roundedRelative;
            cached.finalLevel = finalLevel;
            
            GM_setValue('cached_level_data', cached);
            GM_setValue('cache_timestamp', Date.now());
            
            return finalLevel;
        } catch (error) {
            console.error('Error updating with current rank:', error);
            return cached.finalLevel;
        }
    }

    async function getDisplayLevel() {
        if (!apiKey) {
            return null;
        }

        if (processing) {
            return null;
        }

        const needsRecalc = GM_getValue('needs_full_recalc', false);
        if (needsRecalc) {
            GM_setValue('needs_full_recalc', false);
            GM_setValue('cached_level_data', null);
            GM_setValue('cache_timestamp', 0);
        }

        const now = Date.now();
        if (now - lastCall < RATE_WINDOW && cachedData) {
            return cachedData.finalLevel;
        }

        if (cachedData && (now - cacheTime < CACHE_DURATION)) {
            const needsRecalc = await checkNeedsRecalc(cachedData);
            
            if (!needsRecalc) {
                return await updateRank(cachedData);
            }
        }

        const processStep = GM_getValue('process_step', '');
        const processTime = GM_getValue('process_timestamp', 0);

        if (processStep && (now - processTime < 5 * 60 * 1000)) {
            try {
                if (processStep === 'completed') {
                    const finalLevel = GM_getValue('process_final_level', 0);
                    if (finalLevel > 0) {
                        clearProcess();
                        return finalLevel;
                    }
                } else if (processStep === 'current_level_fetched') {
                    const userLevel = GM_getValue('process_user_level', 0);
                    const userRank = GM_getValue('process_user_rank', 0);
                    const lowerInactive = JSON.parse(GM_getValue('process_lower_level_inactive_data', '{}'));
                    const currentInactive = JSON.parse(GM_getValue('process_current_level_inactive_data', '{}'));

                    if (userLevel > 0 && lowerInactive.position && currentInactive.position) {

                        if (!validateInactive(lowerInactive) || !validateInactive(currentInactive)) {
                            throw new Error('Inactive player validation failed - need to refetch data');
                        }

                        const relativePos = (userRank - currentInactive.position) / (lowerInactive.position - currentInactive.position);
                        let roundedRelative = Math.round(relativePos * 100) / 100;

                        if (userLevel < 100) {
                            roundedRelative = Math.min(roundedRelative, 0.99);
                        }

                        const finalLevel = userLevel + roundedRelative;

                        const result = {
                            level: userLevel,
                            rank: userRank,
                            relativePosition: roundedRelative,
                            finalLevel: finalLevel,
                            lowerLevelInactiveData: lowerInactive,
                            currentLevelInactiveData: currentInactive
                        };

                        GM_setValue('cached_level_data', result);
                        GM_setValue('cache_timestamp', Date.now());

                        clearProcess();

                        return finalLevel;
                    }
                }
            } catch (error) {
                console.error('Error resuming from saved progress:', error);
                clearProcess();
            }
        }

        if (processStep && (now - processTime >= 5 * 60 * 1000)) {
            clearProcess();
        }

        processing = true;

        try {
            const result = await calculateRelativePos();
            if (result === null) {
                processing = false;
                return null;
            }
            displayLevel = result;
            processing = false;
            return result;
        } catch (error) {
            processing = false;
            return null;
        }
    }

    function clearProcess() {
        GM_setValue('process_step', '');
        GM_setValue('process_user_level', 0);
        GM_setValue('process_user_rank', 0);
        GM_setValue('process_lower_level_inactive_data', '');
        GM_setValue('process_current_level_inactive_data', '');
        GM_setValue('process_timestamp', 0);
        GM_setValue('process_final_level', 0);
        GM_setValue('process_error', '');
    }

    let hasUpdatedThisSession = false;

    async function optimizedChange() {
        const needsRecalc = GM_getValue('needs_full_recalc', false);
        if (hasUpdatedThisSession && !needsRecalc) {
            return;
        }

        const blocks = document.querySelectorAll('.point-block___rQyUK');

        for (let block of blocks) {
            const nameSpan = block.querySelector('.name___ChDL3');
            const valueSpan = block.querySelector('.value___mHNGb');

            if (nameSpan && nameSpan.textContent.trim() === 'Level:' && valueSpan) {
                const originalLevel = parseInt(valueSpan.textContent);

                if (originalLevel !== lastLevel && lastLevel !== 0) {
                    GM_setValue('cached_level_data', null);
                    GM_setValue('cache_timestamp', 0);
                    displayLevel = null;
                    processing = false;
                    GM_setValue('last_seen_level', originalLevel);
                    GM_setValue('needs_full_recalc', true);
                    hasUpdatedThisSession = false;
                }

                if (!hasUpdatedThisSession || needsRecalc) {
                    const level = await getDisplayLevel();

                    if (level !== null) {
                        valueSpan.textContent = level.toFixed(2);
                    }
                    hasUpdatedThisSession = true;
                    GM_setValue('needs_full_recalc', false);
                }

                break;
            }
        }
    }

    document.addEventListener('keydown', function(e) {
        if (e.ctrlKey && e.shiftKey && e.key === 'T') {
            showConfig();
        }
    });

    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand('Configure API Key', showConfig);
    }

    if (apiKey) {
        const firstRun = GM_getValue('first_run_completed', false);
        if (!firstRun) {
            GM_setValue('needs_full_recalc', true);
            GM_setValue('first_run_completed', true);
        }

        const processStep = GM_getValue('process_step', '');
        const processTime = GM_getValue('process_timestamp', 0);
        const now = Date.now();

        if (processStep && processStep !== 'completed' && processStep !== 'error' &&
            (now - processTime < 5 * 60 * 1000)) {
        }
    }

    optimizedChange();

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', optimizedChange);
    } else {
        optimizedChange();
    }

    const observer = new MutationObserver(function(mutations) {
        let shouldCheck = false;
        mutations.forEach(function(mutation) {
            if (mutation.addedNodes.length > 0) {
                for (let node of mutation.addedNodes) {
                    if (node.nodeType === 1 && (node.classList.contains('point-block___rQyUK') || node.querySelector('.point-block___rQyUK'))) {
                        shouldCheck = true;
                        break;
                    }
                }
            }
        });

        if (shouldCheck) {
            optimizedChange();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
    });
})();