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.4.1
// @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;

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

    function createInputContainer() {
        // Only create container if on page=25
        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';

        // Add the title
        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 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'
            }
        ];

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

        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', () => {
            localStorage.setItem('deadFrontierUserStats', JSON.stringify(userStats));
            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(); // Load saved stats for all pages
        createInputContainer(); // Only creates container on page=25
        startWatcher();
        injectDPSIntoStaticBoxes();
    }

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

        // Match three-term sums like "96 + 32 + 32 = 160" or "12.12 + 28.28 + 60.6 = 101"
        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 two-term sums like "100 + 50 = 150"
        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
            };
        }

        // Fallback: try to parse as a single number (e.g., "50")
        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;

        // Parse DPH total and critical
        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);

        // Format DPH display based on whether it's a multiplier format or sum format
        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);

        // Format critical DPH display
        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;

        // Determine labels based on weapon category
        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();
})();