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 28: | Line 28: | ||
var data = window.EQOA_BUILDER_DATA; | var data = window.EQOA_BUILDER_DATA; | ||
var equippedItems = {}; | var equippedItems = {}; | ||
var itemCache = {}; | var itemCache = {}; | ||
| Line 73: | Line 72: | ||
function getTrainingPointsForLevel(level) { | function getTrainingPointsForLevel(level) { | ||
level = parseInt(level, 10); | level = parseInt(level, 10); | ||
| Line 82: | Line 80: | ||
for (var lvl = 2; lvl <= level; lvl++) { | for (var lvl = 2; lvl <= level; lvl++) { | ||
if (lvl <= 14) { | if (lvl <= 14) { | ||
tp += 3; | tp += 3; | ||
} | } else if (lvl <= 29) { | ||
tp += 5; | tp += 5; | ||
} | } else if (lvl <= 44) { | ||
tp += 9; | tp += 9; | ||
} | } else if (lvl <= 59) { | ||
tp += 14; | tp += 14; | ||
} else if (lvl === 60) { | |||
tp += 3; | |||
} | } | ||
} | } | ||
return tp; | return tp; | ||
| Line 105: | Line 99: | ||
<div class="character-builder-box"> | <div class="character-builder-box"> | ||
<div class="character-builder-title"> | <div class="character-builder-title">EQOA Character Builder</div> | ||
<div class="character-builder-controls"> | <div class="character-builder-controls"> | ||
<label>Race</label> | <label>Race</label> | ||
<select id="cb-race"></select> | <select id="cb-race"></select> | ||
| Line 123: | Line 114: | ||
</div> | </div> | ||
<div class="character-builder-section-title" style="margin-top:15px;"> | <div class="character-builder-section-title" style="margin-top:15px;">Training Points</div> | ||
<table class="character-builder-stat-table"> | <table class="character-builder-stat-table"> | ||
| Line 140: | Line 129: | ||
</table> | </table> | ||
<div class="character-builder-section-title" style="margin-top:18px;"> | <div class="character-builder-section-title" style="margin-top:18px;">Training Allocation</div> | ||
<table class="character-builder-stat-table"> | <table class="character-builder-stat-table"> | ||
| Line 154: | Line 141: | ||
<th>CHA</th> | <th>CHA</th> | ||
</tr> | </tr> | ||
<tr> | <tr> | ||
<td><input type="number" id="train-str" min="0" value="0"></td> | <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"></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"></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"></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"></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"></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"></td> | <td><input type="number" id="train-cha" min="0" value="0" style="width:60px;"></td> | ||
</tr> | </tr> | ||
</table> | </table> | ||
<div class="character-builder-section-title" style="margin-top:18px;"> | <div class="character-builder-section-title" style="margin-top:18px;">Total Stats</div> | ||
<table class="character-builder-stat-table"> | <table class="character-builder-stat-table"> | ||
<tr> | <tr> | ||
<th>STR</th> | <th>STR</th> | ||
| Line 190: | Line 173: | ||
<th>AR</th> | <th>AR</th> | ||
</tr> | </tr> | ||
<tr> | <tr> | ||
<td id="cb-str">0</td> | <td id="cb-str">0</td> | ||
| Line 209: | Line 191: | ||
<td id="cb-ar">0</td> | <td id="cb-ar">0</td> | ||
</tr> | </tr> | ||
</table> | </table> | ||
<div class="character-builder-section-title" style="margin-top:18px;"> | <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 id="cb-gear-grid" class="character-builder-gear-grid"></div> | ||
| Line 228: | Line 204: | ||
var classSelect = document.getElementById('cb-class'); | var classSelect = document.getElementById('cb-class'); | ||
var levelInput = document.getElementById('cb-level'); | var levelInput = document.getElementById('cb-level'); | ||
var gearGrid = document.getElementById('cb-gear-grid'); | var gearGrid = document.getElementById('cb-gear-grid'); | ||
var loadingMessage = document.getElementById('cb-loading-message'); | var loadingMessage = document.getElementById('cb-loading-message'); | ||
| Line 245: | Line 220: | ||
function getBuilderSlotType(slot) { | function getBuilderSlotType(slot) { | ||
if (slot.indexOf("Bracelet") === 0) return "Bracelet"; | if (slot.indexOf("Bracelet") === 0) return "Bracelet"; | ||
if (slot.indexOf("Ring") === 0) return "Ring"; | if (slot.indexOf("Ring") === 0) return "Ring"; | ||
if (slot.indexOf("Ear") === 0) return "Ear"; | if (slot.indexOf("Ear") === 0) return "Ear"; | ||
if (slot.indexOf("Held") === 0) return "Held"; | if (slot.indexOf("Held") === 0) return "Held"; | ||
return slot; | return slot; | ||
} | } | ||
| Line 263: | Line 236: | ||
function getPagesInCategory(categoryName) { | function getPagesInCategory(categoryName) { | ||
return apiGet({ | return apiGet({ | ||
action: "query", | action: "query", | ||
| Line 272: | Line 244: | ||
format: "json" | format: "json" | ||
}).then(function (data) { | }).then(function (data) { | ||
return data.query.categorymembers || []; | return data.query.categorymembers || []; | ||
}); | }); | ||
| Line 278: | Line 249: | ||
function getPageWikitext(title) { | function getPageWikitext(title) { | ||
return apiGet({ | return apiGet({ | ||
action: "query", | action: "query", | ||
| Line 288: | Line 258: | ||
format: "json" | format: "json" | ||
}).then(function (data) { | }).then(function (data) { | ||
var page = data.query.pages[0]; | var page = data.query.pages[0]; | ||
if (!page || !page.revisions || !page.revisions[0]) | if (!page || !page.revisions || !page.revisions[0]) return ""; | ||
if (page.revisions[0].slots && page.revisions[0].slots.main) { | if (page.revisions[0].slots && page.revisions[0].slots.main) { | ||
| Line 304: | Line 271: | ||
function parseItemBox(wikitext, fallbackName) { | function parseItemBox(wikitext, fallbackName) { | ||
var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i); | var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i); | ||
if (!match) return null; | if (!match) return null; | ||
| Line 318: | Line 283: | ||
lines.forEach(function (line) { | lines.forEach(function (line) { | ||
line = line.trim(); | line = line.trim(); | ||
if (line.indexOf("|") !== 0) return; | if (line.indexOf("|") !== 0) return; | ||
var equalIndex = line.indexOf("="); | var equalIndex = line.indexOf("="); | ||
if (equalIndex === -1) return; | if (equalIndex === -1) return; | ||
| Line 335: | Line 297: | ||
}); | }); | ||
if (!item.name) | if (!item.name) item.name = fallbackName; | ||
statKeys.forEach(function (key) { | statKeys.forEach(function (key) { | ||
if (item[key] !== undefined && item[key] !== "") { | if (item[key] !== undefined && item[key] !== "") { | ||
var num = parseFloat(String(item[key]).replace(/[^\d.-]/g, "")); | |||
var num = parseFloat( | |||
item[key] = isNaN(num) ? 0 : num; | item[key] = isNaN(num) ? 0 : num; | ||
} | } else { | ||
item[key] = 0; | item[key] = 0; | ||
} | } | ||
| Line 355: | Line 309: | ||
if (item.class) { | if (item.class) { | ||
item.classList = item.class.split(",").map(function (x) { | item.classList = item.class.split(",").map(function (x) { | ||
return x.trim().toUpperCase(); | return x.trim().toUpperCase(); | ||
}); | }); | ||
} | } else { | ||
item.classList = []; | item.classList = []; | ||
} | } | ||
| Line 368: | Line 320: | ||
function classCanUseItem(item, classCode) { | function classCanUseItem(item, classCode) { | ||
if (!item.classList || item.classList.length === 0) return true; | if (!item.classList || item.classList.length === 0) return true; | ||
if (item.classList.indexOf("ALL") !== -1) return true; | if (item.classList.indexOf("ALL") !== -1) return true; | ||
return item.classList.indexOf(classCode) !== -1; | return item.classList.indexOf(classCode) !== -1; | ||
} | } | ||
function loadItemsForSlotType(slotType) { | function loadItemsForSlotType(slotType) { | ||
if (itemCache[slotType]) { | if (itemCache[slotType]) { | ||
return Promise.resolve(itemCache[slotType]); | return Promise.resolve(itemCache[slotType]); | ||
| Line 385: | Line 333: | ||
return getPagesInCategory(categoryName).then(function (pages) { | return getPagesInCategory(categoryName).then(function (pages) { | ||
var pagePromises = pages.map(function (page) { | var pagePromises = pages.map(function (page) { | ||
return getPageWikitext(page.title).then(function (wikitext) { | return getPageWikitext(page.title).then(function (wikitext) { | ||
return parseItemBox(wikitext, page.title); | return parseItemBox(wikitext, page.title); | ||
| Line 394: | Line 340: | ||
return Promise.all(pagePromises).then(function (items) { | return Promise.all(pagePromises).then(function (items) { | ||
items = items.filter(function (item) { | items = items.filter(function (item) { | ||
return item !== null; | return item !== null; | ||
| Line 400: | Line 345: | ||
itemCache[slotType] = items; | itemCache[slotType] = items; | ||
return items; | return items; | ||
}); | }); | ||
| Line 407: | Line 351: | ||
function loadAllSlotItems() { | function loadAllSlotItems() { | ||
loadingMessage.textContent = "Loading item categories..."; | loadingMessage.textContent = "Loading item categories..."; | ||
| Line 413: | Line 356: | ||
gearSlots.forEach(function (slot) { | gearSlots.forEach(function (slot) { | ||
var slotType = getBuilderSlotType(slot); | var slotType = getBuilderSlotType(slot); | ||
if (uniqueSlotTypes.indexOf(slotType) === -1) { | if (uniqueSlotTypes.indexOf(slotType) === -1) { | ||
uniqueSlotTypes.push(slotType); | uniqueSlotTypes.push(slotType); | ||
| Line 426: | Line 367: | ||
return Promise.all(promises).then(function () { | return Promise.all(promises).then(function () { | ||
loadingMessage.textContent = "Items loaded."; | loadingMessage.textContent = "Items loaded."; | ||
buildGearSlots(); | buildGearSlots(); | ||
updateStats(); | updateStats(); | ||
}).catch(function (error) { | }).catch(function (error) { | ||
loadingMessage.textContent = "Error loading items."; | loadingMessage.textContent = "Error loading items."; | ||
console.error(error); | console.error(error); | ||
}); | }); | ||
| Line 442: | Line 377: | ||
function loadClasses() { | function loadClasses() { | ||
classSelect.innerHTML = ''; | classSelect.innerHTML = ''; | ||
| Line 448: | Line 382: | ||
Object.keys(data.baseStats[race]).forEach(function (classCode) { | Object.keys(data.baseStats[race]).forEach(function (classCode) { | ||
var className = data.classes[classCode] || classCode; | var className = data.classes[classCode] || classCode; | ||
addOption(classSelect, classCode, classCode + ' - ' + className); | |||
addOption( | |||
}); | }); | ||
equippedItems = {}; | equippedItems = {}; | ||
buildGearSlots(); | buildGearSlots(); | ||
updateStats(); | updateStats(); | ||
} | } | ||
function buildGearSlots() { | function buildGearSlots() { | ||
gearGrid.innerHTML = ''; | gearGrid.innerHTML = ''; | ||
gearSlots.forEach(function (slot) { | gearSlots.forEach(function (slot) { | ||
var slotBox = document.createElement('div'); | var slotBox = document.createElement('div'); | ||
slotBox.className = 'character-builder-slot'; | slotBox.className = 'character-builder-slot'; | ||
| Line 488: | Line 411: | ||
function populateSlotDropdown(slot) { | function populateSlotDropdown(slot) { | ||
var select = document.getElementById('cb-slot-' + slot.toLowerCase()); | |||
var select = document.getElementById( | |||
if (!select) return; | if (!select) return; | ||
var classCode = classSelect.value; | var classCode = classSelect.value; | ||
var slotType = getBuilderSlotType(slot); | var slotType = getBuilderSlotType(slot); | ||
var items = itemCache[slotType] || []; | var items = itemCache[slotType] || []; | ||
items.forEach(function (item, index) { | items.forEach(function (item, index) { | ||
if (!classCanUseItem(item, classCode)) return; | if (!classCanUseItem(item, classCode)) return; | ||
addOption(select, index, item.name); | addOption(select, index, item.name); | ||
}); | }); | ||
select.addEventListener('change', function () { | select.addEventListener('change', function () { | ||
if (select.value === "") { | if (select.value === "") { | ||
delete equippedItems[slot]; | delete equippedItems[slot]; | ||
} | } else { | ||
equippedItems[slot] = items[parseInt(select.value, 10)]; | |||
equippedItems[slot] = items[ | |||
} | } | ||
| Line 523: | Line 435: | ||
function getBaseStats() { | function getBaseStats() { | ||
var race = raceSelect.value; | var race = raceSelect.value; | ||
var classCode = classSelect.value; | var classCode = classSelect.value; | ||
return data.baseStats[race][classCode]; | return data.baseStats[race][classCode]; | ||
} | } | ||
function getSpentTrainingPoints() { | function getSpentTrainingPoints() { | ||
var total = 0; | var total = 0; | ||
| Line 539: | Line 448: | ||
return total; | return total; | ||
} | |||
function clampTrainingInput(changedKey) { | |||
var level = parseInt(levelInput.value, 10) || 1; | |||
if (level < 1) level = 1; | |||
if (level > 60) level = 60; | |||
levelInput.value = level; | |||
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; | |||
} | } | ||
| Line 546: | Line 490: | ||
function updateStats() { | function updateStats() { | ||
var base = getBaseStats(); | |||
if (!base) return; | |||
var level = parseInt(levelInput.value, 10) || 1; | |||
if (level < 1) level = 1; | |||
if (level > 60) level = 60; | |||
levelInput.value = level; | |||
var total = { | var total = { | ||
| Line 575: | Line 524: | ||
Object.keys(equippedItems).forEach(function (slot) { | Object.keys(equippedItems).forEach(function (slot) { | ||
var item = equippedItems[slot]; | var item = equippedItems[slot]; | ||
| Line 595: | Line 543: | ||
addStat(total, item, "ar"); | addStat(total, item, "ar"); | ||
}); | }); | ||
var availableTP = getTrainingPointsForLevel(level); | var availableTP = getTrainingPointsForLevel(level); | ||
var spentTP = getSpentTrainingPoints(); | var spentTP = getSpentTrainingPoints(); | ||
var remainingTP = availableTP - spentTP; | var remainingTP = availableTP - spentTP; | ||
if (remainingTP < 0) remainingTP = 0; | |||
document.getElementById('cb-tp-available').textContent = availableTP; | document.getElementById('cb-tp-available').textContent = availableTP; | ||
| Line 625: | Line 573: | ||
function bindTrainingInputs() { | function bindTrainingInputs() { | ||
Object.keys(trainingStats).forEach(function (key) { | Object.keys(trainingStats).forEach(function (key) { | ||
var input = document.getElementById('train-' + key); | var input = document.getElementById('train-' + key); | ||
input.addEventListener('input', function () { | input.addEventListener('input', function () { | ||
clampTrainingInput(key); | |||
updateStats(); | updateStats(); | ||
}); | }); | ||
| Line 648: | Line 586: | ||
classSelect.addEventListener('change', function () { | classSelect.addEventListener('change', function () { | ||
equippedItems = {}; | equippedItems = {}; | ||
buildGearSlots(); | |||
updateStats(); | |||
}); | |||
levelInput.addEventListener('input', function () { | |||
Object.keys(trainingStats).forEach(function (key) { | |||
clampTrainingInput(key); | |||
}); | |||
updateStats(); | updateStats(); | ||
}); | }); | ||
refreshButton.addEventListener('click', function () { | refreshButton.addEventListener('click', function () { | ||
itemCache = {}; | itemCache = {}; | ||
equippedItems = {}; | equippedItems = {}; | ||
loadAllSlotItems(); | loadAllSlotItems(); | ||
}); | }); | ||
loadClasses(); | loadClasses(); | ||
bindTrainingInputs(); | bindTrainingInputs(); | ||
loadAllSlotItems(); | loadAllSlotItems(); | ||
updateStats(); | updateStats(); | ||
} | } | ||
}); | }); | ||
Revision as of 03:07, 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 = [
"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 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-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>
`;
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');
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 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 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 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;
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 loadItemsForSlotType(slotType) {
if (itemCache[slotType]) {
return Promise.resolve(itemCache[slotType]);
}
var categoryName = getCategoryForSlot(slotType);
return getPagesInCategory(categoryName).then(function (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 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 = '';
gearSlots.forEach(function (slot) {
var slotBox = document.createElement('div');
slotBox.className = 'character-builder-slot';
slotBox.innerHTML =
'<label>' + slot + '</label>' +
'<select id="cb-slot-' + slot.toLowerCase() + '">' +
'<option value="">-- None --</option>' +
'</select>';
gearGrid.appendChild(slotBox);
populateSlotDropdown(slot);
});
}
function populateSlotDropdown(slot) {
var select = document.getElementById('cb-slot-' + slot.toLowerCase());
if (!select) return;
var classCode = classSelect.value;
var slotType = getBuilderSlotType(slot);
var items = itemCache[slotType] || [];
items.forEach(function (item, index) {
if (!classCanUseItem(item, classCode)) return;
addOption(select, index, item.name);
});
select.addEventListener('change', function () {
if (select.value === "") {
delete equippedItems[slot];
} else {
equippedItems[slot] = items[parseInt(select.value, 10)];
}
updateStats();
});
}
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 = parseInt(levelInput.value, 10) || 1;
if (level < 1) level = 1;
if (level > 60) level = 60;
levelInput.value = level;
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 updateStats() {
var base = getBaseStats();
if (!base) return;
var level = parseInt(levelInput.value, 10) || 1;
if (level < 1) level = 1;
if (level > 60) level = 60;
levelInput.value = level;
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;
document.getElementById('cb-str').textContent = total.str;
document.getElementById('cb-sta').textContent = total.sta;
document.getElementById('cb-agi').textContent = total.agi;
document.getElementById('cb-dex').textContent = total.dex;
document.getElementById('cb-wis').textContent = total.wis;
document.getElementById('cb-int').textContent = total.int;
document.getElementById('cb-cha').textContent = total.cha;
document.getElementById('cb-hp').textContent = total.hp;
document.getElementById('cb-pow').textContent = total.pow;
document.getElementById('cb-ac').textContent = total.ac;
document.getElementById('cb-pr').textContent = total.pr;
document.getElementById('cb-dr').textContent = total.dr;
document.getElementById('cb-fr').textContent = total.fr;
document.getElementById('cb-cr').textContent = total.cr;
document.getElementById('cb-lr').textContent = total.lr;
document.getElementById('cb-ar').textContent = total.ar;
}
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);
});
updateStats();
});
refreshButton.addEventListener('click', function () {
itemCache = {};
equippedItems = {};
loadAllSlotItems();
});
loadClasses();
bindTrainingInputs();
loadAllSlotItems();
updateStats();
}
});