MediaWiki:Gadget-hero-comparison.js: Difference between revisions

LVL (talk | contribs)
Changed Spirit Power to Bonus Spirit Power
LVL (talk | contribs)
Toggleable alt fire rows for heroes that have it, bunch of other changes.
Line 1: Line 1:
/**
/**
  * Hero Comparison Table - Interactive Spirit Power and Power Increases inputs
  * Hero Comparison Table – interactive controls.
  * Allows real-time recalculation of hero stats
* Adds Boons / Bonus Spirit Power inputs that recalculate hero stats in real time,
  * expandable alt‑fire sub‑rows for heroes with alternate firing modes,
* and a button to show/hide all alt‑fire rows at once.
  */
  */
(function() {
(function() {
     'use strict';
     'use strict';
      
      
    // Only run on pages with the hero comparison table
     if (!document.getElementById('hero-comparison-container')) return;
     if (!document.getElementById('hero-comparison-container')) return;
      
      
    const CONTAINER_ID = 'hero-comparison-container';
     const TABLE_ID = 'hero-comparison-table';
     const TABLE_ID = 'hero-comparison-table';
      
      
Line 18: Line 18:
         const magnitude = Math.pow(10, power);
         const magnitude = Math.pow(10, power);
         const rounded = Math.round(num * magnitude) / magnitude;
         const rounded = Math.round(num * magnitude) / magnitude;
        // Format to avoid floating point artifacts (e.g., 1.2000000000000002)
         return parseFloat(rounded.toPrecision(sig));
         return parseFloat(rounded.toPrecision(sig));
     }
     }
      
      
    // Helper to calculate standard linear scaling for a given cell
     function calculateLinearStat(cell, spiritPower, powerIncreases) {
     function calculateLinearStat(cell, spiritPower, powerIncreases) {
         if (!cell) return 0;
         if (!cell) return 0;
       
         const baseRaw = cell.dataset.base;
         const baseRaw = cell.dataset.base;
       
         if (baseRaw === "true" || baseRaw === "false") return baseRaw;
        // Check if it's a boolean string first
         if (baseRaw === "true" || baseRaw === "false") {
            return baseRaw;
        }
       
         const base = parseFloat(baseRaw || 0);
         const base = parseFloat(baseRaw || 0);
         const spiritScale = parseFloat(cell.dataset.spiritScale || 0);
         const spiritScale = parseFloat(cell.dataset.spiritScale || 0);
         const levelScale = parseFloat(cell.dataset.levelScale || 0);
         const levelScale = parseFloat(cell.dataset.levelScale || 0);
         const innateSpiritScale = parseFloat(cell.dataset.innateSpiritScale || 0);
         const innateSpiritScale = parseFloat(cell.dataset.innateSpiritScale || 0);
       
         let value = base;
         let value = base;
       
        // Apply Level scaling
         if (levelScale && powerIncreases > 0) {
         if (levelScale && powerIncreases > 0) {
             value = base + (powerIncreases * levelScale);
             value = base + (powerIncreases * levelScale);
         }
         }
       
        // Calculate innate spirit
         const innateSpirit = powerIncreases * innateSpiritScale;
         const innateSpirit = powerIncreases * innateSpiritScale;
       
        // Apply Spirit scaling
         const totalSpirit = innateSpirit + spiritPower;
         const totalSpirit = innateSpirit + spiritPower;
         if (spiritScale && totalSpirit > 0) {
         if (spiritScale && totalSpirit > 0) {
             value += totalSpirit * spiritScale;
             value += totalSpirit * spiritScale;
         }
         }
       
         return value;
         return value;
     }
     }
      
      
     function calculateStat(cell, spiritPower, powerIncreases) {
     function calculateStat(cell, spiritPower, powerIncreases) {
         const statName = cell.dataset.statName;
         return calculateLinearStat(cell, spiritPower, powerIncreases);
       
    }
        // --- MULTIPLICATIVE STATS (DPS & SustainedDPS) ---
   
        // Dynamically compute based on the modified values of sibling cells in the same row
    function toggleAltFireRow(heroKey) {
        if (statName === 'DPS' || statName === 'SustainedDPS') {
        const subRow = document.querySelector(`tr.alt-fire-sub[data-parent="${heroKey}"]`);
    const row = cell.closest('tr');
        if (!subRow) return;
   
        subRow.classList.toggle('visible');
    const dmg = calculateLinearStat(row.querySelector('td[data-stat-name="BulletDamage"]'), spiritPower, powerIncreases);
        const toggleLink = document.querySelector(`a.toggle-alt-fire[data-hero-key="${heroKey}"]`);
    const rpsCell = row.querySelector('td[data-stat-name="RoundsPerSecond"]');
        if (toggleLink) toggleLink.textContent = subRow.classList.contains('visible') ? '−' : '+';
    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();
     const flashTimeouts = new Map();
   
     function forceResetDisplay() {
     function forceResetDisplay() {
         const table = document.getElementById(TABLE_ID);
         const table = document.getElementById(TABLE_ID);
         if (!table) return;
         if (!table) return;
          
         table.querySelectorAll('td[data-base]').forEach((cell, index) => {
        const cells = table.querySelectorAll('td[data-base]');
        cells.forEach((cell, index) => {
             const numSpan = cell.querySelector('.stat-num');
             const numSpan = cell.querySelector('.stat-num');
             if (!numSpan) return;
             if (!numSpan) return;
           
            // Get raw base value from data attribute
             const baseVal = cell.dataset.base;
             const baseVal = cell.dataset.base;
           
            // Explicitly set to base value
             numSpan.textContent = baseVal;
             numSpan.textContent = baseVal;
           
            // Reset the sort attribute to base as well
             cell.setAttribute('data-sort-value', baseVal);
             cell.setAttribute('data-sort-value', baseVal);
           
             if (window.jQuery) jQuery(cell).data('sortValue', 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.classList.remove('stat-changed');
             numSpan.removeAttribute('style');
             numSpan.removeAttribute('style');
           
             if (flashTimeouts.has(index)) { clearTimeout(flashTimeouts.get(index)); flashTimeouts.delete(index); }
            // Clear any pending flash
             if (flashTimeouts.has(index)) {
                clearTimeout(flashTimeouts.get(index));
                flashTimeouts.delete(index);
            }
         });
         });
     }
     }
Line 136: Line 73:
         const table = document.getElementById(TABLE_ID);
         const table = document.getElementById(TABLE_ID);
         if (!table) return;
         if (!table) return;
       
   
        // If this is a reset, use the force reset function instead
         if (isReset || (spiritPower === 0 && powerIncreases === 0)) {
         if (isReset || (spiritPower === 0 && powerIncreases === 0)) {
             forceResetDisplay();
             forceResetDisplay();
           
             if (window.jQuery) jQuery(table).trigger('update');
            // Trigger tablesorter update
             if (window.jQuery) {
                jQuery(table).trigger('update');
            }
             return;
             return;
         }
         }
       
   
         const cells = table.querySelectorAll('td[data-base]');
         const cells = table.querySelectorAll('td[data-base]');
         cells.forEach((cell, index) => {
         cells.forEach((cell, index) => {
             const numSpan = cell.querySelector('.stat-num');
             const numSpan = cell.querySelector('.stat-num');
             if (!numSpan) return;
             if (!numSpan) return;
           
   
             const newValue = calculateStat(cell, spiritPower, powerIncreases);
             const newValue = calculateStat(cell, spiritPower, powerIncreases);
             const baseRaw = cell.dataset.base;
             const baseRaw = cell.dataset.base;
           
   
             // Handle boolean comparison separately
             // Only update if the underlying numeric value has genuinely changed
             let isAtBase;
             let hasNumericChange = false;
             if (baseRaw === "true" || baseRaw === "false") {
             if (baseRaw === "true" || baseRaw === "false") {
                 isAtBase = (newValue === baseRaw);
                 hasNumericChange = (String(newValue) !== baseRaw);
             } else {
             } else {
                 const baseVal = parseFloat(baseRaw || 0);
                 const baseVal = parseFloat(baseRaw || 0);
                 const epsilon = 0.001;
                 const newValNum = (typeof newValue === 'number') ? newValue : parseFloat(newValue);
                 isAtBase = Math.abs(newValue - baseVal) < epsilon;
                 hasNumericChange = Math.abs(newValNum - baseVal) > 0.001;
             }
             }
              
   
             // Update text
             if (!hasNumericChange) return;
             numSpan.textContent = newValue;
   
           
            const displayValue = (typeof newValue === 'number')
             // Update data-sort-value attribute (must be string)
                ? roundToSigFigs(newValue, 3)
             cell.setAttribute('data-sort-value', String(newValue));
                : newValue;
           
   
            // IMPORTANT: Update jQuery's internal cache so tablesorter sees the new value
             // Skip update if the displayed text would be unchanged
             if (window.jQuery) {
             const currentText = numSpan.textContent.trim();
                jQuery(cell).data('sortValue', String(newValue));
            if (currentText === String(displayValue)) return;
            }
   
           
             numSpan.textContent = displayValue;
             // Clear pending flash
             cell.setAttribute('data-sort-value', displayValue);
             if (window.jQuery) jQuery(cell).data('sortValue', displayValue);
   
             // Brief green flash to draw attention to changed values
             if (flashTimeouts.has(index)) {
             if (flashTimeouts.has(index)) {
                 clearTimeout(flashTimeouts.get(index));
                 clearTimeout(flashTimeouts.get(index));
                 flashTimeouts.delete(index);
                 flashTimeouts.delete(index);
             }
             }
              
             numSpan.classList.add('stat-changed');
            if (!isAtBase) {
            numSpan.style.transition = 'none';
                numSpan.classList.add('stat-changed');
            numSpan.style.color = '#4CAF50';
                // Flash effect
            const timeoutId = setTimeout(() => {
                numSpan.style.transition = 'none';
                if (numSpan.parentElement) {
                numSpan.style.color = '#4CAF50';
                    numSpan.style.transition = 'color 0.3s ease';
                const timeoutId = setTimeout(() => {
                    numSpan.style.color = '';
                    if (numSpan.parentElement) {
                }
                        numSpan.style.transition = 'color 0.3s ease';
                flashTimeouts.delete(index);
                        numSpan.style.color = '';
            }, 150);
                    }
            flashTimeouts.set(index, timeoutId);
                    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');
         if (window.jQuery) {
            jQuery(table).trigger('update');
        }
     }
     }
      
      
     function createControls() {
     function createControls() {
         const container = document.getElementById(CONTAINER_ID);
         const container = document.getElementById('hero-comparison-container');
         if (!container) return;
         if (!container || document.getElementById('hero-controls')) 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 maxPower = parseInt(container.dataset.maxPower) || 25;
         const maxSpirit = parseInt(container.dataset.maxSpirit) || 500;
         const maxSpirit = parseInt(container.dataset.maxSpirit) || 500;
          
         container.insertAdjacentHTML('afterbegin', `
        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;">
             <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;">
                 <label style="color: #fff; display: flex; align-items: center; gap: 5px;">
                     <span style="color: #ffaa44; font-weight: bold;">Boons:</span>
                     <span style="color: #fff4df; font-weight: bold;">Boons:</span>
                     <input type="number" id="hero-input-power" min="0" max="${maxPower}" value="0"  
                     <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;">
                           style="width: 60px; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px;">
Line 230: Line 150:
                           style="width: 70px; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px;">
                           style="width: 70px; background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px;">
                 </label>
                 </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-reset-btn" style="padding: 4px 12px; cursor: pointer; background: #444; color: #fff4df; 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>
                 <button id="hero-max-btn" style="padding: 4px 12px; cursor: pointer; background: #444; color: #fff4df; border: 1px solid #666; border-radius: 3px;">Max Boons</button>
                <button id="hero-toggle-all-alt" style="padding: 4px 12px; cursor: pointer; background: #444; color: #fff4df; border: 1px solid #666; border-radius: 3px;">Show Alt‑Fire</button>
                 <span style="color: #888; font-size: 0.85em;">Values update automatically</span>
                 <span style="color: #888; font-size: 0.85em;">Values update automatically</span>
             </div>
             </div>
         `;
         `);
       
        container.insertAdjacentHTML('afterbegin', controlsHtml);
       
        // Event listeners
         const powerInput = document.getElementById('hero-input-power');
         const powerInput = document.getElementById('hero-input-power');
         const spiritInput = document.getElementById('hero-input-spirit');
         const spiritInput = document.getElementById('hero-input-spirit');
         const resetBtn = document.getElementById('hero-reset-btn');
         const resetBtn = document.getElementById('hero-reset-btn');
         const maxBtn = document.getElementById('hero-max-btn');
         const maxBtn = document.getElementById('hero-max-btn');
       
         let updateTimeout;
         let updateTimeout;
         const scheduleUpdate = () => {
         const scheduleUpdate = () => {
Line 250: Line 166:
                 const power = Math.min(parseInt(powerInput.value) || 0, maxPower);
                 const power = Math.min(parseInt(powerInput.value) || 0, maxPower);
                 const spirit = Math.min(parseInt(spiritInput.value) || 0, maxSpirit);
                 const spirit = Math.min(parseInt(spiritInput.value) || 0, maxSpirit);
                // Clamp values
                 if (powerInput.value > maxPower) powerInput.value = maxPower;
                 if (powerInput.value > maxPower) powerInput.value = maxPower;
                 if (spiritInput.value > maxSpirit) spiritInput.value = maxSpirit;
                 if (spiritInput.value > maxSpirit) spiritInput.value = maxSpirit;
Line 256: Line 171:
             }, 50);
             }, 50);
         };
         };
       
         powerInput.addEventListener('input', scheduleUpdate);
         powerInput.addEventListener('input', scheduleUpdate);
         spiritInput.addEventListener('input', scheduleUpdate);
         spiritInput.addEventListener('input', scheduleUpdate);
       
         resetBtn.addEventListener('click', () => { powerInput.value = 0; spiritInput.value = 0; updateTable(0, 0, true); });
         resetBtn.addEventListener('click', () => {
        maxBtn.addEventListener('click', () => { powerInput.value = maxPower; updateTable(parseInt(spiritInput.value) || 0, maxPower); });
            powerInput.value = 0;
        powerInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') scheduleUpdate(); });
            spiritInput.value = 0;
        spiritInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') scheduleUpdate(); });
            updateTable(0, 0, true);
   
        // Toggle all alt‑fire sub‑rows at once
        let allExpanded = false;
        const toggleAllBtn = document.getElementById('hero-toggle-all-alt');
        if (toggleAllBtn) {
            toggleAllBtn.addEventListener('click', () => {
                const rows = document.querySelectorAll('tr.alt-fire-sub');
                const links = document.querySelectorAll('a.toggle-alt-fire');
                if (allExpanded) {
                    rows.forEach(row => row.classList.remove('visible'));
                    links.forEach(link => link.textContent = '+');
                    toggleAllBtn.textContent = 'Show Alt‑Fire';
                    allExpanded = false;
                } else {
                    rows.forEach(row => row.classList.add('visible'));
                    links.forEach(link => link.textContent = '−');
                    toggleAllBtn.textContent = 'Hide Alt‑Fire';
                    allExpanded = true;
                }
            });
        }
    }
   
    function enhanceToggleLinks() {
        document.querySelectorAll('a[href^="#alt-fire-"]').forEach(link => {
            link.classList.add('toggle-alt-fire');
            const heroKey = link.getAttribute('href').replace('#alt-fire-', '');
            link.setAttribute('data-hero-key', heroKey);
            link.setAttribute('title', 'Show alt‑fire weapon stats');
         });
         });
       
    }
        maxBtn.addEventListener('click', () => {
   
            powerInput.value = maxPower;
    function attachToggleListeners() {
            const currentSpirit = parseInt(spiritInput.value) || 0; // Keep current Spirit value
        const table = document.getElementById(TABLE_ID);
            updateTable(currentSpirit, maxPower);
         if (!table) return;
         });
         table.addEventListener('click', function(e) {
          
             const link = e.target.closest('a[href^="#alt-fire-"]');
        // Allow Enter key to update immediately
            if (link) {
        powerInput.addEventListener('keypress', (e) => {
                e.preventDefault();
             if (e.key === 'Enter') scheduleUpdate();
                const heroKey = link.getAttribute('href').replace('#alt-fire-', '');
        });
                toggleAltFireRow(heroKey);
        spiritInput.addEventListener('keypress', (e) => {
            }
            if (e.key === 'Enter') scheduleUpdate();
         });
         });
     }
     }
      
      
    // Initialize
     createControls();
     createControls();
    enhanceToggleLinks();
    attachToggleListeners();
      
      
    // Re-initialize if table is dynamically reloaded (e.g., tab switch)
    // This handles the Tabber extension switching tabs
     if (window.jQuery) {
     if (window.jQuery) {
         $(document).on('tabberchange', function() {
         $(document).on('tabberchange', () => { setTimeout(() => { createControls(); enhanceToggleLinks(); attachToggleListeners(); }, 100); });
            setTimeout(createControls, 100);
        });
     }
     }
})();
})();