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:
ChatGPT
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);


Pasted text.txt
    function initCharacterBuilder() {
Document
        var app = document.getElementById('character-builder-app');
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
        if (!app) return;


        var data = window.EQOA_BUILDER_DATA;
        var equippedItems = {};
        var itemCache = {};
        var pendingSharedBuild = null;
        var sharedBuildApplied = false;
var missingSharedItems = [];


Pasted text.txt
        var statKeys = [
Document
            "lvl", "str", "sta", "agi", "dex", "wis", "int", "cha",
This is my code, im stuck on Step 3
            "hp", "pow", "ac", "pr", "dr", "fr", "cr", "lr", "ar",
            "dmg", "hot", "pot", "dur"
        ];


Generated image: Dark Elf raid tank build guide
        var textKeys = [
    "effect", "proc", "weaponstyle",
    "droppedby", "zone", "droprate"
];


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


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


How would it generate a IMG? Is that something we can do?
            var tp = 20;


It needs to say EQOA: Sandstorm Character Builder as Header,
            for (var lvl = 2; lvl <= level; lvl++) {
At the bottom it needs to say EQOA: Sandstorm Wiki Character Builder
                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;
            }


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


Generated image: EQOA character builder overview
        app.innerHTML = `
            <div class="character-builder-box">


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


Edit
    <button id="cb-theme-toggle" type="button">
        Toggle Theme
    </button>
</div>


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


Perfect, how do I code it so it renders it just like that. Give me the code, step by step
                    <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;">
<label>Build Notes</label>
<textarea id="cb-build-notes" placeholder="Example: Raid tank setup focused on HP, AC, and disease resist." rows="3" style="width:260px;"></textarea>


Image created
                    <button id="cb-refresh-items">Refresh Items</button>
ΓÇó
                    <button id="cb-generate-share-url" type="button">Generate Share URL</button>
Character builder export card tutorial
                    <button id="cb-copy-share-url" type="button">Copy URL</button>
<button id="cb-reset-build" type="button">Reset Build</button>
<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>


No, I want the code updated so it export the img just like this
<div class="character-builder-controls">
    <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>
</div>


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


Image created
<div class="character-builder-controls">
ΓÇó
    <label>Build URL 1</label>
Character builder export card tutorial
    <input type="text" id="cb-compare-url-a" placeholder="Paste first shared build URL">


Stop
    <label>Build URL 2</label>
    <input type="text" id="cb-compare-url-b" placeholder="Paste second shared build URL">


We just worked on Export Character Card feature
    <button id="cb-compare-builds" type="button">Compare Builds</button>
</div>


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


Character builder export card tutorial
<div class="character-builder-controls">
    <label>Search All Items</label>
    <input type="text" id="cb-global-item-search" placeholder="Search item name, class, slot, zone, dropped by...">
</div>


Generated image: Character builder export card tutorial
<div id="cb-global-search-results" class="character-builder-warning-box" style="display:none; margin-top:10px;"></div>
                <div id="cb-warning-box" class="character-builder-warning-box" style="display:none;"></div>


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


Edit
                <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>


Character builder export card tutorial
                <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>


Generated image: Character builder export card tutorial
                <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>


Edit
                <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>


Okay next step?
                <div class="character-builder-equipment-layout">


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


Image created
                </div>
ΓÇó
EQOA: Sandstorm character builder summary


Stop Render




EQOA: Sandstorm character builder summary


Generated image: EQOA: Sandstorm character builder summary


<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">


Edit
        <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>


lets start with step 1
            <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>


Image created
        <div class="cb-export-main-grid">
ΓÇó
            <div class="cb-export-panel">
EQOA: Sandstorm character builder layout
                <div class="cb-export-panel-title">STATS</div>
 
stop render
 
 
EQOA: Sandstorm character builder layout
 
Generated image: EQOA: Sandstorm character builder layout


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


Edit
                <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>


I removed it, but its still showing up
                <div class="cb-export-divider"></div>


Perfect, lock it. Now study this IMG, NO RENDER!  Text  Headers needs to be Center, Core Stats Order should be
                <div class="cb-export-subsection-title">Vitals</div>
STR | WIS
                 <div id="cb-export-vitals" class="cb-export-stats-grid"></div>
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>


             <div class="character-builder-section-title" style="margin-top:15px;">Training Points</div>
             <div class="cb-export-panel">
 
                 <div class="cb-export-panel-title">EQUIPPED ITEMS</div>
            <table class="character-builder-stat-table">
                 <div id="cb-export-items"></div>
                <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>
    <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>


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


        <div class="cb-export-info-box">
             <div class="cb-export-panel cb-export-bottom-box">
             <div class="cb-export-info-label">Level</div>
                <div class="cb-export-panel-title">BUILD NOTES</div>
            <div id="cb-export-level"></div>
                <div id="cb-export-notes" class="cb-export-notes-box"></div>
        </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>


    <div class="cb-export-bottom-row">
         <div class="cb-export-footer">EQOA: Sandstorm Wiki Character Builder</div>
         <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>
</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 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 raceSelect = document.getElementById('cb-race');
    var classSelect = document.getElementById('cb-class');
        var classSelect = document.getElementById('cb-class');
    var levelInput = document.getElementById('cb-level');
        var levelInput = document.getElementById('cb-level');
    var buildNameInput = document.getElementById('cb-build-name');
        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 705: Line 379:
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 712: Line 386:
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) {
            encoded = encoded.substring(4);
         }
         }


         console.error('LZString is not loaded yet.');
        var json = decodeURIComponent(escape(atob(encoded)));
        return JSON.parse(json);
    } catch (e) {
         console.error('Could not decode shared build.', e);
         return null;
         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() {
        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 {
            try {
            var newUrl = baseUrl + '?b=' + encodeURIComponent(encoded);
                var newUrl = baseUrl + '?b=' + encodeURIComponent(encoded);
            window.history.replaceState(null, document.title, newUrl);
                window.history.replaceState(null, document.title, newUrl);
        } catch (e) {}
            } catch (e) {}


        return url;
            return url;
    }
        }


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


        function markCopied() {
            function markCopied() {
            if (shareUrlBox) shareUrlBox.style.display = 'block';
                if (shareUrlBox) shareUrlBox.style.display = 'block';
            if (shareUrlStatus) shareUrlStatus.textContent = 'Copied to clipboard.';
                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) {
            function markManualCopy() {
            navigator.clipboard.writeText(url).then(markCopied).catch(markManualCopy);
                if (shareUrlBox) shareUrlBox.style.display = 'block';
        } else {
            try {
                 if (shareUrlOutput) {
                 if (shareUrlOutput) {
                     shareUrlOutput.focus();
                     shareUrlOutput.focus();
                     shareUrlOutput.select();
                     shareUrlOutput.select();
                 }
                 }
                 document.execCommand('copy');
                 if (shareUrlStatus) shareUrlStatus.textContent = 'Copy blocked by browser. Select the URL above and copy it manually.';
                markCopied();
            }
            } catch (e) {
 
                markManualCopy();
            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() {
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;
         }
         }


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


    function setTrainingInputsFromState() {
         function applySharedBuildBasicState(buildData) {
         Object.keys(trainingStats).forEach(function (key) {
             if (!buildData || sharedBuildApplied) return;
            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 940: Line 617:
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) {
                var classExists = false;
                Array.prototype.forEach.call(classSelect.options, function (option) {
                    if (option.value === buildData.classCode) classExists = true;
                });


        if (buildData.classCode) {
                if (classExists) classSelect.value = 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.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() {
            var encoded = getBuildParamFromUrl();
            if (!encoded) return;
 
            pendingSharedBuild = decodeBuildData(encoded);


    function tryLoadSharedBuildFromUrl() {
            if (pendingSharedBuild) {
        var encoded = getBuildParamFromUrl();
                applySharedBuildBasicState(pendingSharedBuild);
        if (!encoded) return;
            } else if (shareUrlBox && shareUrlStatus) {
                shareUrlBox.style.display = 'block';
                shareUrlStatus.textContent = 'Could not load shared build URL.';
            }
        }


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


         if (pendingSharedBuild) {
         function getCharacterLevel() {
             applySharedBuildBasicState(pendingSharedBuild);
             var level = parseInt(levelInput.value, 10);
        } else if (shareUrlBox && shareUrlStatus) {
            if (isNaN(level) || level < 1) level = 1;
             shareUrlBox.style.display = 'block';
             if (level > 60) level = 60;
             shareUrlStatus.textContent = 'Could not load shared build URL.';
             levelInput.value = level;
            return level;
         }
         }
    }


    Object.keys(data.baseStats).forEach(function (race) {
        function getBuilderSlotType(slot) {
        addOption(raceSelect, race, race);
            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 getCharacterLevel() {
        function getCategoryForSlot(slotType) {
        var level = parseInt(levelInput.value, 10);
            return slotType + " Items";
        if (isNaN(level) || level < 1) level = 1;
         }
         if (level > 60) level = 60;
        levelInput.value = level;
        return level;
    }


    function getBuilderSlotType(slot) {
        function getCategoriesForSlotType(slotType) {
        if (slot.indexOf("Bracelet") === 0) return "Bracelet";
            if (slotType === "Primary") {
        if (slot.indexOf("Ring") === 0) return "Ring";
                return ["Primary Items", "Secondary Items"];
        if (slot.indexOf("Ear") === 0) return "Ear";
            }
        if (slot.indexOf("Held") === 0) return "Held";
            return [getCategoryForSlot(slotType)];
         return slot;
         }
    }


    function getCategoryForSlot(slotType) {
        function apiGet(params) {
        return slotType + " Items";
            return new mw.Api().get(params);
    }
        }


    function getCategoriesForSlotType(slotType) {
        function getPagesInCategory(categoryName) {
        if (slotType === "Primary") {
            return apiGet({
            return ["Primary Items", "Secondary Items"];
                action: "query",
                list: "categorymembers",
                cmtitle: "Category:" + categoryName,
                cmlimit: "500",
                cmnamespace: "0",
                format: "json"
            }).then(function (data) {
                return data.query.categorymembers || [];
            });
         }
         }
        return [getCategoryForSlot(slotType)];
    }


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


    function getPagesInCategory(categoryName) {
            return apiGet({
        return apiGet({
                action: "query",
            action: "query",
                prop: "revisions",
            list: "categorymembers",
                titles: title,
            cmtitle: "Category:" + categoryName,
                rvprop: "content",
            cmlimit: "500",
                rvslots: "main",
            cmnamespace: "0",
                formatversion: "2",
            format: "json"
                format: "json"
        }).then(function (data) {
            }).then(function (data) {
            return data.query.categorymembers || [];
                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) {
            while ((match = regex.exec(wikitext)) !== null) {
        var categories = [];
                categories.push(match[1].trim());
        var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
            }
        var match;


        while ((match = regex.exec(wikitext)) !== null) {
             return categories;
             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");


    function parseItemBox(wikitext, fallbackName) {
            lines.forEach(function (line) {
        var match = wikitext.match(/\{\{ItemBox([\s\S]*?)\}\}/i);
                line = line.trim();
        if (!match) return null;
                if (line.indexOf("|") !== 0) return;


        var content = match[1];
                var equalIndex = line.indexOf("=");
        var item = { name: fallbackName };
                if (equalIndex === -1) return;
        var lines = content.split("\n");


        lines.forEach(function (line) {
                var key = line.substring(1, equalIndex).trim().toLowerCase();
            line = line.trim();
                var value = line.substring(equalIndex + 1).trim();
            if (line.indexOf("|") !== 0) return;


            var equalIndex = line.indexOf("=");
                if (!key) return;
            if (equalIndex === -1) return;


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


             if (!key) return;
             if (!item.name) item.name = fallbackName;


             item[key] = value;
             item.categories = parseCategories(wikitext);
        });


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


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


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


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


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


        item.isSecondary = item.categories.indexOf("Secondary Items") !== -1 ||
            statKeys.forEach(function (key) {
                          String(item.slot || "").toLowerCase() === "secondary";
                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;
                }
            });


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


            var pagePromises = pages.map(function (page) {
                    itemCache[slotType] = items;
                return getPageWikitext(page.title).then(function (wikitext) {
                     return items;
                     return parseItemBox(wikitext, page.title);
                 });
                 });
             });
             });
        }
        function loadAllSlotItems() {
            loadingMessage.textContent = "Loading item categories...";


             return Promise.all(pagePromises).then(function (items) {
             var uniqueSlotTypes = [];
                 items = items.filter(function (item) {
 
                     return item !== null;
            gearSlots.forEach(function (slot) {
                 });
                 var slotType = getBuilderSlotType(slot);
                if (uniqueSlotTypes.indexOf(slotType) === -1) {
                     uniqueSlotTypes.push(slotType);
                 }
            });


                itemCache[slotType] = items;
            var promises = uniqueSlotTypes.map(function (slotType) {
                 return items;
                 return loadItemsForSlotType(slotType);
             });
             });
        });
    }


    function loadAllSlotItems() {
            return Promise.all(promises).then(function () {
        loadingMessage.textContent = "Loading item categories...";
                loadingMessage.textContent = "Items loaded.";


        var uniqueSlotTypes = [];
                if (pendingSharedBuild && !sharedBuildApplied) {
                    applySharedBuildEquipment(pendingSharedBuild);
                    return;
                }


        gearSlots.forEach(function (slot) {
                buildGearSlots();
            var slotType = getBuilderSlotType(slot);
                updateStats();
             if (uniqueSlotTypes.indexOf(slotType) === -1) {
             }).catch(function (error) {
                 uniqueSlotTypes.push(slotType);
                loadingMessage.textContent = "Error loading items.";
             }
                 console.error(error);
         });
             });
 
         }


         var promises = uniqueSlotTypes.map(function (slotType) {
         function getDisabledSlots() {
             return loadItemsForSlotType(slotType);
             var disabled = {};
        });


        return Promise.all(promises).then(function () {
            var primary = equippedItems.Primary;
             loadingMessage.textContent = "Items loaded.";
            var forearm = equippedItems.Forearm;
             var bracelet1 = equippedItems.Bracelet1;
            var bracelet2 = equippedItems.Bracelet2;


             if (pendingSharedBuild && !sharedBuildApplied) {
             if (primary && primary.is2HD) {
                 applySharedBuildEquipment(pendingSharedBuild);
                 disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
                 return;
                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.";
             }
             }


             buildGearSlots();
             if (forearm) {
            updateStats();
                disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
        }).catch(function (error) {
                disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
            loadingMessage.textContent = "Error loading items.";
            }
            console.error(error);
        });
    }


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


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


         if (primary && primary.is2HD) {
         function enforceDisabledSlots() {
             disabled.Secondary = "Disabled because a 2HD weapon is equipped in Primary.";
             var disabled = getDisabledSlots();
            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) {
            Object.keys(disabled).forEach(function (slot) {
            disabled.Bracelet1 = "Disabled because a Forearm item is equipped.";
                if (equippedItems[slot]) {
             disabled.Bracelet2 = "Disabled because a Forearm item is equipped.";
                    delete equippedItems[slot];
                }
             });
         }
         }


         if (bracelet1 || bracelet2) {
         function loadClasses(preserveEquipped) {
             disabled.Forearm = "Disabled because Bracelet1 or Bracelet2 is equipped.";
             classSelect.innerHTML = '';
        }


        return disabled;
            var race = raceSelect.value;
    }


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


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


    function loadClasses(preserveEquipped) {
            buildGearSlots();
         classSelect.innerHTML = '';
            updateStats();
         }


         var race = raceSelect.value;
         function buildGearSlots() {
            gearGrid.innerHTML = '';


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


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


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


     function buildGearSlots() {
                slotBox.innerHTML =
         gearGrid.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>' +


        var disabledSlots = getDisabledSlots();
'<label class="cb-upgrade-toggle">' +
    '<input type="checkbox" id="cb-upgrades-' + slot.toLowerCase() + '">' +
    ' Upgrades Only' +
'</label>' +


        gearSlots.forEach(function (slot) {
'</div>' +
            var slotBox = document.createElement('div');
'<div class="cb-item-picker-list" id="cb-picker-' + slot.toLowerCase() + '"></div>' +
            slotBox.className = 'character-builder-slot';
    '<div class="cb-slot-disabled-note" id="cb-disabled-note-' + slot.toLowerCase() + '"></div>';


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


            slotBox.innerHTML =
                populateItemPicker(slot, disabledSlots[slot]);
'<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' +
'' +


'' +
             updateEquippedSummary();
'' +
         }
'';
 
             gearGrid.appendChild(slotBox);
 
            populateItemPicker(slot, disabledSlots[slot]);
         });


        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) {
 
        var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
        function populateItemPicker(slot, disabledReason) {
        var searchInput = document.getElementById('cb-search-' + slot.toLowerCase());
            var picker = document.getElementById('cb-picker-' + slot.toLowerCase());
        var disabledNote = document.getElementById('cb-disabled-note-' + 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 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,509: Line 1,192:


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,558: Line 1,241:


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


Line 1,565: Line 1,248:
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) {
                        moveCompareTooltip(event);
                    });


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


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


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


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


        rebuildPicker();
            if (searchInput) {
 
    searchInput.addEventListener('input', rebuildPicker);
        if (searchInput) {
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() {
            var warnings = [];


    function validateEquipment() {
            var primary = equippedItems.Primary;
        var warnings = [];
            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;


        var primary = equippedItems.Primary;
            if (forearm && (bracelet1 || bracelet2)) {
        var secondary = equippedItems.Secondary;
                warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
        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)) {
            if (primary && primary.is2HD) {
            warnings.push("Forearm item is equipped with Bracelet1/Bracelet2. Use either 1 Forearm or 2 Bracelets.");
                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 (primary && primary.is2HD) {
            if (shield && secondary) {
            if (secondary) warnings.push("2HD weapon in Primary conflicts with Secondary.");
                warnings.push("Shield conflicts with Secondary weapon.");
            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) {
            if (shield && (held1 || held2)) {
            warnings.push("Shield conflicts with Secondary weapon.");
                warnings.push("Shield conflicts with Held1/Held2.");
        }
            }


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


        if ((held1 || held2) && (primary || secondary || shield)) {
             return warnings;
             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();


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


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


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


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


         warningBox.style.display = "block";
         function formatItemStats(item) {
        warningBox.innerHTML =
            var rows = [];
             "<b>Equipment Warnings</b><br>" +
 
            warnings.map(function (w) {
             function add(label, key) {
                 return "ΓÇó " + w;
                 if (item[key] !== undefined && item[key] !== 0 && item[key] !== "") {
            }).join("<br>");
                    rows.push('<div><span>' + label + '</span> ' + item[key] + '</div>');
    }
                }
            }


    function formatItemStats(item) {
            add("Lvl:", "lvl");
        var rows = [];
            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");


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


         add("Lvl:", "lvl");
         function buildTooltipHtml(item) {
        add("DMG:", "dmg");
            var classText = item.class || "";
        add("AC:", "ac");
            var slotText = item.slot || "";
        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("");
            var html = '';
     }
            html += '<div class="cb-tooltip-title cb-rarity-' +
    normalizeRarity(item.rarity) +
    '">' +
    item.name +
     '</div>';


    function buildTooltipHtml(item) {
            if (slotText) html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + slotText + '</div>';
        var classText = item.class || "";
            if (classText) html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</div>';
        var slotText = item.slot || "";
            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 += '<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>';


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


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


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


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


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


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


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


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


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


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


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


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


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


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


             var cls = diff > 0 ? 'cb-compare-positive' : 'cb-compare-negative';
             addCompare("Lvl:", "lvl");
             var sign = diff > 0 ? '+' : '';
             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");


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


        addCompare("Lvl:", "lvl");
            if (compareRows.length) {
        addCompare("DMG:", "dmg");
                html += '<div class="cb-tooltip-stats">' + compareRows.join("") + '</div>';
        addCompare("AC:", "ac");
            } else {
        addCompare("STR:", "str");
                html += '<div class="cb-tooltip-line">No stat difference.</div>';
        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>';
            html += '<div class="cb-tooltip-line" style="margin-top:6px;"><b>Item Stats</b></div>';
            html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';


        if (compareRows.length) {
             return html;
             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>';
         function showTooltip(item, event) {
        html += '<div class="cb-tooltip-stats">' + formatItemStats(newItem) + '</div>';
            if (!tooltip || !item) return;
            tooltip.className =
    'character-builder-tooltip cb-tooltip-rarity-' +
    normalizeRarity(item.rarity);


        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.innerHTML = buildTooltipHtml(item);
tooltip.style.display = 'block';
            tooltip.style.display = 'block';
moveTooltip(event);
            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 moveTooltip(event) {
        function hideTooltip() {
        if (!tooltip || tooltip.style.display === 'none') return;
            if (!tooltip) return;
        tooltip.style.left = (event.pageX + 14) + 'px';
            tooltip.style.display = 'none';
         tooltip.style.top = (event.pageY + 14) + 'px';
         }
    }


    function hideTooltip() {
        function showCompareTooltip(slot, item, event) {
        if (!tooltip) return;
            if (!compareTooltip || !item) return;
        tooltip.style.display = 'none';
            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() {
        if (!equippedSummaryBody) return;


         equippedSummaryBody.innerHTML = '';
         function updateEquippedSummary() {
            if (!equippedSummaryBody) return;


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


             var slotCell = document.createElement('td');
             gearSlots.forEach(function (slot) {
            slotCell.textContent = slot;
                var item = equippedItems[slot];
            slotCell.style.fontWeight = 'bold';
                var row = document.createElement('tr');


                var slotCell = document.createElement('td');
                slotCell.textContent = slot;
                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('mouseleave', hideTooltip);


                itemSpan.addEventListener('mousemove', moveTooltip);
                    itemCell.appendChild(itemSpan);
                 itemSpan.addEventListener('mouseleave', hideTooltip);
                 } else {
                    itemCell.textContent = '-';
                }


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


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


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


            var nameEl = document.getElementById(prefix + '-name');
                if (!nameEl || !dmgEl || !styleEl || !effectEl) return;
            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 = '';


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


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


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


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


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


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


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


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


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


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


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


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


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


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


    function addStat(total, item, key) {
        function setCappedStat(elementId, value, cap) {
        total[key] += parseFloat(item[key]) || 0;
            var element = document.getElementById(elementId);
    }
            if (!element) return;


    function setCappedStat(elementId, value, cap) {
            value = parseFloat(value) || 0;
        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;
            }
        }


        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) {
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 '' + label + ': ' + formatCompareValue(a) + '';
        return '<div><b>' + label + ':</b> ' + formatCompareValue(a) + '</div>';
}
    }


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 = 'Equipment Differences';
    var html = '<div style="margin-top:10px;"><b>Equipment Differences</b></div>';


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.proc,
        item.effect,
            item.effect,
        item.droppedby,
            item.droppedby,
        item.zone,
            item.zone,
        item.droprate,
            item.droprate,
        item.categories ? item.categories.join(" ") : ""
            item.categories ? item.categories.join(" ") : ""
    ].join(" ").toLowerCase();
        ].join(" ").toLowerCase();


    return searchable.indexOf(query) !== -1;
        return searchable.indexOf(query) !== -1;
}).slice(0, 30);
    }).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,427: Line 2,114:


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-race').textContent =
    document.getElementById('cb-export-class').textContent =
    raceSelect.value || '-';
        classSelect.value || '-';


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


Line 2,476: Line 2,164:


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


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


Line 2,488: Line 2,177:


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 =
    document.getElementById('cb-export-resists').innerHTML =
    resists.map(statLine).join('');
        resists.map(statLine).join('');


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


var itemsHtml = '';
    var itemsHtml = '';


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


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 {
     itemsHtml +=
        '<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,598: Line 2,282:


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 base = getBaseStats();
            if (!base) return;


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


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


        Object.keys(equippedItems).forEach(function (slot) {
            var total = {
             var item = equippedItems[slot];
                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
             };


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


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


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


        setCappedStat('cb-str', total.str, 650);
            if (remainingTP < 0) remainingTP = 0;
        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-tp-available').textContent = availableTP;
        document.getElementById('cb-pow').textContent = total.pow;
            document.getElementById('cb-tp-spent').textContent = spentTP;
        document.getElementById('cb-ac').textContent = total.ac;
            document.getElementById('cb-tp-remaining').textContent = remainingTP;


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


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


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


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


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


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


el.classList.add('cb-stat-flash');
                input.addEventListener('input', function () {
});
                    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];
             }
         });
         });
    }


    function bindTrainingInputs() {
        levelInput.addEventListener('input', function () {
        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();
            enforceDisabledSlots();
            buildGearSlots();
            updateStats();
         });
         });
    }


    raceSelect.addEventListener('change', loadClasses);
        refreshButton.addEventListener('click', function () {
 
            itemCache = {};
    classSelect.addEventListener('change', function () {
            equippedItems = {};
        equippedItems = {};
            pendingSharedBuild = null;
        buildGearSlots();
            sharedBuildApplied = false;
        updateStats();
            hideTooltip();
    });
            hideCompareTooltip();
 
             loadAllSlotItems();
    levelInput.addEventListener('input', function () {
        Object.keys(trainingStats).forEach(function (key) {
             clampTrainingInput(key);
         });
         });


         removeInvalidEquippedItemsForLevel();
         if (generateShareButton) {
        enforceDisabledSlots();
            generateShareButton.addEventListener('click', generateShareUrl);
         buildGearSlots();
         }
        updateStats();
    });


    refreshButton.addEventListener('click', function () {
         if (copyShareButton) {
        itemCache = {};
            copyShareButton.addEventListener('click', copyShareUrl);
        equippedItems = {};
        }
        pendingSharedBuild = null;
        sharedBuildApplied = false;
        hideTooltip();
         hideCompareTooltip();
        loadAllSlotItems();
    });
 
    if (generateShareButton) {
        generateShareButton.addEventListener('click', generateShareUrl);
    }
 
    if (copyShareButton) {
        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 15:02, 14 May 2026

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

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

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

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

    waitForBuilderData(30);

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

        var data = window.EQOA_BUILDER_DATA;
        var equippedItems = {};
        var itemCache = {};
        var 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>

                <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;">
<label>Build Notes</label>
<textarea id="cb-build-notes" placeholder="Example: Raid tank setup focused on HP, AC, and disease resist." rows="3" style="width:260px;"></textarea>

                    <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>
<button id="cb-reset-build" type="button">Reset Build</button>
<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">
    <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>
</div>

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

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

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

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

<div class="character-builder-controls">
    <label>Search All Items</label>
    <input type="text" id="cb-global-item-search" placeholder="Search item name, class, slot, zone, dropped by...">
</div>

<div id="cb-global-search-results" class="character-builder-warning-box" style="display:none; margin-top:10px;"></div>
                <div id="cb-warning-box" class="character-builder-warning-box" style="display:none;"></div>

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





                <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>' +

'<label class="cb-upgrade-toggle">' +
    '<input type="checkbox" id="cb-upgrades-' + slot.toLowerCase() + '">' +
    ' Upgrades Only' +
'</label>' +

'</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);

                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 += '<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>';

            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 '<div><b>' + label + ':</b> ' + formatCompareValue(a) + '</div>';
    }

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

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

    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:Redirect/file/WAR.png',
    PAL: '/eqoawiki/index.php/Special:Redirect/file/PAL.png',
    SK: '/eqoawiki/index.php/Special:Redirect/file/SK.png',
    RGE: '/eqoawiki/index.php/Special:Redirect/file/RGE.png',
    BRD: '/eqoawiki/index.php/Special:Redirect/file/BRD.png',
    RAN: '/eqoawiki/index.php/Special:Redirect/file/RAN.png',
    MNK: '/eqoawiki/index.php/Special:Redirect/file/MNK.png',
    CL: '/eqoawiki/index.php/Special:Redirect/file/CL.png',
    DRD: '/eqoawiki/index.php/Special:Redirect/file/DRD.png',
    SHA: '/eqoawiki/index.php/Special:Redirect/file/SHA.png',
    NEC: '/eqoawiki/index.php/Special:Redirect/file/NEC.png',
    MAG: '/eqoawiki/index.php/Special:Redirect/file/MAG.png',
    ENC: '/eqoawiki/index.php/Special:Redirect/file/ENC.png',
    WIZ: '/eqoawiki/index.php/Special:Redirect/file/WIZ.png',
    ALC: '/eqoawiki/index.php/Special:Redirect/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];

        itemsHtml +=
    '<div>' +
    '<span class="cb-rarity-' +
    normalizeRarity(item.rarity) +
    '">' +
    item.name +
    '</span>' +
    '</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();
        }

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