MediaWiki:ClassMasteryCalculator.js: Difference between revisions
From EQOA Sandstorm Wiki
Jump to navigationJump to search
RenodorDev (talk | contribs) No edit summary |
RenodorDev (talk | contribs) No edit summary |
||
| Line 151: | Line 151: | ||
werehunter: 'werehunter', | werehunter: 'werehunter', | ||
hunterwere: 'werehunter', | hunterwere: 'werehunter', | ||
werebeast: 'werebeast' | werebeast: 'werebeast' | ||
}; | }; | ||
| Line 286: | Line 285: | ||
var spellsAdded = {}; | var spellsAdded = {}; | ||
var spellsRemoved = {}; | var spellsRemoved = {}; | ||
var | var hasStats = false; | ||
var hasSpells = false; | |||
if (!summary) return; | if (!summary) return; | ||
| Line 293: | Line 293: | ||
var cm = selectedMasteries[name]; | var cm = selectedMasteries[name]; | ||
var stats; | var stats; | ||
if (!cm) return; | if (!cm) return; | ||
| Line 308: | Line 307: | ||
statTotals[statName] += value; | statTotals[statName] += value; | ||
hasStats = true; | |||
} | } | ||
}); | }); | ||
| Line 315: | Line 314: | ||
if (spell) { | if (spell) { | ||
spellsAdded[spell] = true; | spellsAdded[spell] = true; | ||
hasSpells = true; | |||
} | } | ||
}); | }); | ||
| Line 322: | Line 321: | ||
if (spell) { | if (spell) { | ||
spellsRemoved[spell] = true; | spellsRemoved[spell] = true; | ||
hasSpells = true; | |||
} | } | ||
}); | }); | ||
}); | }); | ||
var html = ''; | |||
html += '<div class="cm-unlocked-grid">'; | |||
html += '<div class="cm-unlocked-box">'; | |||
html += '<div class="cm-panel-title">Stats</div>'; | |||
if (!hasStats) { | |||
html += '<div class="cm-empty-line">No stat changes yet.</div>'; | |||
} else { | |||
Object.keys(statTotals).sort().forEach(function (statName) { | Object.keys(statTotals).sort().forEach(function (statName) { | ||
var value = statTotals[statName]; | var value = statTotals[statName]; | ||
| Line 342: | Line 341: | ||
html += | html += | ||
'<div class="cm-purchased-item">' + | '<div class="cm-purchased-item cm-stat-row">' + | ||
'<span>' + escapeHtml(statName) + '</span>' + | '<span>' + escapeHtml(statName) + '</span>' + | ||
'<span class="cm-purchased-cost">' + sign + escapeHtml(value) + '</span>' + | '<span class="cm-purchased-cost">' + sign + escapeHtml(value) + '</span>' + | ||
| Line 349: | Line 348: | ||
} | } | ||
if (Object.keys(spellsAdded).length) { | html += '</div>'; | ||
html += '<div class="cm-unlocked-box">'; | |||
html += '<div class="cm-panel-title">Spells</div>'; | |||
if (!hasSpells) { | |||
html += '<div class="cm-empty-line">No spells gained or removed yet.</div>'; | |||
} else { | |||
if (Object.keys(spellsAdded).length) { | |||
html += '<div class="cm-subtitle">Gained</div>'; | |||
Object.keys(spellsAdded).sort().forEach(function (spell) { | |||
html += | |||
'<div class="cm-purchased-item cm-spell-row cm-spell-gained">' + | |||
'<span>+ ' + escapeHtml(spell) + '</span>' + | |||
'</div>'; | |||
}); | |||
} | |||
if (Object.keys(spellsRemoved).length) { | |||
html += '<div class="cm-subtitle">Removed</div>'; | |||
Object.keys(spellsRemoved).sort().forEach(function (spell) { | |||
html += | |||
'<div class="cm-purchased-item cm-spell-row cm-spell-removed">' + | |||
'<span>- ' + escapeHtml(spell) + '</span>' + | |||
'</div>'; | |||
}); | |||
} | |||
} | } | ||
html += '</div>'; | |||
html += '</div>'; | |||
summary.innerHTML = html; | summary.innerHTML = html; | ||
} | } | ||
| Line 388: | Line 400: | ||
updateUnlockedSummary(); | updateUnlockedSummary(); | ||
renderMasteryList(); | renderMasteryList(); | ||
} | } | ||
Revision as of 21:03, 14 May 2026
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";
var visibleMasteries = [];
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;">' +
'<label>Transformation Path</label>' +
'<select id="cm-path">' +
'<option value="">None</option>' +
'<option value="Werelion">Lionwere</option>' +
'<option value="Werebear">Bearwere</option>' +
'<option value="Werewolf">Wolfwere</option>' +
'<option value="Wererat">Ratwere</option>' +
'<option value="Weregator">Gatorwere</option>' +
'<option value="Vampire">Vampire</option>' +
'<option value="Were Hunter">Were-Hunter</option>' +
'</select>' +
'<button type="button" id="cm-reset" class="cm-reset-button">Reset</button>' +
'</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;">Stats / Spells Gained from CM</div>' +
'<div id="cm-unlocked-summary" class="character-builder-warning-box">' +
'No stats or spells gained yet.' +
'</div>' +
'<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 pathSelect = document.getElementById('cm-path');
var resetButton = document.getElementById('cm-reset');
var cmTooltip = document.getElementById('cm-tooltip');
function safeArray(value) {
return Object.prototype.toString.call(value) === '[object Array]' ? value : [];
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function addOption(select, value, label) {
var option = document.createElement('option');
option.value = value;
option.textContent = label;
select.appendChild(option);
}
function normalizePath(value) {
value = String(value || '').toLowerCase().replace(/[\s-]/g, '');
var map = {
lionwere: 'werelion',
werelion: 'werelion',
bearwere: 'werebear',
werebear: 'werebear',
wolfwere: 'werewolf',
werewolf: 'werewolf',
ratwere: 'wererat',
wererat: 'wererat',
gatorwere: 'weregator',
weregator: 'weregator',
vampire: 'vampire',
werehunter: 'werehunter',
hunterwere: 'werehunter',
werebeast: 'werebeast'
};
return map[value] || value;
}
function getBitePath(name) {
name = String(name || '').toLowerCase();
if (name.indexOf('lion bite') !== -1) return 'werelion';
if (name.indexOf('bear bite') !== -1) return 'werebear';
if (name.indexOf('wolf bite') !== -1) return 'werewolf';
if (name.indexOf('rat bite') !== -1) return 'wererat';
if (name.indexOf('gator bite') !== -1) return 'weregator';
return '';
}
function isSharedWereMastery(name) {
return name === 'Lycanthropic Resistances' ||
name === 'Lycanthropic Regeneration' ||
name === 'Combat Senses';
}
function isWerePath(path) {
return path === 'werelion' ||
path === 'werebear' ||
path === 'werewolf' ||
path === 'wererat' ||
path === 'weregator';
}
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 = safeArray((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="' + escapeHtml(name) + '">' +
'<span>' + escapeHtml(name) + '</span>' +
'<span class="cm-purchased-cost">' + escapeHtml(cm.points) + '</span>' +
'</div>'
);
}).join('');
Array.prototype.forEach.call(
list.querySelectorAll('.cm-purchased-item'),
function (row) {
var cmName = row.getAttribute('data-cm-name');
var cm = selectedMasteries[cmName];
row.addEventListener('mouseenter', function () {
showCmTooltip(cm, row);
});
row.addEventListener('mouseleave', hideCmTooltip);
row.addEventListener('click', function () {
delete selectedMasteries[cmName];
hideCmTooltip();
updateSummary();
});
}
);
}
function updateUnlockedSummary() {
var summary = document.getElementById('cm-unlocked-summary');
var statTotals = {};
var spellsAdded = {};
var spellsRemoved = {};
var hasStats = false;
var hasSpells = false;
if (!summary) return;
Object.keys(selectedMasteries).forEach(function (name) {
var cm = selectedMasteries[name];
var stats;
if (!cm) return;
stats = cm.stats || {};
Object.keys(stats).forEach(function (statName) {
var value = parseInt(stats[statName], 10);
if (!isNaN(value) && value !== 0) {
if (!statTotals[statName]) {
statTotals[statName] = 0;
}
statTotals[statName] += value;
hasStats = true;
}
});
safeArray(cm.spellsAdded).forEach(function (spell) {
if (spell) {
spellsAdded[spell] = true;
hasSpells = true;
}
});
safeArray(cm.spellsRemoved).forEach(function (spell) {
if (spell) {
spellsRemoved[spell] = true;
hasSpells = true;
}
});
});
var html = '';
html += '<div class="cm-unlocked-grid">';
html += '<div class="cm-unlocked-box">';
html += '<div class="cm-panel-title">Stats</div>';
if (!hasStats) {
html += '<div class="cm-empty-line">No stat changes yet.</div>';
} else {
Object.keys(statTotals).sort().forEach(function (statName) {
var value = statTotals[statName];
var sign = value > 0 ? '+' : '';
html +=
'<div class="cm-purchased-item cm-stat-row">' +
'<span>' + escapeHtml(statName) + '</span>' +
'<span class="cm-purchased-cost">' + sign + escapeHtml(value) + '</span>' +
'</div>';
});
}
html += '</div>';
html += '<div class="cm-unlocked-box">';
html += '<div class="cm-panel-title">Spells</div>';
if (!hasSpells) {
html += '<div class="cm-empty-line">No spells gained or removed yet.</div>';
} else {
if (Object.keys(spellsAdded).length) {
html += '<div class="cm-subtitle">Gained</div>';
Object.keys(spellsAdded).sort().forEach(function (spell) {
html +=
'<div class="cm-purchased-item cm-spell-row cm-spell-gained">' +
'<span>+ ' + escapeHtml(spell) + '</span>' +
'</div>';
});
}
if (Object.keys(spellsRemoved).length) {
html += '<div class="cm-subtitle">Removed</div>';
Object.keys(spellsRemoved).sort().forEach(function (spell) {
html +=
'<div class="cm-purchased-item cm-spell-row cm-spell-removed">' +
'<span>- ' + escapeHtml(spell) + '</span>' +
'</div>';
});
}
}
html += '</div>';
html += '</div>';
summary.innerHTML = html;
}
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();
updateUnlockedSummary();
renderMasteryList();
}
function buildCmTooltipHtml(cm) {
var statLines = [];
if (!cm) return '';
Object.keys(cm.stats || {}).forEach(function (key) {
var value = cm.stats[key];
if (value) {
var sign = value > 0 ? '+' : '';
statLines.push(
'<div class="cm-tooltip-stat">' +
escapeHtml(key) + ': ' + sign + escapeHtml(value) +
'</div>'
);
}
});
var spellAdds = safeArray(cm.spellsAdded).map(function (spell) {
return '<div class="cm-tooltip-spell-add">+ ' + escapeHtml(spell) + '</div>';
}).join('');
var spellRemoves = safeArray(cm.spellsRemoved).map(function (spell) {
return '<div class="cm-tooltip-spell-remove">- ' + escapeHtml(spell) + '</div>';
}).join('');
var reqHtml = '';
if (safeArray(cm.requirements).length) {
reqHtml =
'<div class="cm-tooltip-req">' +
'Requires: ' + escapeHtml(safeArray(cm.requirements).join(', ')) +
'</div>';
}
return (
'<div class="cm-tooltip-title">' + escapeHtml(cm.name) + '</div>' +
'<div class="cm-tooltip-desc">' + escapeHtml(cm.description) + '</div>' +
'<div class="cm-tooltip-meta">' +
'Level ' + escapeHtml(cm.level) +
' | Cost ' + escapeHtml(cm.points) +
(cm.cmSpentReq ? ' | CM Spent Req ' + escapeHtml(cm.cmSpentReq) : '') +
'</div>' +
reqHtml +
statLines.join('') +
spellAdds +
spellRemoves
);
}
function showCmTooltip(cm, card) {
if (!cmTooltip || !cm) return;
cmTooltip.innerHTML = buildCmTooltipHtml(cm);
cmTooltip.style.display = 'block';
var rect = card.getBoundingClientRect();
var tooltipWidth = 340;
var leftPos = rect.right + 12;
if (leftPos + tooltipWidth > window.innerWidth) {
leftPos = rect.left - tooltipWidth - 12;
}
cmTooltip.style.left = leftPos + 'px';
cmTooltip.style.top = Math.max(10, rect.top) + 'px';
}
function hideCmTooltip() {
if (cmTooltip) {
cmTooltip.style.display = 'none';
}
}
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 selectedPath = pathSelect ? pathSelect.value : '';
var selectedPathNormalized = normalizePath(selectedPath);
var selectedIsWere = isWerePath(selectedPathNormalized);
var spent = getSpentPoints();
var seenMasteries = {};
visibleMasteries = [];
safeArray(data.masteries).forEach(function (cm) {
var cmRaces = safeArray(cm.races);
var cmClasses = safeArray(cm.classes);
var reqs = safeArray(cm.requirements);
var cmName = String(cm.name || '');
var cmPath = normalizePath(cm.lycanthropy);
var bitePath = getBitePath(cmName);
var pathOk = false;
var raceOk =
!cmRaces.length ||
cmRaces.indexOf(selectedRaceCode) !== -1;
var classOk =
!cmClasses.length ||
cmClasses.indexOf(selectedClass) !== -1;
var levelOk =
!cm.level ||
cm.level <= level;
var spentOk =
!cm.cmSpentReq ||
spent >= cm.cmSpentReq;
var reqOk = reqs.every(function (reqName) {
return selectedMasteries[reqName];
});
var tabOk = cm.classification === activeCmTab;
if (bitePath) {
pathOk = selectedPathNormalized === bitePath;
} else if (!cmPath) {
pathOk = true;
} else if (!selectedPathNormalized) {
pathOk = false;
} else if (selectedIsWere && isSharedWereMastery(cmName)) {
pathOk = cmPath === 'werebeast' || isWerePath(cmPath);
} else if (selectedIsWere && cmPath === 'werebeast') {
pathOk = true;
} else {
pathOk = cmPath === selectedPathNormalized;
}
if (
raceOk &&
classOk &&
levelOk &&
spentOk &&
reqOk &&
tabOk &&
pathOk &&
!selectedMasteries[cmName] &&
!seenMasteries[cmName]
) {
seenMasteries[cmName] = true;
visibleMasteries.push(cm);
}
});
if (!visibleMasteries.length) {
masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
return;
}
masteryPanel.innerHTML = visibleMasteries.map(function (cm, index) {
return (
'<div class="cm-card cm-purchasable-card" data-cm-index="' + index + '">' +
'<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
'<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
'<div class="cm-card-meta">' +
'Level: ' + escapeHtml(cm.level) +
' | Cost: ' + escapeHtml(cm.points) +
(cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
'</div>' +
'</div>'
);
}).join('');
Array.prototype.forEach.call(
masteryPanel.querySelectorAll('.cm-purchasable-card'),
function (card) {
var cmIndex = parseInt(card.getAttribute('data-cm-index'), 10);
var cm = visibleMasteries[cmIndex];
card.addEventListener('mouseenter', function () {
showCmTooltip(cm, card);
});
card.addEventListener('mouseleave', hideCmTooltip);
card.addEventListener('click', function () {
if (!cm) return;
var available = getAvailablePoints();
var spent = getSpentPoints();
if (spent + cm.points > available) {
alert('Not enough CM points.');
return;
}
selectedMasteries[cm.name] = cm;
hideCmTooltip();
updateSummary();
});
}
);
}
raceSelect.addEventListener('change', loadClasses);
classSelect.addEventListener('change', updateSummary);
levelInput.addEventListener('input', updateSummary);
if (pathSelect) {
pathSelect.addEventListener('change', 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();
});
}
);
if (resetButton) {
resetButton.addEventListener('click', function () {
selectedMasteries = {};
activeCmTab = 'General';
levelInput.value = 60;
if (pathSelect) {
pathSelect.value = '';
}
Array.prototype.forEach.call(
document.querySelectorAll('.cm-tab-button'),
function (btn) {
btn.classList.remove('cm-tab-active');
}
);
var generalTab = document.querySelector('.cm-tab-button[data-tab="General"]');
if (generalTab) {
generalTab.classList.add('cm-tab-active');
}
hideCmTooltip();
updateSummary();
});
}
loadRaces();
}
});