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 231: | Line 231: | ||
list.innerHTML = purchased.map(function (name) { | list.innerHTML = purchased.map(function (name) { | ||
var cm = selectedMasteries[name]; | var cm = selectedMasteries[name]; | ||
return ( | return ( | ||
| Line 499: | Line 500: | ||
); | ); | ||
} | } | ||
Revision as of 20:29, 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";
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 Array.isArray(value) ? value : [];
}
function escapeHtml(value) {
return String(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 findMasteryByName(name) {
var masteries = safeArray(data.masteries);
var i;
for (i = 0; i < masteries.length; i++) {
if (masteries[i].name === name) {
return masteries[i];
}
}
return null;
}
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 = [];
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 spent = getSpentPoints();
var werePaths = [
"werelion",
"werebear",
"werewolf",
"wererat",
"weregator"
];
var selectedIsWere =
werePaths.indexOf(selectedPathNormalized) !== -1;
var sharedWereMasteries = {
"Lycanthropic Resistances": true,
"Lycanthropic Regeneration": true,
"Combat Senses": true
};
var seenMasteries = {};
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 cmPath = normalizePath(cm.lycanthropy);
var cmIsTransformation = cmPath !== "";
var pathOk = false;
if (!cmIsTransformation) {
pathOk = true;
} else if (!selectedPathNormalized) {
pathOk = false;
} else if (selectedIsWere && cmPath === "werebeast") {
pathOk = true;
} else if (selectedIsWere && sharedWereMasteries[cm.name] && werePaths.indexOf(cmPath) !== -1) {
pathOk = true;
} else {
pathOk = cmPath === selectedPathNormalized;
}
var notPurchased =
!selectedMasteries[cm.name];
if (
!raceOk ||
!classOk ||
!levelOk ||
!spentOk ||
!reqOk ||
!tabOk ||
!pathOk ||
!notPurchased
) {
return false;
}
if (seenMasteries[cm.name]) {
return false;
}
seenMasteries[cm.name] = true;
return true;
});
if (!filtered.length) {
masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
return;
}
var html = '';
filtered.forEach(function (cm) {
html +=
'<div class="cm-card cm-purchasable-card" 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) {
var cmName = card.getAttribute('data-cm-name');
var cm = (data.masteries || []).find(function (x) {
return x.name === cmName;
});
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();
updatePurchasedList();
updateSummary();
});
}
);
}