Dead Frontier Tooltip Details

Add information from the wiki in the weapons infobox with bonus calculations

// ==UserScript==
// @name         Dead Frontier Tooltip Details
// @author       ils94
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Add information from the wiki in the weapons infobox with bonus calculations
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=24
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=28*
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=35
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=50
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=59
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=82*
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=84
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/DF3D/DF3D_InventoryPage.php?page=31*
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=32*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let dpsData = {};
    let userStats = {
        totalDamage: 0,
        attackSpeed: 0,
        meleeBonuses: 0,
        chainsawBonuses: 0,
        pistolBonuses: 0,
        rifleBonuses: 0,
        shotgunBonuses: 0,
        smgBonuses: 0,
        machineGunBonuses: 0,
        explosiveBonuses: 0
    };
    let isShiftPressed = false;
    let presets = JSON.parse(localStorage.getItem('deadFrontierPresets') || '{}');
    let lastSelectedPreset = localStorage.getItem('deadFrontierLastPreset') || '';
    let tooltipVisibility = JSON.parse(localStorage.getItem('deadFrontierTooltipVisibility') || '{}');
    const defaultVisibility = {
        avgDPS: true,
        avgDPSTheoretical: true,
        criticalDPS: true,
        criticalDPSTheoretical: true,
        damagePerHit: true,
        criticalDamagePerHit: true,
        hitsPerSecond: true,
        hitsPerSecondTheoretical: true
    };
    tooltipVisibility = {
        ...defaultVisibility,
        ...tooltipVisibility
    };
    let isTooltipContainerVisible = localStorage.getItem('deadFrontierTooltipContainerVisible') !== 'false';

    document.addEventListener('keydown', (e) => {
        if (e.key === 'Shift') {
            isShiftPressed = true;
        }
    });

    document.addEventListener('keyup', (e) => {
        if (e.key === 'Shift') {
            isShiftPressed = false;
        }
    });

    function loadSavedStats() {
        const saved = localStorage.getItem('deadFrontierUserStats');
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                Object.assign(userStats, parsed);
            } catch (e) {
                console.error('[DPS] Failed to parse saved stats:', e);
            }
        }
        if (lastSelectedPreset && presets[lastSelectedPreset]) {
            loadPreset(lastSelectedPreset);
        }
    }

    function savePreset(name, stats, overwrite = false) {
        if (presets[name] && !overwrite) {
            const confirmOverwrite = confirm(`Preset "${name}" already exists. Do you want to overwrite it?`);
            if (!confirmOverwrite) return false;
        }
        presets[name] = {
            ...stats
        };
        localStorage.setItem('deadFrontierPresets', JSON.stringify(presets));
        updateAllPresetDropdowns();
        return true;
    }

    function loadPreset(name, inputElements = []) {
        if (!presets[name]) return;
        Object.assign(userStats, presets[name]);
        inputElements.forEach(({
            key,
            inputEl
        }) => {
            inputEl.value = userStats[key] || 0;
        });
        localStorage.setItem('deadFrontierUserStats', JSON.stringify(userStats));
        localStorage.setItem('deadFrontierLastPreset', name);
        lastSelectedPreset = name;
        updateAllPresetDropdowns();
        const staticInfoboxPages = ['page=25', 'page=28', 'page=50', 'page=59', 'page=84', 'page=31', 'page=32'];
        if (staticInfoboxPages.some(page => window.location.href.includes(page))) {
            injectDPSIntoStaticBoxes();
        }
    }

    function deletePreset(name) {
        if (!presets[name]) return;
        if (confirm(`Are you sure you want to delete preset "${name}"?`)) {
            delete presets[name];
            localStorage.setItem('deadFrontierPresets', JSON.stringify(presets));
            if (lastSelectedPreset === name) {
                lastSelectedPreset = '';
                localStorage.removeItem('deadFrontierLastPreset');
            }
            updateAllPresetDropdowns();
        }
    }

    function updatePresetDropdown(dropdown) {
        dropdown.innerHTML = '<option value="">Select Preset</option>';
        Object.keys(presets).forEach(name => {
            const option = document.createElement('option');
            option.value = name;
            option.textContent = name;
            dropdown.appendChild(option);
        });
        if (lastSelectedPreset && presets[lastSelectedPreset]) {
            dropdown.value = lastSelectedPreset;
        }
    }

    function updateAllPresetDropdowns() {
        const dropdowns = document.querySelectorAll('.presetDropdown');
        dropdowns.forEach(dropdown => updatePresetDropdown(dropdown));
    }

    function createPresetDropdown() {
        if (window.location.href === 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
            return;
        }

        const presetContainer = document.createElement('div');
        presetContainer.style.position = 'fixed';
        presetContainer.style.top = '50px';
        presetContainer.style.left = '5px';
        presetContainer.style.backgroundColor = '#1a1a1a';
        presetContainer.style.padding = '10px';
        presetContainer.style.border = '2px solid #00FF00';
        presetContainer.style.borderRadius = '8px';
        presetContainer.style.zIndex = '1000';
        presetContainer.style.color = '#00FF00';
        presetContainer.style.fontFamily = 'Arial, sans-serif';
        presetContainer.style.fontSize = '14px';
        presetContainer.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
        presetContainer.style.display = 'flex';
        presetContainer.style.flexDirection = 'column';
        presetContainer.style.gap = '8px';

        const dropdownContainer = document.createElement('div');
        dropdownContainer.style.display = 'flex';
        dropdownContainer.style.alignItems = 'center';
        dropdownContainer.style.gap = '8px';

        const presetLabel = document.createElement('label');
        presetLabel.textContent = 'Presets:';
        presetLabel.style.fontSize = '12px';
        dropdownContainer.appendChild(presetLabel);

        const presetDropdown = document.createElement('select');
        presetDropdown.className = 'presetDropdown';
        presetDropdown.style.width = '120px';
        presetDropdown.style.backgroundColor = '#2a2a2a';
        presetDropdown.style.color = '#00FF00';
        presetDropdown.style.border = '1px solid #00FF00';
        presetDropdown.style.borderRadius = '4px';
        presetDropdown.style.padding = '4px';
        presetDropdown.style.fontSize = '12px';
        updatePresetDropdown(presetDropdown);
        dropdownContainer.appendChild(presetDropdown);

        presetContainer.appendChild(dropdownContainer);

        const toggleButton = document.createElement('button');
        toggleButton.textContent = isTooltipContainerVisible ? 'Hide Options' : 'Show Options';
        toggleButton.style.backgroundColor = '#00FF00';
        toggleButton.style.color = '#000';
        toggleButton.style.border = 'none';
        toggleButton.style.borderRadius = '5px';
        toggleButton.style.padding = '10px 10px';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.fontSize = '12px';
        toggleButton.style.fontWeight = 'bold';
        toggleButton.style.minHeight = '25px';
        toggleButton.style.transition = 'background-color 0.2s';
        toggleButton.style.alignCenter = 'flex-start';

        toggleButton.addEventListener('mouseover', () => {
            toggleButton.style.backgroundColor = '#00CC00';
        });
        toggleButton.addEventListener('mouseout', () => {
            toggleButton.style.backgroundColor = '#00FF00';
        });

        toggleButton.addEventListener('click', () => {
            isTooltipContainerVisible = !isTooltipContainerVisible;
            const tooltipContainer = document.querySelector('.tooltipVisibilityContainer');
            if (tooltipContainer) {
                tooltipContainer.style.display = isTooltipContainerVisible ? 'block' : 'none';
            }
            toggleButton.textContent = isTooltipContainerVisible ? 'Hide Options' : 'Show Options';
            localStorage.setItem('deadFrontierTooltipContainerVisible', isTooltipContainerVisible);
        });

        presetContainer.appendChild(toggleButton);

        presetDropdown.addEventListener('change', () => {
            const selected = presetDropdown.value;
            if (selected) {
                loadPreset(selected);
            }
        });

        document.body.appendChild(presetContainer);
    }

    function createTooltipVisibilityContainer() {
        const container = document.createElement('div');
        container.className = 'tooltipVisibilityContainer';
        container.style.position = 'fixed';
        container.style.top = '140px';
        container.style.left = '5px';
        container.style.backgroundColor = '#1a1a1a';
        container.style.padding = '15px';
        container.style.border = '2px solid #00FF00';
        container.style.borderRadius = '8px';
        container.style.zIndex = '1000';
        container.style.color = '#00FF00';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.fontSize = '14px';
        container.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
        container.style.width = '250px';

        if (window.location.href === 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
            container.style.display = 'block';
        } else {
            container.style.display = isTooltipContainerVisible ? 'block' : 'none';
        }

        const title = document.createElement('div');
        title.textContent = 'Tooltip Display Options';
        title.style.fontSize = '16px';
        title.style.fontWeight = 'bold';
        title.style.textAlign = 'center';
        title.style.marginBottom = '12px';
        container.appendChild(title);

        const checkboxes = [{
                label: 'Avg. DPS',
                key: 'avgDPS'
            },
            {
                label: 'Avg. DPS Theoretical',
                key: 'avgDPSTheoretical'
            },
            {
                label: 'Critical/AoE DPS',
                key: 'criticalDPS'
            },
            {
                label: 'Critical/AoE DPS Theoretical',
                key: 'criticalDPSTheoretical'
            },
            {
                label: 'Damage per Hit',
                key: 'damagePerHit'
            },
            {
                label: 'Critical/AoE Damage per Hit',
                key: 'criticalDamagePerHit'
            },
            {
                label: 'Hit(s) per Second',
                key: 'hitsPerSecond'
            },
            {
                label: 'Hit(s) per Second Theoretical',
                key: 'hitsPerSecondTheoretical'
            }
        ];

        checkboxes.forEach(({
            label,
            key
        }) => {
            const labelEl = document.createElement('label');
            labelEl.style.display = 'flex';
            labelEl.style.alignItems = 'center';
            labelEl.style.marginBottom = '8px';
            labelEl.style.fontSize = '12px';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = tooltipVisibility[key];
            checkbox.style.marginRight = '8px';
            checkbox.style.cursor = 'pointer';

            checkbox.addEventListener('change', () => {
                tooltipVisibility[key] = checkbox.checked;
                localStorage.setItem('deadFrontierTooltipVisibility', JSON.stringify(tooltipVisibility));
                const staticInfoboxPages = ['page=25', 'page=28', 'page=50', 'page=59', 'page=84', 'page=31', 'page=32'];
                if (staticInfoboxPages.some(page => window.location.href.includes(page))) {
                    injectDPSIntoStaticBoxes();
                }
            });

            labelEl.appendChild(checkbox);
            labelEl.appendChild(document.createTextNode(label));
            container.appendChild(labelEl);
        });

        document.body.appendChild(container);
    }

    function createInputContainer() {
        if (window.location.href !== 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
            return;
        }

        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.bottom = '10px';
        container.style.left = '10px';
        container.style.backgroundColor = '#1a1a1a';
        container.style.padding = '15px';
        container.style.border = '2px solid #00FF00';
        container.style.borderRadius = '8px';
        container.style.zIndex = '1000';
        container.style.color = '#00FF00';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.fontSize = '14px';
        container.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
        container.style.width = '250px';

        const title = document.createElement('div');
        title.textContent = 'Implant and Weapons Bonuses';
        title.style.fontSize = '16px';
        title.style.fontWeight = 'bold';
        title.style.textAlign = 'center';
        title.style.marginBottom = '12px';
        container.appendChild(title);

        const presetContainer = document.createElement('div');
        presetContainer.style.display = 'flex';
        presetContainer.style.alignItems = 'center';
        presetContainer.style.marginBottom = '12px';

        const presetLabel = document.createElement('label');
        presetLabel.textContent = 'Presets:';
        presetLabel.style.fontSize = '12px';
        presetContainer.appendChild(presetLabel);

        const presetDropdown = document.createElement('select');
        presetDropdown.className = 'presetDropdown';
        presetDropdown.style.marginLeft = 'auto';
        presetDropdown.style.width = '120px';
        presetDropdown.style.backgroundColor = '#2a2a2a';
        presetDropdown.style.color = '#00FF00';
        presetDropdown.style.border = '1px solid #00FF00';
        presetDropdown.style.borderRadius = '4px';
        presetDropdown.style.padding = '4px';
        presetDropdown.style.fontSize = '12px';
        updatePresetDropdown(presetDropdown);
        presetContainer.appendChild(presetDropdown);

        const deletePresetButton = document.createElement('button');
        deletePresetButton.textContent = 'X';
        deletePresetButton.style.marginLeft = '5px';
        deletePresetButton.style.backgroundColor = '#FF3333';
        deletePresetButton.style.color = '#000';
        deletePresetButton.style.border = 'none';
        deletePresetButton.style.borderRadius = '3px';
        deletePresetButton.style.padding = '5px 5px';
        deletePresetButton.style.cursor = 'pointer';
        deletePresetButton.style.fontSize = '20px';
        deletePresetButton.style.minWidth = '25px';
        deletePresetButton.style.minHeight = '12px';
        deletePresetButton.addEventListener('click', () => {
            const selected = presetDropdown.value;
            if (selected) {
                deletePreset(selected);
            }
        });
        presetContainer.appendChild(deletePresetButton);

        container.appendChild(presetContainer);

        const inputs = [{
                label: 'Total Inflicted Damage:',
                key: 'totalDamage'
            },
            {
                label: 'Total Attack Speed:',
                key: 'attackSpeed'
            },
            {
                label: 'Melee Bonuses:',
                key: 'meleeBonuses'
            },
            {
                label: 'Chainsaw Bonuses:',
                key: 'chainsawBonuses'
            },
            {
                label: 'Pistol Bonuses:',
                key: 'pistolBonuses'
            },
            {
                label: 'Rifle Bonuses:',
                key: 'rifleBonuses'
            },
            {
                label: 'Shotgun Bonuses:',
                key: 'shotgunBonuses'
            },
            {
                label: 'SMG Bonuses:',
                key: 'smgBonuses'
            },
            {
                label: 'Machine Gun Bonuses:',
                key: 'machineGunBonuses'
            },
            {
                label: 'Explosive Bonuses:',
                key: 'explosiveBonuses'
            }
        ];

        const inputElements = [];
        inputs.forEach(input => {
            const label = document.createElement('label');
            label.textContent = input.label;
            label.style.display = 'flex';
            label.style.alignItems = 'center';
            label.style.marginBottom = '8px';
            label.style.fontSize = '12px';

            const inputEl = document.createElement('input');
            inputEl.type = 'text';
            inputEl.inputMode = 'decimal';
            inputEl.value = userStats[input.key];
            inputEl.style.width = '80px';
            inputEl.style.marginLeft = 'auto';
            inputEl.style.backgroundColor = '#2a2a2a';
            inputEl.style.color = '#00FF00';
            inputEl.style.border = '1px solid #00FF00';
            inputEl.style.borderRadius = '4px';
            inputEl.style.padding = '4px';
            inputEl.style.fontSize = '12px';
            inputEl.style.outline = 'none';

            inputEl.addEventListener('input', () => {
                let cleaned = inputEl.value.replace(/[^0-9.]/g, '');
                const parts = cleaned.split('.');
                if (parts.length > 2) {
                    cleaned = parts[0] + '.' + parts.slice(1).join('');
                }
                inputEl.value = cleaned;
                userStats[input.key] = parseFloat(inputEl.value) || 0;
            });

            label.appendChild(inputEl);
            container.appendChild(label);
            inputElements.push({
                key: input.key,
                inputEl
            });
        });

        presetDropdown.addEventListener('change', () => {
            const selected = presetDropdown.value;
            if (selected) {
                loadPreset(selected, inputElements);
            }
        });

        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save';
        saveButton.style.display = 'block';
        saveButton.style.margin = '10px auto 0';
        saveButton.style.backgroundColor = '#00FF00';
        saveButton.style.color = '#000';
        saveButton.style.border = 'none';
        saveButton.style.borderRadius = '5px';
        saveButton.style.padding = '15px 15px';
        saveButton.style.cursor = 'pointer';
        saveButton.style.fontSize = '16px';
        saveButton.style.fontWeight = 'bold';
        saveButton.style.transition = 'background-color 0.2s, transform 0.1s';
        saveButton.style.minWidth = '50px';
        saveButton.style.minHeight = '25px';

        saveButton.addEventListener('mouseover', () => {
            saveButton.style.backgroundColor = '#00CC00';
        });
        saveButton.addEventListener('mouseout', () => {
            saveButton.style.backgroundColor = '#00FF00';
        });

        saveButton.addEventListener('click', () => {
            const presetName = prompt('Enter a name for the preset:');
            if (presetName && presetName.trim()) {
                if (savePreset(presetName.trim(), userStats)) {
                    localStorage.setItem('deadFrontierUserStats', JSON.stringify(userStats));
                    localStorage.setItem('deadFrontierLastPreset', presetName.trim());
                    lastSelectedPreset = presetName.trim();
                    injectDPSIntoStaticBoxes();
                    console.log('[DPS] User stats saved:', userStats);
                }
            }
        });

        container.appendChild(saveButton);
        document.body.appendChild(container);
    }

    function loadDPS() {
        if (typeof window.weaponData === 'undefined') {
            console.error('[DPS] External weapon data not loaded');
            return;
        }

        window.weaponData.weapons.forEach(weapon => {
            const key = weapon.name.toLowerCase();
            dpsData[key] = {
                name: weapon.name,
                category: weapon.category,
                dps: weapon.stats.DPS || {},
                dph: weapon.stats.DPH || {},
                hps: weapon.stats.HPS || {}
            };
        });

        console.log('[DPS] Loaded', Object.keys(dpsData).length, 'entries from external JSON');
        loadSavedStats();
        createPresetDropdown();
        createTooltipVisibilityContainer();
        createInputContainer();
        startWatcher();
        injectDPSIntoStaticBoxes();
    }

    function parseSumExpression(expr) {
        if (!expr || typeof expr !== 'string') return {
            terms: [],
            total: null,
            base: null,
            multiplier: null,
            innerMultiplier: null
        };

        // Remove whitespace and normalize brackets
        expr = expr.replace(/\s+/g, '').replace(/[\[\]]/g, '');

        // Pattern 1: [a + b + c] x m = t (e.g., "[42 + 12 + 6] x 5 = 300")
        let match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)\+(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const sumTerms = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
            const multiplier = parseFloat(match[4]);
            const total = parseFloat(match[5]);
            return {
                terms: sumTerms,
                total,
                base: null,
                multiplier,
                innerMultiplier: null
            };
        }

        // Pattern 2: [a + b] x m = t (e.g., "[14.4 + 3.6] x 16 = 288")
        match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const sumTerms = [parseFloat(match[1]), parseFloat(match[2])];
            const multiplier = parseFloat(match[3]);
            const total = parseFloat(match[4]);
            return {
                terms: sumTerms,
                total,
                base: null,
                multiplier,
                innerMultiplier: null
            };
        }

        // Pattern 3: [a x b] x m = t (e.g., "[12 x 9] x 2 = 216")
        match = expr.match(/^(\d+\.?\d*)x(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const base = parseFloat(match[1]);
            const innerMultiplier = parseFloat(match[2]);
            const multiplier = parseFloat(match[3]);
            const total = parseFloat(match[4]);
            return {
                terms: [base],
                total,
                base,
                multiplier,
                innerMultiplier
            };
        }

        // Pattern 4: a x m = t (e.g., "10 x 4 = 40")
        match = expr.match(/^(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const base = parseFloat(match[1]);
            const multiplier = parseFloat(match[2]);
            const total = parseFloat(match[3]);
            return {
                terms: [base],
                total,
                base,
                multiplier,
                innerMultiplier: null
            };
        }

        // Pattern 5: a + b + c = t (e.g., "10 + 20 + 30 = 60")
        match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)\+(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
            const total = parseFloat(match[4]);
            return {
                terms,
                total,
                base: null,
                multiplier: null,
                innerMultiplier: null
            };
        }

        // Pattern 6: a + b = t (e.g., "10 + 20 = 30")
        match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)=(\d+\.?\d*)$/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2])];
            const total = parseFloat(match[3]);
            return {
                terms,
                total,
                base: null,
                multiplier: null,
                innerMultiplier: null
            };
        }

        // Pattern 7: a + b (e.g., "10 + 20")
        match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)$/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2])];
            const total = terms.reduce((sum, term) => sum + term, 0);
            return {
                terms,
                total,
                base: null,
                multiplier: null,
                innerMultiplier: null
            };
        }

        // Pattern 8: Single number (e.g., "50")
        const singleNumber = parseFloat(expr);
        if (!isNaN(singleNumber)) {
            return {
                terms: [singleNumber],
                total: singleNumber,
                base: null,
                multiplier: null,
                innerMultiplier: null
            };
        }

        return {
            terms: [],
            total: null,
            base: null,
            multiplier: null,
            innerMultiplier: null
        };
    }

    function calculateBonuses(entry) {
        const category = entry.category.toLowerCase();
        let masteryBonus = 0;

        if (category.includes('melee')) masteryBonus = userStats.meleeBonuses;
        if (category.includes('chainsaw')) masteryBonus = userStats.chainsawBonuses;
        if (category.includes('pistol')) masteryBonus = userStats.pistolBonuses;
        if (category.includes('rifle')) masteryBonus = userStats.rifleBonuses;
        if (category.includes('shotgun')) masteryBonus = userStats.shotgunBonuses;
        if (category.includes('smg')) masteryBonus = userStats.smgBonuses;
        if (category.includes('machine gun')) masteryBonus = userStats.machineGunBonuses;
        if (category.includes('grenade launchers') || category.includes('flamethrowers')) masteryBonus = userStats.explosiveBonuses;

        const damageMultiplier = 1 + (userStats.totalDamage + masteryBonus) / 100;
        const speedMultiplier = 1 + userStats.attackSpeed / 100;

        const dphTotalParsed = parseSumExpression(entry.dph.total);
        const dphCriticalParsed = parseSumExpression(entry.dph.critical);

        let dphTotal;
        let dphTotalTerms = [];

        // Handle complex patterns
        if (dphTotalParsed.multiplier !== null) {
            if (dphTotalParsed.innerMultiplier !== null) {
                // Pattern: [base x innerMultiplier] x multiplier = total
                const adjustedBase = (dphTotalParsed.terms[0] * damageMultiplier).toFixed(2);
                dphTotal = (dphTotalParsed.terms[0] * damageMultiplier * dphTotalParsed.innerMultiplier * dphTotalParsed.multiplier).toFixed(2);
                dphTotalTerms = [adjustedBase];
            } else {
                // Pattern: [a + b + c] x multiplier or [a + b] x multiplier = total
                dphTotalTerms = dphTotalParsed.terms.map(term => (term * damageMultiplier).toFixed(2));
                dphTotal = (dphTotalParsed.terms.reduce((sum, term) => sum + term, 0) * damageMultiplier * dphTotalParsed.multiplier).toFixed(2);
            }
        } else if (dphTotalParsed.base !== null && dphTotalParsed.multiplier !== null) {
            // Pattern: base x multiplier = total
            const adjustedBase = (dphTotalParsed.base * damageMultiplier).toFixed(2);
            dphTotal = (dphTotalParsed.base * damageMultiplier * dphTotalParsed.multiplier).toFixed(2);
            dphTotalTerms = [adjustedBase];
        } else {
            // Simple sum or single value
            dphTotalTerms = dphTotalParsed.terms.map(term => (term * damageMultiplier).toFixed(2));
            dphTotal = dphTotalParsed.total !== null ? (dphTotalParsed.total * damageMultiplier).toFixed(2) : 'N/A';
        }

        return {
            dps: {
                real: entry.dps.real ? (entry.dps.real * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
                theoretical: entry.dps.theoretical ? (entry.dps.theoretical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
                critical: entry.dps.critical ? (entry.dps.critical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
                theoretical_critical: entry.dps.theoretical_critical ? (entry.dps.theoretical_critical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A'
            },
            dph: {
                total: dphTotal,
                critical: dphCriticalParsed.total !== null ? (dphCriticalParsed.total * damageMultiplier).toFixed(2) : 'N/A',
                totalTerms: dphTotalTerms,
                criticalTerms: dphCriticalParsed.terms.map(term => (term * damageMultiplier).toFixed(2)),
                base: dphTotalParsed.base,
                multiplier: dphTotalParsed.multiplier,
                innerMultiplier: dphTotalParsed.innerMultiplier
            },
            hps: {
                real: entry.hps.real ? (entry.hps.real * speedMultiplier).toFixed(2) : 'N/A',
                theoretical: entry.hps.theoretical ? (entry.hps.theoretical * speedMultiplier).toFixed(2) : 'N/A'
            }
        };
    }

    function generateStatsHTML(entry) {
        const bonuses = calculateBonuses(entry);

        const dphTotalParsed = parseSumExpression(entry.dph.total);
        let dphDisplay;
        let dphBonusDisplay;

        if (dphTotalParsed.innerMultiplier !== null && dphTotalParsed.multiplier !== null) {
            // Pattern: [base x innerMultiplier] x multiplier = total
            dphDisplay = `[${dphTotalParsed.terms[0]} x ${dphTotalParsed.innerMultiplier}] x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
            dphBonusDisplay = `[${bonuses.dph.totalTerms[0]} x ${dphTotalParsed.innerMultiplier}] x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
        } else if (dphTotalParsed.terms.length >= 2 && dphTotalParsed.multiplier !== null) {
            // Pattern: [a + b + c] x multiplier or [a + b] x multiplier = total
            dphDisplay = `[${dphTotalParsed.terms.join(' + ')}] x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
            dphBonusDisplay = `[${bonuses.dph.totalTerms.join(' + ')}] x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
        } else if (dphTotalParsed.base !== null && dphTotalParsed.multiplier !== null) {
            // Pattern: base x multiplier = total
            dphDisplay = `${dphTotalParsed.base} x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
            dphBonusDisplay = `${bonuses.dph.totalTerms[0]} x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
        } else {
            // Simple sum or single value
            dphDisplay = dphTotalParsed.terms.length > 1 ? dphTotalParsed.terms.join(' + ') + (dphTotalParsed.total ? ` = ${dphTotalParsed.total}` : '') : (dphTotalParsed.total || 'N/A');
            dphBonusDisplay = dphTotalParsed.terms.length > 1 ? `${bonuses.dph.totalTerms.join(' + ')} = ${bonuses.dph.total}` : bonuses.dph.total;
        }

        const dphCriticalParsed = parseSumExpression(entry.dph.critical);
        const dphCriticalDisplay = dphCriticalParsed.terms.length > 1 ? dphCriticalParsed.terms.join(' + ') + (dphCriticalParsed.total ? ` = ${dphCriticalParsed.total}` : '') : (entry.dph.critical || 'N/A');
        const dphCriticalBonusDisplay = dphCriticalParsed.terms.length > 1 ? `${bonuses.dph.criticalTerms.join(' + ')} = ${bonuses.dph.critical}` : bonuses.dph.critical;

        const isExplosive = entry.category === 'Grenade Launchers' || entry.category === 'Flamethrowers';
        const isGrenadeLauncher = entry.category === 'Grenade Launchers';
        const dpsCriticalLabel = isExplosive ? 'Avg. DPS AoE' : 'Avg. DPS Critical';
        const dpsCriticalTheoreticalLabel = isExplosive ? 'Avg. DPS AoE Theoretical' : 'Avg. DPS Critical Theoretical';
        const dphCriticalLabel = isExplosive ? 'Damage per AoE' : 'Damage per Hit Critical';

        const baseStats = [];
        const bonusStats = [];

        if (tooltipVisibility.avgDPS) {
            baseStats.push(`Avg. DPS: ${entry.dps.real || 'N/A'}`);
            bonusStats.push(`Avg. DPS: ${bonuses.dps.real}`);
        }
        if (tooltipVisibility.avgDPSTheoretical) {
            baseStats.push(`Avg. DPS Theoretical: ${entry.dps.theoretical || 'N/A'}`);
            bonusStats.push(`Avg. DPS Theoretical: ${bonuses.dps.theoretical}`);
        }
        if (tooltipVisibility.criticalDPS) {
            baseStats.push(`${dpsCriticalLabel}: ${entry.dps.critical || 'N/A'}`);
            bonusStats.push(`${dpsCriticalLabel}: ${bonuses.dps.critical}`);
        }
        if (tooltipVisibility.criticalDPSTheoretical) {
            baseStats.push(`${dpsCriticalTheoreticalLabel}: ${entry.dps.theoretical_critical || 'N/A'}`);
            bonusStats.push(`${dpsCriticalTheoreticalLabel}: ${bonuses.dps.theoretical_critical}`);
        }

        if (isGrenadeLauncher && tooltipVisibility.damagePerHit) {
            const targetMultipliers = [{
                    count: 1,
                    multiplier: 3.5
                },
                {
                    count: 2,
                    multiplier: 2.0
                },
                {
                    count: 3,
                    multiplier: 1.5
                },
                {
                    count: 4,
                    multiplier: 1.25
                },
                {
                    count: 5,
                    multiplier: 1.0
                }
            ];
            const baseDamages = targetMultipliers.map(({
                count,
                multiplier
            }) => {
                const baseDamage = (dphTotalParsed.total * multiplier).toFixed(2);
                return `${count}: ${baseDamage}`;
            }).join('<br>');
            const bonusDamages = targetMultipliers.map(({
                count,
                multiplier
            }) => {
                const bonusDamage = (bonuses.dph.total * multiplier).toFixed(2);
                return `${count}: ${bonusDamage}`;
            }).join('<br>');
            baseStats.push(`Damage per Target:<br>${baseDamages}`);
            bonusStats.push(`Damage per Target:<br>${bonusDamages}`);
        } else {
            if (tooltipVisibility.damagePerHit) {
                baseStats.push(`Damage per Hit: ${dphDisplay}`);
                bonusStats.push(`Damage per Hit: ${dphBonusDisplay}`);
            }
            if (tooltipVisibility.criticalDamagePerHit && !isGrenadeLauncher) {
                baseStats.push(`${dphCriticalLabel}: ${dphCriticalDisplay}`);
                bonusStats.push(`${dphCriticalLabel}: ${dphCriticalBonusDisplay}`);
            }
        }

        if (tooltipVisibility.hitsPerSecond) {
            baseStats.push(`Hit(s) per Second: ${entry.hps.real || 'N/A'}`);
            bonusStats.push(`Hit(s) per Second: ${bonuses.hps.real}`);
        }
        if (tooltipVisibility.hitsPerSecondTheoretical) {
            baseStats.push(`Hit(s) per Second Theoretical: ${entry.hps.theoretical || 'N/A'}`);
            bonusStats.push(`Hit(s) per Second Theoretical: ${bonuses.hps.theoretical}`);
        }

        const statsHTML = [];
        if (baseStats.length > 0) {
            statsHTML.push('<strong>Base Stats:</strong>', ...baseStats);
        }
        if (bonusStats.length > 0) {
            statsHTML.push('<strong>With Bonuses:</strong>', ...bonusStats);
        }

        return statsHTML.map(line => {
            if (line.startsWith('<strong>')) {
                return `<br>${line}<br>`;
            }
            return line;
        }).join('<br>');
    }

    function startWatcher() {
        let tooltipWindow = null;

        setInterval(() => {
            const box = document.getElementById('infoBox');
            if (!box || box.style.visibility === 'hidden') {
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            if (!isShiftPressed) {
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            // Extract background-image URL
            const bgImage = box.style.backgroundImage;
            if (!bgImage) {
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            // Parse the URL to get the file name (e.g., "xdusksaw.png")
            const urlMatch = bgImage.match(/url\(["']?(.+?)["']?\)/);
            if (!urlMatch || !urlMatch[1]) {
                console.log('[DPS] ✗ No valid background-image URL found');
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            // Extract file name and clean it
            const url = urlMatch[1];
            const fileName = url.split('/').pop(); // Get the last part (e.g., "xdusksaw.png")
            const weaponKey = fileName
                .replace(/\.[^/.]+$/, '') // Remove extension (e.g., ".png")
                .replace(/[^a-zA-Z0-9]/g, '') // Remove special characters
                .toLowerCase(); // Convert to lowercase

            const entry = dpsData[weaponKey];

            if (!entry) {
                console.log(`[DPS] ✗ ${weaponKey} (hover, no exact match)`);
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            if (!tooltipWindow) {
                tooltipWindow = document.createElement('div');
                tooltipWindow.className = 'dpsTooltip';
                tooltipWindow.style.position = 'absolute';
                tooltipWindow.style.backgroundColor = '#1a1a1a';
                tooltipWindow.style.border = '1px solid #00FF00';
                tooltipWindow.style.padding = '10px';
                tooltipWindow.style.color = '#00FF00';
                tooltipWindow.style.fontSize = '12px';
                tooltipWindow.style.zIndex = '1001';
                tooltipWindow.style.borderRadius = '4px';
                tooltipWindow.style.boxShadow = '0 2px 4px rgba(0, 255, 0, 0.3)';
                document.body.appendChild(tooltipWindow);
            }

            const boxRect = box.getBoundingClientRect();
            tooltipWindow.style.left = `${boxRect.right + 10}px`;
            tooltipWindow.style.top = `${boxRect.top}px`;

            tooltipWindow.innerHTML = generateStatsHTML(entry);
            console.log(`[DPS] ✔ ${entry.name} (hover, matched via ${weaponKey})`);
        }, 100);
    }

    function injectDPSIntoStaticBoxes() {
        const staticBoxes = document.querySelectorAll('.itemName');

        staticBoxes.forEach(nameEl => {
            const parent = nameEl.parentElement;
            if (!parent) return;

            const existing = parent.querySelector('.dpsInjected');
            if (existing) existing.remove();

            // Extract background-image from parent
            const bgImage = parent.style.backgroundImage;
            if (!bgImage) {
                console.log('[DPS] ✗ No background-image found for static infobox');
                return;
            }

            // Parse the URL to get the file name
            const urlMatch = bgImage.match(/url\(["']?(.+?)["']?\)/);
            if (!urlMatch || !urlMatch[1]) {
                console.log('[DPS] ✗ No valid background-image URL found for static infobox');
                return;
            }

            const url = urlMatch[1];
            const fileName = url.split('/').pop();
            const weaponKey = fileName
                .replace(/\.[^/.]+$/, '')
                .replace(/[^a-zA-Z0-9]/g, '')
                .toLowerCase();

            const entry = dpsData[weaponKey];

            if (!entry) {
                console.log(`[DPS] ✗ ${weaponKey} (static, no exact match)`);
                return;
            }

            const statsDiv = document.createElement('div');
            statsDiv.className = 'itemData dpsInjected';
            statsDiv.style.color = '#00FF00';
            statsDiv.style.fontSize = '12px';
            statsDiv.innerHTML = generateStatsHTML(entry);
            parent.appendChild(statsDiv);

            console.log(`[DPS] ✔ ${entry.name} (static, matched via ${weaponKey})`);
        });
    }

    function loadExternalScript() {
        const script = document.createElement('script');
        script.src = 'dead_frontier_weapons.js';
        script.onload = () => {
            console.log('[DPS] External JSON script loaded');
            loadDPS();
        };
        script.onerror = () => {
            console.error('[DPS] Failed to load external JSON script');
        };
        document.head.appendChild(script);
    }

    loadExternalScript();
})();