Torn Gym Ratios (PDA Compatible)

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;'}
                    ">&quest;</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;'}
                    ">&minus;</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 &plusmn;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 = '&minus;';

            // 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或关注我们的公众号极客氢云获取最新地址