MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
No edit summary
Line 27: Line 27:


         var data = window.EQOA_BUILDER_DATA;
         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 gearSlots = [
         var gearSlots = [
Line 62: Line 70:
                     <label>Class</label>
                     <label>Class</label>
                     <select id="cb-class"></select>
                     <select id="cb-class"></select>
                    <button id="cb-refresh-items">Refresh Items</button>
                 </div>
                 </div>


                 <div class="character-builder-section-title">Base Stats</div>
                 <div class="character-builder-section-title">Total Stats</div>


                 <table class="character-builder-stat-table">
                 <table class="character-builder-stat-table">
Line 75: Line 85:
                         <th>INT</th>
                         <th>INT</th>
                         <th>CHA</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>
                     <tr>
                     <tr>
Line 84: Line 103:
                         <td id="cb-int">0</td>
                         <td id="cb-int">0</td>
                         <td id="cb-cha">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>
                     </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;">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 96: Line 124:
         var classSelect = document.getElementById('cb-class');
         var classSelect = document.getElementById('cb-class');
         var gearGrid = document.getElementById('cb-gear-grid');
         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) {
         function addOption(select, value, label) {
Line 107: Line 137:
             addOption(raceSelect, race, 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. Check category names and ItemBox pages.";
                console.error(error);
            });
        }


         function loadClasses() {
         function loadClasses() {
Line 118: Line 303:
             });
             });


            equippedItems = {};
            buildGearSlots();
             updateStats();
             updateStats();
        }
        function updateStats() {
            var race = raceSelect.value;
            var classCode = classSelect.value;
            var stats = data.baseStats[race][classCode];
            document.getElementById('cb-str').textContent = stats.str || 0;
            document.getElementById('cb-sta').textContent = stats.sta || 0;
            document.getElementById('cb-agi').textContent = stats.agi || 0;
            document.getElementById('cb-dex').textContent = stats.dex || 0;
            document.getElementById('cb-wis').textContent = stats.wis || 0;
            document.getElementById('cb-int').textContent = stats.int || 0;
            document.getElementById('cb-cha').textContent = stats.cha || 0;
         }
         }


Line 150: Line 322:


                 gearGrid.appendChild(slotBox);
                 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 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(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");
            });
            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;
         }
         }


         raceSelect.addEventListener('change', loadClasses);
         raceSelect.addEventListener('change', loadClasses);
         classSelect.addEventListener('change', updateStats);
 
         classSelect.addEventListener('change', function () {
            equippedItems = {};
            buildGearSlots();
            updateStats();
        });
 
        refreshButton.addEventListener('click', function () {
            itemCache = {};
            equippedItems = {};
            loadAllSlotItems();
        });


         loadClasses();
         loadClasses();
         buildGearSlots();
         loadAllSlotItems();
     }
     }


});
});

Revision as of 02:49, 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 gearSlots = [
            "Head",
            "Chest",
            "Hands",
            "Legs",
            "Feet",
            "Forearm",
            "Bracelet1",
            "Bracelet2",
            "Waist",
            "Neck",
            "Ear1",
            "Ear2",
            "Ring1",
            "Ring2",
            "Primary",
            "Secondary",
            "Shield",
            "Held1",
            "Held2",
            "Robe",
            "Range"
        ];

        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>

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

                <div class="character-builder-section-title">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 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. Check category names and ItemBox pages.";
                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 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(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");
            });

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

        raceSelect.addEventListener('change', loadClasses);

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

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

        loadClasses();
        loadAllSlotItems();
    }

});