MediaWiki:Gadget-hero-comparison.js

From The Deadlock Wiki
Revision as of 15:33, 16 April 2026 by LVL (talk | contribs) (Changed Spirit Power to Bonus Spirit Power)
Jump to navigation Jump to search

Note: After publishing, you may need to reload with your cache disabled to see your changes.

  • Windows / Linux: Press Ctrl-Shift-R
  • macOS: Press ⌘-Shift-R
/**
 * Hero Comparison Table - Interactive Spirit Power and Power Increases inputs
 * Allows real-time recalculation of hero stats
 */
(function() {
    'use strict';
    
    // Only run on pages with the hero comparison table
    if (!document.getElementById('hero-comparison-container')) return;
    
    const CONTAINER_ID = 'hero-comparison-container';
    const TABLE_ID = 'hero-comparison-table';
    
    function roundToSigFigs(num, sig) {
        if (num === 0 || !isFinite(num)) return 0;
        const d = Math.ceil(Math.log10(Math.abs(num)));
        const power = sig - d;
        const magnitude = Math.pow(10, power);
        const rounded = Math.round(num * magnitude) / magnitude;
        // Format to avoid floating point artifacts (e.g., 1.2000000000000002)
        return parseFloat(rounded.toPrecision(sig));
    }
    
    // Helper to calculate standard linear scaling for a given cell
    function calculateLinearStat(cell, spiritPower, powerIncreases) {
        if (!cell) return 0;
        
        const baseRaw = cell.dataset.base;
        
        // Check if it's a boolean string first
        if (baseRaw === "true" || baseRaw === "false") {
            return baseRaw;
        }
        
        const base = parseFloat(baseRaw || 0);
        const spiritScale = parseFloat(cell.dataset.spiritScale || 0);
        const levelScale = parseFloat(cell.dataset.levelScale || 0);
        const innateSpiritScale = parseFloat(cell.dataset.innateSpiritScale || 0);
        
        let value = base;
        
        // Apply Level scaling
        if (levelScale && powerIncreases > 0) {
            value = base + (powerIncreases * levelScale);
        }
        
        // Calculate innate spirit
        const innateSpirit = powerIncreases * innateSpiritScale;
        
        // Apply Spirit scaling
        const totalSpirit = innateSpirit + spiritPower;
        if (spiritScale && totalSpirit > 0) {
            value += totalSpirit * spiritScale;
        }
        
        return value;
    }
    
    function calculateStat(cell, spiritPower, powerIncreases) {
        const statName = cell.dataset.statName;
        
        // --- MULTIPLICATIVE STATS (DPS & SustainedDPS) ---
        // Dynamically compute based on the modified values of sibling cells in the same row
        if (statName === 'DPS' || statName === 'SustainedDPS') {
	    const row = cell.closest('tr');
	    
	    const dmg = calculateLinearStat(row.querySelector('td[data-stat-name="BulletDamage"]'), spiritPower, powerIncreases);
	    const rpsCell = row.querySelector('td[data-stat-name="RoundsPerSecond"]');
	    const maxSpinCell = row.querySelector('td[data-stat-name="RoundsPerSecondAtMaxSpin"]');
	    
	    // Use max spin RPS for spin-up weapons like McGinnis, otherwise use scaled RPS
	    const rps = (maxSpinCell && parseFloat(maxSpinCell.dataset.base) > 0)
	        ? parseFloat(maxSpinCell.dataset.base)
	        : calculateLinearStat(rpsCell, spiritPower, powerIncreases);
	    
	    const bpsCell = row.querySelector('td[data-stat-name="BulletsPerShot"]');
	    const hitOnce = cell.dataset.hitOnce === 'true';
	    const bps = hitOnce ? 1 : (bpsCell && bpsCell.dataset.base ? calculateLinearStat(bpsCell, spiritPower, powerIncreases) : 1);
	    
	    if (statName === 'DPS') {
	        return roundToSigFigs(dmg * rps * Math.max(1, bps), 3);
	    } else if (statName === 'SustainedDPS') {
	        const clip = calculateLinearStat(row.querySelector('td[data-stat-name="ClipSize"]'), spiritPower, powerIncreases);
	        const reload = calculateLinearStat(row.querySelector('td[data-stat-name="ReloadTime"]'), spiritPower, powerIncreases);
	        if (rps === 0 || clip === 0) return 0;
	        const sustained = (clip * dmg * Math.max(1, bps)) / ((clip / rps) + reload);
	        return roundToSigFigs(sustained, 3);
	    }
	}
        
        // --- LINEAR STATS (Everything Else) ---
        const value = calculateLinearStat(cell, spiritPower, powerIncreases);
        if (value === "true" || value === "false") return value;
        return roundToSigFigs(value, 3);
    }
    
    // Store timeouts to clear them on reset
    const flashTimeouts = new Map();
    
    function forceResetDisplay() {
        const table = document.getElementById(TABLE_ID);
        if (!table) return;
        
        const cells = table.querySelectorAll('td[data-base]');
        cells.forEach((cell, index) => {
            const numSpan = cell.querySelector('.stat-num');
            if (!numSpan) return;
            
            // Get raw base value from data attribute
            const baseVal = cell.dataset.base;
            
            // Explicitly set to base value
            numSpan.textContent = baseVal;
            
            // Reset the sort attribute to base as well
            cell.setAttribute('data-sort-value', baseVal);
            
            // IMPORTANT: Update jQuery's internal cache so tablesorter sees the reset value
            if (window.jQuery) {
                jQuery(cell).data('sortValue', baseVal);
            }
            
            // Force remove changed class and styles
            numSpan.classList.remove('stat-changed');
            numSpan.removeAttribute('style');
            
            // Clear any pending flash
            if (flashTimeouts.has(index)) {
                clearTimeout(flashTimeouts.get(index));
                flashTimeouts.delete(index);
            }
        });
    }
    
    function updateTable(spiritPower, powerIncreases, isReset) {
        const table = document.getElementById(TABLE_ID);
        if (!table) return;
        
        // If this is a reset, use the force reset function instead
        if (isReset || (spiritPower === 0 && powerIncreases === 0)) {
            forceResetDisplay();
            
            // Trigger tablesorter update
            if (window.jQuery) {
                jQuery(table).trigger('update');
            }
            return;
        }
        
        const cells = table.querySelectorAll('td[data-base]');
        cells.forEach((cell, index) => {
            const numSpan = cell.querySelector('.stat-num');
            if (!numSpan) return;
            
            const newValue = calculateStat(cell, spiritPower, powerIncreases);
            const baseRaw = cell.dataset.base;
            
            // Handle boolean comparison separately
            let isAtBase;
            if (baseRaw === "true" || baseRaw === "false") {
                isAtBase = (newValue === baseRaw);
            } else {
                const baseVal = parseFloat(baseRaw || 0);
                const epsilon = 0.001;
                isAtBase = Math.abs(newValue - baseVal) < epsilon;
            }
            
            // Update text
            numSpan.textContent = newValue;
            
            // Update data-sort-value attribute (must be string)
            cell.setAttribute('data-sort-value', String(newValue));
            
            // IMPORTANT: Update jQuery's internal cache so tablesorter sees the new value
            if (window.jQuery) {
                jQuery(cell).data('sortValue', String(newValue));
            }
            
            // Clear pending flash
            if (flashTimeouts.has(index)) {
                clearTimeout(flashTimeouts.get(index));
                flashTimeouts.delete(index);
            }
            
            if (!isAtBase) {
                numSpan.classList.add('stat-changed');
                // Flash effect
                numSpan.style.transition = 'none';
                numSpan.style.color = '#4CAF50';
                const timeoutId = setTimeout(() => {
                    if (numSpan.parentElement) {
                        numSpan.style.transition = 'color 0.3s ease';
                        numSpan.style.color = '';
                    }
                    flashTimeouts.delete(index);
                }, 150);
                flashTimeouts.set(index, timeoutId);
            } else {
                numSpan.classList.remove('stat-changed');
                numSpan.removeAttribute('style');
            }
        });
        
        // Trigger tablesorter update (Use jQuery explicitly)
        if (window.jQuery) {
            jQuery(table).trigger('update');
        }
    }
    
    function createControls() {
        const container = document.getElementById(CONTAINER_ID);
        if (!container) return;
        
        // Check if controls already exist (avoid duplicates on re-parse)
        if (document.getElementById('hero-controls')) return;
        
        const maxPower = parseInt(container.dataset.maxPower) || 25;
        const maxSpirit = parseInt(container.dataset.maxSpirit) || 500;
        
        const controlsHtml = `
            <div id="hero-controls" style="margin-bottom: 15px; padding: 12px; background: #1e1e1e; border: 1px solid #333; border-radius: 4px; display: flex; align-items: center; flex-wrap: wrap; gap: 15px;">
                <label style="color: #fff; display: flex; align-items: center; gap: 5px;">
                    <span style="color: #ffaa44; font-weight: bold;">Boons:</span>
                    <input type="number" id="hero-input-power" min="0" max="${maxPower}" value="0" 
                           style="width: 60px; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px;">
                </label>
                <label style="color: #fff; display: flex; align-items: center; gap: 5px;">
                    <span style="color: #cc88ff; font-weight: bold;">Bonus Spirit Power:</span>
                    <input type="number" id="hero-input-spirit" min="0" max="${maxSpirit}" value="0" step="10"
                           style="width: 70px; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px;">
                </label>
                <button id="hero-reset-btn" style="padding: 4px 12px; cursor: pointer; background: #444; color: #fff; border: 1px solid #666; border-radius: 3px;">Reset</button>
                <button id="hero-max-btn" style="padding: 4px 12px; cursor: pointer; background: #444; color: #fff; border: 1px solid #666; border-radius: 3px;">Max Boons</button>
                <span style="color: #888; font-size: 0.85em;">Values update automatically</span>
            </div>
        `;
        
        container.insertAdjacentHTML('afterbegin', controlsHtml);
        
        // Event listeners
        const powerInput = document.getElementById('hero-input-power');
        const spiritInput = document.getElementById('hero-input-spirit');
        const resetBtn = document.getElementById('hero-reset-btn');
        const maxBtn = document.getElementById('hero-max-btn');
        
        let updateTimeout;
        const scheduleUpdate = () => {
            clearTimeout(updateTimeout);
            updateTimeout = setTimeout(() => {
                const power = Math.min(parseInt(powerInput.value) || 0, maxPower);
                const spirit = Math.min(parseInt(spiritInput.value) || 0, maxSpirit);
                // Clamp values
                if (powerInput.value > maxPower) powerInput.value = maxPower;
                if (spiritInput.value > maxSpirit) spiritInput.value = maxSpirit;
                updateTable(spirit, power);
            }, 50);
        };
        
        powerInput.addEventListener('input', scheduleUpdate);
        spiritInput.addEventListener('input', scheduleUpdate);
        
        resetBtn.addEventListener('click', () => {
            powerInput.value = 0;
            spiritInput.value = 0;
            updateTable(0, 0, true);
        });
        
        maxBtn.addEventListener('click', () => {
            powerInput.value = maxPower;
            const currentSpirit = parseInt(spiritInput.value) || 0; // Keep current Spirit value
            updateTable(currentSpirit, maxPower);
        });
        
        // Allow Enter key to update immediately
        powerInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') scheduleUpdate();
        });
        spiritInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') scheduleUpdate();
        });
    }
    
    // Initialize
    createControls();
    
    // Re-initialize if table is dynamically reloaded (e.g., tab switch)
    // This handles the Tabber extension switching tabs
    if (window.jQuery) {
        $(document).on('tabberchange', function() {
            setTimeout(createControls, 100);
        });
    }
})();