您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Gym training helper with target percentages, current distribution display, and optional raw differences
// ==UserScript== // @name Torn Gym Ratios (PDA Compatible) // @namespace http://tampermonkey.net/ // @version 3.2 // @description Gym training helper with target percentages, current distribution display, and optional raw differences // @author Mistborn [3037268] // @match https://www.torn.com/gym.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @run-at document-end // @license MIT // @supportURL https://github.com/MistbornTC/torn-gym-ratios/issues // ==/UserScript== (function() { 'use strict'; console.log('=== TORN GYM RATIOS SCRIPT LOADED ==='); // Constants for better maintainability const CONSTANTS = { TOLERANCE_PERCENT: 1, // Within 1% tolerance for "on target" UPDATE_INTERVAL: 5000, // 5 seconds THEME_CHECK_INTERVAL_PDA: 500, // 500ms for PDA THEME_CHECK_INTERVAL_BROWSER: 1000, // 1000ms for browser CSS_MONITOR_INTERVAL: 100, // 100ms for CSS property monitoring ELEMENT_IDS: { STATS_DISPLAY: 'gym-stats-display', HELPER_DISPLAY: 'gym-helper-display', HELP_BTN: 'gym-help-btn', COLLAPSE_BTN: 'gym-collapse-btn', CONFIG_BTN: 'gym-config-btn', HELP_TOOLTIP: 'gym-help-tooltip', CONFIG_PANEL: 'gym-config-panel', THEME_DETECTOR: 'gym-theme-detector', TOTAL_PERCENTAGE: 'total-percentage', SAVE_TARGETS: 'save-targets', CANCEL_CONFIG: 'cancel-config', SHOW_STAT_DIFFERENCES: 'show-stat-differences' } }; // Enhanced PDA detection function isTornPDA() { const userAgentCheck = navigator.userAgent.includes('com.manuito.tornpda'); const flutterCheck = !!window.flutter_inappwebview; const platformCheck = !!window.__PDA_platformReadyPromise; const httpCheck = !!window.PDA_httpGet; const result = !!(userAgentCheck || flutterCheck || platformCheck || httpCheck); console.log('PDA Detection:', result, { userAgent: userAgentCheck, flutter: flutterCheck, platform: platformCheck, httpGet: httpCheck }); return result; } // Universal storage functions function safeGM_getValue(key, defaultValue) { try { if (typeof GM_getValue !== 'undefined') { return GM_getValue(key, defaultValue); } } catch (e) { console.log('GM_getValue failed, using localStorage fallback'); } try { const stored = localStorage.getItem('gym_' + key); return stored ? JSON.parse(stored) : defaultValue; } catch (e) { console.log('localStorage failed, using default:', defaultValue); return defaultValue; } } function safeGM_setValue(key, value) { try { if (typeof GM_setValue !== 'undefined') { GM_setValue(key, value); return; } } catch (e) { console.log('GM_setValue failed, using localStorage fallback'); } try { localStorage.setItem('gym_' + key, JSON.stringify(value)); } catch (e) { console.log('Storage failed'); } } // Format numbers with abbreviations function formatNumber(num) { if (num < 100000) { return num.toLocaleString(); } else if (num < 1000000) { return Math.round(num / 1000) + 'k'; } else if (num < 1000000000) { const millions = num / 1000000; return millions % 1 < 0.05 ? Math.round(millions) + 'M' : millions.toFixed(1) + 'M'; } else { const billions = num / 1000000000; if (billions % 1 < 0.005) return Math.round(billions) + 'B'; return parseFloat(billions.toFixed(2)) + 'B'; // Auto-trims trailing zeros } } // Calculate stat difference text and color function getStatDifferenceText(statName, currentValue, targetValue, color, percentageDiff) { const difference = Math.abs(currentValue - targetValue); const formattedDiff = formatNumber(Math.round(difference)); const isMobile = window.innerWidth <= 768; if (Math.abs(percentageDiff) <= CONSTANTS.TOLERANCE_PERCENT) { // Within 1% tolerance - use the same logic as color determination const sign = currentValue >= targetValue ? '+' : '-'; return { text: `${statName} on target (${sign}${formattedDiff})`, color: color }; } else if (currentValue > targetValue) { // Over target const suffix = isMobile ? ' over' : ' over target'; return { text: `${statName} ${formattedDiff}${suffix}`, color: color }; } else { // Under target const suffix = isMobile ? ' under' : ' under target'; return { text: `${statName} ${formattedDiff}${suffix}`, color: color }; } } // Wait for element function waitForElement(selector, callback, timeout = 30000) { const startTime = Date.now(); function check() { const element = document.querySelector(selector); if (element) { console.log('Found element:', selector); callback(element); return; } if (Date.now() - startTime > timeout) { console.error('Timeout waiting for element:', selector); return; } setTimeout(check, 100); } check(); } // Extract current stat values function getCurrentStats() { const stats = { strength: 0, defense: 0, speed: 0, dexterity: 0 }; const strengthContainer = document.querySelector('li[class*="strength___"]'); const defenseContainer = document.querySelector('li[class*="defense___"]'); const speedContainer = document.querySelector('li[class*="speed___"]'); const dexterityContainer = document.querySelector('li[class*="dexterity___"]'); function extractValue(container) { if (!container) return 0; const valueEl = container.querySelector('[class*="propertyValue___"]') || container.querySelector('.propertyValue') || container.querySelector('[data-value]'); if (valueEl) { const text = valueEl.textContent || valueEl.getAttribute('data-value') || '0'; return parseInt(text.replace(/,/g, '')) || 0; } return 0; } stats.strength = extractValue(strengthContainer); stats.defense = extractValue(defenseContainer); stats.speed = extractValue(speedContainer); stats.dexterity = extractValue(dexterityContainer); return stats; } // Calculate percentages function calculateCurrentDistribution(stats) { const total = stats.strength + stats.defense + stats.speed + stats.dexterity; if (total === 0) return { strength: 0, defense: 0, speed: 0, dexterity: 0 }; return { strength: (stats.strength / total * 100).toFixed(1), defense: (stats.defense / total * 100).toFixed(1), speed: (stats.speed / total * 100).toFixed(1), dexterity: (stats.dexterity / total * 100).toFixed(1) }; } // Load/save targets and settings function loadTargets() { return { strength: safeGM_getValue('gym_target_strength', 25), defense: safeGM_getValue('gym_target_defense', 25), speed: safeGM_getValue('gym_target_speed', 25), dexterity: safeGM_getValue('gym_target_dexterity', 25) }; } function saveTargets(targets) { safeGM_setValue('gym_target_strength', targets.strength); safeGM_setValue('gym_target_defense', targets.defense); safeGM_setValue('gym_target_speed', targets.speed); safeGM_setValue('gym_target_dexterity', targets.dexterity); } function loadShowStatDifferences() { return safeGM_getValue('gym_show_stat_differences', false); } function saveShowStatDifferences(show) { safeGM_setValue('gym_show_stat_differences', show); } // Theme detection function getTheme() { const body = document.body; const isDarkMode = body.classList.contains('dark-mode') || body.classList.contains('dark') || body.style.background.includes('#191919') || getComputedStyle(body).backgroundColor === 'rgb(25, 25, 25)'; return isDarkMode ? 'dark' : 'light'; } function getThemeColors() { const theme = getTheme(); if (theme === 'dark') { return { panelBg: '#2a2a2a', panelBorder: '#444', configBg: '#333', configBorder: '#555', statBoxBg: '#3a3a3a', statBoxBorder: '#444', statBoxShadow: '0 1px 3px rgba(0,0,0,0.1)', inputBg: '#444', inputBorder: '#555', textPrimary: '#fff', textSecondary: '#ccc', textMuted: '#999', success: '#5cb85c', warning: '#f0ad4e', danger: '#d9534f', primary: '#4a90e2', neutral: '#666' }; } else { return { panelBg: '#f8f9fa', panelBorder: 'rgba(102, 102, 102, 0.3)', configBg: '#ffffff', configBorder: '#ced4da', statBoxBg: '#ffffff', statBoxBorder: 'rgba(102, 102, 102, 0.3)', statBoxShadow: 'rgba(50, 50, 50, 0.2) 0px 0px 2px 0px', inputBg: '#ffffff', inputBorder: '#ced4da', textPrimary: '#212529', textSecondary: '#6c757d', textMuted: '#adb5bd', success: 'rgb(105, 168, 41)', warning: '#f0ad4e', danger: '#dc3545', primary: '#007bff', neutral: '#6c757d' }; } } // Collapse state management function isCollapsed() { if (isTornPDA()) { const statsDisplay = document.getElementById('gym-stats-display'); return statsDisplay ? statsDisplay.style.display === 'none' : false; } const isMobile = window.innerWidth <= 768; const key = isMobile ? 'gym_helper_collapsed_mobile' : 'gym_helper_collapsed_desktop'; return safeGM_getValue(key, false); } function setCollapsed(collapsed) { if (isTornPDA()) { return; // No storage in PDA } const isMobile = window.innerWidth <= 768; const key = isMobile ? 'gym_helper_collapsed_mobile' : 'gym_helper_collapsed_desktop'; safeGM_setValue(key, collapsed); } // Track current theme to detect changes let currentTheme = getTheme(); let themeObserver = null; let themeCheckInterval = null; // Track stats monitoring let statsObserver = null; let statsUpdateInterval = null; // Advanced theme detection with multiple monitoring methods function initAdvancedThemeMonitoring() { // Method 1: MutationObserver for class/style changes if (!isTornPDA() && typeof MutationObserver !== 'undefined') { themeObserver = new MutationObserver((mutations) => { let shouldCheck = false; mutations.forEach((mutation) => { if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'style' || mutation.attributeName === 'data-theme')) { shouldCheck = true; } }); if (shouldCheck) { checkAndUpdateTheme(); } }); themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'], subtree: false }); // Also observe html element for theme changes themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'], subtree: false }); } // Method 2: CSS Custom Property monitoring (for instant updates) if (!isTornPDA()) { try { // Create a test element to monitor CSS custom properties const themeTestEl = document.createElement('div'); themeTestEl.id = 'gym-theme-detector'; themeTestEl.style.cssText = ` position: absolute; top: -9999px; left: -9999px; width: 1px; height: 1px; background: var(--torn-bg-color, rgb(25, 25, 25)); pointer-events: none; z-index: -1; `; document.body.appendChild(themeTestEl); // Monitor background color changes let lastBgColor = getComputedStyle(themeTestEl).backgroundColor; setInterval(() => { const currentBgColor = getComputedStyle(themeTestEl).backgroundColor; if (currentBgColor !== lastBgColor) { lastBgColor = currentBgColor; checkAndUpdateTheme(); } }, CONSTANTS.CSS_MONITOR_INTERVAL); } catch (e) { console.log('CSS monitoring fallback failed, using standard detection'); } } // Method 3: Enhanced interval checking (fallback for PDA) const checkFrequency = isTornPDA() ? CONSTANTS.THEME_CHECK_INTERVAL_PDA : CONSTANTS.THEME_CHECK_INTERVAL_BROWSER; themeCheckInterval = setInterval(checkAndUpdateTheme, checkFrequency); // Method 4: Event listeners for common theme switching scenarios if (!isTornPDA()) { ['focus', 'blur', 'visibilitychange'].forEach(event => { document.addEventListener(event, () => { setTimeout(checkAndUpdateTheme, 50); }); }); } } // Optimized theme check and update function function checkAndUpdateTheme() { const newTheme = getTheme(); if (newTheme !== currentTheme) { console.log('Theme changed from', currentTheme, 'to', newTheme); currentTheme = newTheme; updateMainContainerTheme(); updateStats(); // Refresh stats display with new theme } } // Dynamic theme update function with CSS custom properties function updateMainContainerTheme() { const colors = getThemeColors(); const mainPanel = document.getElementById('gym-helper-display'); if (!mainPanel) return; // Set CSS custom properties for instant theme switching const rootStyle = document.documentElement.style; const customProps = { '--gym-panel-bg': colors.panelBg, '--gym-panel-border': colors.panelBorder, '--gym-text-primary': colors.textPrimary, '--gym-text-secondary': colors.textSecondary, '--gym-text-muted': colors.textMuted, '--gym-stat-box-bg': colors.statBoxBg, '--gym-stat-box-border': colors.statBoxBorder, '--gym-stat-box-shadow': colors.statBoxShadow, '--gym-input-bg': colors.inputBg, '--gym-input-border': colors.inputBorder, '--gym-success': colors.success, '--gym-warning': colors.warning, '--gym-danger': colors.danger, '--gym-primary': colors.primary, '--gym-neutral': colors.neutral }; // Apply all custom properties at once Object.entries(customProps).forEach(([prop, value]) => { rootStyle.setProperty(prop, value); }); // Update main panel styles mainPanel.style.background = colors.panelBg; mainPanel.style.border = '1px solid ' + colors.panelBorder; mainPanel.style.color = colors.textPrimary; mainPanel.style.boxShadow = colors.statBoxShadow; // Update title color const title = mainPanel.querySelector('#gym-header-clickable'); if (title) title.style.color = colors.textPrimary; // Cache elements for better performance const elements = { helpBtn: document.getElementById('gym-help-btn'), collapseBtn: document.getElementById('gym-collapse-btn'), configBtn: document.getElementById('gym-config-btn'), tooltip: document.getElementById('gym-help-tooltip'), configPanel: document.getElementById('gym-config-panel') }; // Update button colors if (elements.helpBtn) elements.helpBtn.style.background = colors.neutral; if (elements.collapseBtn) elements.collapseBtn.style.background = colors.neutral; if (elements.configBtn) elements.configBtn.style.background = colors.primary; // Update help tooltip if (elements.tooltip) { elements.tooltip.style.background = colors.statBoxBg; elements.tooltip.style.border = '1px solid ' + colors.statBoxBorder; elements.tooltip.style.boxShadow = colors.statBoxShadow; const tooltipElements = elements.tooltip.querySelectorAll('div'); tooltipElements.forEach(el => { el.style.color = colors.textPrimary; }); // Update the warning color text const middleDiv = elements.tooltip.children[2]; if (middleDiv) { middleDiv.innerHTML = '<span style="color: ' + colors.warning + '; font-weight: bold; font-size: 16px;">|</span> <span style="font-weight: bold;">Orange:</span> Above target (focus on other stats)'; } } // Update config panel if (elements.configPanel) { elements.configPanel.style.background = colors.configBg; elements.configPanel.style.border = '1px solid ' + colors.configBorder; const configTitle = elements.configPanel.querySelector('h4'); if (configTitle) configTitle.style.color = colors.textPrimary; const configLabels = elements.configPanel.querySelectorAll('label'); configLabels.forEach(label => { label.style.color = colors.textSecondary; }); const configInputs = elements.configPanel.querySelectorAll('input'); configInputs.forEach(input => { input.style.background = colors.inputBg; input.style.border = '1px solid ' + colors.inputBorder; input.style.color = colors.textPrimary; }); const saveBtn = elements.configPanel.querySelector('#save-targets'); const cancelBtn = elements.configPanel.querySelector('#cancel-config'); if (saveBtn) saveBtn.style.background = colors.success; if (cancelBtn) cancelBtn.style.background = colors.danger; } } // Advanced stats monitoring for performance optimization function initStatsMonitoring() { // Method 1: MutationObserver for stat value changes (browser only) if (!isTornPDA() && typeof MutationObserver !== 'undefined') { statsObserver = new MutationObserver((mutations) => { let shouldUpdate = false; mutations.forEach((mutation) => { // Check if any stat-related elements changed if (mutation.type === 'childList' || (mutation.type === 'attributes' && mutation.attributeName === 'data-value') || (mutation.type === 'characterData')) { const target = mutation.target; const container = target.closest ? target.closest('li[class*="strength___"], li[class*="defense___"], li[class*="speed___"], li[class*="dexterity___"]') : null; if (container || target.classList?.contains('propertyValue') || target.className?.includes('propertyValue')) { shouldUpdate = true; } } }); if (shouldUpdate) { console.log('Stats changed detected, updating display'); updateStats(); } }); // Observe the entire gym page for stat changes const gymContainer = document.querySelector('.content-wrapper') || document.body; if (gymContainer) { statsObserver.observe(gymContainer, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-value', 'class'], characterData: true }); console.log('Stats MutationObserver initialized'); } } // Method 2: Fallback polling for PDA or when MutationObserver isn't available if (isTornPDA() || !statsObserver) { console.log('Using fallback polling for stats monitoring'); const pollInterval = isTornPDA() ? 3000 : 10000; // More frequent for PDA, less for browser fallback statsUpdateInterval = setInterval(() => { updateStats(); }, pollInterval); } else { console.log('Using MutationObserver for efficient stats monitoring'); } // Initial update updateStats(); } // Cleanup functions function cleanupStatsMonitoring() { if (statsObserver) { statsObserver.disconnect(); statsObserver = null; } if (statsUpdateInterval) { clearInterval(statsUpdateInterval); statsUpdateInterval = null; } } function cleanupThemeMonitoring() { if (themeObserver) { themeObserver.disconnect(); themeObserver = null; } if (themeCheckInterval) { clearInterval(themeCheckInterval); themeCheckInterval = null; } const themeTestEl = document.getElementById('gym-theme-detector'); if (themeTestEl) { themeTestEl.remove(); } } // Master cleanup function function cleanupAllMonitoring() { cleanupStatsMonitoring(); cleanupThemeMonitoring(); } // Create the main display panel using innerHTML (PDA-compatible) function createDisplayPanel() { const colors = getThemeColors(); const panel = document.createElement('div'); panel.id = 'gym-helper-display'; panel.style.cssText = ` background: ${colors.panelBg}; border: 1px solid ${colors.panelBorder}; border-radius: 5px; padding: 15px; margin: 10px 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: ${colors.textPrimary}; position: relative; box-shadow: ${colors.statBoxShadow}; `; panel.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <h3 id="gym-header-clickable" style=" margin: 0; color: ${colors.textPrimary}; font-size: 16px; user-select: none; cursor: pointer; ">Gym Ratios</h3> <div style="margin-left: auto; margin-right: -21px;"> <button id="gym-help-btn" style=" background: ${colors.neutral}; color: white; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; margin-right: 5px; ${(!isTornPDA() && window.innerWidth > 768) ? '-webkit-transform: translateZ(0); transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden;' : 'outline: none; -webkit-appearance: none; appearance: none; border: none; position: relative; overflow: hidden;'} ">?</button> <button id="gym-collapse-btn" style=" background: ${colors.neutral}; color: white; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; margin-right: 5px; ${(!isTornPDA() && window.innerWidth > 768) ? '-webkit-transform: translateZ(0); transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden;' : 'outline: none; -webkit-appearance: none; appearance: none; border: none; position: relative; overflow: hidden;'} ">−</button> <button id="gym-config-btn" style=" background: ${colors.primary}; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; ${(!isTornPDA() && window.innerWidth > 768) ? '-webkit-transform: translateZ(0); transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden;' : 'outline: none; -webkit-appearance: none; appearance: none; border: none; position: relative; overflow: hidden;'} ">Config</button> </div> </div> <div id="gym-help-tooltip" style=" position: absolute; top: 100%; left: 0; right: 0; background: ${colors.statBoxBg}; border: 1px solid ${colors.statBoxBorder}; border-radius: 5px; padding: 12px; margin-top: 5px; z-index: 1001; display: none; box-shadow: ${colors.statBoxShadow}; font-size: 13px; line-height: 1.4; "> <div style="margin-bottom: 8px; font-weight: bold; color: ${colors.textPrimary};">Color Guide:</div> <div style="margin-bottom: 6px; color: ${colors.textPrimary};"> <span style="color: ${colors.success}; font-weight: bold; font-size: 16px;">|</span> <span style="font-weight: bold;">Green:</span> On target (within ±1% of target) </div> <div style="margin-bottom: 6px; color: ${colors.textPrimary};"> <span style="color: ${colors.warning}; font-weight: bold; font-size: 16px;">|</span> <span style="font-weight: bold;">Orange:</span> Above target (focus on other stats) </div> <div style="color: ${colors.textPrimary};"> <span style="color: ${colors.danger}; font-weight: bold; font-size: 16px;">|</span> <span style="font-weight: bold;">Red:</span> Below target (needs more training) </div> </div> <div id="gym-stats-display" style=" display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; font-size: 13px; "></div> <style> /* CSS Custom Properties for instant theme switching */ :root { --gym-panel-bg: ${colors.panelBg}; --gym-panel-border: ${colors.panelBorder}; --gym-text-primary: ${colors.textPrimary}; --gym-text-secondary: ${colors.textSecondary}; --gym-text-muted: ${colors.textMuted}; --gym-stat-box-bg: ${colors.statBoxBg}; --gym-stat-box-border: ${colors.statBoxBorder}; --gym-stat-box-shadow: ${colors.statBoxShadow}; --gym-input-bg: ${colors.inputBg}; --gym-input-border: ${colors.inputBorder}; --gym-success: ${colors.success}; --gym-warning: ${colors.warning}; --gym-danger: ${colors.danger}; --gym-primary: ${colors.primary}; --gym-neutral: ${colors.neutral}; } /* Instant theme switching classes */ #gym-helper-display { background: var(--gym-panel-bg) !important; border-color: var(--gym-panel-border) !important; color: var(--gym-text-primary) !important; box-shadow: var(--gym-stat-box-shadow) !important; transition: background-color 0.1s ease, border-color 0.1s ease, color 0.1s ease !important; } #gym-header-clickable { color: var(--gym-text-primary) !important; transition: color 0.1s ease !important; } #gym-help-btn, #gym-collapse-btn { background: var(--gym-neutral) !important; transition: background-color 0.1s ease !important; } #gym-config-btn { background: var(--gym-primary) !important; transition: background-color 0.1s ease !important; } #gym-help-tooltip { background: var(--gym-stat-box-bg) !important; border-color: var(--gym-stat-box-border) !important; box-shadow: var(--gym-stat-box-shadow) !important; transition: background-color 0.1s ease, border-color 0.1s ease !important; } #gym-config-panel { background: var(--gym-stat-box-bg) !important; border-color: var(--gym-stat-box-border) !important; transition: background-color 0.1s ease, border-color 0.1s ease !important; } #gym-config-panel h4 { color: var(--gym-text-primary) !important; transition: color 0.1s ease !important; } #gym-config-panel label { color: var(--gym-text-secondary) !important; transition: color 0.1s ease !important; } #gym-config-panel input { background: var(--gym-input-bg) !important; border-color: var(--gym-input-border) !important; color: var(--gym-text-primary) !important; transition: background-color 0.1s ease, border-color 0.1s ease, color 0.1s ease !important; } #save-targets { background: var(--gym-success) !important; transition: background-color 0.1s ease !important; } #cancel-config { background: var(--gym-danger) !important; transition: background-color 0.1s ease !important; } /* Responsive design */ @media (max-width: 768px) { #gym-stats-display { grid-template-columns: repeat(2, 1fr) !important; gap: 8px !important; font-size: 12px !important; } #gym-stats-display > div { padding: 8px !important; } #gym-helper-display { padding: 10px !important; margin: 5px 0 !important; } } @media (max-width: 480px) { #gym-stats-display { grid-template-columns: repeat(2, 1fr) !important; gap: 6px !important; font-size: 11px !important; } #gym-helper-display { margin-left: -5px !important; position: relative !important; z-index: 100 !important; } #gym-config-panel, #gym-help-tooltip { left: 0 !important; right: 0 !important; margin-left: 0 !important; margin-right: 0 !important; z-index: 1010 !important; } } </style> `; return panel; } // Create config panel using innerHTML (PDA-compatible) function createConfigPanel() { const colors = getThemeColors(); const targets = loadTargets(); const showStatDifferences = loadShowStatDifferences(); const configPanel = document.createElement('div'); configPanel.id = 'gym-config-panel'; configPanel.style.cssText = ` position: absolute; top: 100%; left: 0; right: 0; background: ${colors.configBg}; border: 1px solid ${colors.configBorder}; border-radius: 5px; padding: 15px; margin-top: 5px; z-index: 1000; display: none; box-shadow: 0 4px 8px rgba(0,0,0,0.15); `; configPanel.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 15px;"> <h4 style="margin: 0; padding: 0; color: ${colors.textPrimary}; font-size: 16px;">Target Percentages</h4> <span id="total-percentage" style="margin: 0; padding: 0; color: ${colors.textSecondary}; font-weight: bold; font-size: 12px;">Total: 100%</span> </div> <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 15px;"> <div> <label style="display: block; margin-bottom: 5px; color: ${colors.textSecondary};">Strength (%)</label> <input type="number" id="target-strength" value="${targets.strength}" min="0" max="100" step="0.1" style=" width: 100%; padding: 5px; border: 1px solid ${colors.inputBorder}; border-radius: 3px; background: ${colors.inputBg}; color: ${colors.textPrimary}; box-sizing: border-box; "> </div> <div> <label style="display: block; margin-bottom: 5px; color: ${colors.textSecondary};">Defense (%)</label> <input type="number" id="target-defense" value="${targets.defense}" min="0" max="100" step="0.1" style=" width: 100%; padding: 5px; border: 1px solid ${colors.inputBorder}; border-radius: 3px; background: ${colors.inputBg}; color: ${colors.textPrimary}; box-sizing: border-box; "> </div> <div> <label style="display: block; margin-bottom: 5px; color: ${colors.textSecondary};">Speed (%)</label> <input type="number" id="target-speed" value="${targets.speed}" min="0" max="100" step="0.1" style=" width: 100%; padding: 5px; border: 1px solid ${colors.inputBorder}; border-radius: 3px; background: ${colors.inputBg}; color: ${colors.textPrimary}; box-sizing: border-box; "> </div> <div> <label style="display: block; margin-bottom: 5px; color: ${colors.textSecondary};">Dexterity (%)</label> <input type="number" id="target-dexterity" value="${targets.dexterity}" min="0" max="100" step="0.1" style=" width: 100%; padding: 5px; border: 1px solid ${colors.inputBorder}; border-radius: 3px; background: ${colors.inputBg}; color: ${colors.textPrimary}; box-sizing: border-box; "> </div> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center; color: ${colors.textSecondary}; cursor: pointer;"> <input type="checkbox" id="show-stat-differences" ${showStatDifferences ? 'checked' : ''} style=" margin-right: 8px; transform: scale(1.1); "> Include raw stat difference? </label> </div> <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;"> <button id="save-targets" style=" background: ${colors.success}; color: white; border: none; padding: 8px 12px; border-radius: 3px; cursor: pointer; font-size: 12px; width: 100%; box-sizing: border-box; ">Save</button> <button id="cancel-config" style=" background: ${colors.danger}; color: white; border: none; padding: 8px 12px; border-radius: 3px; cursor: pointer; font-size: 12px; width: 100%; box-sizing: border-box; ">Cancel</button> </div> `; return configPanel; } // Toggle collapse state function toggleCollapsed() { const statsDisplay = document.getElementById('gym-stats-display'); const collapseBtn = document.getElementById('gym-collapse-btn'); const configPanel = document.getElementById('gym-config-panel'); const mainPanel = document.getElementById('gym-helper-display'); const header = mainPanel ? mainPanel.querySelector('div:first-child') : null; if (!statsDisplay || !collapseBtn) return; const collapsed = isCollapsed(); const newState = !collapsed; setCollapsed(newState); if (newState) { // Collapse statsDisplay.style.display = 'none'; collapseBtn.innerHTML = '+'; if (configPanel) configPanel.style.display = 'none'; // Adjust spacing when collapsed for tighter layout if (header) header.style.marginBottom = '2px'; if (mainPanel) { if (isTornPDA()) { mainPanel.style.padding = '10px 15px 8px 15px'; mainPanel.style.marginBottom = '5px'; } else { mainPanel.style.padding = '10px 15px 8px 15px'; } } } else { // Expand statsDisplay.style.display = 'grid'; collapseBtn.innerHTML = '−'; // Restore normal spacing when expanded if (header) header.style.marginBottom = '10px'; if (mainPanel) { if (isTornPDA()) { mainPanel.style.padding = '15px'; mainPanel.style.marginBottom = '10px'; } else { mainPanel.style.padding = '15px'; } } updateStats(); } } // Apply saved collapse state function applySavedCollapseState() { if (isTornPDA()) { return; // Always start expanded in PDA } if (isCollapsed()) { setTimeout(() => { const statsDisplay = document.getElementById('gym-stats-display'); const collapseBtn = document.getElementById('gym-collapse-btn'); if (statsDisplay && collapseBtn) { statsDisplay.style.display = 'none'; collapseBtn.innerHTML = '+'; const mainPanel = document.getElementById('gym-helper-display'); if (mainPanel) mainPanel.style.paddingBottom = '5px'; } }, 100); } } // Update stats display function updateStats() { const stats = getCurrentStats(); const dist = calculateCurrentDistribution(stats); const targets = loadTargets(); const showStatDifferences = loadShowStatDifferences(); const colors = getThemeColors(); const statsDiv = document.getElementById('gym-stats-display'); if (!statsDiv) return; if (isCollapsed()) return; // Don't update if collapsed const total = stats.strength + stats.defense + stats.speed + stats.dexterity; if (total === 0) { statsDiv.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: ' + colors.textSecondary + ';">No stats found. Make sure you\'re on the gym page.</div>'; return; } const statNames = ['strength', 'defense', 'speed', 'dexterity']; const statLabels = ['Strength', 'Defense', 'Speed', 'Dexterity']; statsDiv.innerHTML = statNames.map((stat, index) => { const current = parseFloat(dist[stat]); const target = targets[stat]; const diff = current - target; const color = Math.abs(diff) <= CONSTANTS.TOLERANCE_PERCENT ? colors.success : (diff > 0 ? colors.warning : colors.danger); let additionalContent = ''; if (showStatDifferences) { // Calculate target value in absolute numbers const targetValue = (target / 100) * total; const currentValue = stats[stat]; // Get difference text and color const diffInfo = getStatDifferenceText(statLabels[index], currentValue, targetValue, color, diff); additionalContent = ` <div style="font-size: 10px; color: ${colors.textMuted}; margin-bottom: 10px;"> Target: ${target}% </div> <div style="font-size: 10px; color: ${diffInfo.color}; font-weight: bold;"> ${diffInfo.text} </div> `; } else { additionalContent = ` <div style="font-size: 10px; color: ${colors.textMuted};"> Target: ${target}% </div> `; } return ` <div style=" background: ${colors.statBoxBg}; padding: 10px; border-radius: 3px; text-align: center; border-left: 3px solid ${color}; border-top: 1px solid ${colors.statBoxBorder}; border-right: 1px solid ${colors.statBoxBorder}; border-bottom: 1px solid ${colors.statBoxBorder}; box-shadow: ${colors.statBoxShadow}; "> <div style="font-weight: bold; margin-bottom: 5px; color: ${colors.textPrimary};">${statLabels[index]}</div> <div style="color: ${color}; font-weight: bold; margin-bottom: 4px;"> ${current}% (${diff > 0 ? '+' : ''}${diff.toFixed(1)}) </div> ${additionalContent} </div> `; }).join(''); console.log('Stats updated' + (showStatDifferences ? ' with differences' : '')); } // Get all target input values as object function getTargetValues() { return { strength: parseFloat(document.getElementById('target-strength').value) || 0, defense: parseFloat(document.getElementById('target-defense').value) || 0, speed: parseFloat(document.getElementById('target-speed').value) || 0, dexterity: parseFloat(document.getElementById('target-dexterity').value) || 0 }; } // Update total percentage in config function updateTotalPercentage() { const targets = getTargetValues(); const total = targets.strength + targets.defense + targets.speed + targets.dexterity; const totalSpan = document.getElementById('total-percentage'); const colors = getThemeColors(); if (totalSpan) { totalSpan.textContent = 'Total: ' + total.toFixed(1) + '%'; totalSpan.style.color = Math.abs(total - 100) <= 0.1 ? colors.success : colors.danger; } const saveBtn = document.getElementById('save-targets'); if (saveBtn) { saveBtn.disabled = Math.abs(total - 100) > 0.1; saveBtn.style.opacity = saveBtn.disabled ? '0.5' : '1'; } } // SPEED OPTIMIZED INITIALIZATION function initOptimized() { console.log('=== INITIALIZING PDA-FIXED SPEED OPTIMIZED SCRIPT ==='); console.log('Platform:', isTornPDA() ? 'PDA' : 'Browser'); console.log('URL:', window.location.href); if (isTornPDA()) { console.log('Using PDA-compatible (slower) initialization method'); waitForElement('.page-head-delimiter', function(delimiter) { console.log('Found page delimiter, creating panel'); const displayPanel = createDisplayPanel(); const configPanel = createConfigPanel(); displayPanel.appendChild(configPanel); delimiter.parentNode.insertBefore(displayPanel, delimiter.nextSibling); console.log('Panel inserted, waiting for stats to load'); setTimeout(function() { applySavedCollapseState(); initAdvancedThemeMonitoring(); initStatsMonitoring(); }, 2000); setupEventListeners(); }, 15000); } else { console.log('Using browser-optimized (faster) initialization method'); waitForElement('li[class*="strength___"]', function(strengthEl) { console.log('Found strength element, checking for delimiter'); const delimiter = document.querySelector('.page-head-delimiter'); if (!delimiter) { console.log('Delimiter not found, retrying in 100ms'); setTimeout(initOptimized, 100); return; } console.log('Both stats and delimiter found, creating panel'); const displayPanel = createDisplayPanel(); const configPanel = createConfigPanel(); displayPanel.appendChild(configPanel); delimiter.parentNode.insertBefore(displayPanel, delimiter.nextSibling); console.log('Panel inserted, updating stats'); applySavedCollapseState(); initAdvancedThemeMonitoring(); initStatsMonitoring(); setupEventListeners(); }, 5000); } } // Setup event listeners function setupEventListeners() { // Help button document.getElementById('gym-help-btn').addEventListener('click', () => { const tooltip = document.getElementById('gym-help-tooltip'); tooltip.style.display = tooltip.style.display === 'none' ? 'block' : 'none'; const configPanel = document.getElementById('gym-config-panel'); if (tooltip.style.display === 'block') configPanel.style.display = 'none'; }); // Header click and collapse button document.getElementById('gym-header-clickable').addEventListener('click', toggleCollapsed); document.getElementById('gym-collapse-btn').addEventListener('click', toggleCollapsed); // Config button document.getElementById('gym-config-btn').addEventListener('click', () => { if (isCollapsed()) { toggleCollapsed(); setTimeout(() => { const panel = document.getElementById('gym-config-panel'); if (panel) panel.style.display = 'block'; }, 100); } else { const panel = document.getElementById('gym-config-panel'); if (panel) { panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; const tooltip = document.getElementById('gym-help-tooltip'); if (panel.style.display === 'block') tooltip.style.display = 'none'; } } }); // Config panel buttons document.getElementById('cancel-config').addEventListener('click', () => { document.getElementById('gym-config-panel').style.display = 'none'; }); document.getElementById('save-targets').addEventListener('click', () => { const targets = getTargetValues(); const showStatDifferences = document.getElementById('show-stat-differences').checked; saveTargets(targets); saveShowStatDifferences(showStatDifferences); updateStats(); document.getElementById('gym-config-panel').style.display = 'none'; }); // Add input listeners for real-time validation ['target-strength', 'target-defense', 'target-speed', 'target-dexterity'].forEach(id => { const element = document.getElementById(id); if (element) element.addEventListener('input', updateTotalPercentage); }); // Initial total percentage update updateTotalPercentage(); // Cleanup on page unload if (!isTornPDA()) { window.addEventListener('beforeunload', cleanupAllMonitoring); window.addEventListener('unload', cleanupAllMonitoring); } // Close panels when clicking outside document.addEventListener('click', (e) => { const helpBtn = document.getElementById('gym-help-btn'); const tooltip = document.getElementById('gym-help-tooltip'); const configBtn = document.getElementById('gym-config-btn'); const configPanel = document.getElementById('gym-config-panel'); if (!helpBtn.contains(e.target) && !tooltip.contains(e.target)) { tooltip.style.display = 'none'; } if (!configBtn.contains(e.target) && !configPanel.contains(e.target)) { configPanel.style.display = 'none'; } }); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initOptimized); } else { initOptimized(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址