MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
No edit summary
Line 1: Line 1:
mw.hook('wikipage.content').add(function () {
ChatGPT
 
 
 
 
 
Pasted text.txt
Document
this is my current code, I want you to update it so it has Generate Share URL Copy URL Loads build from ?build= automatically Saves race, class, level, TP allocation, and equipped item names give me the full code replacement here
 
 
Pasted text.txt
Document
This is my code, im stuck on Step 3
 
Generated image: Dark Elf raid tank build guide
 
 
Edit
 
 
How would it generate a IMG? Is that something we can do?
 
It needs to say EQOA: Sandstorm Character Builder as Header,
At the bottom it needs to say EQOA: Sandstorm Wiki Character Builder
 
I also need you to include the URL code for the build and barcode(if possible)
 
Generated image: EQOA character builder overview
 
 
Edit
 
 
Perfect, how do I code it so it renders it just like that. Give me the code, step by step
 
 
Image created
ΓÇó
Character builder export card tutorial
 
No, I want the code updated so it export the img just like this
 
 
Image created
ΓÇó
Character builder export card tutorial
 
Stop
 
We just worked on Export Character Card feature
 
 
Character builder export card tutorial
 
Generated image: Character builder export card tutorial
 
 
Edit
 
 
 
Character builder export card tutorial
 
Generated image: Character builder export card tutorial
 
 
Edit
 
 
Okay next step?
 
Lets back track, I want style of Export Card changed. Notice how Stats is seprated from Resist and HP/POW AC and then I want each box(Build URL, Scan to Load, Notes) side by side at the bottom
 
 
Image created
ΓÇó
EQOA: Sandstorm character builder summary
 
Stop Render
 
 
EQOA: Sandstorm character builder summary
 
Generated image: EQOA: Sandstorm character builder summary
 
 
Edit
 
 
lets start with step 1
 
 
Image created
ΓÇó
EQOA: Sandstorm character builder layout
 
stop render
 
 
EQOA: Sandstorm character builder layout
 
Generated image: EQOA: Sandstorm character builder layout
 
 
Edit
 
 
I removed it, but its still showing up
 
Perfect, lock it. Now study this IMG, NO RENDER!  Text  Headers needs to be Center, Core Stats Order should be
STR | WIS
STA | INT
AGI | CHA
DEX
 
Resist order should be
 
FR | AR
CR | PR
LR | DR
 
Vitals order should be
HP
POW
AC
 
THIS IS NOT A RENDER, I want code updated to this, help me
 
Study this img. NO RENDER. I need Underlines like this img under Character Builder, STATS, Equipped Items, Notes and Scan to Load Build NO RENDER, I need the code
 
idk if its working, this is the url is telling me
 
study this img, I want it to have it this placement. NO RENDER, I want the code to make it this way
 
That broke all my columns. I only wanted that done to the Class Box
 
Look what you did, you broke the layout
 
 
Pasted text.txt
Document
i dont see that, this is my code
 
 
Pasted text.txt
Document
this is my code
 
which function, i have 11 updateStats()
 
You want the REAL main stats recalculation function.
 
Search for:
 
function updateStats() {
You should find ONE actual function definition.
 
Inside that function, scroll all the way near the bottom.
 
Find something like:
 
updateWeaponSummary();
OR:
 
updateEquippedItems();
OR the final stat display updates.
 
Add the flash code RIGHT BEFORE the final closing:
 
}
So near the end it becomes:
 
updateWeaponSummary();
 
[
    'str', 'sta', 'agi', 'dex', 'wis', 'int', 'cha',
    'hp', 'pow', 'ac',
    'pr', 'dr', 'fr', 'cr', 'lr', 'ar'
].forEach(function (key) {
    var el = document.getElementById('cb-' + key);
 
    if (!el) return;
 
    el.classList.remove('cb-stat-flash');
 
    void el.offsetWidth;
 
    el.classList.add('cb-stat-flash');
});
 
}
DO NOT put it:
 
inside another loop
 
inside updateWeaponSummary()
 
inside updateEquippedItems()
 
Only inside the main function updateStats() { ... } near the very end.
 
 
done show me list
 
Current Character Builder Upgrade List
Visual / Polish
Paperdoll visual character preview
 
Export card polish
 
item icons
 
EQOA borders/frame art
 
watermark/logo
 
Search result highlighting
 
Slot icons (Head/Chest/Ring/etc)
 
Sticky search/filter bars
 
Animated overcap warnings
 
Theorycrafting / Gameplay
DPS estimator
 
Advanced stat weighting
 
Resist analyzer
 
Softcap efficiency warnings
 
Upgrade Path Mode expansion
 
Item Compare Window
 
Role Presets
 
Armor set bonuses
 
Proc synergy calculations
 
Build efficiency grading
 
QoL / Utility
Favorite Items / Wishlist
 
Recently Viewed Items
 
Saved local browser builds
 
Character save codes
 
Live URL updating
 
Additional global search filters:
 
rarity
 
level range
 
dropped by
 
zone
 
Wiki Ecosystem
Auto-link from item pages into builder
 
Embedded mini-builder on guides/pages
 
Public build gallery
 
Shared build comments/ratings
 
Loot-source upgrade recommendations
 
Technical / Long-Term
Builder analytics dashboard
 
External API support
 
Server-side short build IDs (/build/ABC123)
 
Major Locked/Completed Systems
Shareable Build URLs ✅
 
Compact compressed URLs ✅
 
QR export/load system ✅
 
Character Export Card ✅
 
Build Notes export ✅
 
Compare Shared Builds ✅
 
Missing shared item warnings ✅
 
Reset Build ✅
 
Dark/Light themes ✅
 
Mobile layout polish ✅
 
Item compare tooltips ✅
 
Global item search ✅
 
Loot-source integration foundation ✅
 
Rarity system foundation ✅
 
Tooltip rarity borders/glows ✅
 
Animated compare stat highlights ✅
 
Weapon Summary tooltips ✅
 
Rarity colors across builder ✅
 
Legendary animated rarity effects ✅
 
Relic animated rarity effects ✅
 
Hover lift picker animation ✅
 
Equipped item glow polish ✅
 
Upgrade flash animation ✅
 
Best-in-Slot Helper ✅
 
Upgrades Only toggle ✅
 
Filters + Sorting ✅
 
Clickable item links everywhere ✅
 
Validation/auto-disable system ✅
 
Stat caps + overcap display ✅
 
 
Search result highlighting
 
 
Pasted text.txt
Document
Character Builder is not loading up, page is blank now for some reason


    function waitForBuilderData(attemptsLeft) {
Thought for 5s
        if (window.EQOA_BUILDER_DATA) {
Your blank page is from an extra closing brace in updateStats().
            initCharacterBuilder();
            return;
        }


        if (attemptsLeft <= 0) {
Find this part near the end of updateStats():
            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 () {
    el.classList.add('cb-stat-flash');
            waitForBuilderData(attemptsLeft - 1);
});
        }, 100);
    }


    waitForBuilderData(30);
}
       
Replace it with:


     function initCharacterBuilder() {
     el.classList.add('cb-stat-flash');
        var app = document.getElementById('character-builder-app');
});
        if (!app) return;


        var data = window.EQOA_BUILDER_DATA;
}
        var equippedItems = {};
So you are deleting only one extra:
        var itemCache = {};
        var pendingSharedBuild = null;
        var sharedBuildApplied = false;
var missingSharedItems = [];


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


        var textKeys = [
updateWeaponSummary();
    "effect", "proc", "weaponstyle",
You already have it right above. Your code currently has it twice.
    "droppedby", "zone", "droprate"
];


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


        var gearSlots = [
After that: save → Ctrl + F5.
            "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;
mw.hook('wikipage.content').add(function () {


            for (var lvl = 2; lvl <= level; lvl++) {
function waitForBuilderData(attemptsLeft) {
                if (lvl <= 14) tp += 3;
    if (window.EQOA_BUILDER_DATA) {
                else if (lvl <= 29) tp += 5;
        initCharacterBuilder();
                else if (lvl <= 44) tp += 9;
        return;
                else if (lvl <= 59) tp += 14;
    }
                else if (lvl === 60) tp += 3;
            }


             return tp;
    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);


        app.innerHTML = `
function initCharacterBuilder() {
            <div class="character-builder-box">
    var app = document.getElementById('character-builder-app');
    if (!app) return;


                <div class="character-builder-title-row">
    var data = window.EQOA_BUILDER_DATA;
     <div class="character-builder-title">EQOA: Sandstorm Character Builder</div>
    var equippedItems = {};
     var itemCache = {};


     <button id="cb-theme-toggle" type="button">
     var pendingSharedBuild = null;
         Toggle Theme
    var sharedBuildApplied = false;
     </button>
var missingSharedItems = [];
</div>
 
    var statKeys = [
        "lvl", "str", "sta", "agi", "dex", "wis", "int", "cha",
        "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar",
         "dmg", "hot", "pot", "dur"
     ];


                <div class="character-builder-controls">
    var textKeys = [
                    <label>Race</label>
"effect", "proc", "weaponstyle",
                    <select id="cb-race"></select>
"droppedby", "zone", "droprate"
];


                    <label>Class</label>
    var trainingStats = {
                    <select id="cb-class"></select>
        str: 0,
        sta: 0,
        agi: 0,
        dex: 0,
        wis: 0,
        int: 0,
        cha: 0
    };


                    <label>Level</label>
    var gearSlots = [
                    <input type="number" id="cb-level" min="1" max="60" value="60" style="width:70px;">
        "Head", "Chest", "Hands", "Legs", "Feet",
                    <label>Build Name</label>
        "Forearm", "Bracelet1", "Bracelet2",
                    <input type="text" id="cb-build-name" placeholder="Lv60 Ogre SK Raid Tank" style="width:220px;">
        "Waist", "Neck", "Ear1", "Ear2",
<label>Build Notes</label>
        "Ring1", "Ring2", "Primary", "Secondary",
<textarea id="cb-build-notes" placeholder="Example: Raid tank setup focused on HP, AC, and disease resist." rows="3" style="width:260px;"></textarea>
        "Shield", "Held1", "Held2", "Robe", "Range"
    ];


                    <button id="cb-refresh-items">Refresh Items</button>
    function getTrainingPointsForLevel(level) {
                    <button id="cb-generate-share-url" type="button">Generate Share URL</button>
        level = parseInt(level, 10);
                    <button id="cb-copy-share-url" type="button">Copy URL</button>
        if (isNaN(level) || level < 1) level = 1;
<button id="cb-reset-build" type="button">Reset Build</button>
        if (level > 60) level = 60;
<button id="cb-export-card" type="button">Export Build Card</button>
                </div>
<div class="character-builder-section-title" style="margin-top:12px;">Best-in-Slot Helper</div>


<div class="character-builder-controls">
         var tp = 20;
    <label>Rank By</label>
    <select id="cb-bis-stat">
<option value="">None</option>
        <option value="hp">HP</option>
        <option value="ac">AC</option>
        <option value="pow">POW</option>
        <option value="dmg">DMG</option>
        <option value="str">STR</option>
        <option value="sta">STA</option>
        <option value="agi">AGI</option>
        <option value="dex">DEX</option>
        <option value="wis">WIS</option>
        <option value="int">INT</option>
        <option value="cha">CHA</option>
        <option value="pr">PR</option>
        <option value="dr">DR</option>
        <option value="fr">FR</option>
        <option value="cr">CR</option>
        <option value="lr">LR</option>
         <option value="ar">AR</option>
    </select>


    <button id="cb-show-bis" type="button">Show Top Items</button>
        for (var lvl = 2; lvl <= level; lvl++) {
</div>
            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;
        }


<div id="cb-bis-results" class="character-builder-warning-box" style="display:none; margin-top:10px;"></div>
        return tp;
<div class="character-builder-section-title" style="margin-top:15px;">Compare Shared Builds</div>
    }


<div class="character-builder-controls">
    app.innerHTML = `
    <label>Build URL 1</label>
        <div class="character-builder-box">
    <input type="text" id="cb-compare-url-a" placeholder="Paste first shared build URL">


    <label>Build URL 2</label>
            <div class="character-builder-title-row">
    <input type="text" id="cb-compare-url-b" placeholder="Paste second shared build URL">
<div class="character-builder-title">EQOA: Sandstorm Character Builder</div>


    <button id="cb-compare-builds" type="button">Compare Builds</button>
<button id="cb-theme-toggle" type="button">
</div>
    Toggle Theme
</button>
            <div class="character-builder-controls">
                <label>Race</label>
                <select id="cb-race"></select>


<div id="cb-compare-results" class="character-builder-warning-box" style="display:none; margin-top:10px;"></div>
                <label>Class</label>
<div class="character-builder-section-title" style="margin-top:15px;">Global Item Search</div>
                <select id="cb-class"></select>


<div class="character-builder-controls">
                <label>Level</label>
    <label>Search All Items</label>
                <input type="number" id="cb-level" min="1" max="60" value="60" style="width:70px;">
    <input type="text" id="cb-global-item-search" placeholder="Search item name, class, slot, zone, dropped by...">
                <label>Build Name</label>
</div>
                <input type="text" id="cb-build-name" placeholder="Lv60 Ogre SK Raid Tank" style="width:220px;">
Build Notes


<div id="cb-global-search-results" class="character-builder-warning-box" style="display:none; margin-top:10px;"></div>
                <button id="cb-refresh-items">Refresh Items</button>
                 <div id="cb-warning-box" class="character-builder-warning-box" style="display:none;"></div>
                <button id="cb-generate-share-url" type="button">Generate Share URL</button>
                 <button id="cb-copy-share-url" type="button">Copy URL</button>
Reset Build
Export Build Card


                <div id="cb-share-url-box" class="character-builder-warning-box" style="display:none; margin-top:10px;">
                    <b>Share Build URL</b><br>
                    <input type="text" id="cb-share-url-output" readonly style="width:100%; box-sizing:border-box; margin-top:6px;">
                    <div id="cb-share-url-status" style="margin-top:5px;"></div>
                </div>


                <div class="character-builder-section-title" style="margin-top:15px;">Training Points</div>
<button id="cb-show-bis" type="button">Show Top Items</button>
<label>Build URL 2</label>
<input type="text" id="cb-compare-url-b" placeholder="Paste second shared build URL">


                <table class="character-builder-stat-table">
<button id="cb-compare-builds" type="button">Compare Builds</button>
                    <tr>
            <div id="cb-share-url-box" class="character-builder-warning-box" style="display:none; margin-top:10px;">
                        <th>Available TP</th>
                <b>Share Build URL</b><br>
                        <th>Spent TP</th>
                <input type="text" id="cb-share-url-output" readonly style="width:100%; box-sizing:border-box; margin-top:6px;">
                        <th>Remaining TP</th>
                <div id="cb-share-url-status" style="margin-top:5px;"></div>
                    </tr>
            </div>
                    <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>
            <div class="character-builder-section-title" style="margin-top:15px;">Training Points</div>


                <table class="character-builder-stat-table">
            <table class="character-builder-stat-table">
                    <tr>
                <tr>
                        <th>STR</th>
                    <th>Available TP</th>
                        <th>STA</th>
                    <th>Spent TP</th>
                        <th>AGI</th>
                    <th>Remaining TP</th>
                        <th>DEX</th>
                </tr>
                        <th>WIS</th>
                <tr>
                        <th>INT</th>
                     <td id="cb-tp-available">482</td>
                        <th>CHA</th>
                    <td id="cb-tp-spent">0</td>
                    </tr>
                    <td id="cb-tp-remaining">482</td>
                     <tr>
                </tr>
                        <td><input type="number" id="train-str" min="0" value="0" style="width:60px;"></td>
            </table>
                        <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>
            <div class="character-builder-section-title" style="margin-top:18px;">Training Allocation</div>


                <table class="character-builder-stat-table">
            <table class="character-builder-stat-table">
                    <tr>
                <tr>
                        <th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th>
                    <th>STR</th>
                        <th>HP</th><th>POW</th><th>AC</th>
                    <th>STA</th>
                        <th>PR</th><th>DR</th><th>FR</th><th>CR</th><th>LR</th><th>AR</th>
                    <th>AGI</th>
                    </tr>
                    <th>DEX</th>
                     <tr>
                    <th>WIS</th>
                        <td id="cb-str">0</td><td id="cb-sta">0</td><td id="cb-agi">0</td><td id="cb-dex">0</td>
                    <th>INT</th>
                        <td id="cb-wis">0</td><td id="cb-int">0</td><td id="cb-cha">0</td>
                    <th>CHA</th>
                        <td id="cb-hp">0</td><td id="cb-pow">0</td><td id="cb-ac">0</td>
                </tr>
                        <td id="cb-pr">0</td><td id="cb-dr">0</td><td id="cb-fr">0</td><td id="cb-cr">0</td>
                <tr>
                        <td id="cb-lr">0</td><td id="cb-ar">0</td>
                     <td><input type="number" id="train-str" min="0" value="0" style="width:60px;"></td>
                    </tr>
                    <td><input type="number" id="train-sta" min="0" value="0" style="width:60px;"></td>
                </table>
                    <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;">Weapon Summary</div>
            <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>Slot</th>
                    <th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th>
                        <th>Weapon</th>
                     <th>HP</th><th>POW</th><th>AC</th>
                        <th>DMG</th>
                     <th>PR</th><th>DR</th><th>FR</th><th>CR</th><th>LR</th><th>AR</th>
                        <th>Style</th>
                </tr>
                        <th>Proc / Effect</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>
                     <tr>
                     <td id="cb-wis">0</td><td id="cb-int">0</td><td id="cb-cha">0</td>
                        <td>Primary</td>
                    <td id="cb-hp">0</td><td id="cb-pow">0</td><td id="cb-ac">0</td>
                        <td id="cb-weapon-primary-name">-</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-weapon-primary-dmg">-</td>
                    <td id="cb-lr">0</td><td id="cb-ar">0</td>
                        <td id="cb-weapon-primary-style">-</td>
                </tr>
                        <td id="cb-weapon-primary-effect">-</td>
            </table>
                     </tr>
                    <tr>
                        <td>Secondary</td>
                        <td id="cb-weapon-secondary-name">-</td>
                        <td id="cb-weapon-secondary-dmg">-</td>
                        <td id="cb-weapon-secondary-style">-</td>
                        <td id="cb-weapon-secondary-effect">-</td>
                     </tr>
                    <tr>
                        <td>Range</td>
                        <td id="cb-weapon-range-name">-</td>
                        <td id="cb-weapon-range-dmg">-</td>
                        <td id="cb-weapon-range-style">-</td>
                        <td id="cb-weapon-range-effect">-</td>
                    </tr>
                </table>


                <div class="character-builder-equipment-layout">
            <div class="character-builder-section-title" style="margin-top:18px;">Weapon Summary</div>


                    <div class="character-builder-equipped-summary">
            <table class="character-builder-stat-table">
                        <div class="character-builder-section-title" style="margin-top:18px;">Equipped Items</div>
                <tr>
                        <table class="character-builder-equipped-table">
                    <th>Slot</th>
                            <tbody id="cb-equipped-summary-body"></tbody>
                    <th>Weapon</th>
                        </table>
                    <th>DMG</th>
                     </div>
                    <th>Style</th>
                    <th>Proc / Effect</th>
                </tr>
                <tr>
                    <td>Primary</td>
                    <td id="cb-weapon-primary-name">-</td>
                    <td id="cb-weapon-primary-dmg">-</td>
                    <td id="cb-weapon-primary-style">-</td>
                    <td id="cb-weapon-primary-effect">-</td>
                </tr>
                <tr>
                    <td>Secondary</td>
                    <td id="cb-weapon-secondary-name">-</td>
                    <td id="cb-weapon-secondary-dmg">-</td>
                    <td id="cb-weapon-secondary-style">-</td>
                    <td id="cb-weapon-secondary-effect">-</td>
                </tr>
                <tr>
                    <td>Range</td>
                    <td id="cb-weapon-range-name">-</td>
                    <td id="cb-weapon-range-dmg">-</td>
                    <td id="cb-weapon-range-style">-</td>
                     <td id="cb-weapon-range-effect">-</td>
                </tr>
            </table>


                    <div class="character-builder-gear-column">
            <div class="character-builder-equipment-layout">
                        <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>


                <div class="character-builder-equipped-summary">
                    <div class="character-builder-section-title" style="margin-top:18px;">Equipped Items</div>
                    <table class="character-builder-equipped-table">
                        <tbody id="cb-equipped-summary-body"></tbody>
                    </table>
                 </div>
                 </div>


                <div class="character-builder-gear-column">
                    <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>


            </div>






<div id="cb-export-card-container" style="position:absolute; left:-99999px; top:0; width:1200px;">
    <div id="cb-export-card-inner" class="cb-export-card">


        <div class="cb-export-main-title">EQOA: SANDSTORM</div>
    <div class="cb-export-main-title">EQOA: SANDSTORM</div>
        <div class="cb-export-sub-title">CHARACTER BUILDER</div>
    <div class="cb-export-sub-title">CHARACTER BUILDER</div>
        <div id="cb-export-build-name" class="cb-export-build-name">Unnamed Build</div>
    <div id="cb-export-build-name" class="cb-export-build-name">Unnamed Build</div>


        <div class="cb-export-top-info">
    <div class="cb-export-top-info">
            <div class="cb-export-info-box">
        <div class="cb-export-info-box">
                <div class="cb-export-info-label">Race</div>
            <div class="cb-export-info-label">Race</div>
                <div id="cb-export-race"></div>
            <div id="cb-export-race"></div>
            </div>
        </div>


            <div class="cb-export-info-box">
        <div class="cb-export-info-box">
                <div class="cb-export-info-label">Class</div>
            <div class="cb-export-info-label">Class</div>
                <div id="cb-export-class"></div>
            <div id="cb-export-class"></div>
            </div>
        </div>


            <div class="cb-export-info-box">
        <div class="cb-export-info-box">
                <div class="cb-export-info-label">Level</div>
            <div class="cb-export-info-label">Level</div>
                <div id="cb-export-level"></div>
            <div id="cb-export-level"></div>
            </div>
         </div>
         </div>
    </div>


        <div class="cb-export-main-grid">
    <div class="cb-export-main-grid">
            <div class="cb-export-panel">
        <div class="cb-export-panel">
                <div class="cb-export-panel-title">STATS</div>
            <div class="cb-export-panel-title">STATS</div>


                <div class="cb-export-subsection-title">Core Stats</div>
            <div class="cb-export-subsection-title">Core Stats</div>
                <div id="cb-export-core-stats" class="cb-export-stats-grid"></div>
            <div id="cb-export-core-stats" class="cb-export-stats-grid"></div>


                <div class="cb-export-divider"></div>
            <div class="cb-export-divider"></div>


                <div class="cb-export-subsection-title">Resists</div>
            <div class="cb-export-subsection-title">Resists</div>
                <div id="cb-export-resists" class="cb-export-stats-grid"></div>
            <div id="cb-export-resists" class="cb-export-stats-grid"></div>


                <div class="cb-export-divider"></div>
            <div class="cb-export-divider"></div>


                <div class="cb-export-subsection-title">Vitals</div>
            <div class="cb-export-subsection-title">Vitals</div>
                <div id="cb-export-vitals" class="cb-export-stats-grid"></div>
            <div id="cb-export-vitals" class="cb-export-stats-grid"></div>
            </div>
        </div>


            <div class="cb-export-panel">
        <div class="cb-export-panel">
                <div class="cb-export-panel-title">EQUIPPED ITEMS</div>
            <div class="cb-export-panel-title">EQUIPPED ITEMS</div>
                <div id="cb-export-items"></div>
            <div id="cb-export-items"></div>
            </div>
         </div>
         </div>
    </div>


        <div class="cb-export-bottom-row">
    <div class="cb-export-bottom-row">
            <div class="cb-export-panel cb-export-bottom-box">
        <div class="cb-export-panel cb-export-bottom-box">
                <div class="cb-export-panel-title">SCAN TO LOAD</div>
            <div class="cb-export-panel-title">SCAN TO LOAD</div>
                <div id="cb-export-qr" class="cb-export-qr-box">QR CODE</div>
            <div id="cb-export-qr" class="cb-export-qr-box">QR CODE</div>
            </div>
        </div>


            <div class="cb-export-panel cb-export-bottom-box">
        <div class="cb-export-panel cb-export-bottom-box">
                <div class="cb-export-panel-title">BUILD NOTES</div>
            <div class="cb-export-panel-title">BUILD NOTES</div>
                <div id="cb-export-notes" class="cb-export-notes-box"></div>
            <div id="cb-export-notes" class="cb-export-notes-box"></div>
            </div>
         </div>
         </div>
    </div>


        <div class="cb-export-footer">EQOA: Sandstorm Wiki Character Builder</div>
    <div class="cb-export-footer">EQOA: Sandstorm Wiki Character Builder</div>


    </div>
</div>
</div>


Line 346: Line 673:




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


                <div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div>
        </div>
                <div id="cb-compare-tooltip" class="character-builder-tooltip cb-compare-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 raceSelect = document.getElementById('cb-race');
    var buildNameInput = document.getElementById('cb-build-name');
        var classSelect = document.getElementById('cb-class');
        var levelInput = document.getElementById('cb-level');
        var buildNameInput = document.getElementById('cb-build-name');
var buildNotesInput = document.getElementById('cb-build-notes');
var buildNotesInput = document.getElementById('cb-build-notes');
        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');
        var refreshButton = document.getElementById('cb-refresh-items');
var refreshButton = document.getElementById('cb-refresh-items');
        var generateShareButton = document.getElementById('cb-generate-share-url');
var generateShareButton = document.getElementById('cb-generate-share-url');
        var copyShareButton = document.getElementById('cb-copy-share-url');
var copyShareButton = document.getElementById('cb-copy-share-url');
var resetBuildButton = document.getElementById('cb-reset-build');
var resetBuildButton = document.getElementById('cb-reset-build');
var exportCardButton = document.getElementById('cb-export-card');
var exportCardButton = document.getElementById('cb-export-card');
        var shareUrlBox = document.getElementById('cb-share-url-box');
var shareUrlBox = document.getElementById('cb-share-url-box');
        var shareUrlOutput = document.getElementById('cb-share-url-output');
var shareUrlOutput = document.getElementById('cb-share-url-output');
        var shareUrlStatus = document.getElementById('cb-share-url-status');
var shareUrlStatus = document.getElementById('cb-share-url-status');
        var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
        var warningBox = document.getElementById('cb-warning-box');
var warningBox = document.getElementById('cb-warning-box');
var globalItemSearchInput = document.getElementById('cb-global-item-search');
var globalItemSearchInput = document.getElementById('cb-global-item-search');
var globalSearchResultsBox = document.getElementById('cb-global-search-results');
var globalSearchResultsBox = document.getElementById('cb-global-search-results');
Line 379: Line 705:
var compareBuildsButton = document.getElementById('cb-compare-builds');
var compareBuildsButton = document.getElementById('cb-compare-builds');
var compareResultsBox = document.getElementById('cb-compare-results');
var compareResultsBox = document.getElementById('cb-compare-results');
        var tooltip = document.getElementById('cb-item-tooltip');
var tooltip = document.getElementById('cb-item-tooltip');
        var compareTooltip = document.getElementById('cb-compare-tooltip');
var compareTooltip = document.getElementById('cb-compare-tooltip');
var exportCardInner = document.getElementById('cb-export-card-inner');
var exportCardInner = document.getElementById('cb-export-card-inner');
var exportQrBox = document.getElementById('cb-export-qr');
var exportQrBox = document.getElementById('cb-export-qr');
Line 386: Line 712:
var themeToggleButton = document.getElementById('cb-theme-toggle');
var themeToggleButton = document.getElementById('cb-theme-toggle');


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




        function getBuildParamFromUrl() {
    function getBuildParamFromUrl() {
            try {
        try {
                var params = new URLSearchParams(window.location.search);
            var params = new URLSearchParams(window.location.search);
                return params.get('b') || params.get('build');
            return params.get('b') || params.get('build');
            } catch (e) {
        } catch (e) {
                return null;
            return null;
            }
         }
         }
    }


        function encodeBuildData(buildData) {
    function encodeBuildData(buildData) {
    var json = JSON.stringify(buildData);
var json = JSON.stringify(buildData);


    try {
try {
        if (typeof LZString !== 'undefined' && LZString.compressToEncodedURIComponent) {
    if (typeof LZString !== 'undefined' && LZString.compressToEncodedURIComponent) {
            return 'lz:' + LZString.compressToEncodedURIComponent(json);
        return 'lz:' + LZString.compressToEncodedURIComponent(json);
        }
    } catch (e) {
        console.warn('LZString compression failed. Falling back to base64.', e);
     }
     }
} catch (e) {
    console.warn('LZString compression failed. Falling back to base64.', e);
}


    return 'b64:' + btoa(unescape(encodeURIComponent(json)));
return 'b64:' + btoa(unescape(encodeURIComponent(json)));
}
}


 
    function decodeBuildData(encoded) {
        function decodeBuildData(encoded) {
try {
    try {
    if (encoded.indexOf('lz:') === 0) {
        if (encoded.indexOf('lz:') === 0) {
        if (typeof LZString !== 'undefined' && LZString.decompressFromEncodedURIComponent) {
            if (typeof LZString !== 'undefined' && LZString.decompressFromEncodedURIComponent) {
            var decompressed = LZString.decompressFromEncodedURIComponent(encoded.substring(3));
                var decompressed = LZString.decompressFromEncodedURIComponent(encoded.substring(3));
            return JSON.parse(decompressed);
                return JSON.parse(decompressed);
            }
 
            console.error('LZString is not loaded yet.');
            return null;
         }
         }


         if (encoded.indexOf('b64:') === 0) {
         console.error('LZString is not loaded yet.');
            encoded = encoded.substring(4);
        return null;
        }
    }


        var json = decodeURIComponent(escape(atob(encoded)));
    if (encoded.indexOf('b64:') === 0) {
        return JSON.parse(json);
         encoded = encoded.substring(4);
    } catch (e) {
         console.error('Could not decode shared build.', e);
        return null;
     }
     }
    var json = decodeURIComponent(escape(atob(encoded)));
    return JSON.parse(json);
} catch (e) {
    console.error('Could not decode shared build.', e);
    return null;
}
}
}


        function getCurrentBuildData() {
    function getCurrentBuildData() {
            var gear = {};
        var gear = {};


            gearSlots.forEach(function (slot) {
        gearSlots.forEach(function (slot) {
                gear[slot] = equippedItems[slot] ? equippedItems[slot].name : '';
            gear[slot] = equippedItems[slot] ? equippedItems[slot].name : '';
            });
        });


            return {
        return {
    v: 1,
v: 1,
    n: buildNameInput ? buildNameInput.value.trim() : "",
n: buildNameInput ? buildNameInput.value.trim() : "",
    no: buildNotesInput ? buildNotesInput.value.trim() : "",
no: buildNotesInput ? buildNotesInput.value.trim() : "",
    r: raceSelect.value,
r: raceSelect.value,
    c: classSelect.value,
c: classSelect.value,
    l: getCharacterLevel(),
l: getCharacterLevel(),
    t: {
t: {
        str: parseInt(trainingStats.str, 10) || 0,
    str: parseInt(trainingStats.str, 10) || 0,
        sta: parseInt(trainingStats.sta, 10) || 0,
    sta: parseInt(trainingStats.sta, 10) || 0,
        agi: parseInt(trainingStats.agi, 10) || 0,
    agi: parseInt(trainingStats.agi, 10) || 0,
        dex: parseInt(trainingStats.dex, 10) || 0,
    dex: parseInt(trainingStats.dex, 10) || 0,
        wis: parseInt(trainingStats.wis, 10) || 0,
    wis: parseInt(trainingStats.wis, 10) || 0,
        int: parseInt(trainingStats.int, 10) || 0,
    int: parseInt(trainingStats.int, 10) || 0,
        cha: parseInt(trainingStats.cha, 10) || 0
    cha: parseInt(trainingStats.cha, 10) || 0
    },
},
    i: gear
i: gear
};
};
        }
}


        function generateShareUrl() {
    function generateShareUrl() {
            var buildData = getCurrentBuildData();
        var buildData = getCurrentBuildData();
            var encoded = encodeBuildData(buildData);
        var encoded = encodeBuildData(buildData);
            var baseUrl = window.location.origin + window.location.pathname;
        var baseUrl = window.location.origin + window.location.pathname;
var url = baseUrl + '?b=' + encodeURIComponent(encoded);
var url = baseUrl + '?b=' + encodeURIComponent(encoded);


            if (shareUrlOutput) shareUrlOutput.value = url;
        if (shareUrlOutput) shareUrlOutput.value = url;
            if (shareUrlBox) shareUrlBox.style.display = 'block';
        if (shareUrlBox) shareUrlBox.style.display = 'block';
            if (shareUrlStatus) shareUrlStatus.textContent = 'Share URL generated.';
        if (shareUrlStatus) shareUrlStatus.textContent = 'Share URL generated.';
 
        try {
            var newUrl = baseUrl + '?b=' + encodeURIComponent(encoded);
            window.history.replaceState(null, document.title, newUrl);
        } catch (e) {}
 
        return url;
    }


            try {
    function copyShareUrl() {
                var newUrl = baseUrl + '?b=' + encodeURIComponent(encoded);
        var url = shareUrlOutput && shareUrlOutput.value ? shareUrlOutput.value : generateShareUrl();
                window.history.replaceState(null, document.title, newUrl);
            } catch (e) {}


             return url;
        function markCopied() {
             if (shareUrlBox) shareUrlBox.style.display = 'block';
            if (shareUrlStatus) shareUrlStatus.textContent = 'Copied to clipboard.';
         }
         }


         function copyShareUrl() {
         function markManualCopy() {
             var url = shareUrlOutput && shareUrlOutput.value ? shareUrlOutput.value : generateShareUrl();
             if (shareUrlBox) shareUrlBox.style.display = 'block';
 
            if (shareUrlOutput) {
            function markCopied() {
                shareUrlOutput.focus();
                if (shareUrlBox) shareUrlBox.style.display = 'block';
                shareUrlOutput.select();
                if (shareUrlStatus) shareUrlStatus.textContent = 'Copied to clipboard.';
             }
             }
            if (shareUrlStatus) shareUrlStatus.textContent = 'Copy blocked by browser. Select the URL above and copy it manually.';
        }


            function markManualCopy() {
        if (navigator.clipboard && navigator.clipboard.writeText) {
                if (shareUrlBox) shareUrlBox.style.display = 'block';
            navigator.clipboard.writeText(url).then(markCopied).catch(markManualCopy);
        } else {
            try {
                 if (shareUrlOutput) {
                 if (shareUrlOutput) {
                     shareUrlOutput.focus();
                     shareUrlOutput.focus();
                     shareUrlOutput.select();
                     shareUrlOutput.select();
                 }
                 }
                 if (shareUrlStatus) shareUrlStatus.textContent = 'Copy blocked by browser. Select the URL above and copy it manually.';
                 document.execCommand('copy');
            }
                markCopied();
 
            } catch (e) {
            if (navigator.clipboard && navigator.clipboard.writeText) {
                markManualCopy();
                navigator.clipboard.writeText(url).then(markCopied).catch(markManualCopy);
            } else {
                try {
                    if (shareUrlOutput) {
                        shareUrlOutput.focus();
                        shareUrlOutput.select();
                    }
                    document.execCommand('copy');
 
                    markCopied();
                } catch (e) {
                    markManualCopy();
                }
             }
             }
         }
         }
 
    }
function resetBuild() {
function resetBuild() {
    equippedItems = {};
equippedItems = {};
    pendingSharedBuild = null;
pendingSharedBuild = null;
    sharedBuildApplied = false;
sharedBuildApplied = false;


    levelInput.value = 60;
levelInput.value = 60;
 
    if (buildNameInput) {
        buildNameInput.value = '';
    }


if (buildNameInput) {
    buildNameInput.value = '';
}
if (buildNotesInput) {
if (buildNotesInput) {
    buildNotesInput.value = '';
buildNotesInput.value = '';
}
}


    Object.keys(trainingStats).forEach(function (key) {
Object.keys(trainingStats).forEach(function (key) {
        trainingStats[key] = 0;
    trainingStats[key] = 0;


        var input = document.getElementById('train-' + key);
    var input = document.getElementById('train-' + key);
        if (input) input.value = 0;
    if (input) input.value = 0;
    });
});


    if (shareUrlBox) shareUrlBox.style.display = 'none';
if (shareUrlBox) shareUrlBox.style.display = 'none';
    if (shareUrlOutput) shareUrlOutput.value = '';
if (shareUrlOutput) shareUrlOutput.value = '';
    if (shareUrlStatus) shareUrlStatus.textContent = '';
if (shareUrlStatus) shareUrlStatus.textContent = '';


    try {
try {
        window.history.replaceState(
    window.history.replaceState(
            null,
        null,
            document.title,
        document.title,
            window.location.origin + window.location.pathname
        window.location.origin + window.location.pathname
        );
    );
    } catch (e) {}
} catch (e) {}


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


        function getAllLoadedItemsFlat() {
    function getAllLoadedItemsFlat() {
            var all = [];
        var all = [];
            var seen = {};
        var seen = {};


            Object.keys(itemCache).forEach(function (slotType) {
        Object.keys(itemCache).forEach(function (slotType) {
                (itemCache[slotType] || []).forEach(function (item) {
            (itemCache[slotType] || []).forEach(function (item) {
                    if (!item || !item.name) return;
                if (!item || !item.name) return;
                    var key = item.name.toLowerCase();
                var key = item.name.toLowerCase();
                    if (!seen[key]) {
                if (!seen[key]) {
                        seen[key] = item;
                    seen[key] = item;
                        all.push(item);
                    all.push(item);
                    }
                 }
                 });
             });
             });
        });


            return all;
        return all;
        }
    }


        function findLoadedItemByName(itemName, slot) {
    function findLoadedItemByName(itemName, slot) {
            if (!itemName) return null;
        if (!itemName) return null;


            var name = String(itemName).toLowerCase();
        var name = String(itemName).toLowerCase();
            var slotType = getBuilderSlotType(slot);
        var slotType = getBuilderSlotType(slot);
            var slotItems = itemCache[slotType] || [];
        var slotItems = itemCache[slotType] || [];


            for (var i = 0; i < slotItems.length; i++) {
        for (var i = 0; i < slotItems.length; i++) {
                if (slotItems[i].name && slotItems[i].name.toLowerCase() === name) {
            if (slotItems[i].name && slotItems[i].name.toLowerCase() === name) {
                    return slotItems[i];
                return slotItems[i];
                }
             }
             }
        }


            var allItems = getAllLoadedItemsFlat();
        var allItems = getAllLoadedItemsFlat();
            for (var j = 0; j < allItems.length; j++) {
        for (var j = 0; j < allItems.length; j++) {
                if (allItems[j].name && allItems[j].name.toLowerCase() === name) {
            if (allItems[j].name && allItems[j].name.toLowerCase() === name) {
                    return allItems[j];
                return allItems[j];
                }
             }
             }
            return null;
         }
         }


         function setTrainingInputsFromState() {
         return null;
            Object.keys(trainingStats).forEach(function (key) {
    }
                var input = document.getElementById('train-' + key);
                if (input) input.value = trainingStats[key];
            });
        }


         function applySharedBuildBasicState(buildData) {
    function setTrainingInputsFromState() {
             if (!buildData || sharedBuildApplied) return;
         Object.keys(trainingStats).forEach(function (key) {
            var input = document.getElementById('train-' + key);
             if (input) input.value = trainingStats[key];
        });
    }


    function applySharedBuildBasicState(buildData) {
        if (!buildData || sharedBuildApplied) return;
buildData.buildName = buildData.buildName || buildData.n;
buildData.buildName = buildData.buildName || buildData.n;
buildData.buildNotes = buildData.buildNotes || buildData.no;
buildData.buildNotes = buildData.buildNotes || buildData.no;
Line 618: Line 939:
buildData.equippedItemNames = buildData.equippedItemNames || buildData.i;
buildData.equippedItemNames = buildData.equippedItemNames || buildData.i;


            if (buildData.race && data.baseStats[buildData.race]) {
        if (buildData.race && data.baseStats[buildData.race]) {
                raceSelect.value = buildData.race;
            raceSelect.value = buildData.race;
                loadClasses(true);
            loadClasses(true);
            }
        }


            if (buildData.classCode) {
        if (buildData.classCode) {
                var classExists = false;
            var classExists = false;
                Array.prototype.forEach.call(classSelect.options, function (option) {
            Array.prototype.forEach.call(classSelect.options, function (option) {
                    if (option.value === buildData.classCode) classExists = true;
                if (option.value === buildData.classCode) classExists = true;
                });
            });


                if (classExists) classSelect.value = buildData.classCode;
            if (classExists) classSelect.value = buildData.classCode;
            }
        }
 
            if (buildData.level !== undefined) {
                levelInput.value = buildData.level;
                getCharacterLevel();
            }


        if (buildData.level !== undefined) {
            levelInput.value = buildData.level;
            getCharacterLevel();
        }
if (buildData.buildName && buildNameInput) {
if (buildData.buildName && buildNameInput) {
    buildNameInput.value = buildData.buildName;
buildNameInput.value = buildData.buildName;
}
}


if (buildData.buildNotes && buildNotesInput) {
if (buildData.buildNotes && buildNotesInput) {
    buildNotesInput.value = buildData.buildNotes;
buildNotesInput.value = buildData.buildNotes;
}
}


            if (buildData.trainingStats) {
        if (buildData.trainingStats) {
                Object.keys(trainingStats).forEach(function (key) {
            Object.keys(trainingStats).forEach(function (key) {
                    trainingStats[key] = parseInt(buildData.trainingStats[key], 10) || 0;
                trainingStats[key] = parseInt(buildData.trainingStats[key], 10) || 0;
                });
            });
                setTrainingInputsFromState();
            setTrainingInputsFromState();
                Object.keys(trainingStats).forEach(function (key) {
            Object.keys(trainingStats).forEach(function (key) {
                    clampTrainingInput(key);
                clampTrainingInput(key);
                });
            });
                setTrainingInputsFromState();
            setTrainingInputsFromState();
            }
         }
         }
    }


        function applySharedBuildEquipment(buildData) {
    function applySharedBuildEquipment(buildData) {
            if (!buildData || !buildData.equippedItemNames) return;
        if (!buildData || !buildData.equippedItemNames) return;


            equippedItems = {};
        equippedItems = {};


            gearSlots.forEach(function (slot) {
        gearSlots.forEach(function (slot) {
                var itemName = buildData.equippedItemNames[slot];
            var itemName = buildData.equippedItemNames[slot];
                var item = findLoadedItemByName(itemName, slot);
            var item = findLoadedItemByName(itemName, slot);


                if (item && classCanUseItem(item, classSelect.value) && levelCanUseItem(item)) {
            if (item && classCanUseItem(item, classSelect.value) && levelCanUseItem(item)) {
    equippedItems[slot] = item;
equippedItems[slot] = item;
} else if (itemName) {
} else if (itemName) {
    missingSharedItems.push(itemName);
missingSharedItems.push(itemName);
}
}
            });
});


            enforceDisabledSlots();
        enforceDisabledSlots();
            sharedBuildApplied = true;
        sharedBuildApplied = true;
            buildGearSlots();
        buildGearSlots();
            updateStats();
        updateStats();


            if (shareUrlBox && shareUrlOutput) {
        if (shareUrlBox && shareUrlOutput) {
    shareUrlBox.style.display = 'block';
shareUrlBox.style.display = 'block';
    shareUrlOutput.value = window.location.href;
shareUrlOutput.value = window.location.href;


    if (shareUrlStatus) {
if (shareUrlStatus) {
        if (missingSharedItems.length > 0) {
    if (missingSharedItems.length > 0) {
            shareUrlStatus.innerHTML =
        shareUrlStatus.innerHTML =
                'Shared build loaded, but some items could not be found:<br>ΓÇó ' +
            'Shared build loaded, but some items could not be found:<br>ΓÇó ' +
                missingSharedItems.join('<br>ΓÇó ');
            missingSharedItems.join('<br>ΓÇó ');
        } else {
    } else {
            shareUrlStatus.textContent = 'Shared build loaded.';
        shareUrlStatus.textContent = 'Shared build loaded.';
        }
     }
     }
}
}
        }
}
}


        function tryLoadSharedBuildFromUrl() {
    function tryLoadSharedBuildFromUrl() {
            var encoded = getBuildParamFromUrl();
        var encoded = getBuildParamFromUrl();
            if (!encoded) return;
        if (!encoded) return;


            pendingSharedBuild = decodeBuildData(encoded);
        pendingSharedBuild = decodeBuildData(encoded);


            if (pendingSharedBuild) {
        if (pendingSharedBuild) {
                applySharedBuildBasicState(pendingSharedBuild);
            applySharedBuildBasicState(pendingSharedBuild);
            } else if (shareUrlBox && shareUrlStatus) {
        } else if (shareUrlBox && shareUrlStatus) {
                shareUrlBox.style.display = 'block';
            shareUrlBox.style.display = 'block';
                shareUrlStatus.textContent = 'Could not load shared build URL.';
            shareUrlStatus.textContent = 'Could not load shared build URL.';
            }
         }
         }
    }


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


        function getCharacterLevel() {
    function getCharacterLevel() {
            var level = parseInt(levelInput.value, 10);
        var level = parseInt(levelInput.value, 10);
            if (isNaN(level) || level < 1) level = 1;
        if (isNaN(level) || level < 1) level = 1;
            if (level > 60) level = 60;
        if (level > 60) level = 60;
            levelInput.value = level;
        levelInput.value = level;
            return level;
        return level;
        }
    }


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


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


        function getCategoriesForSlotType(slotType) {
    function getCategoriesForSlotType(slotType) {
            if (slotType === "Primary") {
        if (slotType === "Primary") {
                return ["Primary Items", "Secondary Items"];
            return ["Primary Items", "Secondary Items"];
            }
            return [getCategoryForSlot(slotType)];
         }
         }
        return [getCategoryForSlot(slotType)];
    }


        function apiGet(params) {
    function apiGet(params) {
            return new mw.Api().get(params);
        return new mw.Api().get(params);
        }
    }


        function getPagesInCategory(categoryName) {
    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 || [];
        });
    }


            return apiGet({
    function getPageWikitext(title) {
                action: "query",
        return apiGet({
                list: "categorymembers",
            action: "query",
                cmtitle: "Category:" + categoryName,
            prop: "revisions",
                cmlimit: "500",
            titles: title,
                cmnamespace: "0",
            rvprop: "content",
                format: "json"
            rvslots: "main",
            }).then(function (data) {
            formatversion: "2",
                return data.query.categorymembers || [];
            format: "json"
            });
        }).then(function (data) {
        }
            var page = data.query.pages[0];


        function getPageWikitext(title) {
             if (!page || !page.revisions || !page.revisions[0]) return "";
             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 || "";
            }


                if (page.revisions[0].slots && page.revisions[0].slots.main) {
            return page.revisions[0].content || "";
                    return page.revisions[0].slots.main.content || "";
        });
                }
    }


                return page.revisions[0].content || "";
    function parseCategories(wikitext) {
            });
        var categories = [];
        }
        var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
 
        var match;
        function parseCategories(wikitext) {
            var categories = [];
            var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
            var match;
 
            while ((match = regex.exec(wikitext)) !== null) {
                categories.push(match[1].trim());
            }


             return categories;
        while ((match = regex.exec(wikitext)) !== null) {
             categories.push(match[1].trim());
         }
         }


         function parseItemBox(wikitext, fallbackName) {
         return categories;
            var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i);
    }
            if (!match) return null;


            var content = match[1];
    function parseItemBox(wikitext, fallbackName) {
            var item = { name: fallbackName };
        var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i);
            var lines = content.split("\n");
        if (!match) return null;


        var content = match[1];
        var item = { name: fallbackName };
        var lines = content.split("\n");


            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;


                var key = line.substring(1, equalIndex).trim().toLowerCase();
            var key = line.substring(1, equalIndex).trim().toLowerCase();
                var value = line.substring(equalIndex + 1).trim();
            var value = line.substring(equalIndex + 1).trim();


                if (!key) return;
            if (!key) return;


                item[key] = value;
            item[key] = value;
            });
        });
 
            if (!item.name) item.name = fallbackName;
 
            item.categories = parseCategories(wikitext);


            item.is2HD = item.categories.indexOf("2HD Items") !== -1 ||
        if (!item.name) item.name = fallbackName;
                        String(item.weaponstyle || "").toUpperCase() === "2HD";


            item.isShield = item.categories.indexOf("Shield Items") !== -1 ||
        item.categories = parseCategories(wikitext);
                            String(item.slot || "").toLowerCase() === "shield";


            item.isHeld = item.categories.indexOf("Held Items") !== -1 ||
        item.is2HD = item.categories.indexOf("2HD Items") !== -1 ||
                          String(item.slot || "").toLowerCase() === "held";
                    String(item.weaponstyle || "").toUpperCase() === "2HD";


            item.isSecondary = item.categories.indexOf("Secondary Items") !== -1 ||
        item.isShield = item.categories.indexOf("Shield Items") !== -1 ||
                              String(item.slot || "").toLowerCase() === "secondary";
                        String(item.slot || "").toLowerCase() === "shield";


            item.isPrimary = item.categories.indexOf("Primary Items") !== -1 ||
        item.isHeld = item.categories.indexOf("Held Items") !== -1 ||
                            String(item.slot || "").toLowerCase() === "primary";
                      String(item.slot || "").toLowerCase() === "held";


            statKeys.forEach(function (key) {
        item.isSecondary = item.categories.indexOf("Secondary Items") !== -1 ||
                if (item[key] !== undefined && item[key] !== "") {
                          String(item.slot || "").toLowerCase() === "secondary";
                    var num = parseFloat(String(item[key]).replace(/[^\d.-]/g, ""));
                    item[key] = isNaN(num) ? 0 : num;
                } else {
                    item[key] = 0;
                }
            });


            textKeys.forEach(function (key) {
        item.isPrimary = item.categories.indexOf("Primary Items") !== -1 ||
                if (item[key] === undefined) {
                        String(item.slot || "").toLowerCase() === "primary";
                    item[key] = "";
                }
            });


            if (item.class) {
        statKeys.forEach(function (key) {
                item.classList = item.class.split(",").map(function (x) {
            if (item[key] !== undefined && item[key] !== "") {
                    return x.trim().toUpperCase();
                var num = parseFloat(String(item[key]).replace(/[^\d.-]/g, ""));
                 });
                 item[key] = isNaN(num) ? 0 : num;
             } else {
             } else {
                 item.classList = [];
                 item[key] = 0;
             }
             }
        });


             return item;
        textKeys.forEach(function (key) {
         }
             if (item[key] === undefined) {
                item[key] = "";
            }
         });


         function classCanUseItem(item, classCode) {
         if (item.class) {
             if (!item.classList || item.classList.length === 0) return true;
             item.classList = item.class.split(",").map(function (x) {
            if (item.classList.indexOf("ALL") !== -1) return true;
                return x.trim().toUpperCase();
             return item.classList.indexOf(classCode) !== -1;
            });
        } else {
             item.classList = [];
         }
         }


         function levelCanUseItem(item) {
         return item;
            var characterLevel = getCharacterLevel();
    }
            var itemLevel = parseInt(item.lvl, 10) || 0;
            if (itemLevel <= 0) return true;
            return itemLevel <= characterLevel;
        }


        function dedupePages(pages) {
    function classCanUseItem(item, classCode) {
            var seen = {};
        if (!item.classList || item.classList.length === 0) return true;
            var result = [];
        if (item.classList.indexOf("ALL") !== -1) return true;
        return item.classList.indexOf(classCode) !== -1;
    }


            pages.forEach(function (page) {
    function levelCanUseItem(item) {
                if (!seen[page.title]) {
        var characterLevel = getCharacterLevel();
                    seen[page.title] = true;
        var itemLevel = parseInt(item.lvl, 10) || 0;
                    result.push(page);
        if (itemLevel <= 0) return true;
                }
        return itemLevel <= characterLevel;
            });
    }


            return result;
    function dedupePages(pages) {
         }
        var seen = {};
         var result = [];


         function loadItemsForSlotType(slotType) {
         pages.forEach(function (page) {
             if (itemCache[slotType]) {
             if (!seen[page.title]) {
                 return Promise.resolve(itemCache[slotType]);
                 seen[page.title] = true;
                result.push(page);
             }
             }
        });


            var categories = getCategoriesForSlotType(slotType);
        return result;
    }


            var categoryPromises = categories.map(function (categoryName) {
    function loadItemsForSlotType(slotType) {
                return getPagesInCategory(categoryName);
        if (itemCache[slotType]) {
            });
            return Promise.resolve(itemCache[slotType]);
        }


            return Promise.all(categoryPromises).then(function (categoryResults) {
        var categories = getCategoriesForSlotType(slotType);
                var pages = [];


                categoryResults.forEach(function (result) {
        var categoryPromises = categories.map(function (categoryName) {
                    pages = pages.concat(result);
            return getPagesInCategory(categoryName);
                });
        });


                pages = dedupePages(pages);
        return Promise.all(categoryPromises).then(function (categoryResults) {
            var pages = [];


                var pagePromises = pages.map(function (page) {
            categoryResults.forEach(function (result) {
                    return getPageWikitext(page.title).then(function (wikitext) {
                pages = pages.concat(result);
                        return parseItemBox(wikitext, page.title);
            });
                    });
                });


                return Promise.all(pagePromises).then(function (items) {
            pages = dedupePages(pages);
                    items = items.filter(function (item) {
                        return item !== null;
                    });


                    itemCache[slotType] = items;
            var pagePromises = pages.map(function (page) {
                     return items;
                return getPageWikitext(page.title).then(function (wikitext) {
                     return parseItemBox(wikitext, page.title);
                 });
                 });
             });
             });
        }


        function loadAllSlotItems() {
            return Promise.all(pagePromises).then(function (items) {
            loadingMessage.textContent = "Loading item categories...";
                items = items.filter(function (item) {
 
                    return item !== null;
            var uniqueSlotTypes = [];
                });


            gearSlots.forEach(function (slot) {
                 itemCache[slotType] = items;
                 var slotType = getBuilderSlotType(slot);
                 return items;
                 if (uniqueSlotTypes.indexOf(slotType) === -1) {
                    uniqueSlotTypes.push(slotType);
                }
             });
             });
        });
    }


            var promises = uniqueSlotTypes.map(function (slotType) {
    function loadAllSlotItems() {
                return loadItemsForSlotType(slotType);
        loadingMessage.textContent = "Loading item categories...";
            });


            return Promise.all(promises).then(function () {
        var uniqueSlotTypes = [];
                loadingMessage.textContent = "Items loaded.";


                if (pendingSharedBuild && !sharedBuildApplied) {
        gearSlots.forEach(function (slot) {
                    applySharedBuildEquipment(pendingSharedBuild);
            var slotType = getBuilderSlotType(slot);
                    return;
            if (uniqueSlotTypes.indexOf(slotType) === -1) {
                }
                uniqueSlotTypes.push(slotType);
            }
        });


                buildGearSlots();
        var promises = uniqueSlotTypes.map(function (slotType) {
                updateStats();
            return loadItemsForSlotType(slotType);
            }).catch(function (error) {
        });
                loadingMessage.textContent = "Error loading items.";
                console.error(error);
            });
        }


         function getDisabledSlots() {
         return Promise.all(promises).then(function () {
             var disabled = {};
             loadingMessage.textContent = "Items loaded.";


             var primary = equippedItems.Primary;
             if (pendingSharedBuild && !sharedBuildApplied) {
            var forearm = equippedItems.Forearm;
                applySharedBuildEquipment(pendingSharedBuild);
            var bracelet1 = equippedItems.Bracelet1;
                return;
             var bracelet2 = equippedItems.Bracelet2;
             }


             if (primary && primary.is2HD) {
             buildGearSlots();
                disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
            updateStats();
                disabled.Shield = "Disabled because a 2HD weapon is equipped in Primary.";
        }).catch(function (error) {
                disabled.Held1 = "Disabled because a 2HD weapon is equipped in Primary.";
            loadingMessage.textContent = "Error loading items.";
                disabled.Held2 = "Disabled because a 2HD weapon is equipped in Primary.";
            console.error(error);
            }
        });
    }


            if (forearm) {
    function getDisabledSlots() {
                disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
        var disabled = {};
                disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
            }


            if (bracelet1 || bracelet2) {
        var primary = equippedItems.Primary;
                disabled.Forearm = "Disabled because Bracelet1 or Bracelet2 is equipped.";
        var forearm = equippedItems.Forearm;
            }
        var bracelet1 = equippedItems.Bracelet1;
        var bracelet2 = equippedItems.Bracelet2;


             return disabled;
        if (primary && primary.is2HD) {
             disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Shield = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Held1 = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Held2 = "Disabled because a 2HD weapon is equipped in Primary.";
         }
         }


         function enforceDisabledSlots() {
         if (forearm) {
             var disabled = getDisabledSlots();
             disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
            disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
        }


            Object.keys(disabled).forEach(function (slot) {
        if (bracelet1 || bracelet2) {
                if (equippedItems[slot]) {
             disabled.Forearm = "Disabled because Bracelet1 or Bracelet2 is equipped.";
                    delete equippedItems[slot];
                }
             });
         }
         }


         function loadClasses(preserveEquipped) {
         return disabled;
             classSelect.innerHTML = '';
    }
 
    function enforceDisabledSlots() {
        var disabled = getDisabledSlots();
 
        Object.keys(disabled).forEach(function (slot) {
            if (equippedItems[slot]) {
                delete equippedItems[slot];
             }
        });
    }


            var race = raceSelect.value;
    function loadClasses(preserveEquipped) {
        classSelect.innerHTML = '';


            Object.keys(data.baseStats[race]).forEach(function (classCode) {
        var race = raceSelect.value;
                var className = data.classes[classCode] || classCode;
                addOption(classSelect, classCode, classCode + ' - ' + className);
            });


            if (!preserveEquipped) {
        Object.keys(data.baseStats[race]).forEach(function (classCode) {
                equippedItems = {};
            var className = data.classes[classCode] || classCode;
             }
             addOption(classSelect, classCode, classCode + ' - ' + className);
        });


            buildGearSlots();
        if (!preserveEquipped) {
             updateStats();
             equippedItems = {};
         }
         }


         function buildGearSlots() {
         buildGearSlots();
            gearGrid.innerHTML = '';
        updateStats();
    }


            var disabledSlots = getDisabledSlots();
    function buildGearSlots() {
        gearGrid.innerHTML = '';


            gearSlots.forEach(function (slot) {
        var disabledSlots = getDisabledSlots();
                var slotBox = document.createElement('div');
                slotBox.className = 'character-builder-slot';


                if (disabledSlots[slot]) {
        gearSlots.forEach(function (slot) {
                    slotBox.className += ' cb-slot-disabled';
            var slotBox = document.createElement('div');
                }
            slotBox.className = 'character-builder-slot';


                slotBox.innerHTML =
            if (disabledSlots[slot]) {
    '<label>' + slot + '</label>' +
                slotBox.className += ' cb-slot-disabled';
    '<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' +
             }
    '<div class="cb-picker-tools">' +
        '<select class="cb-slot-filter" id="cb-filter-' + slot.toLowerCase() + '">' +
            '<option value="">Any Stat</option>' +
            '<option value="hp">HP</option>' +
            '<option value="pow">POW</option>' +
            '<option value="ac">AC</option>' +
            '<option value="dmg">DMG</option>' +
            '<option value="str">STR</option>' +
            '<option value="sta">STA</option>' +
            '<option value="agi">AGI</option>' +
            '<option value="dex">DEX</option>' +
            '<option value="wis">WIS</option>' +
            '<option value="int">INT</option>' +
            '<option value="cha">CHA</option>' +
            '<option value="pr">PR</option>' +
            '<option value="dr">DR</option>' +
            '<option value="fr">FR</option>' +
            '<option value="cr">CR</option>' +
            '<option value="lr">LR</option>' +
            '<option value="ar">AR</option>' +
            '<option value="proc">Has Proc</option>' +
            '<option value="effect">Has Effect</option>' +
            '<option value="2hd">2HD</option>' +
        '</select>' +
        '<select class="cb-slot-sort" id="cb-sort-' + slot.toLowerCase() + '">' +
            '<option value="name">Sort: Name</option>' +
            '<option value="lvl">Sort: Level</option>' +
             '<option value="hp">Sort: HP</option>' +
            '<option value="pow">Sort: POW</option>' +
            '<option value="ac">Sort: AC</option>' +
            '<option value="dmg">Sort: DMG</option>' +
            '<option value="str">Sort: STR</option>' +
            '<option value="sta">Sort: STA</option>' +
            '<option value="agi">Sort: AGI</option>' +
            '<option value="dex">Sort: DEX</option>' +
            '<option value="wis">Sort: WIS</option>' +
            '<option value="int">Sort: INT</option>' +
            '<option value="cha">Sort: CHA</option>' +
        '</select>' +


'<label class="cb-upgrade-toggle">' +
            slotBox.innerHTML =
     '<input type="checkbox" id="cb-upgrades-' + slot.toLowerCase() + '">' +
'<label>' + slot + '</label>' +
     ' Upgrades Only' +
'<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' +
'</label>' +
'<div class="cb-picker-tools">' +
     '<select class="cb-slot-filter" id="cb-filter-' + slot.toLowerCase() + '">' +
        '<option value="">Any Stat</option>' +
        '<option value="hp">HP</option>' +
        '<option value="pow">POW</option>' +
        '<option value="ac">AC</option>' +
        '<option value="dmg">DMG</option>' +
        '<option value="str">STR</option>' +
        '<option value="sta">STA</option>' +
        '<option value="agi">AGI</option>' +
        '<option value="dex">DEX</option>' +
        '<option value="wis">WIS</option>' +
        '<option value="int">INT</option>' +
        '<option value="cha">CHA</option>' +
        '<option value="pr">PR</option>' +
        '<option value="dr">DR</option>' +
        '<option value="fr">FR</option>' +
        '<option value="cr">CR</option>' +
        '<option value="lr">LR</option>' +
        '<option value="ar">AR</option>' +
        '<option value="proc">Has Proc</option>' +
        '<option value="effect">Has Effect</option>' +
        '<option value="2hd">2HD</option>' +
    '</select>' +
     '<select class="cb-slot-sort" id="cb-sort-' + slot.toLowerCase() + '">' +
        '<option value="name">Sort: Name</option>' +
        '<option value="lvl">Sort: Level</option>' +
        '<option value="hp">Sort: HP</option>' +
        '<option value="pow">Sort: POW</option>' +
        '<option value="ac">Sort: AC</option>' +
        '<option value="dmg">Sort: DMG</option>' +
        '<option value="str">Sort: STR</option>' +
        '<option value="sta">Sort: STA</option>' +
        '<option value="agi">Sort: AGI</option>' +
        '<option value="dex">Sort: DEX</option>' +
        '<option value="wis">Sort: WIS</option>' +
        '<option value="int">Sort: INT</option>' +
        '<option value="cha">Sort: CHA</option>' +
    '</select>' +
'' +
'' +
' Upgrades Only' +
'' +


'</div>' +
'' +
'<div class="cb-item-picker-list" id="cb-picker-' + slot.toLowerCase() + '"></div>' +
'' +
    '<div class="cb-slot-disabled-note" id="cb-disabled-note-' + slot.toLowerCase() + '"></div>';
'';


                gearGrid.appendChild(slotBox);
            gearGrid.appendChild(slotBox);


                populateItemPicker(slot, disabledSlots[slot]);
            populateItemPicker(slot, disabledSlots[slot]);
            });
        });
 
            updateEquippedSummary();
        }


        updateEquippedSummary();
    }
function isItemUpgrade(slot, newItem) {
function isItemUpgrade(slot, newItem) {
    var currentItem = equippedItems[slot];
var currentItem = equippedItems[slot];


    if (!currentItem) return true;
if (!currentItem) return true;


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


    for (var i = 0; i < compareKeys.length; i++) {
for (var i = 0; i < compareKeys.length; i++) {
        var key = compareKeys[i];
    var key = compareKeys[i];


        var newValue = parseFloat(newItem[key]) || 0;
    var newValue = parseFloat(newItem[key]) || 0;
        var currentValue = parseFloat(currentItem[key]) || 0;
    var currentValue = parseFloat(currentItem[key]) || 0;


        if (newValue > currentValue) {
    if (newValue > currentValue) {
            return true;
        return true;
        }
     }
     }
}


    return false;
return false;
}
}


        function getFilteredItemsForSlot(slot, searchText, filterKey, sortKey) {
    function getFilteredItemsForSlot(slot, searchText, filterKey, sortKey) {
    var classCode = classSelect.value;
var classCode = classSelect.value;
    var slotType = getBuilderSlotType(slot);
var slotType = getBuilderSlotType(slot);
    var items = itemCache[slotType] || [];
var items = itemCache[slotType] || [];


    searchText = (searchText || "").toLowerCase();
searchText = (searchText || "").toLowerCase();
    filterKey = filterKey || "";
filterKey = filterKey || "";
    sortKey = sortKey || "name";
sortKey = sortKey || "name";


    var filtered = items.filter(function (item) {
var filtered = items.filter(function (item) {
        if (!classCanUseItem(item, classCode)) return false;
    if (!classCanUseItem(item, classCode)) return false;
        if (!levelCanUseItem(item)) return false;
    if (!levelCanUseItem(item)) return false;


        if (slot === "Primary") {
    if (slot === "Primary") {
            if (!(item.isPrimary || item.isSecondary)) return false;
        if (!(item.isPrimary || item.isSecondary)) return false;
        }
    }


        if (slot === "Secondary") {
    if (slot === "Secondary") {
            if (!item.isSecondary) return false;
        if (!item.isSecondary) return false;
        }
    }


        if (searchText && item.name.toLowerCase().indexOf(searchText) === -1) {
    if (searchText && item.name.toLowerCase().indexOf(searchText) === -1) {
            return false;
        return false;
        }
    }


        if (filterKey) {
    if (filterKey) {
            if (filterKey === "proc" && !item.proc) return false;
        if (filterKey === "proc" && !item.proc) return false;
            if (filterKey === "effect" && !item.effect) return false;
        if (filterKey === "effect" && !item.effect) return false;
            if (filterKey === "2hd" && !item.is2HD) return false;
        if (filterKey === "2hd" && !item.is2HD) return false;


            if (
        if (
                filterKey !== "proc" &&
            filterKey !== "proc" &&
                filterKey !== "effect" &&
            filterKey !== "effect" &&
                filterKey !== "2hd"
            filterKey !== "2hd"
            ) {
        ) {
                if (!item[filterKey] || parseFloat(item[filterKey]) <= 0) return false;
            if (!item[filterKey] || parseFloat(item[filterKey]) <= 0) return false;
            }
         }
         }
    }


        return true;
    return true;
    });
});


    filtered.sort(function (a, b) {
filtered.sort(function (a, b) {
        if (sortKey === "name") {
    if (sortKey === "name") {
            return String(a.name || "").localeCompare(String(b.name || ""));
        return String(a.name || "").localeCompare(String(b.name || ""));
        }
    }


        var av = parseFloat(a[sortKey]) || 0;
    var av = parseFloat(a[sortKey]) || 0;
        var bv = parseFloat(b[sortKey]) || 0;
    var bv = parseFloat(b[sortKey]) || 0;


        if (bv !== av) return bv - av;
    if (bv !== av) return bv - av;


        return String(a.name || "").localeCompare(String(b.name || ""));
    return String(a.name || "").localeCompare(String(b.name || ""));
    });
});


    return filtered;
return filtered;
}
}


 
    function populateItemPicker(slot, disabledReason) {
        function populateItemPicker(slot, disabledReason) {
        var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
            var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
        var searchInput = document.getElementById('cb-search-' + slot.toLowerCase());
            var searchInput = document.getElementById('cb-search-' + slot.toLowerCase());
        var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase());
            var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase());
var filterSelect = document.getElementById('cb-filter-' + slot.toLowerCase());
var filterSelect = document.getElementById('cb-filter-' + slot.toLowerCase());
var sortSelect = document.getElementById('cb-sort-' + slot.toLowerCase());
var sortSelect = document.getElementById('cb-sort-' + slot.toLowerCase());
var upgradesOnlyToggle = document.getElementById('cb-upgrades-' + slot.toLowerCase());
var upgradesOnlyToggle = document.getElementById('cb-upgrades-' + slot.toLowerCase());


            if (!picker) return;
        if (!picker) return;


            if (disabledReason) {
        if (disabledReason) {
                if (searchInput) searchInput.disabled = true;
            if (searchInput) searchInput.disabled = true;
                picker.innerHTML = '<div class="cb-picker-disabled">Disabled</div>';
            picker.innerHTML = '<div class="cb-picker-disabled">Disabled</div>';
                if (disabledNote) disabledNote.textContent = disabledReason;
            if (disabledNote) disabledNote.textContent = disabledReason;
                return;
            return;
            }
        }


            function rebuildPicker() {
        function rebuildPicker() {
                var searchText = searchInput ? searchInput.value : "";
            var searchText = searchInput ? searchInput.value : "";
var filterKey = filterSelect ? filterSelect.value : "";
var filterKey = filterSelect ? filterSelect.value : "";
var sortKey = sortSelect ? sortSelect.value : "name";
var sortKey = sortSelect ? sortSelect.value : "name";
Line 1,193: Line 1,508:


if (upgradesOnlyToggle && upgradesOnlyToggle.checked) {
if (upgradesOnlyToggle && upgradesOnlyToggle.checked) {
    filteredItems = filteredItems.filter(function (item) {
filteredItems = filteredItems.filter(function (item) {
        return isItemUpgrade(slot, item);
return isItemUpgrade(slot, item);
    });
});
}
}


filteredItems = filteredItems.slice(0, 10);
filteredItems = filteredItems.slice(0, 10);


                var currentItem = equippedItems[slot];
            var currentItem = equippedItems[slot];


                picker.innerHTML = '';
            picker.innerHTML = '';


                var noneRow = document.createElement('div');
            var noneRow = document.createElement('div');
                noneRow.className = 'cb-picker-item cb-picker-none';
            noneRow.className = 'cb-picker-item cb-picker-none';
                noneRow.textContent = '-- None --';
            noneRow.textContent = '-- None --';


                if (!currentItem) {
            if (!currentItem) {
                    noneRow.className += ' cb-picker-selected';
                noneRow.className += ' cb-picker-selected';
                }
            }


                noneRow.addEventListener('click', function () {
            noneRow.addEventListener('click', function () {
                    delete equippedItems[slot];
                delete equippedItems[slot];
                    hideTooltip();
                hideTooltip();
                    hideCompareTooltip();
                hideCompareTooltip();
                    enforceDisabledSlots();
                enforceDisabledSlots();
                    buildGearSlots();
                buildGearSlots();
                    updateStats();
                updateStats();
                });
            });


                picker.appendChild(noneRow);
            picker.appendChild(noneRow);


                filteredItems.forEach(function (item) {
            filteredItems.forEach(function (item) {
                    var row = document.createElement('div');
                var row = document.createElement('div');
                    row.className = 'cb-picker-item';
                row.className = 'cb-picker-item';


                    if (currentItem === item) {
                if (currentItem === item) {
                        row.className += ' cb-picker-selected';
                    row.className += ' cb-picker-selected';
                    }
                }


                    var itemNameSpan = document.createElement('span');
                var itemNameSpan = document.createElement('span');
itemNameSpan.textContent = item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : '');
itemNameSpan.textContent = item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : '');


Line 1,242: Line 1,557:


itemPageLink.addEventListener('click', function (event) {
itemPageLink.addEventListener('click', function (event) {
    event.stopPropagation();
event.stopPropagation();
    openItemPage(item.name);
openItemPage(item.name);
});
});


Line 1,249: Line 1,564:
row.appendChild(itemPageLink);
row.appendChild(itemPageLink);


                    row.addEventListener('mouseenter', function (event) {
                row.addEventListener('mouseenter', function (event) {
                        showCompareTooltip(slot, item, event);
                    showCompareTooltip(slot, item, event);
                    });
                });


                    row.addEventListener('mousemove', function (event) {
                row.addEventListener('mousemove', function (event) {
                        moveCompareTooltip(event);
                    moveCompareTooltip(event);
                    });
                });


                    row.addEventListener('mouseleave', function () {
                row.addEventListener('mouseleave', function () {
                        hideCompareTooltip();
                    hideCompareTooltip();
                    });
                });


                    row.addEventListener('click', function () {
                row.addEventListener('click', function () {
                        equippedItems[slot] = item;
                    equippedItems[slot] = item;
                        hideTooltip();
                    hideTooltip();
                        hideCompareTooltip();
                    hideCompareTooltip();
                        enforceDisabledSlots();
                    enforceDisabledSlots();
                        buildGearSlots();
                    buildGearSlots();
                        updateStats();
                    updateStats();
                    });
                });


                    picker.appendChild(row);
                picker.appendChild(row);
                });
            });
            }
        }


            rebuildPicker();
        rebuildPicker();


            if (searchInput) {
        if (searchInput) {
    searchInput.addEventListener('input', rebuildPicker);
searchInput.addEventListener('input', rebuildPicker);
}
}


if (filterSelect) {
if (filterSelect) {
    filterSelect.addEventListener('change', rebuildPicker);
filterSelect.addEventListener('change', rebuildPicker);
}
}


if (sortSelect) {
if (sortSelect) {
    sortSelect.addEventListener('change', rebuildPicker);
sortSelect.addEventListener('change', rebuildPicker);
}
}


if (upgradesOnlyToggle) {
if (upgradesOnlyToggle) {
    upgradesOnlyToggle.addEventListener('change', rebuildPicker);
upgradesOnlyToggle.addEventListener('change', rebuildPicker);
}
}
}
        }


        function validateEquipment() {
    function validateEquipment() {
            var warnings = [];
        var warnings = [];


            var primary = equippedItems.Primary;
        var primary = equippedItems.Primary;
            var secondary = equippedItems.Secondary;
        var secondary = equippedItems.Secondary;
            var shield = equippedItems.Shield;
        var shield = equippedItems.Shield;
            var held1 = equippedItems.Held1;
        var held1 = equippedItems.Held1;
            var held2 = equippedItems.Held2;
        var held2 = equippedItems.Held2;
            var forearm = equippedItems.Forearm;
        var forearm = equippedItems.Forearm;
            var bracelet1 = equippedItems.Bracelet1;
        var bracelet1 = equippedItems.Bracelet1;
            var bracelet2 = equippedItems.Bracelet2;
        var bracelet2 = equippedItems.Bracelet2;


            if (forearm && (bracelet1 || bracelet2)) {
        if (forearm && (bracelet1 || bracelet2)) {
                warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
            warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
            }
        }


            if (primary && primary.is2HD) {
        if (primary && primary.is2HD) {
                if (secondary) warnings.push("2HD weapon in Primary conflicts with Secondary.");
            if (secondary) warnings.push("2HD weapon in Primary conflicts with Secondary.");
                if (shield) warnings.push("2HD weapon in Primary conflicts with Shield.");
            if (shield) warnings.push("2HD weapon in Primary conflicts with Shield.");
                if (held1 || held2) warnings.push("2HD weapon in Primary conflicts with Held1/Held2.");
            if (held1 || held2) warnings.push("2HD weapon in Primary conflicts with Held1/Held2.");
            }
        }


            if (shield && secondary) {
        if (shield && secondary) {
                warnings.push("Shield conflicts with Secondary weapon.");
            warnings.push("Shield conflicts with Secondary weapon.");
            }
        }


            if (shield && (held1 || held2)) {
        if (shield && (held1 || held2)) {
                warnings.push("Shield conflicts with Held1/Held2.");
            warnings.push("Shield conflicts with Held1/Held2.");
            }
        }


            if ((held1 || held2) && (primary || secondary || shield)) {
        if ((held1 || held2) && (primary || secondary || shield)) {
                warnings.push("Held items may conflict with Primary / Secondary / Shield. Confirm this setup is valid.");
            warnings.push("Held items may conflict with Primary / Secondary / Shield. Confirm this setup is valid.");
            }
 
            return warnings;
         }
         }


         function updateWarnings() {
         return warnings;
            var warnings = validateEquipment();
    }
            var disabledSlots = getDisabledSlots();


            Object.keys(disabledSlots).forEach(function (slot) {
    function updateWarnings() {
                warnings.push(slot + " is " + disabledSlots[slot]);
        var warnings = validateEquipment();
            });
        var disabledSlots = getDisabledSlots();


             if (!warningBox) return;
        Object.keys(disabledSlots).forEach(function (slot) {
             warnings.push(slot + " is " + disabledSlots[slot]);
        });


            if (warnings.length === 0) {
        if (!warningBox) return;
                warningBox.style.display = "none";
                warningBox.innerHTML = "";
                return;
            }


             warningBox.style.display = "block";
        if (warnings.length === 0) {
             warningBox.innerHTML =
             warningBox.style.display = "none";
                "<b>Equipment Warnings</b><br>" +
             warningBox.innerHTML = "";
                warnings.map(function (w) {
            return;
                    return "ΓÇó " + w;
                }).join("<br>");
         }
         }


         function formatItemStats(item) {
         warningBox.style.display = "block";
            var rows = [];
        warningBox.innerHTML =
            "<b>Equipment Warnings</b><br>" +
            warnings.map(function (w) {
                return "ΓÇó " + w;
            }).join("<br>");
    }
 
    function formatItemStats(item) {
        var rows = [];


            function add(label, key) {
        function add(label, key) {
                if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
            if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
                    rows.push('<div><span>' + label + '</span> ' + item[key] + '</div>');
                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) {
         add("Lvl:", "lvl");
            var classText = item.class || "";
        add("DMG:", "dmg");
            var slotText = item.slot || "";
        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");


            var html = '';
        return rows.join("");
            html += '<div class="cb-tooltip-title cb-rarity-' +
     }
    normalizeRarity(item.rarity) +
    '">' +
    item.name +
     '</div>';


            if (slotText) html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + slotText + '</div>';
    function buildTooltipHtml(item) {
            if (classText) html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</div>';
        var classText = item.class || "";
            if (item.is2HD) html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
        var slotText = item.slot || "";
            if (item.weaponstyle) html += '<div class="cb-tooltip-line"><b>Style:</b> ' + item.weaponstyle + '</div>';
            if (item.proc) html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + item.proc + '</div>';
            if (item.effect) html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + item.effect + '</div>';
if (item.droppedby) html += '<div class="cb-tooltip-line"><b>Dropped By:</b> ' + item.droppedby + '</div>';
if (item.zone) html += '<div class="cb-tooltip-line"><b>Zone:</b> ' + item.zone + '</div>';
if (item.droprate) html += '<div class="cb-tooltip-line"><b>Drop Rate:</b> ' + item.droprate + '</div>';


            html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';
        var html = '';
        html += '<div class="cb-tooltip-title cb-rarity-' +
normalizeRarity(item.rarity) +
'">' +
item.name +
'</div>';


            return html;
        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>';
        if (item.is2HD) html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
        if (item.weaponstyle) html += '<div class="cb-tooltip-line"><b>Style:</b> ' + item.weaponstyle + '</div>';
        if (item.proc) html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + item.proc + '</div>';
         if (item.effect) html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + item.effect + '</div>';
if (item.droppedby) html += 'Dropped By: ' + item.droppedby + '';
if (item.zone) html += 'Zone: ' + item.zone + '';
if (item.droprate) html += 'Drop Rate: ' + item.droprate + '';


         function getCompareValue(newItem, oldItem, key) {
         html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';
            var newValue = parseFloat(newItem[key]) || 0;
            var oldValue = oldItem ? (parseFloat(oldItem[key]) || 0) : 0;
            return newValue - oldValue;
        }


         function buildCompareTooltipHtml(slot, newItem) {
         return html;
            var oldItem = equippedItems[slot];
    }
            var html = '';


            html += '<div class="cb-tooltip-title cb-rarity-' +
    function getCompareValue(newItem, oldItem, key) {
    normalizeRarity(newItem.rarity) +
        var newValue = parseFloat(newItem[key]) || 0;
    '">' +
        var oldValue = oldItem ? (parseFloat(oldItem[key]) || 0) : 0;
    newItem.name +
        return newValue - oldValue;
     '</div>';
     }


            if (oldItem) {
    function buildCompareTooltipHtml(slot, newItem) {
                html += '<div class="cb-tooltip-line"><b>Currently:</b> ' + oldItem.name + '</div>';
        var oldItem = equippedItems[slot];
            } else {
        var html = '';
                html += '<div class="cb-tooltip-line"><b>Currently:</b> None</div>';
            }


            if (newItem.slot) {
        html += '<div class="cb-tooltip-title cb-rarity-' +
                html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + newItem.slot + '</div>';
normalizeRarity(newItem.rarity) +
            }
'">' +
newItem.name +
'</div>';


            if (newItem.class) {
        if (oldItem) {
                html += '<div class="cb-tooltip-line"><b>Class:</b> ' + newItem.class + '</div>';
            html += '<div class="cb-tooltip-line"><b>Currently:</b> ' + oldItem.name + '</div>';
             }
        } else {
             html += '<div class="cb-tooltip-line"><b>Currently:</b> None</div>';
        }


            if (newItem.is2HD) {
        if (newItem.slot) {
                html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
            html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + newItem.slot + '</div>';
            }
        }


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


            if (newItem.proc) {
        if (newItem.is2HD) {
                html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + newItem.proc + '</div>';
            html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
            }
        }


            if (newItem.effect) {
        if (newItem.weaponstyle) {
                html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + newItem.effect + '</div>';
            html += '<div class="cb-tooltip-line"><b>Style:</b> ' + newItem.weaponstyle + '</div>';
            }
        }


             var compareRows = [];
        if (newItem.proc) {
             html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + newItem.proc + '</div>';
        }


            function addCompare(label, key) {
        if (newItem.effect) {
                var diff = getCompareValue(newItem, oldItem, key);
            html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + newItem.effect + '</div>';
                if (diff === 0) return;
        }


                var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative';
        var compareRows = [];
                var sign = diff > 0 ? '+' : '';


                compareRows.push(
        function addCompare(label, key) {
                    '<div><span>' + label + '</span> <b class="' + cls + '">' + sign + diff + '</b></div>'
            var diff = getCompareValue(newItem, oldItem, key);
                );
             if (diff === 0) return;
             }


             addCompare("Lvl:", "lvl");
             var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative';
             addCompare("DMG:", "dmg");
             var sign = diff > 0 ? '+' : '';
            addCompare("AC:", "ac");
            addCompare("STR:", "str");
            addCompare("STA:", "sta");
            addCompare("AGI:", "agi");
            addCompare("DEX:", "dex");
            addCompare("WIS:", "wis");
            addCompare("INT:", "int");
            addCompare("CHA:", "cha");
            addCompare("HP:", "hp");
            addCompare("HoT:", "hot");
            addCompare("POW:", "pow");
            addCompare("PoT:", "pot");
            addCompare("PR:", "pr");
            addCompare("DR:", "dr");
            addCompare("FR:", "fr");
            addCompare("CR:", "cr");
            addCompare("LR:", "lr");
            addCompare("AR:", "ar");
            addCompare("DUR:", "dur");


             html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Comparison</b></div>';
             compareRows.push(
                '<div><span>' + label + '</span> <b class="' + cls + '">' + sign + diff + '</b></div>'
            );
        }


            if (compareRows.length) {
        addCompare("Lvl:", "lvl");
                html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>';
        addCompare("DMG:", "dmg");
            } else {
        addCompare("AC:", "ac");
                html += '<div class="cb-tooltip-line">No stat difference.</div>';
        addCompare("STR:", "str");
            }
        addCompare("STA:", "sta");
        addCompare("AGI:", "agi");
        addCompare("DEX:", "dex");
        addCompare("WIS:", "wis");
        addCompare("INT:", "int");
        addCompare("CHA:", "cha");
        addCompare("HP:", "hp");
        addCompare("HoT:", "hot");
        addCompare("POW:", "pow");
        addCompare("PoT:", "pot");
        addCompare("PR:", "pr");
        addCompare("DR:", "dr");
        addCompare("FR:", "fr");
        addCompare("CR:", "cr");
        addCompare("LR:", "lr");
        addCompare("AR:", "ar");
        addCompare("DUR:", "dur");


            html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>';
        html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Comparison</b></div>';
            html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';


             return html;
        if (compareRows.length) {
             html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>';
        } else {
            html += '<div class="cb-tooltip-line">No stat difference.</div>';
         }
         }


         function showTooltip(item, event) {
         html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>';
            if (!tooltip || !item) return;
        html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';
            tooltip.className =
 
    'character-builder-tooltip cb-tooltip-rarity-' +
        return html;
    normalizeRarity(item.rarity);
    }


    function showTooltip(item, event) {
        if (!tooltip || !item) return;
        tooltip.className =
'character-builder-tooltip cb-tooltip-rarity-' +
normalizeRarity(item.rarity);
tooltip.innerHTML = buildTooltipHtml(item);
tooltip.innerHTML = buildTooltipHtml(item);
            tooltip.style.display = 'block';
tooltip.style.display = 'block';
            moveTooltip(event);
moveTooltip(event);
        }
}


        function moveTooltip(event) {
    function moveTooltip(event) {
            if (!tooltip || tooltip.style.display === 'none') return;
        if (!tooltip || tooltip.style.display === 'none') return;
            tooltip.style.left = (event.pageX + 14) + 'px';
        tooltip.style.left = (event.pageX + 14) + 'px';
            tooltip.style.top = (event.pageY + 14) + 'px';
        tooltip.style.top = (event.pageY + 14) + 'px';
        }
    }


        function hideTooltip() {
    function hideTooltip() {
            if (!tooltip) return;
        if (!tooltip) return;
            tooltip.style.display = 'none';
        tooltip.style.display = 'none';
        }
    }
 
        function showCompareTooltip(slot, item, event) {
            if (!compareTooltip || !item) return;
            compareTooltip.className =
    'character-builder-tooltip cb-compare-tooltip cb-tooltip-rarity-' +
    normalizeRarity(item.rarity);


    function showCompareTooltip(slot, item, event) {
        if (!compareTooltip || !item) return;
        compareTooltip.className =
'character-builder-tooltip cb-compare-tooltip cb-tooltip-rarity-' +
normalizeRarity(item.rarity);
compareTooltip.innerHTML =
compareTooltip.innerHTML =
    buildCompareTooltipHtml(slot, item);
buildCompareTooltipHtml(slot, item);
            compareTooltip.style.display = 'block';
compareTooltip.style.display = 'block';
            moveCompareTooltip(event);
moveCompareTooltip(event);
        }
}


        function moveCompareTooltip(event) {
    function moveCompareTooltip(event) {
            if (!compareTooltip || compareTooltip.style.display === 'none') return;
        if (!compareTooltip || compareTooltip.style.display === 'none') return;
            compareTooltip.style.left = (event.pageX + 14) + 'px';
        compareTooltip.style.left = (event.pageX + 14) + 'px';
            compareTooltip.style.top = (event.pageY + 14) + 'px';
        compareTooltip.style.top = (event.pageY + 14) + 'px';
        }
    }


        function hideCompareTooltip() {
    function hideCompareTooltip() {
            if (!compareTooltip) return;
        if (!compareTooltip) return;
            compareTooltip.style.display = 'none';
        compareTooltip.style.display = 'none';
        }
    }


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


            equippedSummaryBody.innerHTML = '';
        equippedSummaryBody.innerHTML = '';


            gearSlots.forEach(function (slot) {
        gearSlots.forEach(function (slot) {
                var item = equippedItems[slot];
            var item = equippedItems[slot];
                var row = document.createElement('tr');
            var row = document.createElement('tr');


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


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


                if (item) {
            if (item) {
                    var itemSpan = document.createElement('span');
                var itemSpan = document.createElement('span');
                    itemSpan.textContent = item.name;
                itemSpan.textContent = item.name;
itemSpan.className =
itemSpan.className =
    'cb-equipped-item-name cb-rarity-' +
'cb-equipped-item-name cb-rarity-' +
    normalizeRarity(item.rarity);
normalizeRarity(item.rarity);
itemSpan.style.cursor = 'pointer';
itemSpan.style.cursor = 'pointer';


itemSpan.addEventListener('click', function () {
itemSpan.addEventListener('click', function () {
    openItemPage(item.name);
openItemPage(item.name);
});
});


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


                    itemSpan.addEventListener('mousemove', moveTooltip);
                itemSpan.addEventListener('mousemove', moveTooltip);
                    itemSpan.addEventListener('mouseleave', hideTooltip);
                itemSpan.addEventListener('mouseleave', hideTooltip);


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


                row.appendChild(slotCell);
            row.appendChild(slotCell);
                row.appendChild(itemCell);
            row.appendChild(itemCell);
                equippedSummaryBody.appendChild(row);
            equippedSummaryBody.appendChild(row);
            });
        });
        }
    }
 
        function updateWeaponSummary() {
            function setWeaponRow(slotName, prefix) {
                var item = equippedItems[slotName];


                var nameEl = document.getElementById(prefix + '-name');
    function updateWeaponSummary() {
                var dmgEl = document.getElementById(prefix + '-dmg');
        function setWeaponRow(slotName, prefix) {
                var styleEl = document.getElementById(prefix + '-style');
            var item = equippedItems[slotName];
                var effectEl = document.getElementById(prefix + '-effect');


                if (!nameEl || !dmgEl || !styleEl || !effectEl) return;
            var nameEl = document.getElementById(prefix + '-name');
            var dmgEl = document.getElementById(prefix + '-dmg');
            var styleEl = document.getElementById(prefix + '-style');
            var effectEl = document.getElementById(prefix + '-effect');


                nameEl.innerHTML = '';
            if (!nameEl || !dmgEl || !styleEl || !effectEl) return;


            nameEl.innerHTML = '';
if (item) {
if (item) {
    var link = document.createElement('span');
var link = document.createElement('span');
    link.textContent = item.name;
link.textContent = item.name;
    link.className =
link.className =
        'cb-equipped-item-name cb-rarity-' +
'cb-equipped-item-name cb-rarity-' +
        normalizeRarity(item.rarity);
normalizeRarity(item.rarity);
    link.style.cursor = 'pointer';
link.style.cursor = 'pointer';


    link.addEventListener('click', function () {
link.addEventListener('click', function () {
        openItemPage(item.name);
    openItemPage(item.name);
    });
});


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


    link.addEventListener('mousemove', moveTooltip);
link.addEventListener('mousemove', moveTooltip);


    link.addEventListener('mouseleave', hideTooltip);
link.addEventListener('mouseleave', hideTooltip);


    nameEl.appendChild(link);
nameEl.appendChild(link);
} else {
} else {
    nameEl.textContent = '-';
nameEl.textContent = '-';
}
}
                dmgEl.textContent = item && item.dmg ? item.dmg : '-';
dmgEl.textContent = item && item.dmg ? item.dmg : '-';


                if (item) {
            if (item) {
                    if (item.weaponstyle) {
                if (item.weaponstyle) {
                        styleEl.textContent = item.weaponstyle;
                    styleEl.textContent = item.weaponstyle;
                    } else if (item.is2HD) {
                } else if (item.is2HD) {
                        styleEl.textContent = '2HD';
                    styleEl.textContent = '2HD';
                    } else if (item.isPrimary || item.isSecondary) {
                } else if (item.isPrimary || item.isSecondary) {
                        styleEl.textContent = '1HD';
                    styleEl.textContent = '1HD';
                    } else {
                        styleEl.textContent = '-';
                    }
                 } else {
                 } else {
                     styleEl.textContent = '-';
                     styleEl.textContent = '-';
                 }
                 }
            } else {
                styleEl.textContent = '-';
            }


                var effectText = '-';
            var effectText = '-';


                if (item) {
            if (item) {
                    if (item.proc) {
                if (item.proc) {
                        effectText = item.proc;
                    effectText = item.proc;
                    } else if (item.effect) {
                } else if (item.effect) {
                        effectText = item.effect;
                    effectText = item.effect;
                    }
                 }
                 }
                effectEl.textContent = effectText;
             }
             }


             setWeaponRow('Primary', 'cb-weapon-primary');
             effectEl.textContent = effectText;
            setWeaponRow('Secondary', 'cb-weapon-secondary');
            setWeaponRow('Range', 'cb-weapon-range');
         }
         }


         function getBaseStats() {
         setWeaponRow('Primary', 'cb-weapon-primary');
            var race = raceSelect.value;
        setWeaponRow('Secondary', 'cb-weapon-secondary');
            var classCode = classSelect.value;
        setWeaponRow('Range', 'cb-weapon-range');
            return data.baseStats[race][classCode];
    }
        }


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


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


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


         function clampTrainingInput(changedKey) {
         return total;
            var level = getCharacterLevel();
    }
            var availableTP = getTrainingPointsForLevel(level);
            var spentOther = 0;


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


             var input = document.getElementById('train-' + changedKey);
        Object.keys(trainingStats).forEach(function (key) {
            var value = parseInt(input.value, 10);
             if (key !== changedKey) {
                spentOther += parseInt(trainingStats[key], 10) || 0;
            }
        });


            if (isNaN(value) || value < 0) value = 0;
        var input = document.getElementById('train-' + changedKey);
        var value = parseInt(input.value, 10);


            var maxAllowed = availableTP - spentOther;
        if (isNaN(value) || value < 0) value = 0;
            if (maxAllowed < 0) maxAllowed = 0;


            if (value > maxAllowed) {
        var maxAllowed = availableTP - spentOther;
                value = maxAllowed;
        if (maxAllowed < 0) maxAllowed = 0;
                input.value = value;
            }


             trainingStats[changedKey] = value;
        if (value > maxAllowed) {
             value = maxAllowed;
            input.value = value;
         }
         }


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


        function setCappedStat(elementId, value, cap) {
    function setCappedStat(elementId, value, cap) {
            var element = document.getElementById(elementId);
        var element = document.getElementById(elementId);
            if (!element) return;
        if (!element) return;


            value = parseFloat(value) || 0;
        value = parseFloat(value) || 0;


            if (value > cap) {
        if (value > cap) {
                var over = value - cap;
            var over = value - cap;
                element.innerHTML = cap + ' <span class="cb-overcap">(+' + over + ')</span>';
            element.innerHTML = cap + ' <span class="cb-overcap">(+' + over + ')</span>';
            } else {
        } else {
                element.textContent = value;
            element.textContent = value;
            }
         }
         }
 
    }
function getTopItemsForSlot(slot, statKey, limit) {
function getTopItemsForSlot(slot, statKey, limit) {
    var items = getFilteredItemsForSlot(slot, "", "", statKey);
var items = getFilteredItemsForSlot(slot, "", "", statKey);


    items = items.filter(function (item) {
items = items.filter(function (item) {
        return (parseFloat(item[statKey]) || 0) > 0;
    return (parseFloat(item[statKey]) || 0) > 0;
    });
});


    return items.slice(0, limit || 5);
return items.slice(0, limit || 5);
}
}


function showBestInSlotHelper() {
function showBestInSlotHelper() {
    if (!bisStatSelect || !bisResultsBox) return;
if (!bisStatSelect || !bisResultsBox) return;
 
    var statKey = bisStatSelect.value;


var statKey = bisStatSelect.value;
if (!statKey) {
if (!statKey) {
    bisResultsBox.style.display = 'none';
bisResultsBox.style.display = 'none';
    bisResultsBox.innerHTML = '';
bisResultsBox.innerHTML = '';
    return;
return;
}
}
    var statLabel = statKey.toUpperCase();
var statLabel = statKey.toUpperCase();


    var html = '<b>Top Items by ' + statLabel + '</b><br>';
var html = '<b>Top Items by ' + statLabel + '</b><br>';


    gearSlots.forEach(function (slot) {
gearSlots.forEach(function (slot) {
        var topItems = getTopItemsForSlot(slot, statKey, 5);
    var topItems = getTopItemsForSlot(slot, statKey, 5);


        html += '<div style="margin-top:8px;"><b>' + slot + '</b></div>';
    html += '<div style="margin-top:8px;"><b>' + slot + '</b></div>';


        if (!topItems.length) {
    if (!topItems.length) {
            html += '<div>No matching items.</div>';
        html += '<div>No matching items.</div>';
            return;
        return;
        }
    }


        topItems.forEach(function (item, index) {
    topItems.forEach(function (item, index) {
            html += '<div>' +
        html += '<div>' +
                (index + 1) + '. ' +
            (index + 1) + '. ' +
                '<span class="cb-equipped-item-name" style="cursor:pointer;" data-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
            '<span class="cb-equipped-item-name" style="cursor:pointer;" data-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
                item.name +
            item.name +
                '</span>' +
            '</span>' +
                ' ΓÇö ' + statLabel + ': ' + (item[statKey] || 0) +
            ' ΓÇö ' + statLabel + ': ' + (item[statKey] || 0) +
                '</div>';
            '</div>';
        });
     });
     });
});


    bisResultsBox.innerHTML = html;
bisResultsBox.innerHTML = html;
    bisResultsBox.style.display = 'block';
bisResultsBox.style.display = 'block';


    Array.prototype.forEach.call(
Array.prototype.forEach.call(
        bisResultsBox.querySelectorAll('[data-item-name]'),
    bisResultsBox.querySelectorAll('[data-item-name]'),
        function (el) {
    function (el) {
            el.addEventListener('click', function () {
        el.addEventListener('click', function () {
                openItemPage(el.getAttribute('data-item-name'));
            openItemPage(el.getAttribute('data-item-name'));
            });
        });
        }
    }
    );
);
}
}


function getBuildDataFromUrl(url) {
function getBuildDataFromUrl(url) {
    try {
try {
        var parsedUrl = new URL(url);
var parsedUrl = new URL(url);
        var encoded = parsedUrl.searchParams.get('build');
var encoded = parsedUrl.searchParams.get('build');


        if (!encoded) return null;
    if (!encoded) return null;


        return decodeBuildData(encoded);
    return decodeBuildData(encoded);
    } catch (e) {
} catch (e) {
        return null;
    return null;
    }
}
}
}


function formatCompareValue(value) {
function formatCompareValue(value) {
    if (value === undefined || value === null || value === '') return '-';
if (value === undefined || value === null || value === '') return '-';
    return value;
return value;
}
}


function compareTextValue(label, a, b) {
function compareTextValue(label, a, b) {
    if (a === b) {
if (a === b) {
        return '<div><b>' + label + ':</b> ' + formatCompareValue(a) + '</div>';
return '' + label + ': ' + formatCompareValue(a) + '';
    }
}


    return '<div><b>' + label + ':</b> ' +
return '<div><b>' + label + ':</b> ' +
        formatCompareValue(a) +
    formatCompareValue(a) +
        ' ΓåÆ <b>' +
    ' ΓåÆ <b>' +
        formatCompareValue(b) +
    formatCompareValue(b) +
        '</b></div>';
    '</b></div>';
}
}


function compareBuildEquipment(buildA, buildB) {
function compareBuildEquipment(buildA, buildB) {
    var html = '<div style="margin-top:10px;"><b>Equipment Differences</b></div>';
var html = 'Equipment Differences';


    gearSlots.forEach(function (slot) {
gearSlots.forEach(function (slot) {
        var itemA = buildA.equippedItemNames && buildA.equippedItemNames[slot]
    var itemA = buildA.equippedItemNames && buildA.equippedItemNames[slot]
            ? buildA.equippedItemNames[slot]
        ? buildA.equippedItemNames[slot]
            : '-';
        : '-';


        var itemB = buildB.equippedItemNames && buildB.equippedItemNames[slot]
    var itemB = buildB.equippedItemNames && buildB.equippedItemNames[slot]
            ? buildB.equippedItemNames[slot]
        ? buildB.equippedItemNames[slot]
            : '-';
        : '-';


        if (itemA !== itemB) {
    if (itemA !== itemB) {
            html += '<div><b>' + slot + ':</b> ' + itemA + ' ΓåÆ <b>' + itemB + '</b></div>';
        html += '<div><b>' + slot + ':</b> ' + itemA + ' ΓåÆ <b>' + itemB + '</b></div>';
        }
    }
    });
});


    return html;
return html;
}
}


function calculateBuildTotalsFromSharedBuild(buildData) {
function calculateBuildTotalsFromSharedBuild(buildData) {
    var race = buildData.race;
var race = buildData.race;
    var classCode = buildData.classCode;
var classCode = buildData.classCode;


    if (!race || !classCode || !data.baseStats[race] || !data.baseStats[race][classCode]) {
if (!race || !classCode || !data.baseStats[race] || !data.baseStats[race][classCode]) {
        return null;
    return null;
    }
}


    var base = data.baseStats[race][classCode];
var base = data.baseStats[race][classCode];


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


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


    if (buildData.equippedItemNames) {
if (buildData.equippedItemNames) {
        gearSlots.forEach(function (slot) {
    gearSlots.forEach(function (slot) {
            var itemName = buildData.equippedItemNames[slot];
        var itemName = buildData.equippedItemNames[slot];
            var item = findLoadedItemByName(itemName, slot);
        var item = findLoadedItemByName(itemName, slot);


            if (!item) return;
        if (!item) return;


            ["str", "sta", "agi", "dex", "wis", "int", "cha", "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar"].forEach(function (key) {
        ["str", "sta", "agi", "dex", "wis", "int", "cha", "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar"].forEach(function (key) {
                total[key] += parseFloat(item[key]) || 0;
            total[key] += parseFloat(item[key]) || 0;
            });
         });
         });
     }
     });
}


    return total;
return total;
}
}


function compareBuildStats(buildA, buildB) {
function compareBuildStats(buildA, buildB) {
    var totalsA = calculateBuildTotalsFromSharedBuild(buildA);
var totalsA = calculateBuildTotalsFromSharedBuild(buildA);
    var totalsB = calculateBuildTotalsFromSharedBuild(buildB);
var totalsB = calculateBuildTotalsFromSharedBuild(buildB);


    if (!totalsA || !totalsB) {
if (!totalsA || !totalsB) {
        return '<div style="margin-top:10px;"><b>Stat Differences</b><br>Could not calculate stats for one or both builds.</div>';
    return '<div style="margin-top:10px;"><b>Stat Differences</b><br>Could not calculate stats for one or both builds.</div>';
    }
}


    var statOrder = [
var statOrder = [
        "str", "sta", "agi", "dex", "wis", "int", "cha",
    "str", "sta", "agi", "dex", "wis", "int", "cha",
        "hp", "pow", "ac",
    "hp", "pow", "ac",
        "pr", "dr", "fr", "cr", "lr", "ar"
    "pr", "dr", "fr", "cr", "lr", "ar"
    ];
];


    var html = '<div style="margin-top:10px;"><b>Stat Differences</b></div>';
var html = '<div style="margin-top:10px;"><b>Stat Differences</b></div>';


    statOrder.forEach(function (key) {
statOrder.forEach(function (key) {
        var a = parseFloat(totalsA[key]) || 0;
    var a = parseFloat(totalsA[key]) || 0;
        var b = parseFloat(totalsB[key]) || 0;
    var b = parseFloat(totalsB[key]) || 0;
        var diff = b - a;
    var diff = b - a;


        var label = key.toUpperCase();
    var label = key.toUpperCase();
        var diffText = diff > 0 ? ' <span class="cb-compare-positive">(+' + diff + ')</span>' :
    var diffText = diff > 0 ? ' <span class="cb-compare-positive">(+' + diff + ')</span>' :
                      diff < 0 ? ' <span class="cb-compare-negative">(' + diff + ')</span>' :
                  diff < 0 ? ' <span class="cb-compare-negative">(' + diff + ')</span>' :
                      ' (0)';
                  ' (0)';


        html += '<div><b>' + label + ':</b> ' + a + ' ΓåÆ ' + b + diffText + '</div>';
    html += '<div><b>' + label + ':</b> ' + a + ' ΓåÆ ' + b + diffText + '</div>';
    });
});


    return html;
return html;
}
}


function compareSharedBuilds() {
function compareSharedBuilds() {
    if (!compareUrlAInput || !compareUrlBInput || !compareResultsBox) return;
if (!compareUrlAInput || !compareUrlBInput || !compareResultsBox) return;


    var buildA = getBuildDataFromUrl(compareUrlAInput.value.trim());
var buildA = getBuildDataFromUrl(compareUrlAInput.value.trim());
    var buildB = getBuildDataFromUrl(compareUrlBInput.value.trim());
var buildB = getBuildDataFromUrl(compareUrlBInput.value.trim());


    if (!buildA || !buildB) {
if (!buildA || !buildB) {
        compareResultsBox.style.display = 'block';
    compareResultsBox.style.display = 'block';
        compareResultsBox.innerHTML = 'Could not compare builds. Make sure both URLs are valid shared build URLs.';
    compareResultsBox.innerHTML = 'Could not compare builds. Make sure both URLs are valid shared build URLs.';
        return;
    return;
    }
}


    var html = '<b>Build Comparison</b><br>';
var html = '<b>Build Comparison</b><br>';


    html += compareTextValue('Build Name', buildA.buildName || '-', buildB.buildName || '-');
html += compareTextValue('Build Name', buildA.buildName || '-', buildB.buildName || '-');
    html += compareTextValue('Race', buildA.race || '-', buildB.race || '-');
html += compareTextValue('Race', buildA.race || '-', buildB.race || '-');
    html += compareTextValue('Class', buildA.classCode || '-', buildB.classCode || '-');
html += compareTextValue('Class', buildA.classCode || '-', buildB.classCode || '-');
    html += compareTextValue('Level', buildA.level || '-', buildB.level || '-');
html += compareTextValue('Level', buildA.level || '-', buildB.level || '-');


    html += compareBuildEquipment(buildA, buildB);
html += compareBuildEquipment(buildA, buildB);
html += compareBuildStats(buildA, buildB);
html += compareBuildStats(buildA, buildB);


    compareResultsBox.innerHTML = html;
compareResultsBox.innerHTML = html;
    compareResultsBox.style.display = 'block';
compareResultsBox.style.display = 'block';
}
}


function applyBuilderTheme(theme) {
function applyBuilderTheme(theme) {
    if (!app) return;
if (!app) return;


    app.classList.remove('cb-theme-dark');
app.classList.remove('cb-theme-dark');
    app.classList.remove('cb-theme-light');
app.classList.remove('cb-theme-light');


    if (theme === 'light') {
if (theme === 'light') {
        app.classList.add('cb-theme-light');
    app.classList.add('cb-theme-light');
    } else {
} else {
        app.classList.add('cb-theme-dark');
    app.classList.add('cb-theme-dark');
    }
}


    try {
try {
        localStorage.setItem('eqoa-builder-theme', theme);
    localStorage.setItem('eqoa-builder-theme', theme);
    } catch (e) {}
} catch (e) {}
}
}


function toggleBuilderTheme() {
function toggleBuilderTheme() {
    var isLight = app.classList.contains('cb-theme-light');
var isLight = app.classList.contains('cb-theme-light');


    if (isLight) {
if (isLight) {
        applyBuilderTheme('dark');
    applyBuilderTheme('dark');
    } else {
} else {
        applyBuilderTheme('light');
    applyBuilderTheme('light');
    }
}
}
}


function loadSavedBuilderTheme() {
function loadSavedBuilderTheme() {
    try {
try {
        var saved = localStorage.getItem('eqoa-builder-theme');
var saved = localStorage.getItem('eqoa-builder-theme');


        if (saved === 'light') {
    if (saved === 'light') {
            applyBuilderTheme('light');
        applyBuilderTheme('light');
        } else {
    } else {
            applyBuilderTheme('dark');
        }
    } catch (e) {
         applyBuilderTheme('dark');
         applyBuilderTheme('dark');
     }
     }
} catch (e) {
    applyBuilderTheme('dark');
}
}
}


function getItemPrimarySlotLabel(item) {
function getItemPrimarySlotLabel(item) {
    if (!item) return "-";
if (!item) return "-";


    if (item.slot) return item.slot;
if (item.slot) return item.slot;
    if (item.isPrimary) return "Primary";
if (item.isPrimary) return "Primary";
    if (item.isSecondary) return "Secondary";
if (item.isSecondary) return "Secondary";
    if (item.isShield) return "Shield";
if (item.isShield) return "Shield";
    if (item.isHeld) return "Held";
if (item.isHeld) return "Held";


    if (item.categories && item.categories.length) {
if (item.categories && item.categories.length) {
        for (var i = 0; i < item.categories.length; i++) {
    for (var i = 0; i < item.categories.length; i++) {
            var cat = item.categories[i];
        var cat = item.categories[i];
            if (cat.indexOf(" Items") !== -1) {
        if (cat.indexOf(" Items") !== -1) {
                return cat.replace(" Items", "");
            return cat.replace(" Items", "");
            }
         }
         }
     }
     }
}


    return "-";
return "-";
}
}


function searchAllLoadedItems(query) {
function searchAllLoadedItems(query) {
    query = String(query || "").toLowerCase().trim();
query = String(query || "").toLowerCase().trim();


    if (!query) return [];
if (!query) return [];


    var allItems = getAllLoadedItemsFlat();
var allItems = getAllLoadedItemsFlat();


    return allItems.filter(function (item) {
return allItems.filter(function (item) {
        var searchable = [
    var searchable = [
            item.name,
        item.name,
            item.class,
        item.class,
            item.slot,
        item.slot,
            item.weaponstyle,
        item.weaponstyle,
            item.proc,
            item.effect,
            item.droppedby,
            item.zone,
            item.droprate,
            item.categories ? item.categories.join(" ") : ""
        ].join(" ").toLowerCase();


         return searchable.indexOf(query) !== -1;
         item.proc,
    }).slice(0, 30);
        item.effect,
        item.droppedby,
        item.zone,
        item.droprate,
        item.categories ? item.categories.join(" ") : ""
    ].join(" ").toLowerCase();
 
    return searchable.indexOf(query) !== -1;
}).slice(0, 30);
}
}


function renderGlobalItemSearchResults() {
function renderGlobalItemSearchResults() {
    if (!globalItemSearchInput || !globalSearchResultsBox) return;
if (!globalItemSearchInput || !globalSearchResultsBox) return;


    var query = globalItemSearchInput.value;
var query = globalItemSearchInput.value;
    var results = searchAllLoadedItems(query);
var results = searchAllLoadedItems(query);


    if (!query || !query.trim()) {
if (!query || !query.trim()) {
        globalSearchResultsBox.style.display = "none";
    globalSearchResultsBox.style.display = "none";
        globalSearchResultsBox.innerHTML = "";
    globalSearchResultsBox.innerHTML = "";
        return;
    return;
    }
}


    globalSearchResultsBox.style.display = "block";
globalSearchResultsBox.style.display = "block";


    if (!results.length) {
if (!results.length) {
        globalSearchResultsBox.innerHTML = "No matching items found.";
    globalSearchResultsBox.innerHTML = "No matching items found.";
        return;
    return;
    }
}


    var html = '<b>Global Item Search Results</b><br>';
var html = '<b>Global Item Search Results</b><br>';


    results.forEach(function (item) {
results.forEach(function (item) {
        var slotLabel = getItemPrimarySlotLabel(item);
    var slotLabel = getItemPrimarySlotLabel(item);
        var lvlText = item.lvl ? "Lv " + item.lvl : "No Lv";
    var lvlText = item.lvl ? "Lv " + item.lvl : "No Lv";
        var classText = item.class ? item.class : "ALL";
    var classText = item.class ? item.class : "ALL";
        var sourceText = "";
    var sourceText = "";


        if (item.zone || item.droppedby) {
    if (item.zone || item.droppedby) {
            sourceText = " ΓÇö " + [
        sourceText = " ΓÇö " + [
                item.zone || "",
            item.zone || "",
                item.droppedby || ""
            item.droppedby || ""
            ].filter(Boolean).join(" / ");
        ].filter(Boolean).join(" / ");
        }
    }


        html += '<div style="margin-top:6px;">' +
    html += '<div style="margin-top:6px;">' +
            '<span class="cb-equipped-item-name" style="cursor:pointer;" data-global-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
        '<span class="cb-equipped-item-name" style="cursor:pointer;" data-global-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
                item.name +
            item.name +
            '</span>' +
        '</span>' +
            ' ΓÇö ' + slotLabel +
        ' ΓÇö ' + slotLabel +
            ' ΓÇö ' + lvlText +
        ' ΓÇö ' + lvlText +
            ' ΓÇö ' + classText +
        ' ΓÇö ' + classText +
            sourceText +
        sourceText +
            '</div>';
        '</div>';
    });
});


    globalSearchResultsBox.innerHTML = html;
globalSearchResultsBox.innerHTML = html;


    Array.prototype.forEach.call(
Array.prototype.forEach.call(
        globalSearchResultsBox.querySelectorAll('[data-global-item-name]'),
    globalSearchResultsBox.querySelectorAll('[data-global-item-name]'),
        function (el) {
    function (el) {
            el.addEventListener('click', function () {
        el.addEventListener('click', function () {
    openItemPage(el.getAttribute('data-global-item-name'));
openItemPage(el.getAttribute('data-global-item-name'));
});
});


el.addEventListener('mouseenter', function (event) {
el.addEventListener('mouseenter', function (event) {
    var itemName = el.getAttribute('data-global-item-name');
var itemName = el.getAttribute('data-global-item-name');
    var item = findLoadedItemByName(itemName, "Primary") || getAllLoadedItemsFlat().find(function (x) {
var item = findLoadedItemByName(itemName, "Primary") || getAllLoadedItemsFlat().find(function (x) {
        return x.name === itemName;
return x.name === itemName;
    });
});


    if (item) {
if (item) {
        showTooltip(item, event);
    showTooltip(item, event);
    }
}
});
});


el.addEventListener('mousemove', moveTooltip);
el.addEventListener('mousemove', moveTooltip);
el.addEventListener('mouseleave', hideTooltip);
el.addEventListener('mouseleave', hideTooltip);
        }
}
    );
);
}
}


Line 2,114: Line 2,425:


function populateExportCard() {
function populateExportCard() {
    var buildName = buildNameInput ? buildNameInput.value.trim() : '';
var buildName = buildNameInput ? buildNameInput.value.trim() : '';
    var shareUrl = generateShareUrl();
var shareUrl = generateShareUrl();
var classIcons = {
var classIcons = {
    WAR: '/images/classes/war.png',
WAR: '/images/classes/war.png',
    PAL: '/images/classes/pal.png',
PAL: '/images/classes/pal.png',
    SK: '/images/classes/sk.png',
SK: '/images/classes/sk.png',
    RGE: '/images/classes/rge.png',
RGE: '/images/classes/rge.png',
    BRD: '/images/classes/brd.png',
BRD: '/images/classes/brd.png',
    RAN: '/images/classes/ran.png',
RAN: '/images/classes/ran.png',
    MNK: '/images/classes/mnk.png',
MNK: '/images/classes/mnk.png',
    CL: '/images/classes/cl.png',
CL: '/images/classes/cl.png',
    DRD: '/images/classes/drd.png',
DRD: '/images/classes/drd.png',
    SHA: '/images/classes/sha.png',
SHA: '/images/classes/sha.png',
    NEC: '/images/classes/nec.png',
NEC: '/images/classes/nec.png',
    MAG: '/images/classes/mag.png',
MAG: '/images/classes/mag.png',
    ENC: '/images/classes/enc.png',
ENC: '/images/classes/enc.png',
    WIZ: '/images/classes/wiz.png',
WIZ: '/images/classes/wiz.png',
    ALC: '/images/classes/alc.png'
ALC: '/images/classes/alc.png'
};
};


    document.getElementById('cb-export-build-name').textContent =
document.getElementById('cb-export-build-name').textContent =
        buildName || 'Unnamed Build';
    buildName || 'Unnamed Build';
 
    document.getElementById('cb-export-race').textContent =
        raceSelect.value || '-';


    document.getElementById('cb-export-class').textContent =
document.getElementById('cb-export-race').textContent =
        classSelect.value || '-';
    raceSelect.value || '-';


document.getElementById('cb-export-class').textContent =
    classSelect.value || '-';
var classIcons = {
var classIcons = {
    WAR: '/eqoawiki/index.php/Special:Redirect/file/WAR.png',
WAR: '/eqoawiki/index.php/Special/file/WAR.png',
    PAL: '/eqoawiki/index.php/Special:Redirect/file/PAL.png',
PAL: '/eqoawiki/index.php/Special/file/PAL.png',
    SK: '/eqoawiki/index.php/Special:Redirect/file/SK.png',
SK: '/eqoawiki/index.php/Special/file/SK.png',
    RGE: '/eqoawiki/index.php/Special:Redirect/file/RGE.png',
RGE: '/eqoawiki/index.php/Special/file/RGE.png',
    BRD: '/eqoawiki/index.php/Special:Redirect/file/BRD.png',
BRD: '/eqoawiki/index.php/Special/file/BRD.png',
    RAN: '/eqoawiki/index.php/Special:Redirect/file/RAN.png',
RAN: '/eqoawiki/index.php/Special/file/RAN.png',
    MNK: '/eqoawiki/index.php/Special:Redirect/file/MNK.png',
MNK: '/eqoawiki/index.php/Special/file/MNK.png',
    CL: '/eqoawiki/index.php/Special:Redirect/file/CL.png',
CL: '/eqoawiki/index.php/Special/file/CL.png',
    DRD: '/eqoawiki/index.php/Special:Redirect/file/DRD.png',
DRD: '/eqoawiki/index.php/Special/file/DRD.png',
    SHA: '/eqoawiki/index.php/Special:Redirect/file/SHA.png',
SHA: '/eqoawiki/index.php/Special/file/SHA.png',
    NEC: '/eqoawiki/index.php/Special:Redirect/file/NEC.png',
NEC: '/eqoawiki/index.php/Special/file/NEC.png',
    MAG: '/eqoawiki/index.php/Special:Redirect/file/MAG.png',
MAG: '/eqoawiki/index.php/Special/file/MAG.png',
    ENC: '/eqoawiki/index.php/Special:Redirect/file/ENC.png',
ENC: '/eqoawiki/index.php/Special/file/ENC.png',
    WIZ: '/eqoawiki/index.php/Special:Redirect/file/WIZ.png',
WIZ: '/eqoawiki/index.php/Special/file/WIZ.png',
    ALC: '/eqoawiki/index.php/Special:Redirect/file/ALC.png'
ALC: '/eqoawiki/index.php/Special/file/ALC.png'
};
};


Line 2,164: Line 2,474:


if (classIcon) {
if (classIcon) {
    classIcon.src =
classIcon.src =
        classIcons[classSelect.value] || '';
classIcons[classSelect.value] || '';


    classIcon.style.display =
classIcon.style.display =
        classIcons[classSelect.value]
    classIcons[classSelect.value]
            ? 'block'
        ? 'block'
            : 'none';
        : 'none';
}
}


Line 2,176: Line 2,486:


if (classIcon) {
if (classIcon) {
    classIcon.src =
classIcon.src =
        classIcons[classSelect.value] || '';
classIcons[classSelect.value] || '';


    classIcon.style.display =
classIcon.style.display =
        classIcons[classSelect.value]
    classIcons[classSelect.value]
            ? 'block'
        ? 'block'
            : 'none';
        : 'none';
}
}


    document.getElementById('cb-export-level').textContent =
document.getElementById('cb-export-level').textContent =
        getCharacterLevel();
    getCharacterLevel();


    function statLine(key) {
function statLine(key) {
        var el = document.getElementById('cb-' + key);
    var el = document.getElementById('cb-' + key);
        var value = el ? el.textContent : '0';
    var value = el ? el.textContent : '0';


        return '<div><b>' + key.toUpperCase() + ':</b> ' + value + '</div>';
    return '<div><b>' + key.toUpperCase() + ':</b> ' + value + '</div>';
    }
}


    var coreStats = [
var coreStats = [
    'str', 'wis',
'str', 'wis',
    'sta', 'int',
'sta', 'int',
    'agi', 'cha',
'agi', 'cha',
    'dex'
'dex'
];
];


var resists = [
var resists = [
    'fr', 'ar',
'fr', 'ar',
    'cr', 'pr',
'cr', 'pr',
    'lr', 'dr'
'lr', 'dr'
];
];


var vitals = [
var vitals = [
    'hp',
'hp',
    'pow',
'pow',
    'ac'
'ac'
];
];


    document.getElementById('cb-export-core-stats').innerHTML =
document.getElementById('cb-export-core-stats').innerHTML =
        coreStats.map(statLine).join('');
    coreStats.map(statLine).join('');
 
document.getElementById('cb-export-resists').innerHTML =
    resists.map(statLine).join('');


    document.getElementById('cb-export-resists').innerHTML =
document.getElementById('cb-export-vitals').innerHTML =
        resists.map(statLine).join('');
    vitals.map(statLine).join('');


    document.getElementById('cb-export-vitals').innerHTML =
var itemsHtml = '';
        vitals.map(statLine).join('');


    var itemsHtml = '';


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


    if (item) {
if (item) {
        itemsHtml +=
    itemsHtml +=
            '<div>' +
        '<div>' +
            '<span class="cb-rarity-' +
        '<span class="cb-rarity-' +
            normalizeRarity(item.rarity) +
        normalizeRarity(item.rarity) +
            '">' +
        '">' +
            item.name +
        item.name +
            '</span>' +
        '</span>' +
            '</div>';
        '</div>';
    } else {
} else {
        itemsHtml +=
    itemsHtml +=
            '<div>-</div>';
        '<div>-</div>';
    }
}
});
});


    document.getElementById('cb-export-items').innerHTML =
document.getElementById('cb-export-items').innerHTML =
        itemsHtml;
    itemsHtml;




    if (exportNotesBox) {
if (exportNotesBox) {
        var notes = buildNotesInput
    var notes = buildNotesInput
            ? buildNotesInput.value.trim()
        ? buildNotesInput.value.trim()
            : '';
        : '';


        exportNotesBox.textContent =
    exportNotesBox.textContent =
            notes || 'No build notes.';
        notes || 'No build notes.';
    }
}


    if (exportQrBox) {
if (exportQrBox) {
    exportQrBox.innerHTML = '';
exportQrBox.innerHTML = '';


    try {
try {
        if (typeof QRCode !== 'undefined') {
    if (typeof QRCode !== 'undefined') {
            new QRCode(exportQrBox, {
        new QRCode(exportQrBox, {
                text: shareUrl,
            text: shareUrl,
                width: 180,
            width: 180,
                height: 180,
            height: 180,
                correctLevel: QRCode.CorrectLevel.L
            correctLevel: QRCode.CorrectLevel.L
            });
        });
        } else {
    } else {
            exportQrBox.textContent = 'QR not loaded';
        exportQrBox.textContent = 'QR not loaded';
        }
    } catch (e) {
        console.error('QR generation failed.', e);
        exportQrBox.textContent = 'QR failed';
     }
     }
} catch (e) {
    console.error('QR generation failed.', e);
    exportQrBox.textContent = 'QR failed';
}
}
}
}
}
Line 2,286: Line 2,597:


function exportBuildCardImage() {
function exportBuildCardImage() {
    if (typeof html2canvas === 'undefined') {
if (typeof html2canvas === 'undefined') {
        alert('Export image tool is still loading. Try again in a moment.');
alert('Export image tool is still loading. Try again in a moment.');
        return;
return;
    }
}


    if (!exportCardInner) {
if (!exportCardInner) {
        alert('Export card container not found.');
    alert('Export card container not found.');
        return;
    return;
    }
}


    populateExportCard();
populateExportCard();


    html2canvas(exportCardInner, {
html2canvas(exportCardInner, {
        backgroundColor: null,
    backgroundColor: null,
        scale: 2,
    scale: 2,
        useCORS: true
    useCORS: true
    }).then(function (canvas) {
}).then(function (canvas) {
        var link = document.createElement('a');
    var link = document.createElement('a');


        var buildName = buildNameInput && buildNameInput.value
    var buildName = buildNameInput && buildNameInput.value
            ? buildNameInput.value.trim()
        ? buildNameInput.value.trim()
            : 'EQOA-Build';
        : 'EQOA-Build';


        link.download = buildName.replace(/[^a-z0-9]/gi, '_') + '.png';
    link.download = buildName.replace(/[^a-z0-9]/gi, '_') + '.png';
        link.href = canvas.toDataURL('image/png');
    link.href = canvas.toDataURL('image/png');
        link.click();
    link.click();
    });
});
}
}


function normalizeRarity(rarity) {
function normalizeRarity(rarity) {
    return String(rarity || 'common')
return String(rarity || 'common')
        .toLowerCase()
.toLowerCase()
        .replace(/\s+/g, '-');
.replace(/\s+/g, '-');
}
}


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


         function updateStats() {
         var total = {
             var base = getBaseStats();
             str: base.str || 0,
             if (!base) return;
            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
        };


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


            var total = {
        Object.keys(equippedItems).forEach(function (slot) {
                str: base.str || 0,
             var item = equippedItems[slot];
                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) {
             addStat(total, item, "str");
                total[key] += parseInt(trainingStats[key], 10) || 0;
            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");
        });


            Object.keys(equippedItems).forEach(function (slot) {
        var availableTP = getTrainingPointsForLevel(level);
                var item = equippedItems[slot];
        var spentTP = getSpentTrainingPoints();
        var remainingTP = availableTP - spentTP;


                addStat(total, item, "str");
        if (remainingTP < 0) remainingTP = 0;
                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);
        document.getElementById('cb-tp-available').textContent = availableTP;
            var spentTP = getSpentTrainingPoints();
        document.getElementById('cb-tp-spent').textContent = spentTP;
            var remainingTP = availableTP - spentTP;
        document.getElementById('cb-tp-remaining').textContent = remainingTP;


            if (remainingTP < 0) remainingTP = 0;
        setCappedStat('cb-str', total.str, 650);
        setCappedStat('cb-sta', total.sta, 650);
        setCappedStat('cb-agi', total.agi, 650);
        setCappedStat('cb-dex', total.dex, 650);
        setCappedStat('cb-wis', total.wis, 650);
        setCappedStat('cb-int', total.int, 650);
        setCappedStat('cb-cha', total.cha, 650);


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


            setCappedStat('cb-str', total.str, 650);
        setCappedStat('cb-pr', total.pr, 315);
            setCappedStat('cb-sta', total.sta, 650);
        setCappedStat('cb-dr', total.dr, 315);
            setCappedStat('cb-agi', total.agi, 650);
        setCappedStat('cb-fr', total.fr, 315);
            setCappedStat('cb-dex', total.dex, 650);
        setCappedStat('cb-cr', total.cr, 315);
            setCappedStat('cb-wis', total.wis, 650);
        setCappedStat('cb-lr', total.lr, 315);
            setCappedStat('cb-int', total.int, 650);
        setCappedStat('cb-ar', total.ar, 315);
            setCappedStat('cb-cha', total.cha, 650);


            document.getElementById('cb-hp').textContent = total.hp;
        updateEquippedSummary();
            document.getElementById('cb-pow').textContent = total.pow;
        updateWeaponSummary();
            document.getElementById('cb-ac').textContent = total.ac;
        updateWarnings();
updateWeaponSummary();


            setCappedStat('cb-pr', total.pr, 315);
[
            setCappedStat('cb-dr', total.dr, 315);
'str', 'sta', 'agi', 'dex', 'wis', 'int', 'cha',
            setCappedStat('cb-fr', total.fr, 315);
'hp', 'pow', 'ac',
            setCappedStat('cb-cr', total.cr, 315);
'pr', 'dr', 'fr', 'cr', 'lr', 'ar'
            setCappedStat('cb-lr', total.lr, 315);
].forEach(function (key) {
            setCappedStat('cb-ar', total.ar, 315);
var el = document.getElementById('cb-' + key);


            updateEquippedSummary();
if (!el) return;
            updateWeaponSummary();
            updateWarnings();
        }


        function removeInvalidEquippedItemsForLevel() {
el.classList.remove('cb-stat-flash');
            Object.keys(equippedItems).forEach(function (slot) {
                if (!levelCanUseItem(equippedItems[slot])) {
                    delete equippedItems[slot];
                }
            });
        }


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


                input.addEventListener('input', function () {
el.classList.add('cb-stat-flash');
                    clampTrainingInput(key);
});
                    updateStats();
                });
            });
        }


        raceSelect.addEventListener('change', loadClasses);
}
}


         classSelect.addEventListener('change', function () {
    function removeInvalidEquippedItemsForLevel() {
             equippedItems = {};
         Object.keys(equippedItems).forEach(function (slot) {
            buildGearSlots();
             if (!levelCanUseItem(equippedItems[slot])) {
             updateStats();
                delete equippedItems[slot];
             }
         });
         });
    }


        levelInput.addEventListener('input', function () {
    function bindTrainingInputs() {
            Object.keys(trainingStats).forEach(function (key) {
        Object.keys(trainingStats).forEach(function (key) {
            var input = document.getElementById('train-' + key);
 
            input.addEventListener('input', function () {
                 clampTrainingInput(key);
                 clampTrainingInput(key);
                updateStats();
             });
             });
        });
    }


            removeInvalidEquippedItemsForLevel();
    raceSelect.addEventListener('change', loadClasses);
            enforceDisabledSlots();
 
            buildGearSlots();
    classSelect.addEventListener('change', function () {
             updateStats();
        equippedItems = {};
        buildGearSlots();
        updateStats();
    });
 
    levelInput.addEventListener('input', function () {
        Object.keys(trainingStats).forEach(function (key) {
             clampTrainingInput(key);
         });
         });


         refreshButton.addEventListener('click', function () {
         removeInvalidEquippedItemsForLevel();
            itemCache = {};
        enforceDisabledSlots();
            equippedItems = {};
        buildGearSlots();
            pendingSharedBuild = null;
        updateStats();
            sharedBuildApplied = false;
    });
            hideTooltip();
 
            hideCompareTooltip();
    refreshButton.addEventListener('click', function () {
            loadAllSlotItems();
        itemCache = {};
        });
        equippedItems = {};
        pendingSharedBuild = null;
        sharedBuildApplied = false;
        hideTooltip();
        hideCompareTooltip();
        loadAllSlotItems();
    });


        if (generateShareButton) {
    if (generateShareButton) {
            generateShareButton.addEventListener('click', generateShareUrl);
        generateShareButton.addEventListener('click', generateShareUrl);
        }
    }


        if (copyShareButton) {
    if (copyShareButton) {
            copyShareButton.addEventListener('click', copyShareUrl);
        copyShareButton.addEventListener('click', copyShareUrl);
        }
    }
if (resetBuildButton) {
if (resetBuildButton) {
    resetBuildButton.addEventListener('click', resetBuild);
resetBuildButton.addEventListener('click', resetBuild);
}
}


if (showBisButton) {
if (showBisButton) {
    showBisButton.addEventListener('click', showBestInSlotHelper);
showBisButton.addEventListener('click', showBestInSlotHelper);
}
}


if (compareBuildsButton) {
if (compareBuildsButton) {
    compareBuildsButton.addEventListener('click', compareSharedBuilds);
compareBuildsButton.addEventListener('click', compareSharedBuilds);
}
}


if (globalItemSearchInput) {
if (globalItemSearchInput) {
    globalItemSearchInput.addEventListener('input', renderGlobalItemSearchResults);
globalItemSearchInput.addEventListener('input', renderGlobalItemSearchResults);
}
}


if (themeToggleButton) {
if (themeToggleButton) {
    themeToggleButton.addEventListener('click', toggleBuilderTheme);
themeToggleButton.addEventListener('click', toggleBuilderTheme);
}
}


if (exportCardButton) {
if (exportCardButton) {
    exportCardButton.addEventListener('click', exportBuildCardImage);
exportCardButton.addEventListener('click', exportBuildCardImage);
}
}


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


});
});


function openItemPage(itemName) {
function openItemPage(itemName) {
    window.open(
window.open(
        mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(itemName)),
mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(itemName)),
        '_blank'
'_blank'
    );
);
}
}
Close

Revision as of 14:59, 14 May 2026

ChatGPT





Pasted text.txt
Document
this is my current code, I want you to update it so it has Generate Share URL Copy URL Loads build from ?build= automatically Saves race, class, level, TP allocation, and equipped item names give me the full code replacement here


Pasted text.txt
Document
This is my code, im stuck on Step 3

Generated image: Dark Elf raid tank build guide


Edit


How would it generate a IMG? Is that something we can do?

It needs to say EQOA: Sandstorm Character Builder as Header,
At the bottom it needs to say EQOA: Sandstorm Wiki Character Builder

I also need you to include the URL code for the build and barcode(if possible)

Generated image: EQOA character builder overview


Edit


Perfect, how do I code it so it renders it just like that. Give me the code, step by step


Image created
ΓÇó
Character builder export card tutorial

No, I want the code updated so it export the img just like this 


Image created
ΓÇó
Character builder export card tutorial

Stop

We just worked on Export Character Card feature


Character builder export card tutorial

Generated image: Character builder export card tutorial


Edit



Character builder export card tutorial

Generated image: Character builder export card tutorial


Edit


Okay next step?

Lets back track, I want style of Export Card changed. Notice how Stats is seprated from Resist and HP/POW AC and then I want each box(Build URL, Scan to Load, Notes) side by side at the bottom


Image created
ΓÇó
EQOA: Sandstorm character builder summary

Stop Render


EQOA: Sandstorm character builder summary

Generated image: EQOA: Sandstorm character builder summary


Edit


lets start with step 1


Image created
ΓÇó
EQOA: Sandstorm character builder layout

stop render


EQOA: Sandstorm character builder layout

Generated image: EQOA: Sandstorm character builder layout


Edit


I removed it, but its still showing up

Perfect, lock it. Now study this IMG, NO RENDER!  Text  Headers needs to be Center, Core Stats Order should be
STR | WIS
STA | INT
AGI | CHA
DEX 

Resist order should be

FR | AR
CR | PR
LR | DR

Vitals order should be 
HP
POW
AC

THIS IS NOT A RENDER, I want code updated to this, help me

Study this img. NO RENDER. I need Underlines like this img under Character Builder, STATS, Equipped Items, Notes and Scan to Load Build NO RENDER, I need the code

idk if its working, this is the url is telling me 

study this img, I want it to have it this placement. NO RENDER, I want the code to make it this way

That broke all my columns. I only wanted that done to the Class Box

Look what you did, you broke the layout


Pasted text.txt
Document
i dont see that, this is my code 


Pasted text.txt
Document
this is my code

which function, i have 11 updateStats()

You want the REAL main stats recalculation function.

Search for:

function updateStats() {
You should find ONE actual function definition.

Inside that function, scroll all the way near the bottom.

Find something like:

updateWeaponSummary();
OR:

updateEquippedItems();
OR the final stat display updates.

Add the flash code RIGHT BEFORE the final closing:

}
So near the end it becomes:

updateWeaponSummary();

[
    'str', 'sta', 'agi', 'dex', 'wis', 'int', 'cha',
    'hp', 'pow', 'ac',
    'pr', 'dr', 'fr', 'cr', 'lr', 'ar'
].forEach(function (key) {
    var el = document.getElementById('cb-' + key);

    if (!el) return;

    el.classList.remove('cb-stat-flash');

    void el.offsetWidth;

    el.classList.add('cb-stat-flash');
});

}
DO NOT put it:

inside another loop

inside updateWeaponSummary()

inside updateEquippedItems()

Only inside the main function updateStats() { ... } near the very end.


done show me list

Current Character Builder Upgrade List
Visual / Polish
Paperdoll visual character preview

Export card polish

item icons

EQOA borders/frame art

watermark/logo

Search result highlighting

Slot icons (Head/Chest/Ring/etc)

Sticky search/filter bars

Animated overcap warnings

Theorycrafting / Gameplay
DPS estimator

Advanced stat weighting

Resist analyzer

Softcap efficiency warnings

Upgrade Path Mode expansion

Item Compare Window

Role Presets

Armor set bonuses

Proc synergy calculations

Build efficiency grading

QoL / Utility
Favorite Items / Wishlist

Recently Viewed Items

Saved local browser builds

Character save codes

Live URL updating

Additional global search filters:

rarity

level range

dropped by

zone

Wiki Ecosystem
Auto-link from item pages into builder

Embedded mini-builder on guides/pages

Public build gallery

Shared build comments/ratings

Loot-source upgrade recommendations

Technical / Long-Term
Builder analytics dashboard

External API support

Server-side short build IDs (/build/ABC123)

Major Locked/Completed Systems
Shareable Build URLs ✅

Compact compressed URLs ✅

QR export/load system ✅

Character Export Card ✅

Build Notes export ✅

Compare Shared Builds ✅

Missing shared item warnings ✅

Reset Build ✅

Dark/Light themes ✅

Mobile layout polish ✅

Item compare tooltips ✅

Global item search ✅

Loot-source integration foundation ✅

Rarity system foundation ✅

Tooltip rarity borders/glows ✅

Animated compare stat highlights ✅

Weapon Summary tooltips ✅

Rarity colors across builder ✅

Legendary animated rarity effects ✅

Relic animated rarity effects ✅

Hover lift picker animation ✅

Equipped item glow polish ✅

Upgrade flash animation ✅

Best-in-Slot Helper ✅

Upgrades Only toggle ✅

Filters + Sorting ✅

Clickable item links everywhere ✅

Validation/auto-disable system ✅

Stat caps + overcap display ✅


Search result highlighting


Pasted text.txt
Document
Character Builder is not loading up, page is blank now for some reason

Thought for 5s
Your blank page is from an extra closing brace in updateStats().

Find this part near the end of updateStats():

    el.classList.add('cb-stat-flash');
});

}
        
Replace it with:

    el.classList.add('cb-stat-flash');
});

}
So you are deleting only one extra:

}
Also remove the duplicate:

updateWeaponSummary();
You already have it right above. Your code currently has it twice. 


After that: save → Ctrl + F5.


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 pendingSharedBuild = null;
    var sharedBuildApplied = false;
var missingSharedItems = [];

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

    var textKeys = [
"effect", "proc", "weaponstyle",
"droppedby", "zone", "droprate"
];

    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-row">
<div class="character-builder-title">EQOA: Sandstorm Character Builder</div>

<button id="cb-theme-toggle" type="button">
    Toggle Theme
</button>
            <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;">
                <label>Build Name</label>
                <input type="text" id="cb-build-name" placeholder="Lv60 Ogre SK Raid Tank" style="width:220px;">
Build Notes

                <button id="cb-refresh-items">Refresh Items</button>
                <button id="cb-generate-share-url" type="button">Generate Share URL</button>
                <button id="cb-copy-share-url" type="button">Copy URL</button>
Reset Build
Export Build Card


<button id="cb-show-bis" type="button">Show Top Items</button>
<label>Build URL 2</label>
<input type="text" id="cb-compare-url-b" placeholder="Paste second shared build URL">

<button id="cb-compare-builds" type="button">Compare Builds</button>
            <div id="cb-share-url-box" class="character-builder-warning-box" style="display:none; margin-top:10px;">
                <b>Share Build URL</b><br>
                <input type="text" id="cb-share-url-output" readonly style="width:100%; box-sizing:border-box; margin-top:6px;">
                <div id="cb-share-url-status" style="margin-top:5px;"></div>
            </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;">Weapon Summary</div>

            <table class="character-builder-stat-table">
                <tr>
                    <th>Slot</th>
                    <th>Weapon</th>
                    <th>DMG</th>
                    <th>Style</th>
                    <th>Proc / Effect</th>
                </tr>
                <tr>
                    <td>Primary</td>
                    <td id="cb-weapon-primary-name">-</td>
                    <td id="cb-weapon-primary-dmg">-</td>
                    <td id="cb-weapon-primary-style">-</td>
                    <td id="cb-weapon-primary-effect">-</td>
                </tr>
                <tr>
                    <td>Secondary</td>
                    <td id="cb-weapon-secondary-name">-</td>
                    <td id="cb-weapon-secondary-dmg">-</td>
                    <td id="cb-weapon-secondary-style">-</td>
                    <td id="cb-weapon-secondary-effect">-</td>
                </tr>
                <tr>
                    <td>Range</td>
                    <td id="cb-weapon-range-name">-</td>
                    <td id="cb-weapon-range-dmg">-</td>
                    <td id="cb-weapon-range-style">-</td>
                    <td id="cb-weapon-range-effect">-</td>
                </tr>
            </table>

            <div class="character-builder-equipment-layout">

                <div class="character-builder-equipped-summary">
                    <div class="character-builder-section-title" style="margin-top:18px;">Equipped Items</div>
                    <table class="character-builder-equipped-table">
                        <tbody id="cb-equipped-summary-body"></tbody>
                    </table>
                </div>

                <div class="character-builder-gear-column">
                    <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>

            </div>




    <div class="cb-export-main-title">EQOA: SANDSTORM</div>
    <div class="cb-export-sub-title">CHARACTER BUILDER</div>
    <div id="cb-export-build-name" class="cb-export-build-name">Unnamed Build</div>

    <div class="cb-export-top-info">
        <div class="cb-export-info-box">
            <div class="cb-export-info-label">Race</div>
            <div id="cb-export-race"></div>
        </div>

        <div class="cb-export-info-box">
            <div class="cb-export-info-label">Class</div>
            <div id="cb-export-class"></div>
        </div>

        <div class="cb-export-info-box">
            <div class="cb-export-info-label">Level</div>
            <div id="cb-export-level"></div>
        </div>
    </div>

    <div class="cb-export-main-grid">
        <div class="cb-export-panel">
            <div class="cb-export-panel-title">STATS</div>

            <div class="cb-export-subsection-title">Core Stats</div>
            <div id="cb-export-core-stats" class="cb-export-stats-grid"></div>

            <div class="cb-export-divider"></div>

            <div class="cb-export-subsection-title">Resists</div>
            <div id="cb-export-resists" class="cb-export-stats-grid"></div>

            <div class="cb-export-divider"></div>

            <div class="cb-export-subsection-title">Vitals</div>
            <div id="cb-export-vitals" class="cb-export-stats-grid"></div>
        </div>

        <div class="cb-export-panel">
            <div class="cb-export-panel-title">EQUIPPED ITEMS</div>
            <div id="cb-export-items"></div>
        </div>
    </div>

    <div class="cb-export-bottom-row">
        <div class="cb-export-panel cb-export-bottom-box">
            <div class="cb-export-panel-title">SCAN TO LOAD</div>
            <div id="cb-export-qr" class="cb-export-qr-box">QR CODE</div>
        </div>

        <div class="cb-export-panel cb-export-bottom-box">
            <div class="cb-export-panel-title">BUILD NOTES</div>
            <div id="cb-export-notes" class="cb-export-notes-box"></div>
        </div>
    </div>

    <div class="cb-export-footer">EQOA: Sandstorm Wiki Character Builder</div>

</div>




            <div id="cb-item-tooltip" class="character-builder-tooltip" style="display:none;"></div>
            <div id="cb-compare-tooltip" class="character-builder-tooltip cb-compare-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 buildNameInput = document.getElementById('cb-build-name');
var buildNotesInput = document.getElementById('cb-build-notes');
var gearGrid = document.getElementById('cb-gear-grid');
var loadingMessage = document.getElementById('cb-loading-message');
var refreshButton = document.getElementById('cb-refresh-items');
var generateShareButton = document.getElementById('cb-generate-share-url');
var copyShareButton = document.getElementById('cb-copy-share-url');
var resetBuildButton = document.getElementById('cb-reset-build');
var exportCardButton = document.getElementById('cb-export-card');
var shareUrlBox = document.getElementById('cb-share-url-box');
var shareUrlOutput = document.getElementById('cb-share-url-output');
var shareUrlStatus = document.getElementById('cb-share-url-status');
var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
var warningBox = document.getElementById('cb-warning-box');
var globalItemSearchInput = document.getElementById('cb-global-item-search');
var globalSearchResultsBox = document.getElementById('cb-global-search-results');
var bisStatSelect = document.getElementById('cb-bis-stat');
var showBisButton = document.getElementById('cb-show-bis');
var bisResultsBox = document.getElementById('cb-bis-results');
var compareUrlAInput = document.getElementById('cb-compare-url-a');
var compareUrlBInput = document.getElementById('cb-compare-url-b');
var compareBuildsButton = document.getElementById('cb-compare-builds');
var compareResultsBox = document.getElementById('cb-compare-results');
var tooltip = document.getElementById('cb-item-tooltip');
var compareTooltip = document.getElementById('cb-compare-tooltip');
var exportCardInner = document.getElementById('cb-export-card-inner');
var exportQrBox = document.getElementById('cb-export-qr');
var exportNotesBox = document.getElementById('cb-export-notes');
var themeToggleButton = document.getElementById('cb-theme-toggle');

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


    function getBuildParamFromUrl() {
        try {
            var params = new URLSearchParams(window.location.search);
            return params.get('b') || params.get('build');
        } catch (e) {
            return null;
        }
    }

    function encodeBuildData(buildData) {
var json = JSON.stringify(buildData);

try {
    if (typeof LZString !== 'undefined' && LZString.compressToEncodedURIComponent) {
        return 'lz:' + LZString.compressToEncodedURIComponent(json);
    }
} catch (e) {
    console.warn('LZString compression failed. Falling back to base64.', e);
}

return 'b64:' + btoa(unescape(encodeURIComponent(json)));
}

    function decodeBuildData(encoded) {
try {
    if (encoded.indexOf('lz:') === 0) {
        if (typeof LZString !== 'undefined' && LZString.decompressFromEncodedURIComponent) {
            var decompressed = LZString.decompressFromEncodedURIComponent(encoded.substring(3));
            return JSON.parse(decompressed);
        }

        console.error('LZString is not loaded yet.');
        return null;
    }

    if (encoded.indexOf('b64:') === 0) {
        encoded = encoded.substring(4);
    }

    var json = decodeURIComponent(escape(atob(encoded)));
    return JSON.parse(json);
} catch (e) {
    console.error('Could not decode shared build.', e);
    return null;
}
}

    function getCurrentBuildData() {
        var gear = {};

        gearSlots.forEach(function (slot) {
            gear[slot] = equippedItems[slot] ? equippedItems[slot].name : '';
        });

        return {
v: 1,
n: buildNameInput ? buildNameInput.value.trim() : "",
no: buildNotesInput ? buildNotesInput.value.trim() : "",
r: raceSelect.value,
c: classSelect.value,
l: getCharacterLevel(),
t: {
    str: parseInt(trainingStats.str, 10) || 0,
    sta: parseInt(trainingStats.sta, 10) || 0,
    agi: parseInt(trainingStats.agi, 10) || 0,
    dex: parseInt(trainingStats.dex, 10) || 0,
    wis: parseInt(trainingStats.wis, 10) || 0,
    int: parseInt(trainingStats.int, 10) || 0,
    cha: parseInt(trainingStats.cha, 10) || 0
},
i: gear
};
}

    function generateShareUrl() {
        var buildData = getCurrentBuildData();
        var encoded = encodeBuildData(buildData);
        var baseUrl = window.location.origin + window.location.pathname;
var url = baseUrl + '?b=' + encodeURIComponent(encoded);

        if (shareUrlOutput) shareUrlOutput.value = url;
        if (shareUrlBox) shareUrlBox.style.display = 'block';
        if (shareUrlStatus) shareUrlStatus.textContent = 'Share URL generated.';

        try {
            var newUrl = baseUrl + '?b=' + encodeURIComponent(encoded);
            window.history.replaceState(null, document.title, newUrl);
        } catch (e) {}

        return url;
    }

    function copyShareUrl() {
        var url = shareUrlOutput && shareUrlOutput.value ? shareUrlOutput.value : generateShareUrl();

        function markCopied() {
            if (shareUrlBox) shareUrlBox.style.display = 'block';
            if (shareUrlStatus) shareUrlStatus.textContent = 'Copied to clipboard.';
        }

        function markManualCopy() {
            if (shareUrlBox) shareUrlBox.style.display = 'block';
            if (shareUrlOutput) {
                shareUrlOutput.focus();
                shareUrlOutput.select();
            }
            if (shareUrlStatus) shareUrlStatus.textContent = 'Copy blocked by browser. Select the URL above and copy it manually.';
        }

        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(url).then(markCopied).catch(markManualCopy);
        } else {
            try {
                if (shareUrlOutput) {
                    shareUrlOutput.focus();
                    shareUrlOutput.select();
                }
                document.execCommand('copy');
                markCopied();
            } catch (e) {
                markManualCopy();
            }
        }
    }
function resetBuild() {
equippedItems = {};
pendingSharedBuild = null;
sharedBuildApplied = false;

levelInput.value = 60;

if (buildNameInput) {
    buildNameInput.value = '';
}
if (buildNotesInput) {
buildNotesInput.value = '';
}

Object.keys(trainingStats).forEach(function (key) {
    trainingStats[key] = 0;

    var input = document.getElementById('train-' + key);
    if (input) input.value = 0;
});

if (shareUrlBox) shareUrlBox.style.display = 'none';
if (shareUrlOutput) shareUrlOutput.value = '';
if (shareUrlStatus) shareUrlStatus.textContent = '';

try {
    window.history.replaceState(
        null,
        document.title,
        window.location.origin + window.location.pathname
    );
} catch (e) {}

buildGearSlots();
updateStats();
}

    function getAllLoadedItemsFlat() {
        var all = [];
        var seen = {};

        Object.keys(itemCache).forEach(function (slotType) {
            (itemCache[slotType] || []).forEach(function (item) {
                if (!item || !item.name) return;
                var key = item.name.toLowerCase();
                if (!seen[key]) {
                    seen[key] = item;
                    all.push(item);
                }
            });
        });

        return all;
    }

    function findLoadedItemByName(itemName, slot) {
        if (!itemName) return null;

        var name = String(itemName).toLowerCase();
        var slotType = getBuilderSlotType(slot);
        var slotItems = itemCache[slotType] || [];

        for (var i = 0; i < slotItems.length; i++) {
            if (slotItems[i].name && slotItems[i].name.toLowerCase() === name) {
                return slotItems[i];
            }
        }

        var allItems = getAllLoadedItemsFlat();
        for (var j = 0; j < allItems.length; j++) {
            if (allItems[j].name && allItems[j].name.toLowerCase() === name) {
                return allItems[j];
            }
        }

        return null;
    }

    function setTrainingInputsFromState() {
        Object.keys(trainingStats).forEach(function (key) {
            var input = document.getElementById('train-' + key);
            if (input) input.value = trainingStats[key];
        });
    }

    function applySharedBuildBasicState(buildData) {
        if (!buildData || sharedBuildApplied) return;
buildData.buildName = buildData.buildName || buildData.n;
buildData.buildNotes = buildData.buildNotes || buildData.no;
buildData.race = buildData.race || buildData.r;
buildData.classCode = buildData.classCode || buildData.c;
buildData.level = buildData.level || buildData.l;
buildData.trainingStats = buildData.trainingStats || buildData.t;
buildData.equippedItemNames = buildData.equippedItemNames || buildData.i;

        if (buildData.race && data.baseStats[buildData.race]) {
            raceSelect.value = buildData.race;
            loadClasses(true);
        }

        if (buildData.classCode) {
            var classExists = false;
            Array.prototype.forEach.call(classSelect.options, function (option) {
                if (option.value === buildData.classCode) classExists = true;
            });

            if (classExists) classSelect.value = buildData.classCode;
        }

        if (buildData.level !== undefined) {
            levelInput.value = buildData.level;
            getCharacterLevel();
        }
if (buildData.buildName && buildNameInput) {
buildNameInput.value = buildData.buildName;
}

if (buildData.buildNotes && buildNotesInput) {
buildNotesInput.value = buildData.buildNotes;
}

        if (buildData.trainingStats) {
            Object.keys(trainingStats).forEach(function (key) {
                trainingStats[key] = parseInt(buildData.trainingStats[key], 10) || 0;
            });
            setTrainingInputsFromState();
            Object.keys(trainingStats).forEach(function (key) {
                clampTrainingInput(key);
            });
            setTrainingInputsFromState();
        }
    }

    function applySharedBuildEquipment(buildData) {
        if (!buildData || !buildData.equippedItemNames) return;

        equippedItems = {};

        gearSlots.forEach(function (slot) {
            var itemName = buildData.equippedItemNames[slot];
            var item = findLoadedItemByName(itemName, slot);

            if (item && classCanUseItem(item, classSelect.value) && levelCanUseItem(item)) {
equippedItems[slot] = item;
} else if (itemName) {
missingSharedItems.push(itemName);
}
});

        enforceDisabledSlots();
        sharedBuildApplied = true;
        buildGearSlots();
        updateStats();

        if (shareUrlBox && shareUrlOutput) {
shareUrlBox.style.display = 'block';
shareUrlOutput.value = window.location.href;

if (shareUrlStatus) {
    if (missingSharedItems.length > 0) {
        shareUrlStatus.innerHTML =
            'Shared build loaded, but some items could not be found:<br>ΓÇó ' +
            missingSharedItems.join('<br>ΓÇó ');
    } else {
        shareUrlStatus.textContent = 'Shared build loaded.';
    }
}
}
}

    function tryLoadSharedBuildFromUrl() {
        var encoded = getBuildParamFromUrl();
        if (!encoded) return;

        pendingSharedBuild = decodeBuildData(encoded);

        if (pendingSharedBuild) {
            applySharedBuildBasicState(pendingSharedBuild);
        } else if (shareUrlBox && shareUrlStatus) {
            shareUrlBox.style.display = 'block';
            shareUrlStatus.textContent = 'Could not load shared build URL.';
        }
    }

    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 getCategoriesForSlotType(slotType) {
        if (slotType === "Primary") {
            return ["Primary Items", "Secondary Items"];
        }
        return [getCategoryForSlot(slotType)];
    }

    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 parseCategories(wikitext) {
        var categories = [];
        var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
        var match;

        while ((match = regex.exec(wikitext)) !== null) {
            categories.push(match[1].trim());
        }

        return categories;
    }

    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;

        item.categories = parseCategories(wikitext);

        item.is2HD = item.categories.indexOf("2HD Items") !== -1 ||
                     String(item.weaponstyle || "").toUpperCase() === "2HD";

        item.isShield = item.categories.indexOf("Shield Items") !== -1 ||
                        String(item.slot || "").toLowerCase() === "shield";

        item.isHeld = item.categories.indexOf("Held Items") !== -1 ||
                      String(item.slot || "").toLowerCase() === "held";

        item.isSecondary = item.categories.indexOf("Secondary Items") !== -1 ||
                           String(item.slot || "").toLowerCase() === "secondary";

        item.isPrimary = item.categories.indexOf("Primary Items") !== -1 ||
                         String(item.slot || "").toLowerCase() === "primary";

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

        textKeys.forEach(function (key) {
            if (item[key] === undefined) {
                item[key] = "";
            }
        });

        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 dedupePages(pages) {
        var seen = {};
        var result = [];

        pages.forEach(function (page) {
            if (!seen[page.title]) {
                seen[page.title] = true;
                result.push(page);
            }
        });

        return result;
    }

    function loadItemsForSlotType(slotType) {
        if (itemCache[slotType]) {
            return Promise.resolve(itemCache[slotType]);
        }

        var categories = getCategoriesForSlotType(slotType);

        var categoryPromises = categories.map(function (categoryName) {
            return getPagesInCategory(categoryName);
        });

        return Promise.all(categoryPromises).then(function (categoryResults) {
            var pages = [];

            categoryResults.forEach(function (result) {
                pages = pages.concat(result);
            });

            pages = dedupePages(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.";

            if (pendingSharedBuild && !sharedBuildApplied) {
                applySharedBuildEquipment(pendingSharedBuild);
                return;
            }

            buildGearSlots();
            updateStats();
        }).catch(function (error) {
            loadingMessage.textContent = "Error loading items.";
            console.error(error);
        });
    }

    function getDisabledSlots() {
        var disabled = {};

        var primary = equippedItems.Primary;
        var forearm = equippedItems.Forearm;
        var bracelet1 = equippedItems.Bracelet1;
        var bracelet2 = equippedItems.Bracelet2;

        if (primary && primary.is2HD) {
            disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Shield = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Held1 = "Disabled because a 2HD weapon is equipped in Primary.";
            disabled.Held2 = "Disabled because a 2HD weapon is equipped in Primary.";
        }

        if (forearm) {
            disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
            disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
        }

        if (bracelet1 || bracelet2) {
            disabled.Forearm = "Disabled because Bracelet1 or Bracelet2 is equipped.";
        }

        return disabled;
    }

    function enforceDisabledSlots() {
        var disabled = getDisabledSlots();

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

    function loadClasses(preserveEquipped) {
        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);
        });

        if (!preserveEquipped) {
            equippedItems = {};
        }

        buildGearSlots();
        updateStats();
    }

    function buildGearSlots() {
        gearGrid.innerHTML = '';

        var disabledSlots = getDisabledSlots();

        gearSlots.forEach(function (slot) {
            var slotBox = document.createElement('div');
            slotBox.className = 'character-builder-slot';

            if (disabledSlots[slot]) {
                slotBox.className += ' cb-slot-disabled';
            }

            slotBox.innerHTML =
'<label>' + slot + '</label>' +
'<input type="text" class="cb-slot-search" id="cb-search-' + slot.toLowerCase() + '" placeholder="Search ' + slot + '">' +
'<div class="cb-picker-tools">' +
    '<select class="cb-slot-filter" id="cb-filter-' + slot.toLowerCase() + '">' +
        '<option value="">Any Stat</option>' +
        '<option value="hp">HP</option>' +
        '<option value="pow">POW</option>' +
        '<option value="ac">AC</option>' +
        '<option value="dmg">DMG</option>' +
        '<option value="str">STR</option>' +
        '<option value="sta">STA</option>' +
        '<option value="agi">AGI</option>' +
        '<option value="dex">DEX</option>' +
        '<option value="wis">WIS</option>' +
        '<option value="int">INT</option>' +
        '<option value="cha">CHA</option>' +
        '<option value="pr">PR</option>' +
        '<option value="dr">DR</option>' +
        '<option value="fr">FR</option>' +
        '<option value="cr">CR</option>' +
        '<option value="lr">LR</option>' +
        '<option value="ar">AR</option>' +
        '<option value="proc">Has Proc</option>' +
        '<option value="effect">Has Effect</option>' +
        '<option value="2hd">2HD</option>' +
    '</select>' +
    '<select class="cb-slot-sort" id="cb-sort-' + slot.toLowerCase() + '">' +
        '<option value="name">Sort: Name</option>' +
        '<option value="lvl">Sort: Level</option>' +
        '<option value="hp">Sort: HP</option>' +
        '<option value="pow">Sort: POW</option>' +
        '<option value="ac">Sort: AC</option>' +
        '<option value="dmg">Sort: DMG</option>' +
        '<option value="str">Sort: STR</option>' +
        '<option value="sta">Sort: STA</option>' +
        '<option value="agi">Sort: AGI</option>' +
        '<option value="dex">Sort: DEX</option>' +
        '<option value="wis">Sort: WIS</option>' +
        '<option value="int">Sort: INT</option>' +
        '<option value="cha">Sort: CHA</option>' +
    '</select>' +
'' +
'' +
' Upgrades Only' +
'' +

'' +
'' +
'';

            gearGrid.appendChild(slotBox);

            populateItemPicker(slot, disabledSlots[slot]);
        });

        updateEquippedSummary();
    }
function isItemUpgrade(slot, newItem) {
var currentItem = equippedItems[slot];

if (!currentItem) return true;

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

for (var i = 0; i < compareKeys.length; i++) {
    var key = compareKeys[i];

    var newValue = parseFloat(newItem[key]) || 0;
    var currentValue = parseFloat(currentItem[key]) || 0;

    if (newValue > currentValue) {
        return true;
    }
}

return false;
}

    function getFilteredItemsForSlot(slot, searchText, filterKey, sortKey) {
var classCode = classSelect.value;
var slotType = getBuilderSlotType(slot);
var items = itemCache[slotType] || [];

searchText = (searchText || "").toLowerCase();
filterKey = filterKey || "";
sortKey = sortKey || "name";

var filtered = items.filter(function (item) {
    if (!classCanUseItem(item, classCode)) return false;
    if (!levelCanUseItem(item)) return false;

    if (slot === "Primary") {
        if (!(item.isPrimary || item.isSecondary)) return false;
    }

    if (slot === "Secondary") {
        if (!item.isSecondary) return false;
    }

    if (searchText && item.name.toLowerCase().indexOf(searchText) === -1) {
        return false;
    }

    if (filterKey) {
        if (filterKey === "proc" && !item.proc) return false;
        if (filterKey === "effect" && !item.effect) return false;
        if (filterKey === "2hd" && !item.is2HD) return false;

        if (
            filterKey !== "proc" &&
            filterKey !== "effect" &&
            filterKey !== "2hd"
        ) {
            if (!item[filterKey] || parseFloat(item[filterKey]) <= 0) return false;
        }
    }

    return true;
});

filtered.sort(function (a, b) {
    if (sortKey === "name") {
        return String(a.name || "").localeCompare(String(b.name || ""));
    }

    var av = parseFloat(a[sortKey]) || 0;
    var bv = parseFloat(b[sortKey]) || 0;

    if (bv !== av) return bv - av;

    return String(a.name || "").localeCompare(String(b.name || ""));
});

return filtered;
}

    function populateItemPicker(slot, disabledReason) {
        var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
        var searchInput = document.getElementById('cb-search-' + slot.toLowerCase());
        var disabledNote = document.getElementById('cb-disabled-note-' + slot.toLowerCase());
var filterSelect = document.getElementById('cb-filter-' + slot.toLowerCase());
var sortSelect = document.getElementById('cb-sort-' + slot.toLowerCase());
var upgradesOnlyToggle = document.getElementById('cb-upgrades-' + slot.toLowerCase());

        if (!picker) return;

        if (disabledReason) {
            if (searchInput) searchInput.disabled = true;
            picker.innerHTML = '<div class="cb-picker-disabled">Disabled</div>';
            if (disabledNote) disabledNote.textContent = disabledReason;
            return;
        }

        function rebuildPicker() {
            var searchText = searchInput ? searchInput.value : "";
var filterKey = filterSelect ? filterSelect.value : "";
var sortKey = sortSelect ? sortSelect.value : "name";
var filteredItems = getFilteredItemsForSlot(slot, searchText, filterKey, sortKey);

if (upgradesOnlyToggle && upgradesOnlyToggle.checked) {
filteredItems = filteredItems.filter(function (item) {
return isItemUpgrade(slot, item);
});
}

filteredItems = filteredItems.slice(0, 10);

            var currentItem = equippedItems[slot];

            picker.innerHTML = '';

            var noneRow = document.createElement('div');
            noneRow.className = 'cb-picker-item cb-picker-none';
            noneRow.textContent = '-- None --';

            if (!currentItem) {
                noneRow.className += ' cb-picker-selected';
            }

            noneRow.addEventListener('click', function () {
                delete equippedItems[slot];
                hideTooltip();
                hideCompareTooltip();
                enforceDisabledSlots();
                buildGearSlots();
                updateStats();
            });

            picker.appendChild(noneRow);

            filteredItems.forEach(function (item) {
                var row = document.createElement('div');
                row.className = 'cb-picker-item';

                if (currentItem === item) {
                    row.className += ' cb-picker-selected';
                }

                var itemNameSpan = document.createElement('span');
itemNameSpan.textContent = item.name + (item.lvl ? ' (Lv ' + item.lvl + ')' : '');

itemNameSpan.className = 'cb-rarity-' + normalizeRarity(item.rarity);

var itemPageLink = document.createElement('span');
itemPageLink.textContent = ' Γåù';
itemPageLink.title = 'Open item page';
itemPageLink.className = 'cb-item-page-link';

itemPageLink.addEventListener('click', function (event) {
event.stopPropagation();
openItemPage(item.name);
});

row.appendChild(itemNameSpan);
row.appendChild(itemPageLink);

                row.addEventListener('mouseenter', function (event) {
                    showCompareTooltip(slot, item, event);
                });

                row.addEventListener('mousemove', function (event) {
                    moveCompareTooltip(event);
                });

                row.addEventListener('mouseleave', function () {
                    hideCompareTooltip();
                });

                row.addEventListener('click', function () {
                    equippedItems[slot] = item;
                    hideTooltip();
                    hideCompareTooltip();
                    enforceDisabledSlots();
                    buildGearSlots();
                    updateStats();
                });

                picker.appendChild(row);
            });
        }

        rebuildPicker();

        if (searchInput) {
searchInput.addEventListener('input', rebuildPicker);
}

if (filterSelect) {
filterSelect.addEventListener('change', rebuildPicker);
}

if (sortSelect) {
sortSelect.addEventListener('change', rebuildPicker);
}

if (upgradesOnlyToggle) {
upgradesOnlyToggle.addEventListener('change', rebuildPicker);
}
}

    function validateEquipment() {
        var warnings = [];

        var primary = equippedItems.Primary;
        var secondary = equippedItems.Secondary;
        var shield = equippedItems.Shield;
        var held1 = equippedItems.Held1;
        var held2 = equippedItems.Held2;
        var forearm = equippedItems.Forearm;
        var bracelet1 = equippedItems.Bracelet1;
        var bracelet2 = equippedItems.Bracelet2;

        if (forearm && (bracelet1 || bracelet2)) {
            warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
        }

        if (primary && primary.is2HD) {
            if (secondary) warnings.push("2HD weapon in Primary conflicts with Secondary.");
            if (shield) warnings.push("2HD weapon in Primary conflicts with Shield.");
            if (held1 || held2) warnings.push("2HD weapon in Primary conflicts with Held1/Held2.");
        }

        if (shield && secondary) {
            warnings.push("Shield conflicts with Secondary weapon.");
        }

        if (shield && (held1 || held2)) {
            warnings.push("Shield conflicts with Held1/Held2.");
        }

        if ((held1 || held2) && (primary || secondary || shield)) {
            warnings.push("Held items may conflict with Primary / Secondary / Shield. Confirm this setup is valid.");
        }

        return warnings;
    }

    function updateWarnings() {
        var warnings = validateEquipment();
        var disabledSlots = getDisabledSlots();

        Object.keys(disabledSlots).forEach(function (slot) {
            warnings.push(slot + " is " + disabledSlots[slot]);
        });

        if (!warningBox) return;

        if (warnings.length === 0) {
            warningBox.style.display = "none";
            warningBox.innerHTML = "";
            return;
        }

        warningBox.style.display = "block";
        warningBox.innerHTML =
            "<b>Equipment Warnings</b><br>" +
            warnings.map(function (w) {
                return "ΓÇó " + w;
            }).join("<br>");
    }

    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 cb-rarity-' +
normalizeRarity(item.rarity) +
'">' +
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>';
        if (item.is2HD) html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
        if (item.weaponstyle) html += '<div class="cb-tooltip-line"><b>Style:</b> ' + item.weaponstyle + '</div>';
        if (item.proc) html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + item.proc + '</div>';
        if (item.effect) html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + item.effect + '</div>';
if (item.droppedby) html += 'Dropped By: ' + item.droppedby + '';
if (item.zone) html += 'Zone: ' + item.zone + '';
if (item.droprate) html += 'Drop Rate: ' + item.droprate + '';

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

        return html;
    }

    function getCompareValue(newItem, oldItem, key) {
        var newValue = parseFloat(newItem[key]) || 0;
        var oldValue = oldItem ? (parseFloat(oldItem[key]) || 0) : 0;
        return newValue - oldValue;
    }

    function buildCompareTooltipHtml(slot, newItem) {
        var oldItem = equippedItems[slot];
        var html = '';

        html += '<div class="cb-tooltip-title cb-rarity-' +
normalizeRarity(newItem.rarity) +
'">' +
newItem.name +
'</div>';

        if (oldItem) {
            html += '<div class="cb-tooltip-line"><b>Currently:</b> ' + oldItem.name + '</div>';
        } else {
            html += '<div class="cb-tooltip-line"><b>Currently:</b> None</div>';
        }

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

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

        if (newItem.is2HD) {
            html += '<div class="cb-tooltip-line"><b>Type:</b> 2HD Weapon</div>';
        }

        if (newItem.weaponstyle) {
            html += '<div class="cb-tooltip-line"><b>Style:</b> ' + newItem.weaponstyle + '</div>';
        }

        if (newItem.proc) {
            html += '<div class="cb-tooltip-line"><b>Proc:</b> ' + newItem.proc + '</div>';
        }

        if (newItem.effect) {
            html += '<div class="cb-tooltip-line"><b>Effect:</b> ' + newItem.effect + '</div>';
        }

        var compareRows = [];

        function addCompare(label, key) {
            var diff = getCompareValue(newItem, oldItem, key);
            if (diff === 0) return;

            var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative';
            var sign = diff > 0 ? '+' : '';

            compareRows.push(
                '<div><span>' + label + '</span> <b class="' + cls + '">' + sign + diff + '</b></div>'
            );
        }

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

        html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Comparison</b></div>';

        if (compareRows.length) {
            html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>';
        } else {
            html += '<div class="cb-tooltip-line">No stat difference.</div>';
        }

        html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>';
        html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';

        return html;
    }

    function showTooltip(item, event) {
        if (!tooltip || !item) return;
        tooltip.className =
'character-builder-tooltip cb-tooltip-rarity-' +
normalizeRarity(item.rarity);
tooltip.innerHTML = buildTooltipHtml(item);
tooltip.style.display = 'block';
moveTooltip(event);
}

    function moveTooltip(event) {
        if (!tooltip || tooltip.style.display === 'none') return;
        tooltip.style.left = (event.pageX + 14) + 'px';
        tooltip.style.top = (event.pageY + 14) + 'px';
    }

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

    function showCompareTooltip(slot, item, event) {
        if (!compareTooltip || !item) return;
        compareTooltip.className =
'character-builder-tooltip cb-compare-tooltip cb-tooltip-rarity-' +
normalizeRarity(item.rarity);
compareTooltip.innerHTML =
buildCompareTooltipHtml(slot, item);
compareTooltip.style.display = 'block';
moveCompareTooltip(event);
}

    function moveCompareTooltip(event) {
        if (!compareTooltip || compareTooltip.style.display === 'none') return;
        compareTooltip.style.left = (event.pageX + 14) + 'px';
        compareTooltip.style.top = (event.pageY + 14) + 'px';
    }

    function hideCompareTooltip() {
        if (!compareTooltip) return;
        compareTooltip.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 cb-rarity-' +
normalizeRarity(item.rarity);
itemSpan.style.cursor = 'pointer';

itemSpan.addEventListener('click', function () {
openItemPage(item.name);
});

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

                itemSpan.addEventListener('mousemove', moveTooltip);
                itemSpan.addEventListener('mouseleave', hideTooltip);

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

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

    function updateWeaponSummary() {
        function setWeaponRow(slotName, prefix) {
            var item = equippedItems[slotName];

            var nameEl = document.getElementById(prefix + '-name');
            var dmgEl = document.getElementById(prefix + '-dmg');
            var styleEl = document.getElementById(prefix + '-style');
            var effectEl = document.getElementById(prefix + '-effect');

            if (!nameEl || !dmgEl || !styleEl || !effectEl) return;

            nameEl.innerHTML = '';
if (item) {
var link = document.createElement('span');
link.textContent = item.name;
link.className =
'cb-equipped-item-name cb-rarity-' +
normalizeRarity(item.rarity);
link.style.cursor = 'pointer';

link.addEventListener('click', function () {
    openItemPage(item.name);
});

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

link.addEventListener('mousemove', moveTooltip);

link.addEventListener('mouseleave', hideTooltip);

nameEl.appendChild(link);
} else {
nameEl.textContent = '-';
}
dmgEl.textContent = item && item.dmg ? item.dmg : '-';

            if (item) {
                if (item.weaponstyle) {
                    styleEl.textContent = item.weaponstyle;
                } else if (item.is2HD) {
                    styleEl.textContent = '2HD';
                } else if (item.isPrimary || item.isSecondary) {
                    styleEl.textContent = '1HD';
                } else {
                    styleEl.textContent = '-';
                }
            } else {
                styleEl.textContent = '-';
            }

            var effectText = '-';

            if (item) {
                if (item.proc) {
                    effectText = item.proc;
                } else if (item.effect) {
                    effectText = item.effect;
                }
            }

            effectEl.textContent = effectText;
        }

        setWeaponRow('Primary', 'cb-weapon-primary');
        setWeaponRow('Secondary', 'cb-weapon-secondary');
        setWeaponRow('Range', 'cb-weapon-range');
    }

    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 setCappedStat(elementId, value, cap) {
        var element = document.getElementById(elementId);
        if (!element) return;

        value = parseFloat(value) || 0;

        if (value > cap) {
            var over = value - cap;
            element.innerHTML = cap + ' <span class="cb-overcap">(+' + over + ')</span>';
        } else {
            element.textContent = value;
        }
    }
function getTopItemsForSlot(slot, statKey, limit) {
var items = getFilteredItemsForSlot(slot, "", "", statKey);

items = items.filter(function (item) {
    return (parseFloat(item[statKey]) || 0) > 0;
});

return items.slice(0, limit || 5);
}

function showBestInSlotHelper() {
if (!bisStatSelect || !bisResultsBox) return;

var statKey = bisStatSelect.value;
if (!statKey) {
bisResultsBox.style.display = 'none';
bisResultsBox.innerHTML = '';
return;
}
var statLabel = statKey.toUpperCase();

var html = '<b>Top Items by ' + statLabel + '</b><br>';

gearSlots.forEach(function (slot) {
    var topItems = getTopItemsForSlot(slot, statKey, 5);

    html += '<div style="margin-top:8px;"><b>' + slot + '</b></div>';

    if (!topItems.length) {
        html += '<div>No matching items.</div>';
        return;
    }

    topItems.forEach(function (item, index) {
        html += '<div>' +
            (index + 1) + '. ' +
            '<span class="cb-equipped-item-name" style="cursor:pointer;" data-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
            item.name +
            '</span>' +
            ' ΓÇö ' + statLabel + ': ' + (item[statKey] || 0) +
            '</div>';
    });
});

bisResultsBox.innerHTML = html;
bisResultsBox.style.display = 'block';

Array.prototype.forEach.call(
    bisResultsBox.querySelectorAll('[data-item-name]'),
    function (el) {
        el.addEventListener('click', function () {
            openItemPage(el.getAttribute('data-item-name'));
        });
    }
);
}

function getBuildDataFromUrl(url) {
try {
var parsedUrl = new URL(url);
var encoded = parsedUrl.searchParams.get('build');

    if (!encoded) return null;

    return decodeBuildData(encoded);
} catch (e) {
    return null;
}
}

function formatCompareValue(value) {
if (value === undefined || value === null || value === '') return '-';
return value;
}

function compareTextValue(label, a, b) {
if (a === b) {
return '' + label + ': ' + formatCompareValue(a) + '';
}

return '<div><b>' + label + ':</b> ' +
    formatCompareValue(a) +
    ' → <b>' +
    formatCompareValue(b) +
    '</b></div>';
}

function compareBuildEquipment(buildA, buildB) {
var html = 'Equipment Differences';

gearSlots.forEach(function (slot) {
    var itemA = buildA.equippedItemNames && buildA.equippedItemNames[slot]
        ? buildA.equippedItemNames[slot]
        : '-';

    var itemB = buildB.equippedItemNames && buildB.equippedItemNames[slot]
        ? buildB.equippedItemNames[slot]
        : '-';

    if (itemA !== itemB) {
        html += '<div><b>' + slot + ':</b> ' + itemA + ' → <b>' + itemB + '</b></div>';
    }
});

return html;
}

function calculateBuildTotalsFromSharedBuild(buildData) {
var race = buildData.race;
var classCode = buildData.classCode;

if (!race || !classCode || !data.baseStats[race] || !data.baseStats[race][classCode]) {
    return null;
}

var base = data.baseStats[race][classCode];

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

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

if (buildData.equippedItemNames) {
    gearSlots.forEach(function (slot) {
        var itemName = buildData.equippedItemNames[slot];
        var item = findLoadedItemByName(itemName, slot);

        if (!item) return;

        ["str", "sta", "agi", "dex", "wis", "int", "cha", "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar"].forEach(function (key) {
            total[key] += parseFloat(item[key]) || 0;
        });
    });
}

return total;
}

function compareBuildStats(buildA, buildB) {
var totalsA = calculateBuildTotalsFromSharedBuild(buildA);
var totalsB = calculateBuildTotalsFromSharedBuild(buildB);

if (!totalsA || !totalsB) {
    return '<div style="margin-top:10px;"><b>Stat Differences</b><br>Could not calculate stats for one or both builds.</div>';
}

var statOrder = [
    "str", "sta", "agi", "dex", "wis", "int", "cha",
    "hp", "pow", "ac",
    "pr", "dr", "fr", "cr", "lr", "ar"
];

var html = '<div style="margin-top:10px;"><b>Stat Differences</b></div>';

statOrder.forEach(function (key) {
    var a = parseFloat(totalsA[key]) || 0;
    var b = parseFloat(totalsB[key]) || 0;
    var diff = b - a;

    var label = key.toUpperCase();
    var diffText = diff > 0 ? ' <span class="cb-compare-positive">(+' + diff + ')</span>' :
                   diff < 0 ? ' <span class="cb-compare-negative">(' + diff + ')</span>' :
                   ' (0)';

    html += '<div><b>' + label + ':</b> ' + a + ' → ' + b + diffText + '</div>';
});

return html;
}

function compareSharedBuilds() {
if (!compareUrlAInput || !compareUrlBInput || !compareResultsBox) return;

var buildA = getBuildDataFromUrl(compareUrlAInput.value.trim());
var buildB = getBuildDataFromUrl(compareUrlBInput.value.trim());

if (!buildA || !buildB) {
    compareResultsBox.style.display = 'block';
    compareResultsBox.innerHTML = 'Could not compare builds. Make sure both URLs are valid shared build URLs.';
    return;
}

var html = '<b>Build Comparison</b><br>';

html += compareTextValue('Build Name', buildA.buildName || '-', buildB.buildName || '-');
html += compareTextValue('Race', buildA.race || '-', buildB.race || '-');
html += compareTextValue('Class', buildA.classCode || '-', buildB.classCode || '-');
html += compareTextValue('Level', buildA.level || '-', buildB.level || '-');

html += compareBuildEquipment(buildA, buildB);
html += compareBuildStats(buildA, buildB);

compareResultsBox.innerHTML = html;
compareResultsBox.style.display = 'block';
}

function applyBuilderTheme(theme) {
if (!app) return;

app.classList.remove('cb-theme-dark');
app.classList.remove('cb-theme-light');

if (theme === 'light') {
    app.classList.add('cb-theme-light');
} else {
    app.classList.add('cb-theme-dark');
}

try {
    localStorage.setItem('eqoa-builder-theme', theme);
} catch (e) {}
}

function toggleBuilderTheme() {
var isLight = app.classList.contains('cb-theme-light');

if (isLight) {
    applyBuilderTheme('dark');
} else {
    applyBuilderTheme('light');
}
}

function loadSavedBuilderTheme() {
try {
var saved = localStorage.getItem('eqoa-builder-theme');

    if (saved === 'light') {
        applyBuilderTheme('light');
    } else {
        applyBuilderTheme('dark');
    }
} catch (e) {
    applyBuilderTheme('dark');
}
}

function getItemPrimarySlotLabel(item) {
if (!item) return "-";

if (item.slot) return item.slot;
if (item.isPrimary) return "Primary";
if (item.isSecondary) return "Secondary";
if (item.isShield) return "Shield";
if (item.isHeld) return "Held";

if (item.categories && item.categories.length) {
    for (var i = 0; i < item.categories.length; i++) {
        var cat = item.categories[i];
        if (cat.indexOf(" Items") !== -1) {
            return cat.replace(" Items", "");
        }
    }
}

return "-";
}

function searchAllLoadedItems(query) {
query = String(query || "").toLowerCase().trim();

if (!query) return [];

var allItems = getAllLoadedItemsFlat();

return allItems.filter(function (item) {
    var searchable = [
        item.name,
        item.class,
        item.slot,
        item.weaponstyle,

        item.proc,
        item.effect,
        item.droppedby,
        item.zone,
        item.droprate,
        item.categories ? item.categories.join(" ") : ""
    ].join(" ").toLowerCase();

    return searchable.indexOf(query) !== -1;
}).slice(0, 30);
}

function renderGlobalItemSearchResults() {
if (!globalItemSearchInput || !globalSearchResultsBox) return;

var query = globalItemSearchInput.value;
var results = searchAllLoadedItems(query);

if (!query || !query.trim()) {
    globalSearchResultsBox.style.display = "none";
    globalSearchResultsBox.innerHTML = "";
    return;
}

globalSearchResultsBox.style.display = "block";

if (!results.length) {
    globalSearchResultsBox.innerHTML = "No matching items found.";
    return;
}

var html = '<b>Global Item Search Results</b><br>';

results.forEach(function (item) {
    var slotLabel = getItemPrimarySlotLabel(item);
    var lvlText = item.lvl ? "Lv " + item.lvl : "No Lv";
    var classText = item.class ? item.class : "ALL";
    var sourceText = "";

    if (item.zone || item.droppedby) {
        sourceText = " ΓÇö " + [
            item.zone || "",
            item.droppedby || ""
        ].filter(Boolean).join(" / ");
    }

    html += '<div style="margin-top:6px;">' +
        '<span class="cb-equipped-item-name" style="cursor:pointer;" data-global-item-name="' + item.name.replace(/"/g, '&quot;') + '">' +
            item.name +
        '</span>' +
        ' ΓÇö ' + slotLabel +
        ' ΓÇö ' + lvlText +
        ' ΓÇö ' + classText +
        sourceText +
        '</div>';
});

globalSearchResultsBox.innerHTML = html;

Array.prototype.forEach.call(
    globalSearchResultsBox.querySelectorAll('[data-global-item-name]'),
    function (el) {
        el.addEventListener('click', function () {
openItemPage(el.getAttribute('data-global-item-name'));
});

el.addEventListener('mouseenter', function (event) {
var itemName = el.getAttribute('data-global-item-name');
var item = findLoadedItemByName(itemName, "Primary") || getAllLoadedItemsFlat().find(function (x) {
return x.name === itemName;
});

if (item) {
    showTooltip(item, event);
}
});

el.addEventListener('mousemove', moveTooltip);
el.addEventListener('mouseleave', hideTooltip);
}
);
}





function populateExportCard() {
var buildName = buildNameInput ? buildNameInput.value.trim() : '';
var shareUrl = generateShareUrl();
var classIcons = {
WAR: '/images/classes/war.png',
PAL: '/images/classes/pal.png',
SK: '/images/classes/sk.png',
RGE: '/images/classes/rge.png',
BRD: '/images/classes/brd.png',
RAN: '/images/classes/ran.png',
MNK: '/images/classes/mnk.png',
CL: '/images/classes/cl.png',
DRD: '/images/classes/drd.png',
SHA: '/images/classes/sha.png',
NEC: '/images/classes/nec.png',
MAG: '/images/classes/mag.png',
ENC: '/images/classes/enc.png',
WIZ: '/images/classes/wiz.png',
ALC: '/images/classes/alc.png'
};

document.getElementById('cb-export-build-name').textContent =
    buildName || 'Unnamed Build';

document.getElementById('cb-export-race').textContent =
    raceSelect.value || '-';

document.getElementById('cb-export-class').textContent =
    classSelect.value || '-';
var classIcons = {
WAR: '/eqoawiki/index.php/Special/file/WAR.png',
PAL: '/eqoawiki/index.php/Special/file/PAL.png',
SK: '/eqoawiki/index.php/Special/file/SK.png',
RGE: '/eqoawiki/index.php/Special/file/RGE.png',
BRD: '/eqoawiki/index.php/Special/file/BRD.png',
RAN: '/eqoawiki/index.php/Special/file/RAN.png',
MNK: '/eqoawiki/index.php/Special/file/MNK.png',
CL: '/eqoawiki/index.php/Special/file/CL.png',
DRD: '/eqoawiki/index.php/Special/file/DRD.png',
SHA: '/eqoawiki/index.php/Special/file/SHA.png',
NEC: '/eqoawiki/index.php/Special/file/NEC.png',
MAG: '/eqoawiki/index.php/Special/file/MAG.png',
ENC: '/eqoawiki/index.php/Special/file/ENC.png',
WIZ: '/eqoawiki/index.php/Special/file/WIZ.png',
ALC: '/eqoawiki/index.php/Special/file/ALC.png'
};

var classIcon = document.getElementById('cb-export-class-icon');

if (classIcon) {
classIcon.src =
classIcons[classSelect.value] || '';

classIcon.style.display =
    classIcons[classSelect.value]
        ? 'block'
        : 'none';
}

var classIcon = document.getElementById('cb-export-class-icon');

if (classIcon) {
classIcon.src =
classIcons[classSelect.value] || '';

classIcon.style.display =
    classIcons[classSelect.value]
        ? 'block'
        : 'none';
}

document.getElementById('cb-export-level').textContent =
    getCharacterLevel();

function statLine(key) {
    var el = document.getElementById('cb-' + key);
    var value = el ? el.textContent : '0';

    return '<div><b>' + key.toUpperCase() + ':</b> ' + value + '</div>';
}

var coreStats = [
'str', 'wis',
'sta', 'int',
'agi', 'cha',
'dex'
];

var resists = [
'fr', 'ar',
'cr', 'pr',
'lr', 'dr'
];

var vitals = [
'hp',
'pow',
'ac'
];

document.getElementById('cb-export-core-stats').innerHTML =
    coreStats.map(statLine).join('');

document.getElementById('cb-export-resists').innerHTML =
    resists.map(statLine).join('');

document.getElementById('cb-export-vitals').innerHTML =
    vitals.map(statLine).join('');

var itemsHtml = '';


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

if (item) {
    itemsHtml +=
        '<div>' +
        '<span class="cb-rarity-' +
        normalizeRarity(item.rarity) +
        '">' +
        item.name +
        '</span>' +
        '</div>';
} else {
    itemsHtml +=
        '<div>-</div>';
}
});

document.getElementById('cb-export-items').innerHTML =
    itemsHtml;


if (exportNotesBox) {
    var notes = buildNotesInput
        ? buildNotesInput.value.trim()
        : '';

    exportNotesBox.textContent =
        notes || 'No build notes.';
}

if (exportQrBox) {
exportQrBox.innerHTML = '';

try {
    if (typeof QRCode !== 'undefined') {
        new QRCode(exportQrBox, {
            text: shareUrl,
            width: 180,
            height: 180,
            correctLevel: QRCode.CorrectLevel.L
        });
    } else {
        exportQrBox.textContent = 'QR not loaded';
    }
} catch (e) {
    console.error('QR generation failed.', e);
    exportQrBox.textContent = 'QR failed';
}
}
}









function exportBuildCardImage() {
if (typeof html2canvas === 'undefined') {
alert('Export image tool is still loading. Try again in a moment.');
return;
}

if (!exportCardInner) {
    alert('Export card container not found.');
    return;
}

populateExportCard();

html2canvas(exportCardInner, {
    backgroundColor: null,
    scale: 2,
    useCORS: true
}).then(function (canvas) {
    var link = document.createElement('a');

    var buildName = buildNameInput && buildNameInput.value
        ? buildNameInput.value.trim()
        : 'EQOA-Build';

    link.download = buildName.replace(/[^a-z0-9]/gi, '_') + '.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
});
}

function normalizeRarity(rarity) {
return String(rarity || 'common')
.toLowerCase()
.replace(/\s+/g, '-');
}

    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;

        setCappedStat('cb-str', total.str, 650);
        setCappedStat('cb-sta', total.sta, 650);
        setCappedStat('cb-agi', total.agi, 650);
        setCappedStat('cb-dex', total.dex, 650);
        setCappedStat('cb-wis', total.wis, 650);
        setCappedStat('cb-int', total.int, 650);
        setCappedStat('cb-cha', total.cha, 650);

        document.getElementById('cb-hp').textContent = total.hp;
        document.getElementById('cb-pow').textContent = total.pow;
        document.getElementById('cb-ac').textContent = total.ac;

        setCappedStat('cb-pr', total.pr, 315);
        setCappedStat('cb-dr', total.dr, 315);
        setCappedStat('cb-fr', total.fr, 315);
        setCappedStat('cb-cr', total.cr, 315);
        setCappedStat('cb-lr', total.lr, 315);
        setCappedStat('cb-ar', total.ar, 315);

        updateEquippedSummary();
        updateWeaponSummary();
        updateWarnings();
updateWeaponSummary();

[
'str', 'sta', 'agi', 'dex', 'wis', 'int', 'cha',
'hp', 'pow', 'ac',
'pr', 'dr', 'fr', 'cr', 'lr', 'ar'
].forEach(function (key) {
var el = document.getElementById('cb-' + key);

if (!el) return;

el.classList.remove('cb-stat-flash');

void el.offsetWidth;

el.classList.add('cb-stat-flash');
});

}
}

    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();
        enforceDisabledSlots();
        buildGearSlots();
        updateStats();
    });

    refreshButton.addEventListener('click', function () {
        itemCache = {};
        equippedItems = {};
        pendingSharedBuild = null;
        sharedBuildApplied = false;
        hideTooltip();
        hideCompareTooltip();
        loadAllSlotItems();
    });

    if (generateShareButton) {
        generateShareButton.addEventListener('click', generateShareUrl);
    }

    if (copyShareButton) {
        copyShareButton.addEventListener('click', copyShareUrl);
    }
if (resetBuildButton) {
resetBuildButton.addEventListener('click', resetBuild);
}

if (showBisButton) {
showBisButton.addEventListener('click', showBestInSlotHelper);
}

if (compareBuildsButton) {
compareBuildsButton.addEventListener('click', compareSharedBuilds);
}

if (globalItemSearchInput) {
globalItemSearchInput.addEventListener('input', renderGlobalItemSearchResults);
}

if (themeToggleButton) {
themeToggleButton.addEventListener('click', toggleBuilderTheme);
}

if (exportCardButton) {
exportCardButton.addEventListener('click', exportBuildCardImage);
}

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

});

function openItemPage(itemName) {
window.open(
mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(itemName)),
'_blank'
);
}


Close