MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
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) {
                else if (lvl <= 29) {
                     tp += 5;
                     tp += 5;
                 }
                 } else if (lvl <= 44) {
                else if (lvl <= 44) {
                     tp += 9;
                     tp += 9;
                 }
                 } else if (lvl <= 59) {
                else {
                     tp += 14;
                     tp += 14;
                } else if (lvl === 60) {
                    tp += 3;
                 }
                 }
             }
             }
            tp -= 11;


             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>
                    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>
                    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>
                    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>
                    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>
                    Equipment
                </div>
 
                <div id="cb-loading-message">
                    Loading item categories...
                </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 "";
                    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;
                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(
                        String(item[key]).replace(/[^\d.-]/g, "")
                    );
 
                     item[key] = isNaN(num) ? 0 : num;
                     item[key] = isNaN(num) ? 0 : num;
                 }
                 } else {
                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 {
            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(
                    classSelect,
                    classCode,
                    classCode + ' - ' + className
                );
             });
             });


             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(
                'cb-slot-' + slot.toLowerCase()
            );
 
             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 {
                else {
                     equippedItems[slot] = items[parseInt(select.value, 10)];
                     equippedItems[slot] = items[
                        parseInt(select.value, 10)
                    ];
                 }
                 }


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;


             var base = getBaseStats();
             if (level < 1) level = 1;
            if (level > 60) level = 60;


             if (!base) return;
             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 level = parseInt(levelInput.value, 10) || 1;


             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);
                     var value = parseInt(input.value, 10);
 
                    if (isNaN(value) || value < 0) {
                        value = 0;
                    }
 
                    trainingStats[key] = value;
 
                     updateStats();
                     updateStats();
                 });
                 });
Line 648: Line 586:


         classSelect.addEventListener('change', function () {
         classSelect.addEventListener('change', function () {
             equippedItems = {};
             equippedItems = {};
            buildGearSlots();
            updateStats();
        });


             buildGearSlots();
        levelInput.addEventListener('input', function () {
             Object.keys(trainingStats).forEach(function (key) {
                clampTrainingInput(key);
            });


             updateStats();
             updateStats();
         });
         });
        levelInput.addEventListener('input', 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();
    }

});