MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
No edit summary
Line 207: Line 207:
                     </div>
                     </div>
                 </div>
                 </div>
                <div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div>


             </div>
             </div>
Line 218: Line 220:
         var refreshButton = document.getElementById('cb-refresh-items');
         var refreshButton = document.getElementById('cb-refresh-items');
         var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
         var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
        var tooltip = document.getElementById('cb-item-tooltip');


         function addOption(select, value, label) {
         function addOption(select, value, label) {
Line 450: Line 453:
             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) {
Line 469: Line 471:
                 if (select.value === "") {
                 if (select.value === "") {
                     delete equippedItems[slot];
                     delete equippedItems[slot];
                    hideTooltip();
                 } else {
                 } else {
                     equippedItems[slot] = items[parseInt(select.value, 10)];
                     equippedItems[slot] = items[parseInt(select.value, 10)];
Line 475: Line 478:
                 updateStats();
                 updateStats();
             });
             });
            select.addEventListener('mouseenter', function (event) {
                if (equippedItems[slot]) {
                    showTooltip(equippedItems[slot], event);
                }
            });
            select.addEventListener('mousemove', function (event) {
                moveTooltip(event);
            });
            select.addEventListener('mouseleave', hideTooltip);
        }
        function formatItemStats(item) {
            var rows = [];
            function add(label, key) {
                if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
                    rows.push('<div><span>' + label + '</span> ' + item[key] + '</div>');
                }
            }
            add("Lvl:", "lvl");
            add("DMG:", "dmg");
            add("AC:", "ac");
            add("STR:", "str");
            add("STA:", "sta");
            add("AGI:", "agi");
            add("DEX:", "dex");
            add("WIS:", "wis");
            add("INT:", "int");
            add("CHA:", "cha");
            add("HP:", "hp");
            add("HoT:", "hot");
            add("POW:", "pow");
            add("PoT:", "pot");
            add("PR:", "pr");
            add("DR:", "dr");
            add("FR:", "fr");
            add("CR:", "cr");
            add("LR:", "lr");
            add("AR:", "ar");
            add("DUR:", "dur");
            return rows.join("");
        }
        function buildTooltipHtml(item) {
            var classText = item.class || "";
            var slotText = item.slot || "";
            var html = '';
            html += '<div class="cb-tooltip-title">' + item.name + '</div>';
            if (slotText) {
                html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + slotText + '</div>';
            }
            if (classText) {
                html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</div>';
            }
            html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';
            return html;
        }
        function showTooltip(item, event) {
            if (!tooltip || !item) return;
            tooltip.innerHTML = buildTooltipHtml(item);
            tooltip.style.display = 'block';
            moveTooltip(event);
        }
        function moveTooltip(event) {
            if (!tooltip || tooltip.style.display === 'none') return;
            var x = event.pageX + 14;
            var y = event.pageY + 14;
            tooltip.style.left = x + 'px';
            tooltip.style.top = y + 'px';
        }
        function hideTooltip() {
            if (!tooltip) return;
            tooltip.style.display = 'none';
         }
         }


Line 492: Line 584:


                 var itemCell = document.createElement('td');
                 var itemCell = document.createElement('td');
                 itemCell.textContent = item ? item.name : '-';
 
                 if (item) {
                    var itemSpan = document.createElement('span');
                    itemSpan.textContent = item.name;
                    itemSpan.className = 'cb-equipped-item-name';
 
                    itemSpan.addEventListener('mouseenter', function (event) {
                        showTooltip(item, event);
                    });
 
                    itemSpan.addEventListener('mousemove', function (event) {
                        moveTooltip(event);
                    });
 
                    itemSpan.addEventListener('mouseleave', hideTooltip);
 
                    itemCell.appendChild(itemSpan);
                } else {
                    itemCell.textContent = '-';
                }


                 row.appendChild(slotCell);
                 row.appendChild(slotCell);
Line 555: Line 666:


             var level = getCharacterLevel();
             var level = getCharacterLevel();


             var total = {
             var total = {
Line 670: Line 782:
             itemCache = {};
             itemCache = {};
             equippedItems = {};
             equippedItems = {};
            hideTooltip();
             loadAllSlotItems();
             loadAllSlotItems();
         });
         });

Revision as of 03:51, 14 May 2026

mw.hook('wikipage.content').add(function () {

    function waitForBuilderData(attemptsLeft) {
        if (window.EQOA_BUILDER_DATA) {
            initCharacterBuilder();
            return;
        }

        if (attemptsLeft <= 0) {
            var app = document.getElementById('character-builder-app');
            if (app) {
                app.innerHTML = '<div style="color:red;">EQOA_BUILDER_DATA failed to load.</div>';
            }
            return;
        }

        setTimeout(function () {
            waitForBuilderData(attemptsLeft - 1);
        }, 100);
    }

    waitForBuilderData(30);

    function initCharacterBuilder() {

        var app = document.getElementById('character-builder-app');
        if (!app) return;

        var data = window.EQOA_BUILDER_DATA;
        var equippedItems = {};
        var itemCache = {};

        var statKeys = [
            "lvl", "str", "sta", "agi", "dex", "wis", "int", "cha",
            "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar",
            "dmg", "hot", "pot", "dur"
        ];

        var trainingStats = {
            str: 0,
            sta: 0,
            agi: 0,
            dex: 0,
            wis: 0,
            int: 0,
            cha: 0
        };

        var gearSlots = [
            "Head",
            "Chest",
            "Hands",
            "Legs",
            "Feet",
            "Forearm",
            "Bracelet1",
            "Bracelet2",
            "Waist",
            "Neck",
            "Ear1",
            "Ear2",
            "Ring1",
            "Ring2",
            "Primary",
            "Secondary",
            "Shield",
            "Held1",
            "Held2",
            "Robe",
            "Range"
        ];

        function getTrainingPointsForLevel(level) {
            level = parseInt(level, 10);

            if (isNaN(level) || level < 1) level = 1;
            if (level > 60) level = 60;

            var tp = 20;

            for (var lvl = 2; lvl <= level; lvl++) {
                if (lvl <= 14) {
                    tp += 3;
                } else if (lvl <= 29) {
                    tp += 5;
                } else if (lvl <= 44) {
                    tp += 9;
                } else if (lvl <= 59) {
                    tp += 14;
                } else if (lvl === 60) {
                    tp += 3;
                }
            }

            return tp;
        }

        app.innerHTML = `
            <div class="character-builder-box">

                <div class="character-builder-title">EQOA Character Builder</div>

                <div class="character-builder-controls">
                    <label>Race</label>
                    <select id="cb-race"></select>

                    <label>Class</label>
                    <select id="cb-class"></select>

                    <label>Level</label>
                    <input type="number" id="cb-level" min="1" max="60" value="60" style="width:70px;">

                    <button id="cb-refresh-items">Refresh Items</button>
                </div>

                <div 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 class="character-builder-equipment-layout">
                    <div id="cb-gear-grid" class="character-builder-gear-grid"></div>

                    <div class="character-builder-equipped-summary">
                        <div class="character-builder-section-title">Equipped Items</div>
                        <table class="character-builder-equipped-table">
                            <tbody id="cb-equipped-summary-body"></tbody>
                        </table>
                    </div>
                </div>

                <div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div>

            </div>
        `;

        var raceSelect = document.getElementById('cb-race');
        var classSelect = document.getElementById('cb-class');
        var levelInput = document.getElementById('cb-level');
        var gearGrid = document.getElementById('cb-gear-grid');
        var loadingMessage = document.getElementById('cb-loading-message');
        var refreshButton = document.getElementById('cb-refresh-items');
        var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
        var tooltip = document.getElementById('cb-item-tooltip');

        function addOption(select, value, label) {
            var option = document.createElement('option');
            option.value = value;
            option.textContent = label;
            select.appendChild(option);
        }

        Object.keys(data.baseStats).forEach(function (race) {
            addOption(raceSelect, race, race);
        });

        function getCharacterLevel() {
            var level = parseInt(levelInput.value, 10);

            if (isNaN(level) || level < 1) level = 1;
            if (level > 60) level = 60;

            levelInput.value = level;

            return level;
        }

        function getBuilderSlotType(slot) {
            if (slot.indexOf("Bracelet") === 0) return "Bracelet";
            if (slot.indexOf("Ring") === 0) return "Ring";
            if (slot.indexOf("Ear") === 0) return "Ear";
            if (slot.indexOf("Held") === 0) return "Held";
            return slot;
        }

        function getCategoryForSlot(slotType) {
            return slotType + " Items";
        }

        function 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 levelCanUseItem(item) {
            var characterLevel = getCharacterLevel();
            var itemLevel = parseInt(item.lvl, 10) || 0;

            if (itemLevel <= 0) return true;

            return itemLevel <= characterLevel;
        }

        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);
            });

            updateEquippedSummary();
        }

        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;
                if (!levelCanUseItem(item)) return;

                addOption(select, index, item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : ''));
            });

            if (equippedItems[slot]) {
                var equippedIndex = items.indexOf(equippedItems[slot]);
                if (equippedIndex !== -1) {
                    select.value = equippedIndex;
                }
            }

            select.addEventListener('change', function () {
                if (select.value === "") {
                    delete equippedItems[slot];
                    hideTooltip();
                } else {
                    equippedItems[slot] = items[parseInt(select.value, 10)];
                }

                updateStats();
            });

            select.addEventListener('mouseenter', function (event) {
                if (equippedItems[slot]) {
                    showTooltip(equippedItems[slot], event);
                }
            });

            select.addEventListener('mousemove', function (event) {
                moveTooltip(event);
            });

            select.addEventListener('mouseleave', hideTooltip);
        }

        function formatItemStats(item) {
            var rows = [];

            function add(label, key) {
                if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
                    rows.push('<div><span>' + label + '</span> ' + item[key] + '</div>');
                }
            }

            add("Lvl:", "lvl");
            add("DMG:", "dmg");
            add("AC:", "ac");
            add("STR:", "str");
            add("STA:", "sta");
            add("AGI:", "agi");
            add("DEX:", "dex");
            add("WIS:", "wis");
            add("INT:", "int");
            add("CHA:", "cha");
            add("HP:", "hp");
            add("HoT:", "hot");
            add("POW:", "pow");
            add("PoT:", "pot");
            add("PR:", "pr");
            add("DR:", "dr");
            add("FR:", "fr");
            add("CR:", "cr");
            add("LR:", "lr");
            add("AR:", "ar");
            add("DUR:", "dur");

            return rows.join("");
        }

        function buildTooltipHtml(item) {
            var classText = item.class || "";
            var slotText = item.slot || "";

            var html = '';
            html += '<div class="cb-tooltip-title">' + item.name + '</div>';

            if (slotText) {
                html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + slotText + '</div>';
            }

            if (classText) {
                html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</div>';
            }

            html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';

            return html;
        }

        function showTooltip(item, event) {
            if (!tooltip || !item) return;

            tooltip.innerHTML = buildTooltipHtml(item);
            tooltip.style.display = 'block';
            moveTooltip(event);
        }

        function moveTooltip(event) {
            if (!tooltip || tooltip.style.display === 'none') return;

            var x = event.pageX + 14;
            var y = event.pageY + 14;

            tooltip.style.left = x + 'px';
            tooltip.style.top = y + 'px';
        }

        function hideTooltip() {
            if (!tooltip) return;
            tooltip.style.display = 'none';
        }

        function updateEquippedSummary() {
            if (!equippedSummaryBody) return;

            equippedSummaryBody.innerHTML = '';

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

                var row = document.createElement('tr');

                var slotCell = document.createElement('td');
                slotCell.textContent = slot;
                slotCell.style.fontWeight = 'bold';

                var itemCell = document.createElement('td');

                if (item) {
                    var itemSpan = document.createElement('span');
                    itemSpan.textContent = item.name;
                    itemSpan.className = 'cb-equipped-item-name';

                    itemSpan.addEventListener('mouseenter', function (event) {
                        showTooltip(item, event);
                    });

                    itemSpan.addEventListener('mousemove', function (event) {
                        moveTooltip(event);
                    });

                    itemSpan.addEventListener('mouseleave', hideTooltip);

                    itemCell.appendChild(itemSpan);
                } else {
                    itemCell.textContent = '-';
                }

                row.appendChild(slotCell);
                row.appendChild(itemCell);

                equippedSummaryBody.appendChild(row);
            });
        }

        function getBaseStats() {
            var race = raceSelect.value;
            var classCode = classSelect.value;
            return data.baseStats[race][classCode];
        }

        function getSpentTrainingPoints() {
            var total = 0;

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

            return total;
        }

        function clampTrainingInput(changedKey) {
            var level = getCharacterLevel();
            var availableTP = getTrainingPointsForLevel(level);

            var spentOther = 0;

            Object.keys(trainingStats).forEach(function (key) {
                if (key !== changedKey) {
                    spentOther += parseInt(trainingStats[key], 10) || 0;
                }
            });

            var input = document.getElementById('train-' + changedKey);
            var value = parseInt(input.value, 10);

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

            var maxAllowed = availableTP - spentOther;

            if (maxAllowed < 0) maxAllowed = 0;

            if (value > maxAllowed) {
                value = maxAllowed;
                input.value = value;
            }

            trainingStats[changedKey] = value;
        }

        function addStat(total, item, key) {
            total[key] += parseFloat(item[key]) || 0;
        }

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

            var level = getCharacterLevel();


            var total = {
                str: base.str || 0,
                sta: base.sta || 0,
                agi: base.agi || 0,
                dex: base.dex || 0,
                wis: base.wis || 0,
                int: base.int || 0,
                cha: base.cha || 0,
                hp: base.hp || 0,
                pow: base.pow || 0,
                ac: base.ac || 0,
                pr: 0,
                dr: 0,
                fr: 0,
                cr: 0,
                lr: 0,
                ar: 0
            };

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

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

                addStat(total, item, "str");
                addStat(total, item, "sta");
                addStat(total, item, "agi");
                addStat(total, item, "dex");
                addStat(total, item, "wis");
                addStat(total, item, "int");
                addStat(total, item, "cha");
                addStat(total, item, "hp");
                addStat(total, item, "pow");
                addStat(total, item, "ac");
                addStat(total, item, "pr");
                addStat(total, item, "dr");
                addStat(total, item, "fr");
                addStat(total, item, "cr");
                addStat(total, item, "lr");
                addStat(total, item, "ar");
            });

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

            if (remainingTP < 0) remainingTP = 0;

            document.getElementById('cb-tp-available').textContent = availableTP;
            document.getElementById('cb-tp-spent').textContent = spentTP;
            document.getElementById('cb-tp-remaining').textContent = remainingTP;

            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;

            updateEquippedSummary();
        }

        function removeInvalidEquippedItemsForLevel() {
            Object.keys(equippedItems).forEach(function (slot) {
                if (!levelCanUseItem(equippedItems[slot])) {
                    delete equippedItems[slot];
                }
            });
        }

        function bindTrainingInputs() {
            Object.keys(trainingStats).forEach(function (key) {
                var input = document.getElementById('train-' + key);

                input.addEventListener('input', function () {
                    clampTrainingInput(key);
                    updateStats();
                });
            });
        }

        raceSelect.addEventListener('change', loadClasses);

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

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

            removeInvalidEquippedItemsForLevel();
            buildGearSlots();
            updateStats();
        });

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

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

});