MediaWiki:Gadget-hero-comparison.jsGive feedback
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, 3)
: 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); });
}
})();