MediaWiki:Gadget-hero-comparison.js

Revision as of 17:44, 14 August 2026 by LVL (talk | contribs) (Sig fig 3 to 4)

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 controls.
 * 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() {
    'use strict';
    
    if (!document.getElementById('hero-comparison-container')) return;
    
    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;
        return parseFloat(rounded.toPrecision(sig));
    }
    
    function calculateLinearStat(cell, spiritPower, powerIncreases) {
        if (!cell) return 0;
        const baseRaw = cell.dataset.base;
        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;
        if (levelScale && powerIncreases > 0) {
            value = base + (powerIncreases * levelScale);
        }
        const innateSpirit = powerIncreases * innateSpiritScale;
        const totalSpirit = innateSpirit + spiritPower;
        if (spiritScale && totalSpirit > 0) {
            value += totalSpirit * spiritScale;
        }
        return value;
    }
    
    function calculateStat(cell, spiritPower, powerIncreases) {
        return calculateLinearStat(cell, spiritPower, powerIncreases);
    }
    
    function toggleAltFireRow(heroKey) {
        const subRow = document.querySelector(`tr.alt-fire-sub[data-parent="${heroKey}"]`);
        if (!subRow) return;
        subRow.classList.toggle('visible');
        const toggleLink = document.querySelector(`a.toggle-alt-fire[data-hero-key="${heroKey}"]`);
        if (toggleLink) toggleLink.textContent = subRow.classList.contains('visible') ? '−' : '+';
    }
    
    const flashTimeouts = new Map();
    function forceResetDisplay() {
        const table = document.getElementById(TABLE_ID);
        if (!table) return;
        table.querySelectorAll('td[data-base]').forEach((cell, index) => {
            const numSpan = cell.querySelector('.stat-num');
            if (!numSpan) return;
            const baseVal = cell.dataset.base;
            numSpan.textContent = baseVal;
            cell.setAttribute('data-sort-value', baseVal);
            if (window.jQuery) jQuery(cell).data('sortValue', baseVal);
            numSpan.classList.remove('stat-changed');
            numSpan.removeAttribute('style');
            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 (isReset || (spiritPower === 0 && powerIncreases === 0)) {
            forceResetDisplay();
            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;
    
            // Only update if the underlying numeric value has genuinely changed
            let hasNumericChange = false;
            if (baseRaw === "true" || baseRaw === "false") {
                hasNumericChange = (String(newValue) !== baseRaw);
            } else {
                const baseVal = parseFloat(baseRaw || 0);
                const newValNum = (typeof newValue === 'number') ? newValue : parseFloat(newValue);
                hasNumericChange = Math.abs(newValNum - baseVal) > 0.001;
            }
    
            if (!hasNumericChange) return;
    
            const displayValue = (typeof newValue === 'number')
                ? roundToSigFigs(newValue, 4)
                : newValue;
    
            // Skip update if the displayed text would be unchanged
            const currentText = numSpan.textContent.trim();
            if (currentText === String(displayValue)) return;
    
            numSpan.textContent = displayValue;
            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)) {
                clearTimeout(flashTimeouts.get(index));
                flashTimeouts.delete(index);
            }
            numSpan.classList.add('stat-changed');
            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);
        });
    
        if (window.jQuery) jQuery(table).trigger('update');
    }
    
    function createControls() {
        const container = document.getElementById('hero-comparison-container');
        if (!container || document.getElementById('hero-controls')) return;
        const maxPower = parseInt(container.dataset.maxPower) || 25;
        const maxSpirit = parseInt(container.dataset.maxSpirit) || 500;
        container.insertAdjacentHTML('afterbegin', `
            <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: #fff4df; 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: #fff4df; border: 1px solid #666; border-radius: 3px;">Reset</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>
            </div>
        `);
        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);
                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; updateTable(parseInt(spiritInput.value) || 0, maxPower); });
        powerInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') scheduleUpdate(); });
        spiritInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') scheduleUpdate(); });
    
        // 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');
        });
    }
    
    function attachToggleListeners() {
        const table = document.getElementById(TABLE_ID);
        if (!table) return;
        table.addEventListener('click', function(e) {
            const link = e.target.closest('a[href^="#alt-fire-"]');
            if (link) {
                e.preventDefault();
                const heroKey = link.getAttribute('href').replace('#alt-fire-', '');
                toggleAltFireRow(heroKey);
            }
        });
    }
    
    createControls();
    enhanceToggleLinks();
    attachToggleListeners();
    
    if (window.jQuery) {
        $(document).on('tabberchange', () => { setTimeout(() => { createControls(); enhanceToggleLinks(); attachToggleListeners(); }, 100); });
    }
})();