Dead Frontier Tooltip Details

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

当前为 2025-05-09 提交的版本,查看 最新版本

// ==UserScript==
// @name         Dead Frontier Tooltip Details
// @author       ils94
// @namespace    http://tampermonkey.net/
// @version      1.5
// @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') || '';

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

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

    // Load saved stats from localStorage
    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);
            }
        }
        // Load last selected preset if it exists
        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();
        // Only update static infoboxes on specific pages
        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 = '60px';
        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.alignItems = 'center';

        const presetLabel = document.createElement('label');
        presetLabel.textContent = 'Presets:';
        presetLabel.style.fontSize = '12px';
        presetLabel.style.marginRight = '8px';
        presetContainer.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);
        presetContainer.appendChild(presetDropdown);

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

        document.body.appendChild(presetContainer);
    }

    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);

        // Preset Dropdown
        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();
        createInputContainer();
        startWatcher();
        injectDPSIntoStaticBoxes();
    }

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

        let match = expr.match(/([\d.]+)\s*\+\s*([\d.]+)\s*\+\s*([\d.]+)\s*=\s*([\d.]+)/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
            const total = parseFloat(match[4]);
            return {
                terms,
                total
            };
        }

        match = expr.match(/([\d.]+)\s*\+\s*([\d.]+)\s*=\s*([\d.]+)/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2])];
            const total = parseFloat(match[3]);
            return {
                terms,
                total
            };
        }

        match = expr.match(/([\d.]+)\s*\+\s*([\d.]+)/);
        if (match) {
            const terms = [parseFloat(match[1]), parseFloat(match[2])];
            const total = terms.reduce((sum, term) => sum + term, 0);
            return {
                terms,
                total
            };
        }

        const singleNumber = parseFloat(expr);
        if (!isNaN(singleNumber)) {
            return {
                terms: [singleNumber],
                total: singleNumber
            };
        }

        return {
            terms: [],
            total: 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);

        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: dphTotalParsed.total !== null ? (dphTotalParsed.total * damageMultiplier).toFixed(2) : 'N/A',
                critical: dphCriticalParsed.total !== null ? (dphCriticalParsed.total * damageMultiplier).toFixed(2) : 'N/A',
                totalTerms: dphTotalParsed.terms.map(term => (term * damageMultiplier).toFixed(2)),
                criticalTerms: dphCriticalParsed.terms.map(term => (term * damageMultiplier).toFixed(2))
            },
            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);
        const dphDisplay = entry.dph.base && entry.dph.multiplier ?
            `${entry.dph.base} x ${entry.dph.multiplier} = ${entry.dph.total}` :
            (dphTotalParsed.terms.length > 1 ? entry.dph.total : (entry.dph.total || 'N/A'));

        const dphBonusDisplay = entry.dph.base && entry.dph.multiplier ?
            `${(entry.dph.base * (1 + (userStats.totalDamage + (userStats[entry.category.toLowerCase().replace(' ', '') + 'Bonuses'] || 0)) / 100)).toFixed(2)} x ${entry.dph.multiplier} = ${bonuses.dph.total}` :
            (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 ? entry.dph.critical : (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 dpsCriticalLabel = isExplosive ? 'Avg. DPS AoE' : 'Avg. DPS Critical';
        const dpsCriticalTheoreticalLabel = isExplosive ? 'Avg. DPS AoE Theoretical' : 'Avg. DPS Critical Theoretical';
        const dphCriticalLabel = isExplosive ? 'Damage per Hit AoE' : 'Damage per Hit Critical';

        let statsHTML = `
            <strong>Base Stats:</strong><br>
            <br>Avg. DPS: ${entry.dps.real || 'N/A'}<br>
            Avg. DPS Theoretical: ${entry.dps.theoretical || 'N/A'}<br>
            ${dpsCriticalLabel}: ${entry.dps.critical || 'N/A'}<br>
            ${dpsCriticalTheoreticalLabel}: ${entry.dps.theoretical_critical || 'N/A'}<br>
            Damage per Hit: ${dphDisplay}<br>
            ${dphCriticalLabel}: ${dphCriticalDisplay}<br>
            Hit(s) per Second: ${entry.hps.real || 'N/A'}<br>
            Hit(s) per Second Theoretical: ${entry.hps.theoretical || 'N/A'}<br>
            <br><strong>With Bonuses:</strong><br>
            <br>Avg. DPS: ${bonuses.dps.real}<br>
            Avg. DPS Theoretical: ${bonuses.dps.theoretical}<br>
            ${dpsCriticalLabel}: ${bonuses.dps.critical}<br>
            ${dpsCriticalTheoreticalLabel}: ${bonuses.dps.theoretical_critical}<br>
            Damage per Hit: ${dphBonusDisplay}<br>
            ${dphCriticalLabel}: ${dphCriticalBonusDisplay}<br>
            Hit(s) per Second: ${bonuses.hps.real}<br>
            Hit(s) per Second Theoretical: ${bonuses.hps.theoretical}<br>
        `;
        return statsHTML;
    }

    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;
            }

            const nameEl = box.querySelector('.itemName');
            if (!nameEl) {
                if (tooltipWindow) {
                    tooltipWindow.remove();
                    tooltipWindow = null;
                }
                return;
            }

            const weapon = nameEl.textContent.trim();
            const key = weapon.toLowerCase();
            const entry = dpsData[key];

            if (!entry) {
                console.log(`[DPS] ✗ ${weapon} (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] ✔ ${weapon} (hover)`);
        }, 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();

            const weapon = nameEl.textContent.trim();
            const key = weapon.toLowerCase();
            const entry = dpsData[key];

            if (!entry) {
                console.log(`[DPS] ✗ ${weapon} (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] ✔ ${weapon} (static)`);
        });
    }

    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();
})();