MediaWiki:Gadget-hero-comparison.js: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Brought back dps calculations for warden while also accounting for heroes with spinup. |
Move controls panel styling out of inline styles into the gadget CSS so it follows the light/dark theme (with help from vergir-bot LLM) |
||
| (3 intermediate revisions by one other user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* Hero Comparison Table | * 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() { | (function() { | ||
'use strict'; | 'use strict'; | ||
if (!document.getElementById('hero-comparison-container')) return; | if (!document.getElementById('hero-comparison-container')) return; | ||
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; | ||
return parseFloat(rounded.toPrecision(sig)); | return parseFloat(rounded.toPrecision(sig)); | ||
} | } | ||
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; | |||
if (baseRaw === "true" || baseRaw === "false") | |||
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; | ||
if (levelScale && powerIncreases > 0) { | if (levelScale && powerIncreases > 0) { | ||
value = base + (powerIncreases * levelScale); | value = base + (powerIncreases * levelScale); | ||
} | } | ||
const innateSpirit = powerIncreases * innateSpiritScale; | const innateSpirit = powerIncreases * innateSpiritScale; | ||
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) { | ||
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(); | 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 numSpan = cell.querySelector('.stat-num'); | const numSpan = cell.querySelector('.stat-num'); | ||
if (!numSpan) return; | if (!numSpan) return; | ||
const baseVal = cell.dataset.base; | const baseVal = cell.dataset.base; | ||
numSpan.textContent = baseVal; | numSpan.textContent = baseVal; | ||
cell.setAttribute('data-sort-value', baseVal); | cell.setAttribute('data-sort-value', baseVal); | ||
if (window.jQuery) jQuery(cell).data('sortValue', baseVal); | |||
if (window.jQuery) | |||
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); } | |||
if (flashTimeouts.has(index)) { | |||
}); | }); | ||
} | } | ||
| Line 136: | Line 73: | ||
const table = document.getElementById(TABLE_ID); | const table = document.getElementById(TABLE_ID); | ||
if (!table) return; | if (!table) return; | ||
if (isReset || (spiritPower === 0 && powerIncreases === 0)) { | if (isReset || (spiritPower === 0 && powerIncreases === 0)) { | ||
forceResetDisplay(); | forceResetDisplay(); | ||
if (window.jQuery) jQuery(table).trigger('update'); | |||
if (window.jQuery) | |||
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; | ||
// | // Only update if the underlying numeric value has genuinely changed | ||
let | let hasNumericChange = false; | ||
if (baseRaw === "true" || baseRaw === "false") { | if (baseRaw === "true" || baseRaw === "false") { | ||
hasNumericChange = (String(newValue) !== baseRaw); | |||
} else { | } else { | ||
const baseVal = parseFloat(baseRaw || 0); | const baseVal = parseFloat(baseRaw || 0); | ||
const | const newValNum = (typeof newValue === 'number') ? newValue : parseFloat(newValue); | ||
hasNumericChange = Math.abs(newValNum - baseVal) > 0.001; | |||
} | } | ||
// | if (!hasNumericChange) return; | ||
numSpan.textContent = | |||
const displayValue = (typeof newValue === 'number') | |||
? roundToSigFigs(newValue, 4) | |||
cell.setAttribute('data-sort-value', | : newValue; | ||
// Skip update if the displayed text would be unchanged | |||
if (window.jQuery) | 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)) { | if (flashTimeouts.has(index)) { | ||
clearTimeout(flashTimeouts.get(index)); | clearTimeout(flashTimeouts.get(index)); | ||
flashTimeouts.delete(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'); | |||
if (window.jQuery) | |||
} | } | ||
function createControls() { | function createControls() { | ||
const container = document.getElementById( | const container = document.getElementById('hero-comparison-container'); | ||
if (!container | if (!container || 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', ` | |||
<div id="hero-controls"> | |||
<div id="hero-controls | <label class="hero-control-label"> | ||
<label | <span class="hero-label-boons">Boons:</span> | ||
<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 | |||
</label> | </label> | ||
<label | <label class="hero-control-label"> | ||
<span | <span class="hero-label-spirit">Bonus Spirit Power:</span> | ||
<input type="number" id="hero-input-spirit" min="0" max="${maxSpirit}" value="0" step="10 | <input type="number" id="hero-input-spirit" min="0" max="${maxSpirit}" value="0" step="10"> | ||
</label> | </label> | ||
<button id="hero-reset-btn | <button id="hero-reset-btn">Reset</button> | ||
<button id="hero-max-btn" | <button id="hero-max-btn">Max Boons</button> | ||
<span | <button id="hero-toggle-all-alt">Show Alt‑Fire</button> | ||
<span class="hero-control-note">Values update automatically</span> | |||
</div> | </div> | ||
` | `); | ||
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 164: | ||
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); | ||
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 169: | ||
}, 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.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(); | createControls(); | ||
enhanceToggleLinks(); | |||
attachToggleListeners(); | |||
if (window.jQuery) { | if (window.jQuery) { | ||
$(document).on('tabberchange', | $(document).on('tabberchange', () => { setTimeout(() => { createControls(); enhanceToggleLinks(); attachToggleListeners(); }, 100); }); | ||
} | } | ||
})(); | })(); | ||
Latest revision as of 00:31, 17 August 2026
/**
* 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">
<label class="hero-control-label">
<span class="hero-label-boons">Boons:</span>
<input type="number" id="hero-input-power" min="0" max="${maxPower}" value="0">
</label>
<label class="hero-control-label">
<span class="hero-label-spirit">Bonus Spirit Power:</span>
<input type="number" id="hero-input-spirit" min="0" max="${maxSpirit}" value="0" step="10">
</label>
<button id="hero-reset-btn">Reset</button>
<button id="hero-max-btn">Max Boons</button>
<button id="hero-toggle-all-alt">Show Alt‑Fire</button>
<span class="hero-control-note">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); });
}
})();