您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Various QOL enhancements for FlatMMO with XP tracking and session stats
// ==UserScript== // @name KatieQoL // @namespace http://tampermonkey.net/ // @version 1.1.3 // @description Various QOL enhancements for FlatMMO with XP tracking and session stats // @author Straightmale // @match *://flatmmo.com/play.php // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'katieqol_settings'; let settings = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); if (!settings.pickupNotifier) settings.pickupNotifier = { enabled: true }; if (!settings.xpTracker) settings.xpTracker = { enabled: true, visibleSkills: {} }; if (!settings.visuals) settings.visuals = { backgroundColor: 'rgba(0,0,0,0.85)', textColor: 'white' }; if (!settings.uiLock) settings.uiLock = { locked: false }; const skills = [ 'melee', 'archery', 'health', 'magic', 'worship', 'mining', 'forging', 'crafting', 'enchantment', 'fishing', 'woodcutting', 'firemake', 'cooking', 'brewing', 'farming', 'hunting' ]; skills.forEach(s => { if (!(s in settings.xpTracker.visibleSkills)) settings.xpTracker.visibleSkills[s] = true; }); function saveSettings() { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } // --- Session stats tracking --- let sessionItemsCollected = {}; let sessionXpCollected = {}; let sessionStartTime = Date.now(); let sessionStatsUpdateInterval; skills.forEach(skill => sessionXpCollected[skill] = 0); function formatPlaytime(ms) { const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } function calculateRates(count, playtimeMs) { const playtimeHours = playtimeMs / 3600000; const playtimeMinutes = playtimeMs / 60000; const perMin = playtimeMinutes > 0 ? count / playtimeMinutes : 0; const perHour = playtimeHours > 0 ? count / playtimeHours : 0; return { perMin: Math.round(perMin * 10) / 10, perHour: Math.round(perHour * 10) / 10 }; } let lastInventory = {}; const notifierContainer = document.createElement('div'); notifierContainer.style.position = 'fixed'; notifierContainer.style.bottom = '60px'; notifierContainer.style.right = '20px'; notifierContainer.style.zIndex = '99999'; notifierContainer.style.fontFamily = 'Arial,sans-serif'; notifierContainer.style.fontSize = '13px'; notifierContainer.style.color = settings.visuals.textColor; notifierContainer.style.maxWidth = '300px'; notifierContainer.style.pointerEvents = settings.uiLock.locked ? 'none' : 'auto'; notifierContainer.style.background = settings.visuals.backgroundColor; notifierContainer.style.padding = '8px'; notifierContainer.style.borderRadius = '8px'; notifierContainer.style.boxShadow = '0 0 15px rgba(0,0,0,0.7)'; document.body.appendChild(notifierContainer); function applyVisuals() { notifierContainer.style.background = settings.visuals.backgroundColor; notifierContainer.style.color = settings.visuals.textColor; xpPanel.style.background = settings.visuals.backgroundColor; xpPanel.style.color = settings.visuals.textColor; settingsPanel.style.background = settings.visuals.backgroundColor; settingsPanel.style.color = settings.visuals.textColor; toggleSettingsBtn.style.background = settings.visuals.backgroundColor; toggleSettingsBtn.style.color = settings.visuals.textColor; toggleSettingsBtn.style.border = `1px solid ${settings.visuals.textColor}`; sessionStatsPanel.style.background = settings.visuals.backgroundColor; sessionStatsPanel.style.color = settings.visuals.textColor; viewSessionStatsBtn.style.background = settings.visuals.backgroundColor; viewSessionStatsBtn.style.color = settings.visuals.textColor; viewSessionStatsBtn.style.border = `1px solid ${settings.visuals.textColor}`; } function showPickupMessage(text) { if (!settings.pickupNotifier.enabled) return; const el = document.createElement('div'); el.textContent = text; el.style.background = 'rgba(0,0,0,0.6)'; el.style.padding = '6px 10px'; el.style.marginTop = '4px'; el.style.borderRadius = '6px'; el.style.boxShadow = '0 0 6px black'; el.style.opacity = '1'; el.style.transition = 'opacity 1s ease'; notifierContainer.appendChild(el); setTimeout(() => { el.style.opacity = '0'; setTimeout(() => notifierContainer.removeChild(el), 1000); }, 3000); } // Smoothed XP tracking data const xpData = {}; const now = () => Date.now(); skills.forEach(skill => { xpData[skill] = { lastXP: 0, history: [] }; }); const xpTable = [ 0,9,28,66,131,228,368,557,805,1123,1519, 2006,2596,3301,4136,5114,6251,7565,9073,10795, 12750,14961,17450,20243,23365,26846,30714,35001,39742,44971, 50728,57053,63988,71580,79876,88929,98793,109525,121186,133842, 147562,162417,178485,195848,214591,234806,256589,280041,305272,332394, 361528,392800,426345,462304,500828,542072,586205,633401,683845,737733, 795271,856676,922177,992014,1066442,1145728,1230154,1320018,1415632,1517325, 1625444,1740353,1862437,1992099,2129766,2275884,2430923,2595379,2769772,2954650, 3150588,3358190,3578094,3810967,4057513,4318468,4594610,4886754,5195754,5522512, 5867972,6233126,6619016,7026737,7457436,7912321,8392658,8899775,9435068,10000000 ]; function xpToLevel(xp) { for(let lvl = 1; lvl < xpTable.length; lvl++) { if(xp < xpTable[lvl]) return lvl; } return xpTable.length; } function formatTime(seconds) { if (seconds === Infinity) return '∞'; if (seconds < 60) return `${Math.round(seconds)}s`; if (seconds < 3600) return `${Math.floor(seconds/60)}m ${Math.round(seconds%60)}s`; return `${Math.floor(seconds/3600)}h ${Math.floor((seconds%3600)/60)}m`; } const xpPanel = document.createElement('div'); xpPanel.style.position = 'fixed'; xpPanel.style.top = '60px'; xpPanel.style.right = '20px'; xpPanel.style.width = '380px'; xpPanel.style.maxHeight = '400px'; xpPanel.style.overflowY = 'auto'; xpPanel.style.background = settings.visuals.backgroundColor; xpPanel.style.color = settings.visuals.textColor; xpPanel.style.padding = '10px'; xpPanel.style.fontFamily = 'Arial,sans-serif'; xpPanel.style.fontSize = '13px'; xpPanel.style.borderRadius = '8px'; xpPanel.style.zIndex = '99999'; xpPanel.style.userSelect = 'none'; xpPanel.style.boxShadow = '0 0 15px rgba(0,0,0,0.7)'; xpPanel.style.pointerEvents = settings.uiLock.locked ? 'none' : 'auto'; document.body.appendChild(xpPanel); function formatNumber(num) { return num.toLocaleString(undefined, {maximumFractionDigits: 2}); } function updateXPDisplay() { if (!settings.xpTracker.enabled) { xpPanel.style.display = 'none'; return; } xpPanel.style.display = 'block'; let html = '<table style="width:100%; border-collapse: collapse;">'; html += `<thead><tr><th style="text-align:left; padding:2px 6px;">Skill</th><th style="text-align:right; padding:2px 6px;">XP/min</th><th style="text-align:right; padding:2px 6px;">XP/hour</th><th style="text-align:right; padding:2px 6px;">Time to Next Level</th></tr></thead><tbody>`; const nowTime = now(); skills.forEach(skill => { if (!settings.xpTracker.visibleSkills[skill]) return; const data = xpData[skill]; if (!data.history || data.history.length < 2) return; const firstSample = data.history[0]; const lastSample = data.history[data.history.length - 1]; const elapsedMs = lastSample.time - firstSample.time; const xpGained = lastSample.xp - firstSample.xp; if (elapsedMs <= 0 || xpGained <= 0) { html += `<tr> <td style="padding:2px 6px;">${skill.charAt(0).toUpperCase() + skill.slice(1)}</td> <td style="text-align:right; padding:2px 6px;">0</td> <td style="text-align:right; padding:2px 6px;">0</td> <td style="text-align:right; padding:2px 6px;">∞</td> </tr>`; return; } const xpPerMs = xpGained / elapsedMs; const xpPerMin = xpPerMs * 60 * 1000; const xpPerHour = xpPerMin * 60; const currentLevel = xpToLevel(lastSample.xp); const nextLevelXP = xpTable[currentLevel] ?? xpTable[xpTable.length - 1]; const xpNeeded = nextLevelXP - lastSample.xp; let timeRemaining = Infinity; if (xpPerMs > 0) timeRemaining = xpNeeded / xpPerMs / 1000; html += `<tr> <td style="padding:2px 6px;">${skill.charAt(0).toUpperCase() + skill.slice(1)}</td> <td style="text-align:right; padding:2px 6px;">${formatNumber(xpPerMin)}</td> <td style="text-align:right; padding:2px 6px;">${formatNumber(xpPerHour)}</td> <td style="text-align:right; padding:2px 6px;">${timeRemaining === Infinity ? '∞' : formatTime(timeRemaining)}</td> </tr>`; }); html += '</tbody></table>'; xpPanel.innerHTML = `<h3 style="margin-top:0;margin-bottom:8px;">Skill XP Rate Tracker</h3>${html}`; } // Settings panel const settingsPanel = document.createElement('div'); settingsPanel.style.position = 'fixed'; settingsPanel.style.top = '50%'; settingsPanel.style.left = '50%'; settingsPanel.style.transform = 'translate(-50%, -50%)'; settingsPanel.style.width = '420px'; settingsPanel.style.maxHeight = '480px'; settingsPanel.style.overflowY = 'auto'; settingsPanel.style.background = settings.visuals.backgroundColor; settingsPanel.style.color = settings.visuals.textColor; settingsPanel.style.padding = '15px 20px'; settingsPanel.style.fontFamily = 'Arial,sans-serif'; settingsPanel.style.fontSize = '14px'; settingsPanel.style.borderRadius = '12px'; settingsPanel.style.zIndex = '100000'; settingsPanel.style.boxShadow = '0 0 25px rgba(0,0,0,0.8)'; settingsPanel.style.display = 'none'; settingsPanel.style.pointerEvents = 'auto'; document.body.appendChild(settingsPanel); // Button to toggle settings panel const toggleSettingsBtn = document.createElement('button'); toggleSettingsBtn.textContent = '⚙'; toggleSettingsBtn.style.position = 'fixed'; toggleSettingsBtn.style.bottom = '20px'; toggleSettingsBtn.style.left = '50%'; toggleSettingsBtn.style.transform = 'translateX(-50%)'; toggleSettingsBtn.style.zIndex = '100001'; toggleSettingsBtn.style.padding = '8px 16px'; toggleSettingsBtn.style.fontFamily = 'Arial,sans-serif'; toggleSettingsBtn.style.cursor = 'pointer'; toggleSettingsBtn.style.borderRadius = '8px'; toggleSettingsBtn.style.border = `1px solid ${settings.visuals.textColor}`; toggleSettingsBtn.style.background = settings.visuals.backgroundColor; toggleSettingsBtn.style.color = settings.visuals.textColor; toggleSettingsBtn.style.pointerEvents = 'auto'; document.body.appendChild(toggleSettingsBtn); toggleSettingsBtn.addEventListener('click', () => { if (settingsPanel.style.display === 'none') { renderSettingsUI(); settingsPanel.style.display = 'block'; } else { settingsPanel.style.display = 'none'; } }); // Session stats panel const sessionStatsPanel = document.createElement('div'); sessionStatsPanel.style.position = 'fixed'; sessionStatsPanel.style.top = '60px'; sessionStatsPanel.style.left = '20px'; sessionStatsPanel.style.width = '350px'; sessionStatsPanel.style.maxHeight = '400px'; sessionStatsPanel.style.overflowY = 'auto'; sessionStatsPanel.style.background = settings.visuals.backgroundColor; sessionStatsPanel.style.color = settings.visuals.textColor; sessionStatsPanel.style.padding = '10px'; sessionStatsPanel.style.fontFamily = 'Arial,sans-serif'; sessionStatsPanel.style.fontSize = '13px'; sessionStatsPanel.style.borderRadius = '8px'; sessionStatsPanel.style.zIndex = '99999'; sessionStatsPanel.style.userSelect = 'none'; sessionStatsPanel.style.boxShadow = '0 0 15px rgba(0,0,0,0.7)'; sessionStatsPanel.style.display = 'none'; sessionStatsPanel.style.pointerEvents = settings.uiLock.locked ? 'none' : 'auto'; document.body.appendChild(sessionStatsPanel); // Button to toggle session stats panel const viewSessionStatsBtn = document.createElement('button'); viewSessionStatsBtn.textContent = '📊 Session Stats'; viewSessionStatsBtn.title = 'View total items & XP collected this session'; viewSessionStatsBtn.style.position = 'fixed'; viewSessionStatsBtn.style.bottom = '20px'; viewSessionStatsBtn.style.left = '20px'; viewSessionStatsBtn.style.zIndex = '100001'; viewSessionStatsBtn.style.padding = '8px 16px'; viewSessionStatsBtn.style.fontFamily = 'Arial,sans-serif'; viewSessionStatsBtn.style.cursor = 'pointer'; viewSessionStatsBtn.style.borderRadius = '8px'; viewSessionStatsBtn.style.border = `1px solid ${settings.visuals.textColor}`; viewSessionStatsBtn.style.background = settings.visuals.backgroundColor; viewSessionStatsBtn.style.color = settings.visuals.textColor; viewSessionStatsBtn.style.pointerEvents = 'auto'; document.body.appendChild(viewSessionStatsBtn); function renderSessionStats() { const playtimeMs = Date.now() - sessionStartTime; const playtimeFormatted = formatPlaytime(playtimeMs); let html = `<h3 style="margin-top:0; margin-bottom:8px;">Session Stats</h3>`; html += `<p style="margin-bottom:10px;"><strong>Playtime:</strong> ${playtimeFormatted}</p>`; html += '<h4>Items Collected</h4>'; if (Object.keys(sessionItemsCollected).length === 0) { html += '<p>No items collected yet.</p>'; } else { html += '<ul style="max-height:150px; overflow-y:auto; padding-left:18px;">'; for (const [item, count] of Object.entries(sessionItemsCollected)) { const rates = calculateRates(count, playtimeMs); html += `<li>${item.replace(/_/g, ' ')}: ${count.toLocaleString()} <span style="color:#aaa;">(${rates.perMin}/m, ${rates.perHour}/h)</span></li>`; } html += '</ul>'; } html += '<h4>XP Gained</h4>'; html += '<ul style="max-height:150px; overflow-y:auto; padding-left:18px;">'; skills.forEach(skill => { const xp = sessionXpCollected[skill]; if (xp > 0) { const rates = calculateRates(xp, playtimeMs); html += `<li>${skill.charAt(0).toUpperCase() + skill.slice(1)}: ${xp.toLocaleString()} <span style="color:#aaa;">(${rates.perMin}/m, ${rates.perHour}/h)</span></li>`; } }); html += '</ul>'; sessionStatsPanel.innerHTML = html; } function resetSessionStats() { sessionItemsCollected = {}; sessionXpCollected = {}; sessionStartTime = Date.now(); skills.forEach(skill => sessionXpCollected[skill] = 0); if (sessionStatsPanel.style.display !== 'none') { renderSessionStats(); } } function resetCalculator() { skills.forEach(skill => { xpData[skill] = { lastXP: 0, history: [] }; }); updateXPDisplay(); } function updateUILockState() { const elements = [notifierContainer, xpPanel, sessionStatsPanel]; elements.forEach(el => { el.style.pointerEvents = settings.uiLock.locked ? 'none' : 'auto'; }); if (settings.uiLock.locked) { makeUndraggable(notifierContainer); makeUndraggable(xpPanel); makeUndraggable(sessionStatsPanel); } else { makeDraggable(notifierContainer); makeDraggable(xpPanel); makeDraggable(sessionStatsPanel); } } function makeDraggable(el, handleSelector = null) { let isDragging = false; let offsetX = 0; let offsetY = 0; const handle = handleSelector ? el.querySelector(handleSelector) : el; if (!handle) return; handle.style.cursor = 'move'; const dragMouseDown = (e) => { isDragging = true; offsetX = e.clientX - el.getBoundingClientRect().left; offsetY = e.clientY - el.getBoundingClientRect().top; document.body.style.userSelect = 'none'; }; handle.addEventListener('mousedown', dragMouseDown); document.addEventListener('mousemove', (e) => { if (!isDragging) return; el.style.left = (e.clientX - offsetX) + 'px'; el.style.top = (e.clientY - offsetY) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; el.style.position = 'fixed'; }); document.addEventListener('mouseup', () => { isDragging = false; document.body.style.userSelect = ''; }); } function makeUndraggable(el) { const handle = el.querySelector('[style*="cursor: move"]'); if (handle) { handle.style.cursor = 'default'; const newHandle = handle.cloneNode(true); handle.parentNode.replaceChild(newHandle, handle); } } function rgbToHex(rgb) { if (rgb.startsWith('#')) return rgb; const match = rgb.match(/\d+/g); if (!match) return '#000000'; const r = parseInt(match[0]); const g = parseInt(match[1]); const b = parseInt(match[2]); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } function renderSettingsUI() { settingsPanel.innerHTML = ` <h3 style="margin-top:0;margin-bottom:8px;">UI Behavior</h3> <label style="display:block; margin-bottom:12px; cursor:pointer;"> <input type="checkbox" id="uiLock" ${settings.uiLock.locked ? 'checked' : ''}> Lock UI (make panels click-through) </label> <hr style="border:1px solid rgba(255,255,255,0.2); margin:12px 0;"> <h3 style="margin-top:0;margin-bottom:8px;">Item Pickup Notifier</h3> <label style="display:block; margin-bottom:6px; cursor:pointer;"> <input type="checkbox" id="pickupEnabled" ${settings.pickupNotifier.enabled ? 'checked' : ''}> Enabled </label> <hr style="border:1px solid rgba(255,255,255,0.2); margin:12px 0;"> <h3 style="margin-top:0;margin-bottom:8px;">XP Tracker</h3> <label style="display:block; margin-bottom:6px; cursor:pointer;"> <input type="checkbox" id="xpEnabled" ${settings.xpTracker.enabled ? 'checked' : ''}> Enabled </label> <p style="margin:8px 0 4px 0;">Visible Skills:</p> <div id="skillsToggles" style="max-height:160px; overflow-y:auto; border:1px solid rgba(255,255,255,0.2); padding:4px; border-radius:4px;"> </div> <hr style="border:1px solid rgba(255,255,255,0.2); margin:12px 0;"> <h3 style="margin-top:0;margin-bottom:8px;">Visual Customization</h3> <label style="display:block; margin-bottom:6px;"> Background Color: <input type="color" id="backgroundColorPicker" value="${rgbToHex(settings.visuals.backgroundColor)}" style="margin-left:10px;"> </label> <label style="display:block; margin-bottom:6px;"> Text Color: <input type="color" id="textColorPicker" value="${rgbToHex(settings.visuals.textColor)}" style="margin-left:10px;"> </label> <hr style="border:1px solid rgba(255,255,255,0.2); margin:12px 0;"> <h3 style="margin-top:0;margin-bottom:8px;">Reset Options</h3> <button id="resetSessionBtn" style="margin-right:10px; padding:6px 12px; background:#d9534f; color:white; border:none; border-radius:4px; cursor:pointer;">Reset Session Stats</button> <button id="resetCalculatorBtn" style="padding:6px 12px; background:#f0ad4e; color:white; border:none; border-radius:4px; cursor:pointer;">Reset XP Calculator</button> `; const skillsContainer = document.getElementById('skillsToggles'); skillsContainer.innerHTML = ''; skills.forEach(skill => { const checked = settings.xpTracker.visibleSkills[skill]; const label = document.createElement('label'); label.style.display = 'block'; label.style.cursor = 'pointer'; label.style.userSelect = 'none'; label.innerHTML = `<input type="checkbox" data-skill="${skill}" ${checked ? 'checked' : ''} style="margin-right:6px;">${skill.charAt(0).toUpperCase() + skill.slice(1)}`; skillsContainer.appendChild(label); }); document.getElementById('uiLock').onchange = e => { settings.uiLock.locked = e.target.checked; saveSettings(); updateUILockState(); }; document.getElementById('pickupEnabled').onchange = e => { settings.pickupNotifier.enabled = e.target.checked; saveSettings(); }; document.getElementById('xpEnabled').onchange = e => { settings.xpTracker.enabled = e.target.checked; saveSettings(); updateXPDisplay(); }; skillsContainer.querySelectorAll('input[type=checkbox]').forEach(cb => { cb.onchange = e => { const skill = e.target.dataset.skill; settings.xpTracker.visibleSkills[skill] = e.target.checked; saveSettings(); updateXPDisplay(); }; }); document.getElementById('backgroundColorPicker').oninput = e => { settings.visuals.backgroundColor = e.target.value; saveSettings(); applyVisuals(); }; document.getElementById('textColorPicker').oninput = e => { settings.visuals.textColor = e.target.value; saveSettings(); applyVisuals(); }; document.getElementById('resetSessionBtn').addEventListener('click', resetSessionStats); document.getElementById('resetCalculatorBtn').addEventListener('click', resetCalculator); } // WebSocket hook to track inventory changes and XP updates const OriginalWebSocket = window.WebSocket; window.WebSocket = function(...args) { const ws = new OriginalWebSocket(...args); ws.addEventListener('message', (event) => { const data = event.data; if (typeof data !== 'string') return; // Inventory items update if (data.startsWith('SET_INVENTORY_ITEMS=')) { if (!settings.pickupNotifier.enabled) return; const msg = data.substring('SET_INVENTORY_ITEMS='.length); const parts = msg.split('~'); const inventory = {}; for (let i = 0; i < parts.length; i += 4) { const itemId = parts[i]; const count = parseInt(parts[i+1], 10) || 0; inventory[itemId] = (inventory[itemId] || 0) + count; } for (const [itemId, count] of Object.entries(inventory)) { const oldCount = lastInventory[itemId] || 0; if (count > oldCount) { const gained = count - oldCount; showPickupMessage(`+${gained} ${itemId.replace(/_/g, ' ')}`); // Track session items collected sessionItemsCollected[itemId] = (sessionItemsCollected[itemId] || 0) + gained; } } lastInventory = inventory; // Update session stats if panel is open if (sessionStatsPanel.style.display !== 'none') { renderSessionStats(); } } // XP updates if (data.startsWith('REFRESH_VAR=')) { if (!settings.xpTracker.enabled) return; const msg = data.substring('REFRESH_VAR='.length); const parts = msg.split('~'); if (parts.length >= 3) { let skillXpKey = parts[1]; const xpValue = Number(parts[2]); if (skills.includes(skillXpKey.replace('_xp',''))) { const skill = skillXpKey.replace('_xp',''); const data = xpData[skill]; // Add sample to history, keep last 10 samples for smoothing (~50s if update every 5s) const nowTime = now(); data.history.push({ xp: xpValue, time: nowTime }); if (data.history.length > 10) data.history.shift(); // Calculate gained XP this update for session total const lastSample = data.history.length > 1 ? data.history[data.history.length - 2] : null; if (lastSample && xpValue > lastSample.xp) { sessionXpCollected[skill] += xpValue - lastSample.xp; } data.lastXP = xpValue; // Update session stats if panel is open if (sessionStatsPanel.style.display !== 'none') { renderSessionStats(); } } } } }); return ws; }; viewSessionStatsBtn.addEventListener('click', () => { if (sessionStatsPanel.style.display === 'none') { renderSessionStats(); sessionStatsPanel.style.display = 'block'; // Update stats every second while panel is open sessionStatsUpdateInterval = setInterval(renderSessionStats, 1000); } else { sessionStatsPanel.style.display = 'none'; clearInterval(sessionStatsUpdateInterval); } }); setInterval(updateXPDisplay, 5000); makeDraggable(xpPanel); makeDraggable(notifierContainer); makeDraggable(sessionStatsPanel); applyVisuals(); renderSettingsUI(); updateUILockState(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址