MediaWiki:CharacterBuilder.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 23: | Line 23: | ||
function initCharacterBuilder() { | function initCharacterBuilder() { | ||
var app = document.getElementById('character-builder-app'); | var app = document.getElementById('character-builder-app'); | ||
if (!app) return; | if (!app) return; | ||
| Line 135: | Line 134: | ||
<table class="character-builder-stat-table"> | <table class="character-builder-stat-table"> | ||
<tr> | <tr> | ||
<th>STR</th> | <th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th> | ||
<th>HP</th><th>POW</th><th>AC</th> | |||
<th>PR</th><th>DR</th><th>FR</th><th>CR</th><th>LR</th><th>AR</th> | |||
<th>HP</th> | |||
<th>PR</th> | |||
</tr> | </tr> | ||
<tr> | <tr> | ||
<td id="cb-str">0</td> | <td id="cb-str">0</td><td id="cb-sta">0</td><td id="cb-agi">0</td><td id="cb-dex">0</td> | ||
<td id="cb-wis">0</td><td id="cb-int">0</td><td id="cb-cha">0</td> | |||
<td id="cb-hp">0</td><td id="cb-pow">0</td><td id="cb-ac">0</td> | |||
<td id="cb-pr">0</td><td id="cb-dr">0</td><td id="cb-fr">0</td><td id="cb-cr">0</td> | |||
<td id="cb-wis">0</td> | <td id="cb-lr">0</td><td id="cb-ar">0</td> | ||
<td id="cb-hp">0</td> | |||
<td id="cb-pr">0</td> | |||
<td id="cb-lr">0</td> | |||
</tr> | </tr> | ||
</table> | </table> | ||
| Line 190: | Line 165: | ||
<div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div> | <div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div> | ||
<div id="cb-compare-tooltip" class="character-builder-tooltip cb-compare-tooltip" style="display:none;"></div> | |||
</div> | </div> | ||
| Line 203: | Line 179: | ||
var warningBox = document.getElementById('cb-warning-box'); | var warningBox = document.getElementById('cb-warning-box'); | ||
var tooltip = document.getElementById('cb-item-tooltip'); | var tooltip = document.getElementById('cb-item-tooltip'); | ||
var compareTooltip = document.getElementById('cb-compare-tooltip'); | |||
function addOption(select, value, label) { | function addOption(select, value, label) { | ||
| Line 239: | Line 216: | ||
return ["Primary Items", "Secondary Items"]; | return ["Primary Items", "Secondary Items"]; | ||
} | } | ||
return [getCategoryForSlot(slotType)]; | return [getCategoryForSlot(slotType)]; | ||
} | } | ||
| Line 267: | Line 243: | ||
rvprop: "content", | rvprop: "content", | ||
rvslots: "main", | rvslots: "main", | ||
formatversion: "2", | formatversion: "2", | ||
format: "json" | format: "json" | ||
| Line 365: | Line 342: | ||
var characterLevel = getCharacterLevel(); | var characterLevel = getCharacterLevel(); | ||
var itemLevel = parseInt(item.lvl, 10) || 0; | var itemLevel = parseInt(item.lvl, 10) || 0; | ||
if (itemLevel <= 0) return true; | if (itemLevel <= 0) return true; | ||
return itemLevel <= characterLevel; | return itemLevel <= characterLevel; | ||
} | } | ||
| Line 516: | Line 491: | ||
'<label>' + slot + '</label>' + | '<label>' + slot + '</label>' + | ||
'<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' + | '<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' + | ||
'< | '<div class="cb-item-picker-list" id="cb-picker-' + slot.toLowerCase() + '"></div>' + | ||
'<div class="cb-slot-disabled-note" id="cb-disabled-note-' + slot.toLowerCase() + '"></div>'; | '<div class="cb-slot-disabled-note" id="cb-disabled-note-' + slot.toLowerCase() + '"></div>'; | ||
gearGrid.appendChild(slotBox); | gearGrid.appendChild(slotBox); | ||
populateItemPicker(slot, disabledSlots[slot]); | |||
}); | }); | ||
| Line 556: | Line 529: | ||
} | } | ||
function | function populateItemPicker(slot, disabledReason) { | ||
var | var picker = document.getElementById('cb-picker-' + slot.toLowerCase()); | ||
var searchInput = document.getElementById('cb-search-' + slot.toLowerCase()); | var searchInput = document.getElementById('cb-search-' + slot.toLowerCase()); | ||
var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase()); | var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase()); | ||
if (! | if (!picker) return; | ||
if (disabledReason) { | if (disabledReason) { | ||
if (searchInput) searchInput.disabled = true; | if (searchInput) searchInput.disabled = true; | ||
picker.innerHTML = '<div class="cb-picker-disabled">Disabled</div>'; | |||
if (disabledNote) disabledNote.textContent = disabledReason; | if (disabledNote) disabledNote.textContent = disabledReason; | ||
return; | return; | ||
} | } | ||
function | function rebuildPicker() { | ||
var searchText = searchInput ? searchInput.value : ""; | var searchText = searchInput ? searchInput.value : ""; | ||
var filteredItems = getFilteredItemsForSlot(slot, searchText); | var filteredItems = getFilteredItemsForSlot(slot, searchText); | ||
var currentItem = equippedItems[slot]; | var currentItem = equippedItems[slot]; | ||
picker.innerHTML = ''; | |||
var noneRow = document.createElement('div'); | |||
noneRow.className = 'cb-picker-item cb-picker-none'; | |||
noneRow.textContent = '-- None --'; | |||
if (!currentItem) { | |||
noneRow.className += ' cb-picker-selected'; | |||
} | |||
noneRow.addEventListener('click', function () { | |||
delete equippedItems[slot]; | |||
hideTooltip(); | |||
hideCompareTooltip(); | |||
enforceDisabledSlots(); | |||
buildGearSlots(); | |||
updateStats(); | |||
}); | }); | ||
picker.appendChild(noneRow); | |||
var | |||
filteredItems.forEach(function (item) { | |||
var row = document.createElement('div'); | |||
row.className = 'cb-picker-item'; | |||
if ( | if (currentItem === item) { | ||
row.className += ' cb-picker-selected'; | |||
} | } | ||
row.textContent = item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : ''); | |||
row.addEventListener('mouseenter', function (event) { | |||
showCompareTooltip(slot, item, event); | |||
}); | |||
row.addEventListener('mousemove', function (event) { | |||
moveCompareTooltip(event); | |||
}); | |||
row.addEventListener('mouseleave', function () { | |||
hideCompareTooltip(); | |||
}); | |||
row.addEventListener('click', function () { | |||
equippedItems[slot] = item; | |||
hideTooltip(); | |||
hideCompareTooltip(); | |||
enforceDisabledSlots(); | |||
buildGearSlots(); | |||
updateStats(); | |||
}); | |||
picker.appendChild(row); | |||
}); | |||
} | |||
} | |||
rebuildPicker(); | |||
if (searchInput) { | |||
searchInput.addEventListener('input', rebuildPicker); | |||
} | |||
} | } | ||
| Line 670: | Line 654: | ||
Object.keys(disabledSlots).forEach(function (slot) { | Object.keys(disabledSlots).forEach(function (slot) { | ||
warnings.push(slot + " is " + disabledSlots[slot]); | warnings.push(slot + " is " + disabledSlots[slot]); | ||
}); | }); | ||
| Line 734: | Line 719: | ||
html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>'; | html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>'; | ||
return html; | |||
} | |||
function getCompareValue(newItem, oldItem, key) { | |||
var newValue = parseFloat(newItem[key]) || 0; | |||
var oldValue = oldItem ? (parseFloat(oldItem[key]) || 0) : 0; | |||
return newValue - oldValue; | |||
} | |||
function buildCompareTooltipHtml(slot, newItem) { | |||
var oldItem = equippedItems[slot]; | |||
var html = ''; | |||
html += '<div class="cb-tooltip-title">' + newItem.name + '</div>'; | |||
if (oldItem) { | |||
html += '<div class="cb-tooltip-line"><b>Currently:</b> ' + oldItem.name + '</div>'; | |||
} else { | |||
html += '<div class="cb-tooltip-line"><b>Currently:</b> None</div>'; | |||
} | |||
if (newItem.slot) { | |||
html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + newItem.slot + '</div>'; | |||
} | |||
if (newItem.class) { | |||
html += '<div class="cb-tooltip-line"><b>Class:</b> ' + newItem.class + '</div>'; | |||
} | |||
if (newItem.is2HD) { | |||
html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>'; | |||
} | |||
var compareRows = []; | |||
function addCompare(label, key) { | |||
var diff = getCompareValue(newItem, oldItem, key); | |||
if (diff === 0) return; | |||
var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative'; | |||
var sign = diff > 0 ? '+' : ''; | |||
compareRows.push( | |||
'<div><span>' + label + '</span> <b class="' + cls + '">' + sign + diff + '</b></div>' | |||
); | |||
} | |||
addCompare("Lvl:", "lvl"); | |||
addCompare("DMG:", "dmg"); | |||
addCompare("AC:", "ac"); | |||
addCompare("STR:", "str"); | |||
addCompare("STA:", "sta"); | |||
addCompare("AGI:", "agi"); | |||
addCompare("DEX:", "dex"); | |||
addCompare("WIS:", "wis"); | |||
addCompare("INT:", "int"); | |||
addCompare("CHA:", "cha"); | |||
addCompare("HP:", "hp"); | |||
addCompare("HoT:", "hot"); | |||
addCompare("POW:", "pow"); | |||
addCompare("PoT:", "pot"); | |||
addCompare("PR:", "pr"); | |||
addCompare("DR:", "dr"); | |||
addCompare("FR:", "fr"); | |||
addCompare("CR:", "cr"); | |||
addCompare("LR:", "lr"); | |||
addCompare("AR:", "ar"); | |||
addCompare("DUR:", "dur"); | |||
html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Comparison</b></div>'; | |||
if (compareRows.length) { | |||
html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>'; | |||
} else { | |||
html += '<div class="cb-tooltip-line">No stat difference.</div>'; | |||
} | |||
html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>'; | |||
html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>'; | |||
return html; | return html; | ||
| Line 754: | Line 819: | ||
if (!tooltip) return; | if (!tooltip) return; | ||
tooltip.style.display = 'none'; | tooltip.style.display = 'none'; | ||
} | |||
function showCompareTooltip(slot, item, event) { | |||
if (!compareTooltip || !item) return; | |||
compareTooltip.innerHTML = buildCompareTooltipHtml(slot, item); | |||
compareTooltip.style.display = 'block'; | |||
moveCompareTooltip(event); | |||
} | |||
function moveCompareTooltip(event) { | |||
if (!compareTooltip || compareTooltip.style.display === 'none') return; | |||
compareTooltip.style.left = (event.pageX + 14) + 'px'; | |||
compareTooltip.style.top = (event.pageY + 14) + 'px'; | |||
} | |||
function hideCompareTooltip() { | |||
if (!compareTooltip) return; | |||
compareTooltip.style.display = 'none'; | |||
} | } | ||
| Line 795: | Line 878: | ||
function getBaseStats() { | function getBaseStats() { | ||
var race = raceSelect.value; | var race = raceSelect.value; | ||
var classCode = classSelect.value; | var classCode = classSelect.value; | ||
| Line 840: | Line 922: | ||
function addStat(total, item, key) { | function addStat(total, item, key) { | ||
total[key] += parseFloat(item[key]) || 0; | total[key] += parseFloat(item[key]) || 0; | ||
} | |||
function setCappedStat(elementId, value, cap) { | |||
var element = document.getElementById(elementId); | |||
if (!element) return; | |||
value = parseFloat(value) || 0; | |||
if (value > cap) { | |||
var over = value - cap; | |||
element.innerHTML = cap + ' <span class="cb-overcap">(+' + over + ')</span>'; | |||
} else { | |||
element.textContent = value; | |||
} | |||
} | } | ||
| Line 869: | Line 965: | ||
Object.keys(trainingStats).forEach(function (key) { | Object.keys(trainingStats).forEach(function (key) { | ||
total[key] += parseInt(trainingStats[key], 10) || 0; | total[key] += parseInt(trainingStats[key], 10) || 0; | ||
}); | }); | ||
| Line 902: | Line 999: | ||
document.getElementById('cb-tp-remaining').textContent = remainingTP; | document.getElementById('cb-tp-remaining').textContent = remainingTP; | ||
setCappedStat('cb-str', total.str, 650); | |||
setCappedStat('cb-sta', total.sta, 650); | |||
setCappedStat('cb-agi', total.agi, 650); | |||
setCappedStat('cb-dex', total.dex, 650); | |||
setCappedStat('cb-wis', total.wis, 650); | |||
setCappedStat('cb-int', total.int, 650); | |||
setCappedStat('cb-cha', total.cha, 650); | |||
setCappedStat('cb-str', total.str, 650); | |||
setCappedStat('cb-sta', total.sta, 650); | |||
setCappedStat('cb-agi', total.agi, 650); | |||
setCappedStat('cb-dex', total.dex, 650); | |||
setCappedStat('cb-wis', total.wis, 650); | |||
setCappedStat('cb-int', total.int, 650); | |||
setCappedStat('cb-cha', total.cha, 650); | |||
document.getElementById('cb-hp').textContent = total.hp; | document.getElementById('cb-hp').textContent = total.hp; | ||
document.getElementById('cb-pow').textContent = total.pow; | document.getElementById('cb-pow').textContent = total.pow; | ||
document.getElementById('cb-ac').textContent = total.ac; | document.getElementById('cb-ac').textContent = total.ac; | ||
setCappedStat('cb-pr', total.pr, 315); | setCappedStat('cb-pr', total.pr, 315); | ||
setCappedStat('cb-dr', total.dr, 315); | setCappedStat('cb-dr', total.dr, 315); | ||
setCappedStat('cb-fr', total.fr, 315); | setCappedStat('cb-fr', total.fr, 315); | ||
setCappedStat('cb-cr', total.cr, 315); | setCappedStat('cb-cr', total.cr, 315); | ||
setCappedStat('cb-lr', total.lr, 315); | setCappedStat('cb-lr', total.lr, 315); | ||
setCappedStat('cb-ar', total.ar, 315); | setCappedStat('cb-ar', total.ar, 315); | ||
updateEquippedSummary(); | updateEquippedSummary(); | ||
| Line 991: | Line 1,064: | ||
equippedItems = {}; | equippedItems = {}; | ||
hideTooltip(); | hideTooltip(); | ||
hideCompareTooltip(); | |||
loadAllSlotItems(); | loadAllSlotItems(); | ||
}); | }); | ||
Revision as of 05:19, 14 May 2026
mw.hook('wikipage.content').add(function () {
function waitForBuilderData(attemptsLeft) {
if (window.EQOA_BUILDER_DATA) {
initCharacterBuilder();
return;
}
if (attemptsLeft <= 0) {
var app = document.getElementById('character-builder-app');
if (app) {
app.innerHTML = '<div style="color:red;">EQOA_BUILDER_DATA failed to load.</div>';
}
return;
}
setTimeout(function () {
waitForBuilderData(attemptsLeft - 1);
}, 100);
}
waitForBuilderData(30);
function initCharacterBuilder() {
var app = document.getElementById('character-builder-app');
if (!app) return;
var data = window.EQOA_BUILDER_DATA;
var equippedItems = {};
var itemCache = {};
var statKeys = [
"lvl", "str", "sta", "agi", "dex", "wis", "int", "cha",
"hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar",
"dmg", "hot", "pot", "dur"
];
var trainingStats = {
str: 0,
sta: 0,
agi: 0,
dex: 0,
wis: 0,
int: 0,
cha: 0
};
var gearSlots = [
"Head", "Chest", "Hands", "Legs", "Feet",
"Forearm", "Bracelet1", "Bracelet2",
"Waist", "Neck", "Ear1", "Ear2",
"Ring1", "Ring2", "Primary", "Secondary",
"Shield", "Held1", "Held2", "Robe", "Range"
];
function getTrainingPointsForLevel(level) {
level = parseInt(level, 10);
if (isNaN(level) || level < 1) level = 1;
if (level > 60) level = 60;
var tp = 20;
for (var lvl = 2; lvl <= level; lvl++) {
if (lvl <= 14) tp += 3;
else if (lvl <= 29) tp += 5;
else if (lvl <= 44) tp += 9;
else if (lvl <= 59) tp += 14;
else if (lvl === 60) tp += 3;
}
return tp;
}
app.innerHTML = `
<div class="character-builder-box">
<div class="character-builder-title">EQOA Character Builder</div>
<div class="character-builder-controls">
<label>Race</label>
<select id="cb-race"></select>
<label>Class</label>
<select id="cb-class"></select>
<label>Level</label>
<input type="number" id="cb-level" min="1" max="60" value="60" style="width:70px;">
<button id="cb-refresh-items">Refresh Items</button>
</div>
<div id="cb-warning-box" class="character-builder-warning-box" style="display:none;"></div>
<div class="character-builder-section-title" style="margin-top:15px;">Training Points</div>
<table class="character-builder-stat-table">
<tr>
<th>Available TP</th>
<th>Spent TP</th>
<th>Remaining TP</th>
</tr>
<tr>
<td id="cb-tp-available">482</td>
<td id="cb-tp-spent">0</td>
<td id="cb-tp-remaining">482</td>
</tr>
</table>
<div class="character-builder-section-title" style="margin-top:18px;">Training Allocation</div>
<table class="character-builder-stat-table">
<tr>
<th>STR</th>
<th>STA</th>
<th>AGI</th>
<th>DEX</th>
<th>WIS</th>
<th>INT</th>
<th>CHA</th>
</tr>
<tr>
<td><input type="number" id="train-str" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-sta" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-agi" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-dex" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-wis" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-int" min="0" value="0" style="width:60px;"></td>
<td><input type="number" id="train-cha" min="0" value="0" style="width:60px;"></td>
</tr>
</table>
<div class="character-builder-section-title" style="margin-top:18px;">Total Stats</div>
<table class="character-builder-stat-table">
<tr>
<th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th>
<th>HP</th><th>POW</th><th>AC</th>
<th>PR</th><th>DR</th><th>FR</th><th>CR</th><th>LR</th><th>AR</th>
</tr>
<tr>
<td id="cb-str">0</td><td id="cb-sta">0</td><td id="cb-agi">0</td><td id="cb-dex">0</td>
<td id="cb-wis">0</td><td id="cb-int">0</td><td id="cb-cha">0</td>
<td id="cb-hp">0</td><td id="cb-pow">0</td><td id="cb-ac">0</td>
<td id="cb-pr">0</td><td id="cb-dr">0</td><td id="cb-fr">0</td><td id="cb-cr">0</td>
<td id="cb-lr">0</td><td id="cb-ar">0</td>
</tr>
</table>
<div class="character-builder-equipment-layout">
<div class="character-builder-equipped-summary">
<div class="character-builder-section-title" style="margin-top:18px;">Equipped Items</div>
<table class="character-builder-equipped-table">
<tbody id="cb-equipped-summary-body"></tbody>
</table>
</div>
<div class="character-builder-gear-column">
<div class="character-builder-section-title" style="margin-top:18px;">Equipment</div>
<div id="cb-loading-message">Loading item categories...</div>
<div id="cb-gear-grid" class="character-builder-gear-grid"></div>
</div>
</div>
<div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div>
<div id="cb-compare-tooltip" class="character-builder-tooltip cb-compare-tooltip" style="display:none;"></div>
</div>
`;
var raceSelect = document.getElementById('cb-race');
var classSelect = document.getElementById('cb-class');
var levelInput = document.getElementById('cb-level');
var gearGrid = document.getElementById('cb-gear-grid');
var loadingMessage = document.getElementById('cb-loading-message');
var refreshButton = document.getElementById('cb-refresh-items');
var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
var warningBox = document.getElementById('cb-warning-box');
var tooltip = document.getElementById('cb-item-tooltip');
var compareTooltip = document.getElementById('cb-compare-tooltip');
function addOption(select, value, label) {
var option = document.createElement('option');
option.value = value;
option.textContent = label;
select.appendChild(option);
}
Object.keys(data.baseStats).forEach(function (race) {
addOption(raceSelect, race, race);
});
function getCharacterLevel() {
var level = parseInt(levelInput.value, 10);
if (isNaN(level) || level < 1) level = 1;
if (level > 60) level = 60;
levelInput.value = level;
return level;
}
function getBuilderSlotType(slot) {
if (slot.indexOf("Bracelet") === 0) return "Bracelet";
if (slot.indexOf("Ring") === 0) return "Ring";
if (slot.indexOf("Ear") === 0) return "Ear";
if (slot.indexOf("Held") === 0) return "Held";
return slot;
}
function getCategoryForSlot(slotType) {
return slotType + " Items";
}
function getCategoriesForSlotType(slotType) {
if (slotType === "Primary") {
return ["Primary Items", "Secondary Items"];
}
return [getCategoryForSlot(slotType)];
}
function apiGet(params) {
return new mw.Api().get(params);
}
function getPagesInCategory(categoryName) {
return apiGet({
action: "query",
list: "categorymembers",
cmtitle: "Category:" + categoryName,
cmlimit: "500",
cmnamespace: "0",
format: "json"
}).then(function (data) {
return data.query.categorymembers || [];
});
}
function getPageWikitext(title) {
return apiGet({
action: "query",
prop: "revisions",
titles: title,
rvprop: "content",
rvslots: "main",
formatversion: "2",
format: "json"
}).then(function (data) {
var page = data.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) return "";
if (page.revisions[0].slots && page.revisions[0].slots.main) {
return page.revisions[0].slots.main.content || "";
}
return page.revisions[0].content || "";
});
}
function parseCategories(wikitext) {
var categories = [];
var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
var match;
while ((match = regex.exec(wikitext)) !== null) {
categories.push(match[1].trim());
}
return categories;
}
function parseItemBox(wikitext, fallbackName) {
var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i);
if (!match) return null;
var content = match[1];
var item = { name: fallbackName };
var lines = content.split("\n");
lines.forEach(function (line) {
line = line.trim();
if (line.indexOf("|") !== 0) return;
var equalIndex = line.indexOf("=");
if (equalIndex === -1) return;
var key = line.substring(1, equalIndex).trim().toLowerCase();
var value = line.substring(equalIndex + 1).trim();
if (!key) return;
item[key] = value;
});
if (!item.name) item.name = fallbackName;
item.categories = parseCategories(wikitext);
item.is2HD = item.categories.indexOf("2HD Items") !== -1 ||
String(item.weaponstyle || "").toUpperCase() === "2HD";
item.isShield = item.categories.indexOf("Shield Items") !== -1 ||
String(item.slot || "").toLowerCase() === "shield";
item.isHeld = item.categories.indexOf("Held Items") !== -1 ||
String(item.slot || "").toLowerCase() === "held";
item.isSecondary = item.categories.indexOf("Secondary Items") !== -1 ||
String(item.slot || "").toLowerCase() === "secondary";
item.isPrimary = item.categories.indexOf("Primary Items") !== -1 ||
String(item.slot || "").toLowerCase() === "primary";
statKeys.forEach(function (key) {
if (item[key] !== undefined && item[key] !== "") {
var num = parseFloat(String(item[key]).replace(/[^\d.-]/g, ""));
item[key] = isNaN(num) ? 0 : num;
} else {
item[key] = 0;
}
});
if (item.class) {
item.classList = item.class.split(",").map(function (x) {
return x.trim().toUpperCase();
});
} else {
item.classList = [];
}
return item;
}
function classCanUseItem(item, classCode) {
if (!item.classList || item.classList.length === 0) return true;
if (item.classList.indexOf("ALL") !== -1) return true;
return item.classList.indexOf(classCode) !== -1;
}
function levelCanUseItem(item) {
var characterLevel = getCharacterLevel();
var itemLevel = parseInt(item.lvl, 10) || 0;
if (itemLevel <= 0) return true;
return itemLevel <= characterLevel;
}
function dedupePages(pages) {
var seen = {};
var result = [];
pages.forEach(function (page) {
if (!seen[page.title]) {
seen[page.title] = true;
result.push(page);
}
});
return result;
}
function loadItemsForSlotType(slotType) {
if (itemCache[slotType]) {
return Promise.resolve(itemCache[slotType]);
}
var categories = getCategoriesForSlotType(slotType);
var categoryPromises = categories.map(function (categoryName) {
return getPagesInCategory(categoryName);
});
return Promise.all(categoryPromises).then(function (categoryResults) {
var pages = [];
categoryResults.forEach(function (result) {
pages = pages.concat(result);
});
pages = dedupePages(pages);
var pagePromises = pages.map(function (page) {
return getPageWikitext(page.title).then(function (wikitext) {
return parseItemBox(wikitext, page.title);
});
});
return Promise.all(pagePromises).then(function (items) {
items = items.filter(function (item) {
return item !== null;
});
itemCache[slotType] = items;
return items;
});
});
}
function loadAllSlotItems() {
loadingMessage.textContent = "Loading item categories...";
var uniqueSlotTypes = [];
gearSlots.forEach(function (slot) {
var slotType = getBuilderSlotType(slot);
if (uniqueSlotTypes.indexOf(slotType) === -1) {
uniqueSlotTypes.push(slotType);
}
});
var promises = uniqueSlotTypes.map(function (slotType) {
return loadItemsForSlotType(slotType);
});
return Promise.all(promises).then(function () {
loadingMessage.textContent = "Items loaded.";
buildGearSlots();
updateStats();
}).catch(function (error) {
loadingMessage.textContent = "Error loading items.";
console.error(error);
});
}
function getDisabledSlots() {
var disabled = {};
var primary = equippedItems.Primary;
var forearm = equippedItems.Forearm;
var bracelet1 = equippedItems.Bracelet1;
var bracelet2 = equippedItems.Bracelet2;
if (primary && primary.is2HD) {
disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
disabled.Shield = "Disabled because a 2HD weapon is equipped in Primary.";
disabled.Held1 = "Disabled because a 2HD weapon is equipped in Primary.";
disabled.Held2 = "Disabled because a 2HD weapon is equipped in Primary.";
}
if (forearm) {
disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
}
if (bracelet1 || bracelet2) {
disabled.Forearm = "Disabled because Bracelet1 or Bracelet2 is equipped.";
}
return disabled;
}
function enforceDisabledSlots() {
var disabled = getDisabledSlots();
Object.keys(disabled).forEach(function (slot) {
if (equippedItems[slot]) {
delete equippedItems[slot];
}
});
}
function loadClasses() {
classSelect.innerHTML = '';
var race = raceSelect.value;
Object.keys(data.baseStats[race]).forEach(function (classCode) {
var className = data.classes[classCode] || classCode;
addOption(classSelect, classCode, classCode + ' - ' + className);
});
equippedItems = {};
buildGearSlots();
updateStats();
}
function buildGearSlots() {
gearGrid.innerHTML = '';
var disabledSlots = getDisabledSlots();
gearSlots.forEach(function (slot) {
var slotBox = document.createElement('div');
slotBox.className = 'character-builder-slot';
if (disabledSlots[slot]) {
slotBox.className += ' cb-slot-disabled';
}
slotBox.innerHTML =
'<label>' + slot + '</label>' +
'<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' +
'<div class="cb-item-picker-list" id="cb-picker-' + slot.toLowerCase() + '"></div>' +
'<div class="cb-slot-disabled-note" id="cb-disabled-note-' + slot.toLowerCase() + '"></div>';
gearGrid.appendChild(slotBox);
populateItemPicker(slot, disabledSlots[slot]);
});
updateEquippedSummary();
}
function getFilteredItemsForSlot(slot, searchText) {
var classCode = classSelect.value;
var slotType = getBuilderSlotType(slot);
var items = itemCache[slotType] || [];
searchText = (searchText || "").toLowerCase();
return items.filter(function (item) {
if (!classCanUseItem(item, classCode)) return false;
if (!levelCanUseItem(item)) return false;
if (slot === "Primary") {
if (!(item.isPrimary || item.isSecondary)) return false;
}
if (slot === "Secondary") {
if (!item.isSecondary) return false;
}
if (searchText && item.name.toLowerCase().indexOf(searchText) === -1) {
return false;
}
return true;
});
}
function populateItemPicker(slot, disabledReason) {
var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
var searchInput = document.getElementById('cb-search-' + slot.toLowerCase());
var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase());
if (!picker) return;
if (disabledReason) {
if (searchInput) searchInput.disabled = true;
picker.innerHTML = '<div class="cb-picker-disabled">Disabled</div>';
if (disabledNote) disabledNote.textContent = disabledReason;
return;
}
function rebuildPicker() {
var searchText = searchInput ? searchInput.value : "";
var filteredItems = getFilteredItemsForSlot(slot, searchText);
var currentItem = equippedItems[slot];
picker.innerHTML = '';
var noneRow = document.createElement('div');
noneRow.className = 'cb-picker-item cb-picker-none';
noneRow.textContent = '-- None --';
if (!currentItem) {
noneRow.className += ' cb-picker-selected';
}
noneRow.addEventListener('click', function () {
delete equippedItems[slot];
hideTooltip();
hideCompareTooltip();
enforceDisabledSlots();
buildGearSlots();
updateStats();
});
picker.appendChild(noneRow);
filteredItems.forEach(function (item) {
var row = document.createElement('div');
row.className = 'cb-picker-item';
if (currentItem === item) {
row.className += ' cb-picker-selected';
}
row.textContent = item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : '');
row.addEventListener('mouseenter', function (event) {
showCompareTooltip(slot, item, event);
});
row.addEventListener('mousemove', function (event) {
moveCompareTooltip(event);
});
row.addEventListener('mouseleave', function () {
hideCompareTooltip();
});
row.addEventListener('click', function () {
equippedItems[slot] = item;
hideTooltip();
hideCompareTooltip();
enforceDisabledSlots();
buildGearSlots();
updateStats();
});
picker.appendChild(row);
});
}
rebuildPicker();
if (searchInput) {
searchInput.addEventListener('input', rebuildPicker);
}
}
function validateEquipment() {
var warnings = [];
var primary = equippedItems.Primary;
var secondary = equippedItems.Secondary;
var shield = equippedItems.Shield;
var held1 = equippedItems.Held1;
var held2 = equippedItems.Held2;
var forearm = equippedItems.Forearm;
var bracelet1 = equippedItems.Bracelet1;
var bracelet2 = equippedItems.Bracelet2;
if (forearm && (bracelet1 || bracelet2)) {
warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
}
if (primary && primary.is2HD) {
if (secondary) warnings.push("2HD weapon in Primary conflicts with Secondary.");
if (shield) warnings.push("2HD weapon in Primary conflicts with Shield.");
if (held1 || held2) warnings.push("2HD weapon in Primary conflicts with Held1/Held2.");
}
if (shield && secondary) {
warnings.push("Shield conflicts with Secondary weapon.");
}
if (shield && (held1 || held2)) {
warnings.push("Shield conflicts with Held1/Held2.");
}
if ((held1 || held2) && (primary || secondary || shield)) {
warnings.push("Held items may conflict with Primary / Secondary / Shield. Confirm this setup is valid.");
}
return warnings;
}
function updateWarnings() {
var warnings = validateEquipment();
var disabledSlots = getDisabledSlots();
Object.keys(disabledSlots).forEach(function (slot) {
warnings.push(slot + " is " + disabledSlots[slot]);
});
if (!warningBox) return;
if (warnings.length === 0) {
warningBox.style.display = "none";
warningBox.innerHTML = "";
return;
}
warningBox.style.display = "block";
warningBox.innerHTML =
"<b>Equipment Warnings</b><br>" +
warnings.map(function (w) {
return "ΓÇó " + w;
}).join("<br>");
}
function formatItemStats(item) {
var rows = [];
function add(label, key) {
if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
rows.push('<div><span>' + label + '</span> ' + item[key] + '</div>');
}
}
add("Lvl:", "lvl");
add("DMG:", "dmg");
add("AC:", "ac");
add("STR:", "str");
add("STA:", "sta");
add("AGI:", "agi");
add("DEX:", "dex");
add("WIS:", "wis");
add("INT:", "int");
add("CHA:", "cha");
add("HP:", "hp");
add("HoT:", "hot");
add("POW:", "pow");
add("PoT:", "pot");
add("PR:", "pr");
add("DR:", "dr");
add("FR:", "fr");
add("CR:", "cr");
add("LR:", "lr");
add("AR:", "ar");
add("DUR:", "dur");
return rows.join("");
}
function buildTooltipHtml(item) {
var classText = item.class || "";
var slotText = item.slot || "";
var html = '';
html += '<div class="cb-tooltip-title">' + item.name + '</div>';
if (slotText) html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + slotText + '</div>';
if (classText) html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</div>';
if (item.is2HD) html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';
return html;
}
function getCompareValue(newItem, oldItem, key) {
var newValue = parseFloat(newItem[key]) || 0;
var oldValue = oldItem ? (parseFloat(oldItem[key]) || 0) : 0;
return newValue - oldValue;
}
function buildCompareTooltipHtml(slot, newItem) {
var oldItem = equippedItems[slot];
var html = '';
html += '<div class="cb-tooltip-title">' + newItem.name + '</div>';
if (oldItem) {
html += '<div class="cb-tooltip-line"><b>Currently:</b> ' + oldItem.name + '</div>';
} else {
html += '<div class="cb-tooltip-line"><b>Currently:</b> None</div>';
}
if (newItem.slot) {
html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + newItem.slot + '</div>';
}
if (newItem.class) {
html += '<div class="cb-tooltip-line"><b>Class:</b> ' + newItem.class + '</div>';
}
if (newItem.is2HD) {
html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
}
var compareRows = [];
function addCompare(label, key) {
var diff = getCompareValue(newItem, oldItem, key);
if (diff === 0) return;
var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative';
var sign = diff > 0 ? '+' : '';
compareRows.push(
'<div><span>' + label + '</span> <b class="' + cls + '">' + sign + diff + '</b></div>'
);
}
addCompare("Lvl:", "lvl");
addCompare("DMG:", "dmg");
addCompare("AC:", "ac");
addCompare("STR:", "str");
addCompare("STA:", "sta");
addCompare("AGI:", "agi");
addCompare("DEX:", "dex");
addCompare("WIS:", "wis");
addCompare("INT:", "int");
addCompare("CHA:", "cha");
addCompare("HP:", "hp");
addCompare("HoT:", "hot");
addCompare("POW:", "pow");
addCompare("PoT:", "pot");
addCompare("PR:", "pr");
addCompare("DR:", "dr");
addCompare("FR:", "fr");
addCompare("CR:", "cr");
addCompare("LR:", "lr");
addCompare("AR:", "ar");
addCompare("DUR:", "dur");
html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Comparison</b></div>';
if (compareRows.length) {
html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>';
} else {
html += '<div class="cb-tooltip-line">No stat difference.</div>';
}
html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>';
html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';
return html;
}
function showTooltip(item, event) {
if (!tooltip || !item) return;
tooltip.innerHTML = buildTooltipHtml(item);
tooltip.style.display = 'block';
moveTooltip(event);
}
function moveTooltip(event) {
if (!tooltip || tooltip.style.display === 'none') return;
tooltip.style.left = (event.pageX + 14) + 'px';
tooltip.style.top = (event.pageY + 14) + 'px';
}
function hideTooltip() {
if (!tooltip) return;
tooltip.style.display = 'none';
}
function showCompareTooltip(slot, item, event) {
if (!compareTooltip || !item) return;
compareTooltip.innerHTML = buildCompareTooltipHtml(slot, item);
compareTooltip.style.display = 'block';
moveCompareTooltip(event);
}
function moveCompareTooltip(event) {
if (!compareTooltip || compareTooltip.style.display === 'none') return;
compareTooltip.style.left = (event.pageX + 14) + 'px';
compareTooltip.style.top = (event.pageY + 14) + 'px';
}
function hideCompareTooltip() {
if (!compareTooltip) return;
compareTooltip.style.display = 'none';
}
function updateEquippedSummary() {
if (!equippedSummaryBody) return;
equippedSummaryBody.innerHTML = '';
gearSlots.forEach(function (slot) {
var item = equippedItems[slot];
var row = document.createElement('tr');
var slotCell = document.createElement('td');
slotCell.textContent = slot;
slotCell.style.fontWeight = 'bold';
var itemCell = document.createElement('td');
if (item) {
var itemSpan = document.createElement('span');
itemSpan.textContent = item.name;
itemSpan.className = 'cb-equipped-item-name';
itemSpan.addEventListener('mouseenter', function (event) {
showTooltip(item, event);
});
itemSpan.addEventListener('mousemove', moveTooltip);
itemSpan.addEventListener('mouseleave', hideTooltip);
itemCell.appendChild(itemSpan);
} else {
itemCell.textContent = '-';
}
row.appendChild(slotCell);
row.appendChild(itemCell);
equippedSummaryBody.appendChild(row);
});
}
function getBaseStats() {
var race = raceSelect.value;
var classCode = classSelect.value;
return data.baseStats[race][classCode];
}
function getSpentTrainingPoints() {
var total = 0;
Object.keys(trainingStats).forEach(function (key) {
total += parseInt(trainingStats[key], 10) || 0;
});
return total;
}
function clampTrainingInput(changedKey) {
var level = getCharacterLevel();
var availableTP = getTrainingPointsForLevel(level);
var spentOther = 0;
Object.keys(trainingStats).forEach(function (key) {
if (key !== changedKey) {
spentOther += parseInt(trainingStats[key], 10) || 0;
}
});
var input = document.getElementById('train-' + changedKey);
var value = parseInt(input.value, 10);
if (isNaN(value) || value < 0) value = 0;
var maxAllowed = availableTP - spentOther;
if (maxAllowed < 0) maxAllowed = 0;
if (value > maxAllowed) {
value = maxAllowed;
input.value = value;
}
trainingStats[changedKey] = value;
}
function addStat(total, item, key) {
total[key] += parseFloat(item[key]) || 0;
}
function setCappedStat(elementId, value, cap) {
var element = document.getElementById(elementId);
if (!element) return;
value = parseFloat(value) || 0;
if (value > cap) {
var over = value - cap;
element.innerHTML = cap + ' <span class="cb-overcap">(+' + over + ')</span>';
} else {
element.textContent = value;
}
}
function updateStats() {
var base = getBaseStats();
if (!base) return;
var level = getCharacterLevel();
var total = {
str: base.str || 0,
sta: base.sta || 0,
agi: base.agi || 0,
dex: base.dex || 0,
wis: base.wis || 0,
int: base.int || 0,
cha: base.cha || 0,
hp: base.hp || 0,
pow: base.pow || 0,
ac: base.ac || 0,
pr: 0,
dr: 0,
fr: 0,
cr: 0,
lr: 0,
ar: 0
};
Object.keys(trainingStats).forEach(function (key) {
total[key] += parseInt(trainingStats[key], 10) || 0;
});
Object.keys(equippedItems).forEach(function (slot) {
var item = equippedItems[slot];
addStat(total, item, "str");
addStat(total, item, "sta");
addStat(total, item, "agi");
addStat(total, item, "dex");
addStat(total, item, "wis");
addStat(total, item, "int");
addStat(total, item, "cha");
addStat(total, item, "hp");
addStat(total, item, "pow");
addStat(total, item, "ac");
addStat(total, item, "pr");
addStat(total, item, "dr");
addStat(total, item, "fr");
addStat(total, item, "cr");
addStat(total, item, "lr");
addStat(total, item, "ar");
});
var availableTP = getTrainingPointsForLevel(level);
var spentTP = getSpentTrainingPoints();
var remainingTP = availableTP - spentTP;
if (remainingTP < 0) remainingTP = 0;
document.getElementById('cb-tp-available').textContent = availableTP;
document.getElementById('cb-tp-spent').textContent = spentTP;
document.getElementById('cb-tp-remaining').textContent = remainingTP;
setCappedStat('cb-str', total.str, 650);
setCappedStat('cb-sta', total.sta, 650);
setCappedStat('cb-agi', total.agi, 650);
setCappedStat('cb-dex', total.dex, 650);
setCappedStat('cb-wis', total.wis, 650);
setCappedStat('cb-int', total.int, 650);
setCappedStat('cb-cha', total.cha, 650);
document.getElementById('cb-hp').textContent = total.hp;
document.getElementById('cb-pow').textContent = total.pow;
document.getElementById('cb-ac').textContent = total.ac;
setCappedStat('cb-pr', total.pr, 315);
setCappedStat('cb-dr', total.dr, 315);
setCappedStat('cb-fr', total.fr, 315);
setCappedStat('cb-cr', total.cr, 315);
setCappedStat('cb-lr', total.lr, 315);
setCappedStat('cb-ar', total.ar, 315);
updateEquippedSummary();
updateWarnings();
}
function removeInvalidEquippedItemsForLevel() {
Object.keys(equippedItems).forEach(function (slot) {
if (!levelCanUseItem(equippedItems[slot])) {
delete equippedItems[slot];
}
});
}
function bindTrainingInputs() {
Object.keys(trainingStats).forEach(function (key) {
var input = document.getElementById('train-' + key);
input.addEventListener('input', function () {
clampTrainingInput(key);
updateStats();
});
});
}
raceSelect.addEventListener('change', loadClasses);
classSelect.addEventListener('change', function () {
equippedItems = {};
buildGearSlots();
updateStats();
});
levelInput.addEventListener('input', function () {
Object.keys(trainingStats).forEach(function (key) {
clampTrainingInput(key);
});
removeInvalidEquippedItemsForLevel();
enforceDisabledSlots();
buildGearSlots();
updateStats();
});
refreshButton.addEventListener('click', function () {
itemCache = {};
equippedItems = {};
hideTooltip();
hideCompareTooltip();
loadAllSlotItems();
});
loadClasses();
bindTrainingInputs();
loadAllSlotItems();
updateStats();
}
});