您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show your level progress on sidebar.
// ==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 }); })();