MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
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;


         var data = window.EQOA_BUILDER_DATA;
         var data = window.EQOA_BUILDER_DATA;
         var equippedItems = {};
         var equippedItems = {};
         var itemCache = {};
         var itemCache = {};
Line 35: Line 37:
             "dmg", "hot", "pot", "dur"
             "dmg", "hot", "pot", "dur"
         ];
         ];
        var trainingStats = {
            str: 0,
            sta: 0,
            agi: 0,
            dex: 0,
            wis: 0,
            int: 0,
            cha: 0
        };


         var gearSlots = [
         var gearSlots = [
Line 59: Line 71:
             "Range"
             "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 {
                    tp += 14;
                }
            }
            tp -= 11;
            return tp;
        }


         app.innerHTML = `
         app.innerHTML = `
             <div class="character-builder-box">
             <div class="character-builder-box">
                 <div class="character-builder-title">EQOA Character Builder</div>
 
                 <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 70: Line 116:
                     <label>Class</label>
                     <label>Class</label>
                     <select id="cb-class"></select>
                     <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>
                     <button id="cb-refresh-items">Refresh Items</button>
                 </div>
                 </div>


                 <div class="character-builder-section-title">Total Stats</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"></td>
                        <td><input type="number" id="train-sta" min="0" value="0"></td>
                        <td><input type="number" id="train-agi" min="0" value="0"></td>
                        <td><input type="number" id="train-dex" min="0" value="0"></td>
                        <td><input type="number" id="train-wis" min="0" value="0"></td>
                        <td><input type="number" id="train-int" min="0" value="0"></td>
                        <td><input type="number" id="train-cha" min="0" value="0"></td>
                    </tr>
                </table>
 
                <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 95: Line 190:
                         <th>AR</th>
                         <th>AR</th>
                     </tr>
                     </tr>
                     <tr>
                     <tr>
                         <td id="cb-str">0</td>
                         <td id="cb-str">0</td>
Line 113: Line 209:
                         <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;">Equipment</div>
                 <div class="character-builder-section-title" style="margin-top:18px;">
                 <div id="cb-loading-message">Loading item categories...</div>
                    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>
             </div>
             </div>
         `;
         `;
Line 123: Line 227:
         var raceSelect = document.getElementById('cb-race');
         var raceSelect = document.getElementById('cb-race');
         var classSelect = document.getElementById('cb-class');
         var classSelect = document.getElementById('cb-class');
        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 139: Line 245:


         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 155: Line 263:


         function getPagesInCategory(categoryName) {
         function getPagesInCategory(categoryName) {
             return apiGet({
             return apiGet({
                 action: "query",
                 action: "query",
Line 163: Line 272:
                 format: "json"
                 format: "json"
             }).then(function (data) {
             }).then(function (data) {
                 return data.query.categorymembers || [];
                 return data.query.categorymembers || [];
             });
             });
Line 168: Line 278:


         function getPageWikitext(title) {
         function getPageWikitext(title) {
             return apiGet({
             return apiGet({
                 action: "query",
                 action: "query",
Line 177: Line 288:
                 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]) return "";
 
                 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 189: Line 304:


         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;


             var content = match[1];
             var content = match[1];
             var item = {
             var item = {
                 name: fallbackName
                 name: fallbackName
Line 200: Line 318:


             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 214: Line 335:
             });
             });


             if (!item.name) item.name = fallbackName;
             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(
                        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 226: Line 355:


             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 237: Line 368:


         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 250: Line 385:


             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 257: Line 394:


                 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 262: Line 400:


                     itemCache[slotType] = items;
                     itemCache[slotType] = items;
                     return items;
                     return items;
                 });
                 });
Line 268: Line 407:


         function loadAllSlotItems() {
         function loadAllSlotItems() {
             loadingMessage.textContent = "Loading item categories...";
             loadingMessage.textContent = "Loading item categories...";


Line 273: Line 413:


             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 284: Line 426:


             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. Check category names and ItemBox pages.";
 
                 loadingMessage.textContent = "Error loading items.";
 
                 console.error(error);
                 console.error(error);
             });
             });
Line 294: Line 442:


         function loadClasses() {
         function loadClasses() {
             classSelect.innerHTML = '';
             classSelect.innerHTML = '';


Line 299: Line 448:


             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 328: Line 488:


         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;


Line 342: Line 508:


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


Line 353: Line 523:


         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() {
            var total = 0;
            Object.keys(trainingStats).forEach(function (key) {
                total += parseInt(trainingStats[key], 10) || 0;
            });
            return total;
         }
         }


Line 364: Line 546:


         function updateStats() {
         function updateStats() {
             var base = getBaseStats();
             var base = getBaseStats();
             if (!base) return;
             if (!base) return;


Line 385: Line 569:
                 ar: 0
                 ar: 0
             };
             };
            Object.keys(trainingStats).forEach(function (key) {
                total[key] += parseInt(trainingStats[key], 10) || 0;
            });


             Object.keys(equippedItems).forEach(function (slot) {
             Object.keys(equippedItems).forEach(function (slot) {
                 var item = equippedItems[slot];
                 var item = equippedItems[slot];


Line 406: Line 595:
                 addStat(total, item, "ar");
                 addStat(total, item, "ar");
             });
             });
            var level = parseInt(levelInput.value, 10) || 1;
            var availableTP = getTrainingPointsForLevel(level);
            var spentTP = getSpentTrainingPoints();
            var remainingTP = availableTP - spentTP;
            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-str').textContent = total.str;
Line 423: Line 622:
             document.getElementById('cb-lr').textContent = total.lr;
             document.getElementById('cb-lr').textContent = total.lr;
             document.getElementById('cb-ar').textContent = total.ar;
             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 () {
                    var value = parseInt(input.value, 10);
                    if (isNaN(value) || value < 0) {
                        value = 0;
                    }
                    trainingStats[key] = value;
                    updateStats();
                });
            });
         }
         }


Line 428: Line 648:


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


         refreshButton.addEventListener('click', function () {
         refreshButton.addEventListener('click', function () {
             itemCache = {};
             itemCache = {};
             equippedItems = {};
             equippedItems = {};
             loadAllSlotItems();
             loadAllSlotItems();
         });
         });


         loadClasses();
         loadClasses();
        bindTrainingInputs();
         loadAllSlotItems();
         loadAllSlotItems();
        updateStats();
     }
     }


});
});

Revision as of 03:01, 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 {
                    tp += 14;
                }
            }

            tp -= 11;

            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"></td>
                        <td><input type="number" id="train-sta" min="0" value="0"></td>
                        <td><input type="number" id="train-agi" min="0" value="0"></td>
                        <td><input type="number" id="train-dex" min="0" value="0"></td>
                        <td><input type="number" id="train-wis" min="0" value="0"></td>
                        <td><input type="number" id="train-int" min="0" value="0"></td>
                        <td><input type="number" id="train-cha" min="0" value="0"></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 addStat(total, item, key) {
            total[key] += parseFloat(item[key]) || 0;
        }

        function updateStats() {

            var base = getBaseStats();

            if (!base) return;

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

            var availableTP = getTrainingPointsForLevel(level);
            var spentTP = getSpentTrainingPoints();
            var remainingTP = availableTP - spentTP;

            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 () {

                    var value = parseInt(input.value, 10);

                    if (isNaN(value) || value < 0) {
                        value = 0;
                    }

                    trainingStats[key] = value;

                    updateStats();
                });
            });
        }

        raceSelect.addEventListener('change', loadClasses);

        classSelect.addEventListener('change', function () {

            equippedItems = {};

            buildGearSlots();

            updateStats();
        });

        levelInput.addEventListener('input', updateStats);

        refreshButton.addEventListener('click', function () {

            itemCache = {};
            equippedItems = {};

            loadAllSlotItems();
        });

        loadClasses();

        bindTrainingInputs();

        loadAllSlotItems();

        updateStats();
    }

});