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 29: | Line 29: | ||
var selectedMasteries = {}; | var selectedMasteries = {}; | ||
var activeCmTab = "General"; | var activeCmTab = "General"; | ||
var visibleMasteries = []; | |||
app.innerHTML = | app.innerHTML = | ||
| Line 102: | Line 103: | ||
function safeArray(value) { | function safeArray(value) { | ||
return | return Object.prototype.toString.call(value) === '[object Array]' ? value : []; | ||
} | } | ||
function escapeHtml(value) { | function escapeHtml(value) { | ||
return String(value | return String(value == null ? '' : value) | ||
.replace(/&/g, '&') | .replace(/&/g, '&') | ||
.replace(/</g, '<') | .replace(/</g, '<') | ||
| Line 122: | Line 123: | ||
function normalizePath(value) { | 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 | |||
} | } | ||
| Line 231: | Line 236: | ||
list.innerHTML = purchased.map(function (name) { | list.innerHTML = purchased.map(function (name) { | ||
var cm = selectedMasteries[name]; | var cm = selectedMasteries[name]; | ||
return ( | return ( | ||
| Line 250: | Line 254: | ||
showCmTooltip(cm, row); | showCmTooltip(cm, row); | ||
}); | }); | ||
row.addEventListener('mouseleave', hideCmTooltip); | row.addEventListener('mouseleave', hideCmTooltip); | ||
| Line 277: | Line 282: | ||
function buildCmTooltipHtml(cm) { | function buildCmTooltipHtml(cm) { | ||
var statLines = []; | var statLines = []; | ||
if (!cm) return ''; | |||
Object.keys(cm.stats || {}).forEach(function (key) { | Object.keys(cm.stats || {}).forEach(function (key) { | ||
| Line 348: | Line 355: | ||
function 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 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(); | |||
}); | |||
} | |||
); | |||
loadRaces(); | |||
} | |||
}); | |||
Revision as of 20:31, 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>' +
'</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 pathSelect = document.getElementById('cm-path');
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 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 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();
});
}
);
loadRaces();
}
});