MediaWiki:ClassMasteryCalculator.js
From EQOA Sandstorm Wiki
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
mw.hook('wikipage.content').add(function () {
function waitForCMData(attemptsLeft) {
if (window.EQOA_CM_DATA) {
initClassMasteryCalculator();
return;
}
if (attemptsLeft <= 0) {
var app = document.getElementById('cm-calculator-app');
if (app) {
app.innerHTML = '<div style="color:red;">EQOA_CM_DATA failed to load.</div>';
}
return;
}
setTimeout(function () {
waitForCMData(attemptsLeft - 1);
}, 100);
}
waitForCMData(30);
function initClassMasteryCalculator() {
var app = document.getElementById('cm-calculator-app');
if (!app) return;
var data = window.EQOA_CM_DATA;
var selectedMasteries = {};
var activeCmTab = "General";
app.innerHTML = `
<div class="cm-calculator-box">
<div class="character-builder-title">Class Mastery Calculator</div>
<div class="character-builder-controls">
<label>Race</label>
<select id="cm-race"></select>
<label>Class</label>
<select id="cm-class"></select>
<label>Character Level</label>
<input type="number" id="cm-level" min="1" max="60" value="60" style="width:70px;">
</div>
<div class="character-builder-section-title" style="margin-top:15px;">Point Summary</div>
<table class="character-builder-stat-table">
<tr>
<th>Available</th>
<th>Spent</th>
<th>Remaining</th>
</tr>
<tr>
<td id="cm-available">1500</td>
<td id="cm-spent">0</td>
<td id="cm-remaining">1500</td>
</tr>
</table>
<div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>
<div class="cm-tabs">
<button type="button" class="cm-tab-button cm-tab-active" data-tab="General">General</button>
<button type="button" class="cm-tab-button" data-tab="Racial">Racial</button>
<button type="button" class="cm-tab-button" data-tab="Archetype">Archetype</button>
<button type="button" class="cm-tab-button" data-tab="Class">Class</button>
</div>
<div class="cm-main-layout">
<div class="cm-purchased-panel">
<div class="cm-panel-title">CM Purchased</div>
<div id="cm-purchased-list">None purchased.</div>
</div>
<div id="cm-mastery-panel" class="character-builder-warning-box">
Select a race and class to begin.
</div>
</div>
<div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>
</div>
`;
var raceSelect = document.getElementById('cm-race');
var classSelect = document.getElementById('cm-class');
var levelInput = document.getElementById('cm-level');
var cmTooltip = document.getElementById('cm-tooltip');
function addOption(select, value, label) {
var option = document.createElement('option');
option.value = value;
option.textContent = label;
select.appendChild(option);
}
function loadRaces() {
raceSelect.innerHTML = '';
Object.keys(data.raceClasses).forEach(function (race) {
addOption(raceSelect, race, race);
});
loadClasses();
}
function loadClasses() {
classSelect.innerHTML = '';
var race = raceSelect.value;
var classList = data.raceClasses[race] || [];
classList.forEach(function (classCode) {
addOption(
classSelect,
classCode,
classCode + ' - ' + (data.classes[classCode] || classCode)
);
});
updateSummary();
}
function getAvailablePoints() {
var level = parseInt(levelInput.value, 10);
if (isNaN(level) || level < 1) level = 1;
if (level > 60) level = 60;
levelInput.value = level;
return level * 25;
}
function getSpentPoints() {
var total = 0;
Object.keys(selectedMasteries).forEach(function (name) {
if (selectedMasteries[name]) {
total += selectedMasteries[name].points || 0;
}
});
return total;
}
function updatePurchasedList() {
var list = document.getElementById('cm-purchased-list');
if (!list) return;
var purchased = Object.keys(selectedMasteries).filter(function (name) {
return selectedMasteries[name];
});
if (!purchased.length) {
list.innerHTML = 'None purchased.';
return;
}
list.innerHTML = purchased.map(function (name) {
var cm = selectedMasteries[name];
return (
'<div class="cm-purchased-item" data-cm-name="' + name.replace(/"/g, '"') + '">' +
'<span>' + name + '</span>' +
'<span class="cm-purchased-cost">' + cm.points + '</span>' +
'</div>'
);
}).join('');
Array.prototype.forEach.call(
list.querySelectorAll('.cm-purchased-item'),
function (row) {
row.addEventListener('click', function () {
var cmName = row.getAttribute('data-cm-name');
delete selectedMasteries[cmName];
updateSummary();
});
}
);
}
function updateSummary() {
var available = getAvailablePoints();
var spent = getSpentPoints();
var remaining = available - spent;
document.getElementById('cm-available').textContent = available;
document.getElementById('cm-spent').textContent = spent;
document.getElementById('cm-remaining').textContent = remaining;
updatePurchasedList();
renderMasteryList();
}
function renderMasteryList() {
var masteryPanel = document.getElementById('cm-mastery-panel');
if (!masteryPanel) return;
var selectedRace = raceSelect.value;
var selectedRaceCode = data.raceCodes[selectedRace];
var selectedClass = classSelect.value;
var level = parseInt(levelInput.value, 10) || 1;
var spent = getSpentPoints();
var filtered = (data.masteries || []).filter(function (cm) {
var raceOk =
!cm.races.length ||
cm.races.indexOf(selectedRaceCode) !== -1;
var classOk =
!cm.classes.length ||
cm.classes.indexOf(selectedClass) !== -1;
var levelOk =
!cm.level ||
cm.level <= level;
var spentOk =
!cm.cmSpentReq ||
spent >= cm.cmSpentReq;
var reqs = cm.requirements || [];
var reqOk = reqs.every(function (reqName) {
return selectedMasteries[reqName];
});
var tabOk = cm.classification === activeCmTab;
var notPurchased =
!selectedMasteries[cm.name];
return raceOk && classOk && levelOk && spentOk && reqOk && tabOk && notPurchased;
});
if (!filtered.length) {
masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
return;
}
var html = '';
filtered.forEach(function (cm) {
var selectedClass = selectedMasteries[cm.name] ? ' cm-card-selected' : '';
html +=
'<div class="cm-card cm-purchasable-card' + selectedClass + '" data-cm-name="' + cm.name.replace(/"/g, '"') + '">' +
'<div class="cm-card-title">' + cm.name + '</div>' +
'<div class="cm-card-desc">' + cm.description + '</div>' +
'<div class="cm-card-meta">' +
'Level: ' + cm.level +
' | Cost: ' + cm.points +
(cm.cmSpentReq ? ' | CM Spent Req: ' + cm.cmSpentReq : '') +
'</div>' +
'</div>';
});
masteryPanel.innerHTML = html;
Array.prototype.forEach.call(
masteryPanel.querySelectorAll('.cm-purchasable-card'),
function (card) {
card.addEventListener('click', function () {
var cmName = card.getAttribute('data-cm-name');
var cm = (data.masteries || []).find(function (x) {
return x.name === cmName;
});
if (!cm) return;
if (selectedMasteries[cm.name]) {
delete selectedMasteries[cm.name];
} else {
var available = getAvailablePoints();
var spent = getSpentPoints();
if (spent + cm.points > available) {
alert('Not enough CM points.');
return;
}
selectedMasteries[cm.name] = cm;
}
updatePurchasedList();
updateSummary();
});
}
);
}
raceSelect.addEventListener('change', loadClasses);
classSelect.addEventListener('change', updateSummary);
levelInput.addEventListener('input', updateSummary);
Array.prototype.forEach.call(
document.querySelectorAll('.cm-tab-button'),
function (button) {
button.addEventListener('click', function () {
activeCmTab = button.getAttribute('data-tab');
Array.prototype.forEach.call(
document.querySelectorAll('.cm-tab-button'),
function (btn) {
btn.classList.remove('cm-tab-active');
}
);
button.classList.add('cm-tab-active');
updateSummary();
});
}
);
loadRaces();
}
});