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